137 lines
5.9 KiB
Bash
137 lines
5.9 KiB
Bash
|
|
#!/usr/bin/env bash
|
||
|
|
# Shared plumbing for the two Hermes demo drivers.
|
||
|
|
#
|
||
|
|
# The triage demo and the code demo are separate scripts on purpose: they prove
|
||
|
|
# different halves of the Test Automation Diagram, they reset different things,
|
||
|
|
# and mixing them behind one command invited exactly the confusion of running
|
||
|
|
# the wrong subcommand in front of an audience. What they genuinely share -
|
||
|
|
# credentials, Jenkins access, the Ariadne tick reader - lives here, so a fix
|
||
|
|
# to any of it applies to both instead of being made twice and drifting.
|
||
|
|
#
|
||
|
|
# Not executable on its own; both drivers source it.
|
||
|
|
|
||
|
|
# Local, git-ignored credentials. `hermes_demo.env` is the current name;
|
||
|
|
# `hermes_triage_demo.env` is still read so an existing filled-in file keeps
|
||
|
|
# working after the split.
|
||
|
|
_DEMO_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||
|
|
DEMO_ENV=""
|
||
|
|
for _candidate in "$_DEMO_DIR/hermes_demo.env" "$_DEMO_DIR/hermes_triage_demo.env"; do
|
||
|
|
if [ -r "$_candidate" ]; then
|
||
|
|
DEMO_ENV="$_candidate"
|
||
|
|
# shellcheck disable=SC1090
|
||
|
|
. "$_candidate"
|
||
|
|
break
|
||
|
|
fi
|
||
|
|
done
|
||
|
|
[ -n "$DEMO_ENV" ] || DEMO_ENV="$_DEMO_DIR/hermes_demo.env"
|
||
|
|
|
||
|
|
JENKINS_URL="${JENKINS_URL:-https://ci.bstein.dev}"
|
||
|
|
GITEA_URL="${GITEA_URL:-https://scm.bstein.dev}"
|
||
|
|
FIXTURE_JOB="hermes-triage-demo"
|
||
|
|
CODE_JOB="hermes-code-demo"
|
||
|
|
DEMO_NS="hermes-triage-demo"
|
||
|
|
CODE_REPO_DIR="${CODE_REPO_DIR:-$HOME/Development/hermes-code-demo}"
|
||
|
|
|
||
|
|
say() { printf '\n\033[1m[%s] %s\033[0m\n' "$(date -u +%H:%M:%S)" "$*"; }
|
||
|
|
note() { printf ' %s\n' "$*"; }
|
||
|
|
|
||
|
|
require_jenkins() {
|
||
|
|
if [ -z "${JENKINS_USER:-}" ] || [ -z "${JENKINS_TOKEN:-}" ]; then
|
||
|
|
echo "Missing Jenkins credentials." >&2
|
||
|
|
echo "Create $DEMO_ENV from hermes_demo.env.example and fill it in." >&2
|
||
|
|
exit 1
|
||
|
|
fi
|
||
|
|
}
|
||
|
|
|
||
|
|
# Every remote call is time-bounded. A hung curl during a demo is worse than a
|
||
|
|
# failed one: a failure says what to do next, a hang says nothing at all.
|
||
|
|
jenkins_get() { curl -sk --max-time 25 -u "$JENKINS_USER:$JENKINS_TOKEN" "$JENKINS_URL$1"; }
|
||
|
|
jenkins_post() {
|
||
|
|
curl -sk --max-time 25 -o /dev/null -w '%{http_code}' \
|
||
|
|
-u "$JENKINS_USER:$JENKINS_TOKEN" -X POST "$JENKINS_URL$1"
|
||
|
|
}
|
||
|
|
gitea_get() { curl -s --max-time 25 -H "Authorization: token ${GITEA_TOKEN:-}" "$GITEA_URL$1"; }
|
||
|
|
|
||
|
|
last_build_number() {
|
||
|
|
jenkins_get "/job/$1/api/json?tree=lastBuild[number]" |
|
||
|
|
python3 -c 'import json,sys; print(json.load(sys.stdin)["lastBuild"]["number"])'
|
||
|
|
}
|
||
|
|
|
||
|
|
wait_for_build() { # job number [tries] -> prints result
|
||
|
|
local job="$1" num="$2" tries="${3:-120}"
|
||
|
|
for _ in $(seq 1 "$tries"); do
|
||
|
|
sleep 10
|
||
|
|
local body result building
|
||
|
|
body="$(jenkins_get "/job/$job/$num/api/json?tree=result,building" || true)"
|
||
|
|
building="$(printf '%s' "$body" |
|
||
|
|
python3 -c 'import json,sys; print(json.load(sys.stdin).get("building"))' 2>/dev/null || echo unknown)"
|
||
|
|
if [ "$building" = "False" ]; then
|
||
|
|
result="$(printf '%s' "$body" | python3 -c 'import json,sys; print(json.load(sys.stdin).get("result"))')"
|
||
|
|
printf '%s' "$result"
|
||
|
|
return 0
|
||
|
|
fi
|
||
|
|
done
|
||
|
|
printf 'TIMEOUT'
|
||
|
|
}
|
||
|
|
|
||
|
|
ariadne_ticks() { # tail the autotriage decisions in human-readable form
|
||
|
|
kubectl -n maintenance logs deploy/ariadne -c ariadne --tail="${1:-400}" 2>/dev/null |
|
||
|
|
grep 'hermes autotriage tick' |
|
||
|
|
python3 -c '
|
||
|
|
import sys, json
|
||
|
|
for line in sys.stdin:
|
||
|
|
try:
|
||
|
|
d = json.loads(line)
|
||
|
|
except ValueError:
|
||
|
|
continue
|
||
|
|
print(" ", d["timestamp"][11:19], d.get("jobs"))' | tail -"${2:-5}"
|
||
|
|
}
|
||
|
|
|
||
|
|
# Both drivers narrate the same Test Automation Diagram; the monitor selects
|
||
|
|
# which branch of it to follow from MONITOR_JOB.
|
||
|
|
run_monitor() { # job
|
||
|
|
export MONITOR_JOB="$1"
|
||
|
|
exec python3 "$_DEMO_DIR/hermes_triage_monitor.py"
|
||
|
|
}
|
||
|
|
|
||
|
|
# The checks that are true of the lab regardless of which demo is running.
|
||
|
|
shared_preflight() {
|
||
|
|
note "ariadne image: $(kubectl -n maintenance get deploy ariadne -o jsonpath='{.spec.template.spec.containers[0].image}')"
|
||
|
|
note "autoremediation: $(kubectl -n maintenance exec deploy/ariadne -c ariadne -- printenv ARIADNE_HERMES_AUTOREMEDIATION_ENABLED 2>/dev/null)"
|
||
|
|
# Printed as a list rather than the raw comma-separated setting: this is the
|
||
|
|
# outermost safety boundary, so it is worth being able to read at a glance.
|
||
|
|
local allowlist count
|
||
|
|
allowlist="$(kubectl -n maintenance exec deploy/ariadne -c ariadne -- printenv ARIADNE_HERMES_AUTOTRIAGE_JOB_ALLOWLIST 2>/dev/null | tr ',' ' ')"
|
||
|
|
count=0
|
||
|
|
for _job in $allowlist; do count=$((count + 1)); done
|
||
|
|
note "jobs Ariadne may triage ($count):"
|
||
|
|
for _job in $allowlist; do note " - $_job"; done
|
||
|
|
note "hermes: $(kubectl -n hermes get pods -l app=hermes --no-headers | awk '{print $2, $3}')"
|
||
|
|
local queued
|
||
|
|
queued="$(jenkins_get '/queue/api/json' | python3 -c 'import json,sys; print(len(json.load(sys.stdin)["items"]))')"
|
||
|
|
note "jenkins queue depth: $queued (demo is fastest when this is 0)"
|
||
|
|
# The Kubernetes cloud caps concurrent agent pods at containerCapStr. When
|
||
|
|
# real CI saturates that cap the demo build sits in the queue reporting
|
||
|
|
# "all nodes are offline" and the timings in the runbook do not apply.
|
||
|
|
local agents cap
|
||
|
|
cap="$(kubectl -n jenkins get cm jenkins-jcasc -o jsonpath='{.data.jenkins\.yaml}' 2>/dev/null |
|
||
|
|
grep -o 'containerCapStr: "[0-9]*"' | head -1 | grep -o '[0-9]*' || echo 5)"
|
||
|
|
agents="$(kubectl -n jenkins get pods --no-headers 2>/dev/null | grep -cE '\-[a-z0-9]{5}-[a-z0-9]{5}-[a-z0-9]{5}' || true)"
|
||
|
|
note "jenkins agent pods: ${agents:-0}/${cap:-5} (a full pool stalls the demo — wait for a free slot)"
|
||
|
|
}
|
||
|
|
|
||
|
|
# The alert and incident state both demos are judged by.
|
||
|
|
shared_status() {
|
||
|
|
say "Incident state (last ticks)"
|
||
|
|
ariadne_ticks 600 8
|
||
|
|
say "Firing alerts"
|
||
|
|
kubectl -n monitoring exec deploy/vmalert-atlas-availability -- wget -qO- localhost:8880/api/v1/alerts 2>/dev/null |
|
||
|
|
python3 -c '
|
||
|
|
import json,sys
|
||
|
|
alerts = json.load(sys.stdin).get("data", {}).get("alerts", [])
|
||
|
|
print(" none" if not alerts else "")
|
||
|
|
for a in alerts:
|
||
|
|
print(" ", a["name"], a["state"], "build", a.get("labels", {}).get("build"))' 2>/dev/null ||
|
||
|
|
note "(query vmalert directly if this fails)"
|
||
|
|
}
|