#!/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 < "$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."