Some checks failed
Tests / Declarative: Post Actions failed: 2, passed: 142
One script with a 'fixture' and a 'code' subcommand meant the wrong subcommand was always one word away, in front of an audience, with different blast radii behind each: the triage demo touches only a ConfigMap, the code demo pushes to a repository and deletes issues. Those should not share a command line. Each demo is now its own driver with the same five verbs - monitor, reset, preflight, run, status - so knowing one teaches the other. What they genuinely share (credentials, Jenkins access, the tick reader, the lab-wide preflight checks) moved to hermes_demo_lib.sh rather than being duplicated, because the reason to split was clarity at the command line, not two copies of the same helper drifting apart. Each reset now covers only its own demo. The triage reset no longer reaches into a Gitea repository it never writes to, and the code reset owns the repository cleanup entirely. The credentials file is now hermes_demo.env since both read it; the old hermes_triage_demo.env is still sourced as a fallback so a filled-in file keeps working, and both names stay git-ignored. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
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)"
|
|
}
|