180 lines
5.5 KiB
Bash
Executable file
180 lines
5.5 KiB
Bash
Executable file
#!/usr/bin/env bash
|
|
|
|
set -euo pipefail
|
|
|
|
readonly SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
|
readonly REPO_ROOT="$(cd "${SCRIPT_DIR}/../.." && pwd)"
|
|
readonly CLUSTER_NAME="nereus"
|
|
readonly CONTEXT="k3d-${CLUSTER_NAME}"
|
|
readonly ARGO_CHART_VERSION="2.41.1"
|
|
readonly PROMETHEUS_CHART_VERSION="88.5.2"
|
|
|
|
require_commands() {
|
|
local command_name
|
|
for command_name in docker k3d helm kubectl; do
|
|
command -v "${command_name}" >/dev/null || {
|
|
echo "missing required command: ${command_name}" >&2
|
|
exit 1
|
|
}
|
|
done
|
|
docker info >/dev/null
|
|
}
|
|
|
|
use_context() {
|
|
kubectl config use-context "${CONTEXT}" >/dev/null
|
|
}
|
|
|
|
cluster_exists() {
|
|
k3d cluster get "${CLUSTER_NAME}" >/dev/null 2>&1
|
|
}
|
|
|
|
up() {
|
|
require_commands
|
|
if ! cluster_exists; then
|
|
k3d cluster create --config "${REPO_ROOT}/scripts/k3d-nereus.yaml"
|
|
else
|
|
k3d cluster start "${CLUSTER_NAME}"
|
|
fi
|
|
use_context
|
|
|
|
helm repo add argo https://argoproj.github.io/argo-helm --force-update
|
|
helm repo add prometheus-community https://prometheus-community.github.io/helm-charts --force-update
|
|
helm repo update
|
|
|
|
helm upgrade --install argo-rollouts argo/argo-rollouts \
|
|
--namespace argo-rollouts \
|
|
--create-namespace \
|
|
--version "${ARGO_CHART_VERSION}" \
|
|
--wait \
|
|
--timeout 5m
|
|
|
|
helm upgrade --install kube-prometheus-stack prometheus-community/kube-prometheus-stack \
|
|
--namespace observability \
|
|
--create-namespace \
|
|
--version "${PROMETHEUS_CHART_VERSION}" \
|
|
--values "${SCRIPT_DIR}/kube-prometheus-stack.values.yaml" \
|
|
--wait \
|
|
--timeout 10m
|
|
|
|
kubectl create namespace nereus --dry-run=client -o yaml | kubectl apply -f -
|
|
kubectl apply -f "${SCRIPT_DIR}/analysis-harness/harness.yaml"
|
|
check
|
|
}
|
|
|
|
check() {
|
|
require_commands
|
|
cluster_exists || {
|
|
echo "cluster ${CLUSTER_NAME} does not exist; run $0 up" >&2
|
|
exit 1
|
|
}
|
|
use_context
|
|
|
|
kubectl wait node --all --for=condition=Ready --timeout=3m
|
|
kubectl wait deployment --all --namespace argo-rollouts --for=condition=Available --timeout=3m
|
|
kubectl wait deployment --all --namespace observability --for=condition=Available --timeout=5m
|
|
kubectl get rollout probe --namespace nereus >/dev/null
|
|
kubectl get analysistemplate error-rate --namespace nereus >/dev/null
|
|
echo "k3d rollback lab is ready"
|
|
}
|
|
|
|
analysis_name() {
|
|
kubectl get rollout probe --namespace nereus \
|
|
-o jsonpath='{.status.blueGreen.prePromotionAnalysisRunStatus.name}'
|
|
}
|
|
|
|
wait_for_analysis() {
|
|
local previous_name="$1"
|
|
local expected_phase="$2"
|
|
local analysis_run=""
|
|
local phase=""
|
|
local attempt
|
|
|
|
for attempt in {1..90}; do
|
|
analysis_run="$(analysis_name)"
|
|
if [[ -n "${analysis_run}" && "${analysis_run}" != "${previous_name}" ]]; then
|
|
phase="$(kubectl get analysisrun "${analysis_run}" --namespace nereus -o jsonpath='{.status.phase}')"
|
|
if [[ "${phase}" == "${expected_phase}" ]]; then
|
|
echo "${analysis_run} reached ${expected_phase}"
|
|
return 0
|
|
fi
|
|
if [[ "${phase}" == "Error" || "${phase}" == "Inconclusive" ]]; then
|
|
echo "${analysis_run} ended unexpectedly with ${phase}" >&2
|
|
return 1
|
|
fi
|
|
fi
|
|
sleep 2
|
|
done
|
|
|
|
echo "analysis did not reach ${expected_phase} within 180 seconds" >&2
|
|
return 1
|
|
}
|
|
|
|
set_proof_revision() {
|
|
local query="$1"
|
|
local revision="$2"
|
|
kubectl patch rollout probe --namespace nereus --type=merge --patch \
|
|
"{\"spec\":{\"strategy\":{\"blueGreen\":{\"prePromotionAnalysis\":{\"args\":[{\"name\":\"query\",\"value\":\"${query}\"}]}}},\"template\":{\"metadata\":{\"annotations\":{\"nereus.fiwlabs.dev/proof\":\"${revision}\"}}}}}"
|
|
}
|
|
|
|
prove() {
|
|
check
|
|
|
|
local proof_id
|
|
local previous_analysis
|
|
local healthy_analysis
|
|
local healthy_revision=""
|
|
local active_revision=""
|
|
local stable_revision=""
|
|
local attempt
|
|
proof_id="$(date -u +%Y%m%d%H%M%S)"
|
|
|
|
previous_analysis="$(analysis_name)"
|
|
set_proof_revision "vector(0.0)" "healthy-${proof_id}"
|
|
wait_for_analysis "${previous_analysis}" Successful
|
|
healthy_analysis="$(analysis_name)"
|
|
|
|
for attempt in {1..30}; do
|
|
healthy_revision="$(kubectl get rollout probe --namespace nereus -o jsonpath='{.status.stableRS}')"
|
|
active_revision="$(kubectl get service probe-active --namespace nereus -o jsonpath='{.spec.selector.rollouts-pod-template-hash}')"
|
|
if [[ -n "${healthy_revision}" && "${active_revision}" == "${healthy_revision}" ]]; then
|
|
break
|
|
fi
|
|
sleep 2
|
|
done
|
|
[[ -n "${healthy_revision}" && "${active_revision}" == "${healthy_revision}" ]] || {
|
|
echo "healthy revision was not promoted" >&2
|
|
return 1
|
|
}
|
|
|
|
set_proof_revision "vector(1.0)" "failing-${proof_id}"
|
|
wait_for_analysis "${healthy_analysis}" Failed
|
|
stable_revision="$(kubectl get rollout probe --namespace nereus -o jsonpath='{.status.stableRS}')"
|
|
active_revision="$(kubectl get service probe-active --namespace nereus -o jsonpath='{.spec.selector.rollouts-pod-template-hash}')"
|
|
[[ "${stable_revision}" == "${healthy_revision}" && "${active_revision}" == "${healthy_revision}" ]] || {
|
|
echo "failed revision replaced the active healthy revision" >&2
|
|
return 1
|
|
}
|
|
|
|
set_proof_revision "vector(0.0)" "healthy-${proof_id}"
|
|
echo "rollback proof passed; active revision stayed ${healthy_revision}"
|
|
}
|
|
|
|
destroy() {
|
|
require_commands
|
|
if cluster_exists; then
|
|
k3d cluster delete "${CLUSTER_NAME}"
|
|
else
|
|
echo "cluster ${CLUSTER_NAME} does not exist"
|
|
fi
|
|
}
|
|
|
|
case "${1:-}" in
|
|
up) up ;;
|
|
check) check ;;
|
|
prove) prove ;;
|
|
destroy) destroy ;;
|
|
*)
|
|
echo "usage: $0 {up|check|prove|destroy}" >&2
|
|
exit 2
|
|
;;
|
|
esac
|