KUBECTL_ROUTE (agent default, founder-sudo = ssh railiance01 sudo -n) for the bounded proof scripts; one attended script with go/no-go gates, typed ADOPT confirmation, prune/force-off SSA sync and post-checks; evaluator and tests. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Assistant: claude-code Assistant-Model: opus Assistant-Process: 527080@bnt-lap001 Assistant-Session: 5a0e864a-67be-4605-a77a-16635f7dd292
193 lines
10 KiB
Bash
Executable file
193 lines
10 KiB
Bash
Executable file
#!/usr/bin/env bash
|
|
# RPF-WP-0044-T06 attended ESO adoption. The founder runs this in their own terminal.
|
|
#
|
|
# scripts/rpf_wp_0044_t06_attended.sh [--dry-run] [--precheck-only]
|
|
#
|
|
# Cluster route: KUBECTL_ROUTE=founder-sudo (default here) runs
|
|
# ssh railiance01 sudo -n kubectl ...
|
|
# sudo -n never prompts, so pipes stay byte-clean (no ssh -t).
|
|
#
|
|
# Safety:
|
|
# - Credential-kind objects are never fetched, compared or printed. The render is split first.
|
|
# - A no-go stops before anything changes.
|
|
# - The Application is created only after every pre-check passes.
|
|
# - The sync needs the typed word ADOPT and runs with prune=false, force=false and ServerSideApply.
|
|
# - No prune and no delete ever happen. On post-check failure the rollback steps are printed, not run.
|
|
set -euo pipefail
|
|
|
|
DRY=0; PRE_ONLY=0
|
|
for arg in "$@"; do
|
|
case "$arg" in
|
|
--dry-run) DRY=1 ;;
|
|
--precheck-only) PRE_ONLY=1 ;;
|
|
*) echo "unknown argument: $arg" >&2; exit 64 ;;
|
|
esac
|
|
done
|
|
|
|
ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
|
|
ISSUE_CORE="${ISSUE_CORE_ROOT:-$HOME/rapp-issue-core}"
|
|
TARGET_REVENUE="${TARGET_REVENUE_ROOT:-$HOME/target-revenue}"
|
|
export KUBECTL_ROUTE="${KUBECTL_ROUTE:-founder-sudo}"
|
|
case "$KUBECTL_ROUTE" in
|
|
founder-sudo) PREFIX=(sudo -n) ;;
|
|
agent) PREFIX=() ;;
|
|
*) echo "unknown KUBECTL_ROUTE" >&2; exit 64 ;;
|
|
esac
|
|
DRAFT="$ROOT/argocd/railiance01/drafts/external-secrets.application.yaml"
|
|
STAMP="$(date -u +%Y%m%dT%H%M%SZ)"
|
|
EVID="$ROOT/docs/evidence/2026-10-21-eso-adoption-prep/attended-$STAMP"
|
|
CHECK=(python3 "$ROOT/scripts/eso_attended_checks.py")
|
|
ISSUE_REV=186c817b67d3919a64acdddbd3889e539841f946
|
|
ISSUE_URL=ssh://git@forgejo.coulomb.social:30022/coulomb/rapp-issue-core.git
|
|
SYNC_JSON='{"operation":{"initiatedBy":{"username":"rpf-wp-0044-t06"},"sync":{"revision":"0.16.1","prune":false,"syncOptions":["ServerSideApply=true"],"syncStrategy":{"apply":{"force":false}}}}}'
|
|
|
|
say() { printf '\n== %s\n' "$*"; }
|
|
nogo() { printf '\nNO-GO: %s\nNothing was changed after this point. Receipts: %s\n' "$*" "$EVID" >&2; exit 2; }
|
|
show() { printf '+'; printf ' %q' "$@"; printf '\n'; }
|
|
# kube ARGS... : kubectl on railiance01 through the selected route (stdin passes through untouched)
|
|
kube() {
|
|
local remote; remote="$(printf '%q ' "${PREFIX[@]}" kubectl --request-timeout=30s "$@")"
|
|
if [ "$DRY" = 1 ]; then show ssh railiance01 "$remote"; return 0; fi
|
|
ssh -o BatchMode=yes -o ConnectTimeout=10 railiance01 "$remote"
|
|
}
|
|
step() { if [ "$DRY" = 1 ]; then show "$@"; else "$@"; fi; }
|
|
# check SUBCOMMAND ARGS... OUTFILE : run evaluator, keep receipt, return its verdict
|
|
check() {
|
|
local out="${*: -1}"; local args=("${@:1:$#-1}")
|
|
if [ "$DRY" = 1 ]; then show "${CHECK[@]}" "${args[@]}"; echo " > $out"; return 0; fi
|
|
"${CHECK[@]}" "${args[@]}" | tee "$out"
|
|
}
|
|
rollback_help() {
|
|
cat >&2 <<EOF
|
|
|
|
ROLLBACK (do not prune, do not delete with cascade):
|
|
1. Leave the Application object in place. It has no automated sync and no finalizer.
|
|
2. Revert the Application in Git to the previously reviewed chart 0.16.1 values, then
|
|
re-apply the child and run the same manual sync (prune=false):
|
|
ssh railiance01 "sudo -n kubectl -n argocd patch application external-secrets --type merge -p '$(printf '%s' "$SYNC_JSON" | sed 's/"/\\"/g')'"
|
|
3. To stop Argo management without touching workloads, delete only the Application object:
|
|
ssh railiance01 "sudo -n kubectl -n argocd delete application external-secrets --cascade=orphan"
|
|
The CRDs carry Prune=false,Delete=false.
|
|
Abort owner: railiance-platform. Receipts: $EVID
|
|
EOF
|
|
}
|
|
|
|
[ "$DRY" = 1 ] || mkdir -p "$EVID"
|
|
WORK="$(mktemp -d)"; chmod 700 "$WORK"; trap 'rm -rf "$WORK"' EXIT
|
|
say "route=$KUBECTL_ROUTE dry_run=$DRY evidence=$EVID"
|
|
step kube get nodes -o name
|
|
|
|
say "1/6 fresh predecessor proofs (300 s window starts now)"
|
|
if [ "$DRY" = 1 ]; then
|
|
show make -s -C "$ISSUE_CORE" verify-live "VERIFY_ARGS=--expected-revision $ISSUE_REV --expected-source-url $ISSUE_URL --smoke"
|
|
else
|
|
make -s -C "$ISSUE_CORE" verify-live VERIFY_ARGS="--expected-revision $ISSUE_REV --expected-source-url $ISSUE_URL --smoke" \
|
|
> "$EVID/issue-core-proof.json" || nogo "issue-core verify-live --smoke failed (see issue-core-proof.json)"
|
|
fi
|
|
step git -C "$TARGET_REVENUE" fetch -q origin
|
|
step python3 "$ROOT/scripts/workload_adoption_proof.py" --live --source-root "$TARGET_REVENUE" \
|
|
--observation "$EVID/target-revenue-observation.json" --receipt "$EVID/target-revenue-proof.json" >/dev/null \
|
|
|| nogo "target-revenue adoption proof failed"
|
|
if [ "$DRY" = 1 ]; then
|
|
show python3 "$ROOT/scripts/eso_adoption_readiness.py" --draft "$DRAFT" --issue-core-proof "$EVID/issue-core-proof.json" --target-revenue-proof "$EVID/target-revenue-proof.json"
|
|
else
|
|
python3 "$ROOT/scripts/eso_adoption_readiness.py" --draft "$DRAFT" --issue-core-proof "$EVID/issue-core-proof.json" \
|
|
--target-revenue-proof "$EVID/target-revenue-proof.json" > "$EVID/readiness.json" || true
|
|
fi
|
|
check gate --readiness "$EVID/readiness.json" "$EVID/gate.json" || nogo "readiness gate"
|
|
|
|
say "2/6 render chart 0.16.1, split out credential-kind objects"
|
|
step python3 -c "import yaml,sys;print(yaml.safe_load(open(sys.argv[1]))['spec']['source']['helm']['values'])" "$DRAFT" > "$WORK/values.yaml"
|
|
step helm template external-secrets external-secrets --repo https://charts.external-secrets.io --version 0.16.1 \
|
|
-n external-secrets -f "$WORK/values.yaml" > "$WORK/render.yaml"
|
|
check split --render "$WORK/render.yaml" --out "$WORK/comparable.yaml" "$EVID/render-split.json" || nogo "render split"
|
|
[ "$DRY" = 1 ] || sha256sum "$WORK/render.yaml" | cut -d' ' -f1 > "$EVID/render.sha256"
|
|
|
|
say "3/6 client-side diff (T05 method) + structured classification"
|
|
if [ "$DRY" = 1 ]; then kube diff -f -; else
|
|
set +e; kube diff -f - < "$WORK/comparable.yaml" > "$EVID/client-diff.txt"; rc=$?; set -e
|
|
echo "kubectl diff rc=$rc" | tee "$EVID/client-diff.rc"
|
|
[ "$rc" -le 1 ] || nogo "kubectl diff error rc=$rc"
|
|
fi
|
|
if [ "$DRY" = 1 ]; then kube get -f - --ignore-not-found -o json; else
|
|
kube get -f - --ignore-not-found -o json < "$WORK/comparable.yaml" > "$WORK/live.json" || nogo "live read"
|
|
fi
|
|
check classify --render "$WORK/render.yaml" --live "$WORK/live.json" "$EVID/classification.json" \
|
|
|| nogo "diff classification (CRD spec change, missing CRD or spec change needing review)"
|
|
|
|
say "4/6 ClusterSecretStore baseline (status conditions only)"
|
|
if [ "$DRY" = 1 ]; then kube get clustersecretstores.external-secrets.io -o json; else
|
|
kube get clustersecretstores.external-secrets.io -o json > "$WORK/stores.json" || nogo "store read"
|
|
fi
|
|
check stores --live "$WORK/stores.json" "$EVID/store-baseline-before.json" || nogo "unexpected not-ready ClusterSecretStore"
|
|
|
|
say "PRE-CHECK: GO"
|
|
if [ "$PRE_ONLY" = 1 ]; then echo "--precheck-only: stopping before any change."; exit 0; fi
|
|
|
|
say "5/6 create the Application (no automated sync, no finalizer) and confirm the sync"
|
|
if [ "$DRY" = 1 ]; then kube -n argocd get application external-secrets --ignore-not-found -o name; EXISTS=""; else
|
|
EXISTS="$(kube -n argocd get application external-secrets --ignore-not-found -o name)"; fi
|
|
if [ -z "$EXISTS" ]; then
|
|
if [ "$DRY" = 1 ]; then kube apply --dry-run=server -f -; kube apply -f -; else
|
|
kube apply --dry-run=server -f - < "$DRAFT" >/dev/null || nogo "Application server dry-run"
|
|
kube apply -f - < "$DRAFT"
|
|
fi
|
|
fi
|
|
if [ "$DRY" = 1 ]; then kube -n argocd get application external-secrets -o json; else
|
|
for _ in $(seq 1 30); do
|
|
kube -n argocd get application external-secrets -o json > "$WORK/app.json"
|
|
python3 -c "import json,sys;s=json.load(open(sys.argv[1])).get('status',{});sys.exit(0 if s.get('resources') else 1)" "$WORK/app.json" && break
|
|
sleep 5
|
|
done
|
|
fi
|
|
if [ "$DRY" = 1 ]; then TOTAL=39; else
|
|
TOTAL="$(python3 -c "import json,sys;print(json.load(open(sys.argv[1]))['total'])" "$EVID/render-split.json")"; fi
|
|
check precheck-app --live "$WORK/app.json" --expected-resources "$TOTAL" "$EVID/argo-precheck.json" || nogo "Argo comparison"
|
|
|
|
printf '\nAbout to sync external-secrets: revision 0.16.1, prune=false, force=false, ServerSideApply.\n'
|
|
if [ "$DRY" = 1 ]; then echo "(dry-run: would ask for ADOPT)"; else
|
|
read -r -p "Type ADOPT to sync, anything else aborts: " answer
|
|
[ "$answer" = "ADOPT" ] || nogo "founder did not confirm. The Application exists but nothing was synced."
|
|
fi
|
|
kube -n argocd patch application external-secrets --type merge -p "$SYNC_JSON" -o name
|
|
[ "$DRY" = 1 ] || date -u +%FT%TZ > "$EVID/sync-requested-at"
|
|
|
|
say "6/6 wait for the operation, then post-checks"
|
|
if [ "$DRY" = 1 ]; then kube -n argocd get application external-secrets -o json; else
|
|
for _ in $(seq 1 120); do
|
|
kube -n argocd get application external-secrets -o json > "$WORK/app.json"
|
|
python3 - "$WORK/app.json" <<'PY' && break
|
|
import json, sys
|
|
a = json.load(open(sys.argv[1])); s = a.get('status', {}); phase = s.get('operationState', {}).get('phase')
|
|
done = phase in ('Failed', 'Error') or (phase == 'Succeeded' and s.get('health', {}).get('status') == 'Healthy')
|
|
sys.exit(0 if done and not a.get('operation') else 1)
|
|
PY
|
|
sleep 5
|
|
done
|
|
fi
|
|
if [ "$DRY" = 1 ]; then kube get crd -o name; kube get clustersecretstores.external-secrets.io -o json; else
|
|
kube get crd -o name > "$WORK/crds.txt"
|
|
kube get clustersecretstores.external-secrets.io -o json > "$WORK/stores-after.json"
|
|
fi
|
|
check stores --live "$WORK/stores-after.json" "$EVID/store-baseline-after.json" || true
|
|
check postcheck --app "$WORK/app.json" --crds "$WORK/crds.txt" --before "$EVID/store-baseline-before.json" \
|
|
--after "$EVID/store-baseline-after.json" "$EVID/postcheck.json" || { rollback_help; exit 4; }
|
|
step kube -n external-secrets get deploy
|
|
|
|
say "ExternalSecret refresh sample (issue-core/issue-core-runtime)"
|
|
if [ "$DRY" = 1 ]; then kube -n issue-core get externalsecret issue-core-runtime -o json; else
|
|
kube -n issue-core get externalsecret issue-core-runtime -o json > "$WORK/es-before.json"
|
|
fi
|
|
kube -n issue-core annotate externalsecret issue-core-runtime "force-sync=$STAMP" --overwrite -o name
|
|
if [ "$DRY" = 1 ]; then kube -n issue-core get externalsecret issue-core-runtime -o json; else
|
|
for _ in $(seq 1 12); do
|
|
sleep 5
|
|
kube -n issue-core get externalsecret issue-core-runtime -o json > "$WORK/es-after.json"
|
|
"${CHECK[@]}" refresh --before "$WORK/es-before.json" --after "$WORK/es-after.json" >/dev/null && break
|
|
done
|
|
fi
|
|
check refresh --before "$WORK/es-before.json" --after "$WORK/es-after.json" "$EVID/refresh-sample.json" \
|
|
|| { rollback_help; exit 4; }
|
|
|
|
say "ADOPTED: external-secrets is Synced/Healthy at 0.16.1. Receipts: $EVID"
|
|
echo "Next (agent): move the draft into argocd/railiance01/applications/, commit receipts, and update T06."
|