{{- /*
OIDC client-secret bootstrap Job (qa-loop iter-1 monitor Fix #125,
prov #11 secondary).

ROOT CAUSE
----------
The chart's guacamole-deployment.yaml mounts an OIDC client-secret via
`secretKeyRef.name = .Values.guacamole.oidc.clientSecretRef.name`
(default: `guacamole-oidc`) with `optional: false`. Pre-this-fix the
ONLY chart artefact carrying this name is `keycloak-client-secret.yaml`,
which renders a SealedSecret with an EMPTY `encryptedData.client-secret`
placeholder. The two failure modes seen in prov #11 (TC-230 / TC-370 /
TC-402):

  1. Sovereigns without bp-sealed-secrets pre-provisioned: the
     SealedSecret CRD is absent → controller never materialises a
     downstream `Secret guacamole-oidc` → the webapp pod sits in
     `CreateContainerConfigError`:

        Error: secret "guacamole-oidc" not found

  2. Sovereigns WITH bp-sealed-secrets but operator has not yet sealed
     a real value: the SealedSecret renders as `encryptedData.client-
     secret: ""`, the controller refuses to decrypt the empty bundle
     → no downstream Secret → same `CreateContainerConfigError`.

Either way the webapp Deployment is permanently wedged because the
chart NEVER provisions a usable `guacamole-oidc` Secret on its own,
and Fix #78's lesson (Gap E for k8s-ws-proxy-hmac) is the same here
applied to a different Secret name.

FIX PATTERN (mirrors Fix #78 + Fix #95 hook-weight ordering)
-----------------------------------------------------------
Helm pre-install/pre-upgrade Job that:

  1. Idempotency probe — GET the target Secret. 200 = exists (operator
     pre-provisioned via SealedSecret OR a prior install ran this Job)
     → exit 0. 404 = proceed to create. Any other code = FATAL.
  2. Generate 32 random bytes via `head -c 32 /dev/urandom`, pipe
     DIRECTLY to `base64 | tr -d '\n'`, and use only as the
     in-process value for the Secret POST body. Per global CLAUDE.md
     §10 the bytes are NEVER echoed/cat'd/printed; the env vars
     holding them are unset immediately after the POST.
  3. POST to /api/v1/namespaces/<ns>/secrets. 201/200 = success.
     409 = race-with-operator pre-provision → accept (preserves
     existing key, never rotates). Any other = FATAL.

Rationale for runtime-generated client secret: every Sovereign is
isolated; the OIDC client Keycloak holds for `guacamole` is unique to
that Sovereign. The bp-keycloak realm-config Job uses the SAME
`guacamole-oidc` Secret (when present) to register the client value —
so generating once before either side comes up keeps both ends in
sync.

KEY ROTATION
------------
Operator-driven: `kubectl delete secret guacamole-oidc -n catalyst-
system && flux reconcile helmrelease bp-guacamole -n flux-system`. The
Job re-runs and regenerates a new value. Coupled rotation in Keycloak
(via `keycloak-config-cli` re-run) is the operator's responsibility.

HOOK ORDERING (Fix #95 lesson)
------------------------------
Within the same hook phase, LOWER weights run FIRST. The Job
references its ServiceAccount + Role + RoleBinding, so those MUST be
applied BEFORE the Job. Target ordering:

  ServiceAccount:        helm.sh/hook-weight: "-20" (first)
  Role:                  helm.sh/hook-weight: "-15"
  RoleBinding:           helm.sh/hook-weight: "-15"
  Job:                   helm.sh/hook-weight: "-10" (last)

Same-phase same-weight ordering is undefined (Helm sorts by weight
then file path then name). Always: SA most-negative, RBAC
intermediate, Job last.

RBAC SPLIT (feedback_rbac_create_no_resourcenames.md)
-----------------------------------------------------
`create` MUST be in its own rule WITHOUT resourceNames. The combined-
rule pattern (verbs:[create,get,patch] resourceNames:[guacamole-oidc])
is rejected silently with HTTP 403 by the apiserver authz layer; this
was the silent root cause of the bp-openbao 6+ chart-iteration debug
loop.

CANONICAL SEAM
--------------
Modelled directly after platform/k8s-ws-proxy/chart/templates/
hmac-bootstrap-job.yaml (Fix #78 + Fix #95). Same image
(curlimages/curl:8.10.1), same RBAC split, same ordering, same
hook-delete policy, same idempotency probe.
*/}}
{{- if .Values.guacamole.enabled -}}
{{- $secretName := .Values.guacamole.oidc.clientSecretRef.name }}
{{- $secretKey := .Values.guacamole.oidc.clientSecretRef.key }}
{{- $ns := .Release.Namespace }}
---
apiVersion: v1
kind: ServiceAccount
metadata:
  name: bp-guacamole-oidc-bootstrap
  namespace: {{ $ns }}
  labels:
    {{- include "bp-guacamole.labels" . | nindent 4 }}
    catalyst.openova.io/component: oidc-bootstrap
  annotations:
    "helm.sh/hook": "pre-install,pre-upgrade"
    "helm.sh/hook-weight": "-20"
    "helm.sh/hook-delete-policy": "before-hook-creation"
{{- with .Values.guacamole.imagePullSecrets }}
imagePullSecrets:
  {{- toYaml . | nindent 2 }}
{{- end }}
---
# Per memory/feedback_rbac_create_no_resourcenames.md:
# `create` is in a SEPARATE rule WITH NO resourceNames. The apiserver
# rejects POST /secrets with 403 if `create` carries resourceNames.
# Splitting the verbs makes the create path correct AND keeps the
# read path scoped to ONLY this Secret name.
apiVersion: rbac.authorization.k8s.io/v1
kind: Role
metadata:
  name: bp-guacamole-oidc-bootstrap
  namespace: {{ $ns }}
  labels:
    {{- include "bp-guacamole.labels" . | nindent 4 }}
    catalyst.openova.io/component: oidc-bootstrap
  annotations:
    "helm.sh/hook": "pre-install,pre-upgrade"
    "helm.sh/hook-weight": "-15"
    "helm.sh/hook-delete-policy": "before-hook-creation"
rules:
  # Rule 1: create — NO resourceNames (apiserver authz rejects otherwise).
  - apiGroups: [""]
    resources: ["secrets"]
    verbs: ["create"]
  # Rule 2: read — scoped to the single Secret this Job manages.
  - apiGroups: [""]
    resources: ["secrets"]
    resourceNames: [{{ $secretName | quote }}]
    verbs: ["get"]
---
apiVersion: rbac.authorization.k8s.io/v1
kind: RoleBinding
metadata:
  name: bp-guacamole-oidc-bootstrap
  namespace: {{ $ns }}
  labels:
    {{- include "bp-guacamole.labels" . | nindent 4 }}
    catalyst.openova.io/component: oidc-bootstrap
  annotations:
    "helm.sh/hook": "pre-install,pre-upgrade"
    "helm.sh/hook-weight": "-15"
    "helm.sh/hook-delete-policy": "before-hook-creation"
roleRef:
  apiGroup: rbac.authorization.k8s.io
  kind: Role
  name: bp-guacamole-oidc-bootstrap
subjects:
  - kind: ServiceAccount
    name: bp-guacamole-oidc-bootstrap
    namespace: {{ $ns }}
---
apiVersion: batch/v1
kind: Job
metadata:
  name: bp-guacamole-oidc-bootstrap
  namespace: {{ $ns }}
  labels:
    {{- include "bp-guacamole.labels" . | nindent 4 }}
    catalyst.openova.io/component: oidc-bootstrap
  annotations:
    "helm.sh/hook": "pre-install,pre-upgrade"
    "helm.sh/hook-weight": "-10"
    "helm.sh/hook-delete-policy": "before-hook-creation,hook-succeeded"
spec:
  backoffLimit: 6
  ttlSecondsAfterFinished: 300
  template:
    metadata:
      labels:
        {{- include "bp-guacamole.labels" . | nindent 8 }}
        catalyst.openova.io/component: oidc-bootstrap
    spec:
      serviceAccountName: bp-guacamole-oidc-bootstrap
      restartPolicy: Never
      {{- with .Values.guacamole.imagePullSecrets }}
      imagePullSecrets:
        {{- toYaml . | nindent 8 }}
      {{- end }}
      securityContext:
        runAsNonRoot: true
        runAsUser: 65534
        runAsGroup: 65534
        seccompProfile:
          type: RuntimeDefault
      containers:
        - name: bootstrap
          # curlimages/curl is the canonical Catalyst seam for in-chart
          # k8s-API operations from a Job — alpine-based, has /bin/sh,
          # has /dev/urandom, has base64. Identical to the image used
          # by Fix #78 (k8s-ws-proxy hmac-bootstrap).
          # Fix #163 (2026-05-11, MIRROR-EVERYTHING): explicit
          # harbor.openova.io/proxy-dockerhub prefix per CLAUDE.md
          # inviolable rule.
          image: harbor.openova.io/proxy-dockerhub/curlimages/curl:8.10.1
          imagePullPolicy: IfNotPresent
          env:
            - name: SECRET_NAME
              value: {{ $secretName | quote }}
            - name: SECRET_KEY
              value: {{ $secretKey | quote }}
            - name: NAMESPACE
              value: {{ $ns | quote }}
          command:
            - /bin/sh
            - -c
            - |
              set -eu
              APISERVER="https://kubernetes.default.svc"
              TOKEN=$(cat /var/run/secrets/kubernetes.io/serviceaccount/token)
              CA=/var/run/secrets/kubernetes.io/serviceaccount/ca.crt

              echo "[oidc-bootstrap] target: ${NAMESPACE}/${SECRET_NAME} (key=${SECRET_KEY})"

              # ─── Step 1: idempotency probe ─────────────────────────────
              # GET /secrets/<name>. 200 = exists → operator pre-provisioned
              # OR a prior install ran this Job → skip generate. 404 = not
              # found → proceed to create. Any other code is FATAL.
              code=$(curl -sS -o /tmp/probe.json -w '%{http_code}' \
                --cacert "${CA}" \
                -H "Authorization: Bearer ${TOKEN}" \
                "${APISERVER}/api/v1/namespaces/${NAMESPACE}/secrets/${SECRET_NAME}")
              if [ "${code}" = "200" ]; then
                echo "[oidc-bootstrap] Secret already exists — idempotent skip (preserves OIDC client-secret across upgrades)"
                exit 0
              fi
              if [ "${code}" != "404" ]; then
                echo "[oidc-bootstrap] FATAL: idempotency probe returned HTTP ${code}" >&2
                cat /tmp/probe.json >&2 || true
                exit 1
              fi

              # ─── Step 2: generate 32 random bytes → base64 ──────────────
              # CRITICAL (global CLAUDE.md §10): the OIDC client-secret
              # bytes are NEVER echoed/cat/printed. /dev/urandom → base64
              # → variable consumed only by the JSON-builder printf. The
              # Secret data field is itself base64 (k8s wire format) — so
              # the value POSTed is base64(<random_bytes_b64>). Guacamole
              # consumes the inner base64 string as the OIDC client-secret
              # via the secretKeyRef in guacamole-deployment.yaml.
              CLIENT_SECRET_B64=$(head -c 32 /dev/urandom | base64 | tr -d '\n')
              if [ -z "${CLIENT_SECRET_B64}" ]; then
                echo "[oidc-bootstrap] FATAL: empty random output (no entropy?)" >&2
                exit 1
              fi
              # The Secret data value must itself be base64 of the file
              # content. Inner content is the printable token Guacamole
              # uses; wire-format value is base64(base64(raw_bytes)).
              WIRE_B64=$(printf '%s' "${CLIENT_SECRET_B64}" | base64 | tr -d '\n')

              # ─── Step 3: POST Secret ────────────────────────────────────
              BODY=$(printf '{"apiVersion":"v1","kind":"Secret","metadata":{"name":"%s","namespace":"%s","labels":{"catalyst.openova.io/blueprint":"bp-guacamole","catalyst.openova.io/component":"oidc-bootstrap","app.kubernetes.io/managed-by":"Helm"}},"type":"Opaque","data":{"%s":"%s"}}' \
                "${SECRET_NAME}" "${NAMESPACE}" "${SECRET_KEY}" "${WIRE_B64}")
              code=$(curl -sS -o /tmp/create.json -w '%{http_code}' \
                --cacert "${CA}" \
                -H "Authorization: Bearer ${TOKEN}" \
                -H "Content-Type: application/json" \
                -X POST --data "${BODY}" \
                "${APISERVER}/api/v1/namespaces/${NAMESPACE}/secrets")
              # Clear the secret bytes from env immediately after POST.
              CLIENT_SECRET_B64="" ; WIRE_B64="" ; BODY=""
              if [ "${code}" = "201" ] || [ "${code}" = "200" ]; then
                echo "[oidc-bootstrap] Secret ${NAMESPACE}/${SECRET_NAME} created"
                exit 0
              fi
              # 409 — race with operator pre-provision (or another concurrent
              # bootstrap on a multi-replica HelmRelease retry). Treat as
              # success: the existing Secret is preserved.
              if [ "${code}" = "409" ]; then
                echo "[oidc-bootstrap] Secret already created concurrently (HTTP 409) — accepting (preserves existing key)"
                exit 0
              fi
              echo "[oidc-bootstrap] FATAL: POST returned HTTP ${code}" >&2
              cat /tmp/create.json >&2 || true
              exit 1
          resources:
            requests:
              cpu: 10m
              memory: 32Mi
            limits:
              cpu: 100m
              memory: 64Mi
          securityContext:
            allowPrivilegeEscalation: false
            readOnlyRootFilesystem: true
            capabilities:
              drop: ["ALL"]
          volumeMounts:
            - name: tmp
              mountPath: /tmp
      volumes:
        - name: tmp
          emptyDir:
            medium: Memory
            sizeLimit: 8Mi
{{- end }}
