{{- /*
Catalyst auto-unseal init Job — bp-openbao (issue #316).

Helm post-install/post-upgrade hook (weight 5, runs AFTER the
auto-unseal-rbac.yaml SA at weight 0 and BEFORE the auth-bootstrap Job
at weight 10). On a fresh Sovereign:

  1. Wait for openbao-0 Pod to be Running (the upstream chart spins up
     the StatefulSet immediately on Helm install — sealed but reachable
     on port 8200 for status checks).
  2. `bao status` — exit 0 immediately if Initialized=true (idempotent
     re-run on helm upgrade, or on a redeploy where the seed Secret is
     gone but the cluster is already initialized via the preserved
     PV).
  3. Read the recovery seed from the K8s Secret created by cloud-init
     (see infra/hetzner/cloudinit-control-plane.tftpl). The seed is a
     32-byte random value used as the recovery key for OpenBao's
     auto-unseal config.
  4. `bao operator init -recovery-shares=1 -recovery-threshold=1
     -recovery-pgp-keys=<seed>`. This initializes OpenBao with auto-
     unseal active, so subsequent pod restarts unseal automatically
     without operator intervention.
  5. On success, write a bootstrap-marker Secret (`openbao-init-marker`)
     in the openbao namespace recording the init timestamp + checksum.
     The auth-bootstrap Job (second hook) keys off this marker.
  6. Delete the seed Secret. The recovery seed has now done its single-
     use job; persisting it in K8s would defeat the auto-unseal model.

Failure modes per issue #316:

  - Seed Secret missing → fail loud after retry budget exhausted; the
    init Job pod logs print the manual-recovery runbook URL.
  - Init Job crashes mid-init → on retry, `bao status` reports
    Initialized=true (init is durable in the Raft log), the script
    detects this and skips the init step — moves straight to the
    marker write + seed cleanup.

Skip-render pattern (per #402 lesson — never `{{ fail }}`): rendered
ONLY when `.Values.autoUnseal.enabled=true`. Default-values render path
emits nothing.
*/}}
{{- $au := .Values.autoUnseal | default dict -}}
{{- if $au.enabled -}}
{{- $img := $au.image | default dict -}}
{{- $repo := $img.repository | default "quay.io/openbao/openbao" -}}
{{- $registry := .Values.global.imageRegistry | default "" -}}
{{- if $registry }}{{- $repo = printf "%s/%s" $registry $repo -}}{{- end -}}
{{- $tag := $img.tag | default "2.1.0" -}}
{{- $pullPolicy := $img.pullPolicy | default "IfNotPresent" -}}
{{- $seedSecret := $au.seedSecret | default dict -}}
{{- $seedName := $seedSecret.name | default "openbao-recovery-seed" -}}
{{- $seedKey := $seedSecret.key | default "recovery-seed" -}}
{{- $baoAddr := $au.baoAddress | default (printf "http://%s-openbao:8200" .Release.Name) -}}
{{- $deadline := $au.activeDeadlineSeconds | default 600 -}}
{{- $backoff := $au.backoffLimit | default 6 -}}
---
apiVersion: batch/v1
kind: Job
metadata:
  name: openbao-init
  namespace: {{ .Release.Namespace | quote }}
  annotations:
    "helm.sh/hook": post-install,post-upgrade
    "helm.sh/hook-weight": "5"
    "helm.sh/hook-delete-policy": before-hook-creation,hook-succeeded
  labels:
    catalyst.openova.io/blueprint: bp-openbao
    catalyst.openova.io/component: openbao-init
spec:
  activeDeadlineSeconds: {{ $deadline }}
  backoffLimit: {{ $backoff }}
  ttlSecondsAfterFinished: 600
  template:
    metadata:
      labels:
        catalyst.openova.io/blueprint: bp-openbao
        catalyst.openova.io/component: openbao-init
    spec:
      serviceAccountName: openbao-auto-unseal
      restartPolicy: OnFailure
      securityContext:
        runAsNonRoot: true
        runAsUser: 100
        runAsGroup: 1000
        fsGroup: 1000
      containers:
        - name: init
          image: {{ printf "%s:%s" $repo $tag | quote }}
          imagePullPolicy: {{ $pullPolicy | quote }}
          env:
            - name: BAO_ADDR
              value: {{ $baoAddr | quote }}
            - name: SEED_SECRET_NAME
              value: {{ $seedName | quote }}
            - name: SEED_SECRET_KEY
              value: {{ $seedKey | quote }}
            - name: NAMESPACE
              valueFrom:
                fieldRef:
                  fieldPath: metadata.namespace
          command: ["/bin/sh", "-c"]
          args:
            - |
              set -eu
              # ─── Step 1: wait for openbao-0 to answer status checks ────
              # Upstream chart name pattern: <release>-openbao-0. Skip if
              # already responsive.
              echo "[openbao-init] target BAO_ADDR=$BAO_ADDR"
              ATTEMPTS=0
              MAX_ATTEMPTS=60   # 60 * 5s = 5 minutes
              until bao status >/dev/null 2>&1 || [ "$(bao status -format=json 2>/dev/null | grep -c initialized || true)" -gt 0 ]; do
                ATTEMPTS=$((ATTEMPTS+1))
                if [ "$ATTEMPTS" -ge "$MAX_ATTEMPTS" ]; then
                  echo "[openbao-init] FATAL: openbao endpoint $BAO_ADDR not reachable after $MAX_ATTEMPTS attempts"
                  echo "[openbao-init] manual recovery: docs/RUNBOOK-PROVISIONING.md §openbao-auto-unseal"
                  exit 1
                fi
                echo "[openbao-init] openbao not ready yet (attempt $ATTEMPTS/$MAX_ATTEMPTS), sleeping 5s..."
                sleep 5
              done

              # ─── Step 2: idempotency check — skip init if already initialised ─
              # `bao status` exit code semantics:
              #   0 — initialized AND unsealed
              #   1 — error (not reachable)
              #   2 — initialized but sealed
              # We treat 0 OR 2 as "already initialized".
              STATUS_RC=0
              bao status >/dev/null 2>&1 || STATUS_RC=$?
              if [ "$STATUS_RC" = "0" ] || [ "$STATUS_RC" = "2" ]; then
                INIT_FLAG=$(bao status -format=json 2>/dev/null | grep -E '"initialized"' | head -1 | tr -d ' ,' | cut -d: -f2 || echo "false")
                if [ "$INIT_FLAG" = "true" ]; then
                  echo "[openbao-init] OpenBao already initialized — recording marker + cleaning seed (idempotent path)"
                  # Continue to step 4+5 only — skip step 3.
                  SKIP_INIT=1
                fi
              fi
              # k8s API context used in multiple steps below (idempotent
              # unseal in Step 2a, marker write in Step 4, seed delete in
              # Step 5). Define once.
              TOKEN=$(cat /var/run/secrets/kubernetes.io/serviceaccount/token)
              CACERT=/var/run/secrets/kubernetes.io/serviceaccount/ca.crt
              APISERVER="https://kubernetes.default.svc"
              UNSEAL_SECRET_NAME="openbao-unseal-keys"

              # ─── Step 2a: idempotent-path unseal (issue #539) ──────────
              # PR #528 only unsealed on the FRESH init path. If a previous
              # Job pod completed `bao operator init` but crashed before
              # the unseal block (or the unseal block failed), the next
              # Job run takes the idempotent branch and would silently
              # leave the vault sealed — the symptom #539 captured. Fix:
              # on every run, if Sealed=true, fetch the persisted
              # unseal-keys Secret (written by Step 3 on the original
              # fresh init) and apply unseal here.
              if [ -n "${SKIP_INIT:-}" ]; then
                SEALED=$(bao status -format=json 2>/dev/null | grep -E '"sealed"' | head -1 | sed -E 's/.*"sealed"[[:space:]]*:[[:space:]]*(true|false).*/\1/')
                if [ "$SEALED" = "true" ]; then
                  echo "[openbao-init] vault is sealed — fetching persisted unseal keys from $UNSEAL_SECRET_NAME"
                  UNSEAL_GET_RC=0
                  UNSEAL_RESPONSE=$(wget -qO- --no-check-certificate \
                    --header="Authorization: Bearer $TOKEN" \
                    "$APISERVER/api/v1/namespaces/$NAMESPACE/secrets/$UNSEAL_SECRET_NAME") || UNSEAL_GET_RC=$?
                  if [ "$UNSEAL_GET_RC" -ne 0 ] || [ -z "$UNSEAL_RESPONSE" ]; then
                    echo "[openbao-init] FATAL: cannot fetch $UNSEAL_SECRET_NAME — vault is sealed but the unseal-keys Secret is missing."
                    echo "[openbao-init] This means a prior init completed but never persisted keys (chart <1.2.4)."
                    echo "[openbao-init] Manual recovery: docs/RUNBOOK-PROVISIONING.md §openbao-auto-unseal — wipe data-openbao-0 PVC and let init run fresh."
                    exit 1
                  fi
                  KEYS_B64_FIELD=$(echo "$UNSEAL_RESPONSE" | tr -d '\n' | sed -E 's/.*"unseal-keys-b64"[[:space:]]*:[[:space:]]*"([^"]*)".*/\1/')
                  THRESHOLD_FIELD=$(echo "$UNSEAL_RESPONSE" | tr -d '\n' | sed -E 's/.*"unseal-threshold"[[:space:]]*:[[:space:]]*"([^"]*)".*/\1/')
                  if [ -z "$KEYS_B64_FIELD" ] || [ "$KEYS_B64_FIELD" = "$UNSEAL_RESPONSE" ]; then
                    echo "[openbao-init] FATAL: $UNSEAL_SECRET_NAME has no unseal-keys-b64 field"
                    exit 1
                  fi
                  # K8s Secret data fields are base64-encoded. Outer base64
                  # is the K8s wrapper; the decoded payload itself is a
                  # newline-separated list of unseal keys (each key is
                  # itself base64 — that's the OpenBao wire format).
                  echo "$KEYS_B64_FIELD" | base64 -d > /tmp/.unseal-keys
                  if [ -z "$THRESHOLD_FIELD" ] || [ "$THRESHOLD_FIELD" = "$UNSEAL_RESPONSE" ]; then
                    UNSEAL_THRESHOLD=1
                  else
                    UNSEAL_THRESHOLD=$(echo "$THRESHOLD_FIELD" | base64 -d)
                  fi
                  if [ -z "$UNSEAL_THRESHOLD" ] || [ "$UNSEAL_THRESHOLD" -lt 1 ] 2>/dev/null; then
                    UNSEAL_THRESHOLD=1
                  fi
                  KEY_COUNT=$(wc -l < /tmp/.unseal-keys | tr -d ' ')
                  if [ "$KEY_COUNT" -lt "$UNSEAL_THRESHOLD" ]; then
                    echo "[openbao-init] FATAL: persisted $KEY_COUNT key(s) but threshold=$UNSEAL_THRESHOLD"
                    exit 1
                  fi
                  echo "[openbao-init] applying $UNSEAL_THRESHOLD unseal key(s) from persisted Secret"
                  I=0
                  while [ "$I" -lt "$UNSEAL_THRESHOLD" ]; do
                    I=$((I+1))
                    KEY=$(sed -n "${I}p" /tmp/.unseal-keys)
                    if [ -z "$KEY" ]; then
                      echo "[openbao-init] FATAL: empty key at slot $I"
                      exit 1
                    fi
                    bao operator unseal "$KEY" >/dev/null
                  done
                  rm -f /tmp/.unseal-keys
                  SEALED_AFTER=$(bao status -format=json 2>/dev/null | grep -E '"sealed"' | head -1 | sed -E 's/.*"sealed"[[:space:]]*:[[:space:]]*(true|false).*/\1/')
                  if [ "$SEALED_AFTER" != "false" ]; then
                    echo "[openbao-init] FATAL: vault still sealed after applying $UNSEAL_THRESHOLD key(s) — sealed=$SEALED_AFTER"
                    bao status || true
                    exit 1
                  fi
                  echo "[openbao-init] resume-unsealed (sealed=false)"
                else
                  echo "[openbao-init] vault already unsealed — nothing to do on idempotent path"
                fi
              fi

              # ─── Step 3: read seed and run `bao operator init` ──────────
              if [ -z "${SKIP_INIT:-}" ]; then
                echo "[openbao-init] reading seed Secret $NAMESPACE/$SEED_SECRET_NAME"
                # TOKEN/CACERT/APISERVER were defined in Step 2 above
                # (used by both Step 2a unseal-resume and Step 3+4+5).
                SEED_B64=$(wget -qO- --no-check-certificate \
                  --header="Authorization: Bearer $TOKEN" \
                  "$APISERVER/api/v1/namespaces/$NAMESPACE/secrets/$SEED_SECRET_NAME" \
                  | grep -E "\"$SEED_SECRET_KEY\"" | head -1 | sed -E 's/.*"'"$SEED_SECRET_KEY"'"[[:space:]]*:[[:space:]]*"([^"]*)".*/\1/' )
                if [ -z "$SEED_B64" ]; then
                  echo "[openbao-init] FATAL: seed Secret $SEED_SECRET_NAME has no key $SEED_SECRET_KEY"
                  echo "[openbao-init] manual recovery: docs/RUNBOOK-PROVISIONING.md §openbao-auto-unseal"
                  exit 1
                fi
                # Recovery key file is single-use; deleted in step 5.
                echo "$SEED_B64" | base64 -d > /tmp/recovery-seed.bin
                # Issue #517 (cont): -recovery-shares/-recovery-threshold are
                # only valid for auto-unseal seal types (gcpckms, awskms,
                # transit, etc.). The upstream openbao chart's default
                # config uses `seal "shamir"` (no auto-unseal stanza in
                # values.standalone.config / values.ha.config), so the
                # right flags are -key-shares / -key-threshold. With
                # auto-unseal seal types added later, swap back to
                # -recovery-* — but per the current chart values this
                # NEVER worked.
                echo "[openbao-init] running bao operator init (key-shares=1, key-threshold=1, shamir seal)"
                bao operator init \
                  -key-shares=1 \
                  -key-threshold=1 \
                  -format=json \
                  > /tmp/init-output.json
                # Store init outputs INSIDE OpenBao itself via the recovery-key.
                # The root token is captured into a temporary file used only
                # within this Job's lifetime; we do NOT persist it as a K8s
                # Secret per acceptance criterion #6.
                ROOT_TOKEN=$(grep -E '"root_token"' /tmp/init-output.json | head -1 | sed -E 's/.*"root_token"[[:space:]]*:[[:space:]]*"([^"]*)".*/\1/')
                if [ -z "$ROOT_TOKEN" ]; then
                  echo "[openbao-init] FATAL: bao operator init produced no root_token — see /tmp/init-output.json"
                  exit 1
                fi
                echo "$ROOT_TOKEN" > /tmp/.root-token

                # ─── Step 3a: unseal the freshly-initialised vault (issue #527) ─
                # `bao operator init` returns the cluster initialised but
                # SEALED. Without an explicit `bao operator unseal <key>`
                # call the StatefulSet pod stays sealed forever, the HR
                # never goes Ready=True, and every dependent blueprint
                # (bp-external-secrets, bp-external-secrets-stores,
                # bp-cnpg consumers, …) blocks on this dep.
                #
                # The init JSON has shape:
                #   {
                #     "unseal_keys_b64": ["<key1>", "<key2>", ...],
                #     "unseal_keys_hex": [...],
                #     "unseal_threshold": 1,
                #     "unseal_shares": 1,
                #     "root_token": "...",
                #     ...
                #   }
                #
                # With key-shares=1 / key-threshold=1 we need exactly one
                # unseal key. The line returned by sed below isolates
                # whatever sits between the FIRST `[` and the FIRST `]`
                # following `"unseal_keys_b64"`, then a second sed strips
                # quotes / commas to leave one key per line. We take the
                # first $UNSEAL_THRESHOLD lines and call `bao operator
                # unseal` once per key. After threshold is met `bao
                # status` reports Sealed=false and the rest of the script
                # can proceed.
                UNSEAL_THRESHOLD=$(grep -E '"unseal_threshold"' /tmp/init-output.json | head -1 | sed -E 's/.*"unseal_threshold"[[:space:]]*:[[:space:]]*([0-9]+).*/\1/')
                if [ -z "$UNSEAL_THRESHOLD" ] || [ "$UNSEAL_THRESHOLD" -lt 1 ] 2>/dev/null; then
                  UNSEAL_THRESHOLD=1
                fi
                echo "[openbao-init] unseal threshold = $UNSEAL_THRESHOLD; extracting keys"
                # Pull the unseal_keys_b64 array, split into one key per line.
                tr -d '\n' < /tmp/init-output.json \
                  | sed -E 's/.*"unseal_keys_b64"[[:space:]]*:[[:space:]]*\[([^]]*)\].*/\1/' \
                  | tr ',' '\n' \
                  | sed -E 's/.*"([^"]+)".*/\1/' \
                  | sed '/^[[:space:]]*$/d' \
                  > /tmp/.unseal-keys
                KEY_COUNT=$(wc -l < /tmp/.unseal-keys | tr -d ' ')
                if [ "$KEY_COUNT" -lt "$UNSEAL_THRESHOLD" ]; then
                  echo "[openbao-init] FATAL: extracted $KEY_COUNT unseal key(s) but threshold=$UNSEAL_THRESHOLD — see /tmp/init-output.json"
                  exit 1
                fi

                # ─── Persist unseal keys for idempotent recovery (issue #539) ─
                # Store the unseal keys as a K8s Secret so future Job runs
                # can take the idempotent path (Step 2a) and resume an
                # already-initialised-but-still-sealed vault. Without this
                # persistence, any pod restart of openbao-0 leaves it
                # sealed forever (shamir seal type re-seals on restart),
                # because the keys produced by `bao operator init` only
                # ever existed in /tmp/.unseal-keys of the original Job
                # pod. Writing this secret BEFORE unseal means even if
                # the unseal block crashes the next retry can recover.
                echo "[openbao-init] persisting unseal keys to Secret $UNSEAL_SECRET_NAME"
                KEYS_PAYLOAD_B64=$(base64 < /tmp/.unseal-keys | tr -d '\n')
                THRESHOLD_PAYLOAD_B64=$(printf '%s' "$UNSEAL_THRESHOLD" | base64 | tr -d '\n')
                UNSEAL_SECRET_BODY=$(printf '{"apiVersion":"v1","kind":"Secret","metadata":{"name":"%s","namespace":"%s","labels":{"catalyst.openova.io/blueprint":"bp-openbao","catalyst.openova.io/component":"openbao-unseal-keys"}},"type":"Opaque","data":{"unseal-keys-b64":"%s","unseal-threshold":"%s"}}' \
                  "$UNSEAL_SECRET_NAME" "$NAMESPACE" "$KEYS_PAYLOAD_B64" "$THRESHOLD_PAYLOAD_B64")
                US_CREATE_RC=0
                wget -qO- --no-check-certificate \
                  --header="Authorization: Bearer $TOKEN" \
                  --header="Content-Type: application/json" \
                  --post-data="$UNSEAL_SECRET_BODY" \
                  "$APISERVER/api/v1/namespaces/$NAMESPACE/secrets" >/dev/null 2>&1 || US_CREATE_RC=$?
                if [ "$US_CREATE_RC" -ne 0 ]; then
                  echo "[openbao-init] $UNSEAL_SECRET_NAME exists, replacing via PUT"
                  wget -qO- --no-check-certificate \
                    --header="Authorization: Bearer $TOKEN" \
                    --header="Content-Type: application/json" \
                    --method=PUT \
                    --body-data="$UNSEAL_SECRET_BODY" \
                    "$APISERVER/api/v1/namespaces/$NAMESPACE/secrets/$UNSEAL_SECRET_NAME" >/dev/null 2>&1 || true
                fi

                I=0
                while [ "$I" -lt "$UNSEAL_THRESHOLD" ]; do
                  I=$((I+1))
                  KEY=$(sed -n "${I}p" /tmp/.unseal-keys)
                  if [ -z "$KEY" ]; then
                    echo "[openbao-init] FATAL: empty key at slot $I"
                    exit 1
                  fi
                  echo "[openbao-init] applying unseal key $I/$UNSEAL_THRESHOLD"
                  bao operator unseal "$KEY" >/dev/null
                done
                # Verify the seal is actually broken — exit non-zero if
                # status still reports sealed=true (this is the assertion
                # that closes #527).
                SEALED=$(bao status -format=json 2>/dev/null | grep -E '"sealed"' | head -1 | sed -E 's/.*"sealed"[[:space:]]*:[[:space:]]*(true|false).*/\1/')
                if [ "$SEALED" != "false" ]; then
                  echo "[openbao-init] FATAL: vault still sealed after applying $UNSEAL_THRESHOLD key(s) — sealed=$SEALED"
                  bao status || true
                  exit 1
                fi
                echo "[openbao-init] vault unsealed (sealed=false)"
                rm -f /tmp/.unseal-keys /tmp/recovery-seed.bin
              fi

              # ─── Step 4: write bootstrap-marker Secret ──────────────────
              echo "[openbao-init] writing bootstrap-marker openbao-init-marker"
              # TOKEN/CACERT/APISERVER were defined in Step 2 above.
              MARKER_PAYLOAD=$(printf '{"apiVersion":"v1","kind":"Secret","metadata":{"name":"openbao-init-marker","namespace":"%s","labels":{"catalyst.openova.io/blueprint":"bp-openbao","catalyst.openova.io/component":"openbao-init-marker"}},"type":"Opaque","data":{"initialised-at":"%s"}}' \
                "$NAMESPACE" "$(date -u +%Y-%m-%dT%H:%M:%SZ | base64 | tr -d '\n')")
              # Try create; if it exists, patch.
              CREATE_RC=0
              echo "$MARKER_PAYLOAD" | wget -qO- --no-check-certificate \
                --header="Authorization: Bearer $TOKEN" \
                --header="Content-Type: application/json" \
                --post-data="$MARKER_PAYLOAD" \
                "$APISERVER/api/v1/namespaces/$NAMESPACE/secrets" >/dev/null 2>&1 || CREATE_RC=$?
              if [ "$CREATE_RC" -ne 0 ]; then
                echo "[openbao-init] marker exists, leaving as-is"
              fi

              # ─── Step 5: delete seed Secret ─────────────────────────────
              if [ -z "${SKIP_INIT:-}" ]; then
                echo "[openbao-init] deleting seed Secret (single-use consumed)"
                wget -qO- --no-check-certificate \
                  --header="Authorization: Bearer $TOKEN" \
                  --method=DELETE \
                  "$APISERVER/api/v1/namespaces/$NAMESPACE/secrets/$SEED_SECRET_NAME" >/dev/null 2>&1 || true
              fi

              echo "[openbao-init] auto-unseal init complete"
          resources:
            requests:
              cpu: 50m
              memory: 64Mi
            limits:
              memory: 256Mi
          securityContext:
            allowPrivilegeEscalation: false
            readOnlyRootFilesystem: false
            capabilities:
              drop: ["ALL"]
{{- end }}
