titan-iac/scripts/ops/hermes_demo_lib.sh
jenkins 35ae4d4bab
Some checks failed
Tests / Declarative: Post Actions failed: 2, passed: 142
refactor(demo): split the two demos into two scripts
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>
2026-08-06 21:59:23 -03:00

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)"
}