{{- /*
Auto-provision catalyst-openova-kc-credentials Secret on Sovereign install
(issue #901, login PIN-issue 503 chain).

Why this template exists
========================
templates/api-deployment.yaml lines 676-739 reference a secretKeyRef on
`catalyst-openova-kc-credentials` for the full PIN-auth env block:
  - kc-addr, kc-realm, kc-sa-client-id, kc-sa-client-secret, kc-audience
  - smtp-host, smtp-port, smtp-from, smtp-user, smtp-pass

On Catalyst-Zero (contabo-mkt) this Secret is hand-rolled by the operator
under clusters/contabo-mkt/apps/.../catalyst-openova-kc-credentials.yaml
and points at the openova-realm running in keycloak-zero (separate ns).

On a freshly franchised Sovereign nothing equivalent existed — every
secretKeyRef has optional=true, so the catalyst-api Pod started, but
POST /api/v1/auth/pin/issue then 503'd with
  "CATALYST_OPENOVA_KC_SA_CLIENT_SECRET not set".
Caught live on otech103, 2026-05-05.

Sovereign-vs-Mothership gate (load-bearing)
===========================================
The canonical KC SA source on a Sovereign is the `catalyst-kc-sa-credentials`
Secret in the `keycloak` namespace (created by bp-keycloak's openbao-bridge
post-install hook — see platform/keycloak/chart/templates/, issue #781).
On contabo-mkt Catalyst-Zero uses `keycloak-zero` (separate Helm release
in its own namespace) — there is NO `catalyst-kc-sa-credentials` Secret in
the `keycloak` namespace there.

We gate render on `lookup "v1" "Secret" "keycloak" "catalyst-kc-sa-credentials"`
returning non-nil. This means:
  - On a Sovereign: lookup returns the secret → template renders → the
    chart auto-provisions catalyst-openova-kc-credentials in
    catalyst-system from the keycloak SA Secret + sovereign.smtp values.
  - On contabo-mkt: lookup returns nil → template renders empty bytes →
    the existing hand-rolled Secret in clusters/contabo-mkt/apps/...
    is untouched (no helm-vs-kustomize ownership flap).

Persistence across reconciles (1.4.19, issue #910 Bug 3 fix)
============================================================
Per the marketplace-api/secret.yaml + sme-secrets.yaml + valkey-cross-ns-
secret.yaml pattern (issues #859/#863/#866/#887), this Secret MUST survive
helm upgrade / Flux reconciliation. It carries the SMTP password (rotated
out-of-band by the operator) and the keycloak client-secret (auto-rotated
by openbao).

The lookup contract differs by FIELD CLASS:

  KC fields (kc-addr, kc-realm, kc-sa-client-id, kc-sa-client-secret,
  kc-audience): EXISTING TARGET WINS over source `keycloak/catalyst-kc-
  sa-credentials`. Rationale: bp-keycloak's openbao-bridge post-install
  hook auto-rotates the client-secret on every Helm upgrade; the
  catalyst-api Pod's secretKeyRef → catalyst-openova-kc-credentials must
  survive that rotation without restarting until the operator
  explicitly forces it (delete the target Secret to re-mirror from
  source).

  SMTP fields (smtp-user, smtp-pass): SOURCE WINS over existing target
  `catalyst-system/sovereign-smtp-credentials`. Rationale: A5's
  provisioner (issue #883) seeds the source Secret AFTER the chart
  install fires. Pre-1.4.19 target-wins meant first-install rendered
  empty bytes that NEVER got refreshed once A5 finished — POST
  /api/v1/auth/pin/issue 502'd with `email-send-failed` for the life of
  the cluster. 1.4.19 makes source the canonical seam: every reconcile
  re-reads sovereign-smtp-credentials, so as soon as A5 lands the next
  Flux 1m tick picks up the bytes. Operator rotation: edit
  sovereign-smtp-credentials (the operator-facing seam) — never the
  derived target. The target is a projection.

  SMTP non-secret fields (smtp-host, smtp-port, smtp-from): EXISTING
  TARGET WINS over `.Values.sovereign.smtp.*`. Rationale: these are
  operator-tunable infrastructure addresses (mail.openova.io vs a
  Sovereign-local Stalwart relay); once an operator overrides via the
  per-Sovereign overlay or a direct kubectl edit, the bytes persist.

  First install (lookup returns nil for both sources): renders empty
  bytes for SMTP creds, falls back to .Values.sovereign.smtp.* for the
  non-secret fields. The catalyst-api Pod starts cleanly because every
  secretKeyRef has optional=true; PIN email send returns a clear
  `email-send-failed` log line until A5's seed lands and the next Flux
  tick refreshes the target.

helm.sh/resource-policy: keep — survives helm uninstall so a re-install
picks up the same bytes via lookup.

Per docs/INVIOLABLE-PRINCIPLES.md #10: NO plaintext credentials in this
template. All values flow through Helm `lookup` of existing K8s Secrets
or through .Values.sovereign.smtp.{host,port,from} which are non-secret
infrastructure addresses (mail.openova.io / 587 / noreply@openova.io
defaults).
*/}}
{{- $kcSrc := lookup "v1" "Secret" "keycloak" "catalyst-kc-sa-credentials" -}}
{{- if and $kcSrc $kcSrc.data -}}
{{- $secretName := "catalyst-openova-kc-credentials" -}}
{{- $namespace := .Release.Namespace -}}
{{- /* ---- Persistent target lookup ---- */ -}}
{{- $existing := lookup "v1" "Secret" $namespace $secretName -}}
{{- /* ---- KC fields: prefer existing target → fall back to source Secret ---- */ -}}
{{- $kcAddr := "" -}}
{{- $kcRealm := "" -}}
{{- $kcClientID := "" -}}
{{- $kcClientSecret := "" -}}
{{- $kcAudience := "" -}}
{{- if and $existing $existing.data (index $existing.data "kc-addr") -}}
  {{- $kcAddr = index $existing.data "kc-addr" | b64dec -}}
{{- else if (index $kcSrc.data "addr") -}}
  {{- $kcAddr = index $kcSrc.data "addr" | b64dec -}}
{{- end -}}
{{- if and $existing $existing.data (index $existing.data "kc-realm") -}}
  {{- $kcRealm = index $existing.data "kc-realm" | b64dec -}}
{{- else if (index $kcSrc.data "realm") -}}
  {{- $kcRealm = index $kcSrc.data "realm" | b64dec -}}
{{- end -}}
{{- if and $existing $existing.data (index $existing.data "kc-sa-client-id") -}}
  {{- $kcClientID = index $existing.data "kc-sa-client-id" | b64dec -}}
{{- else if (index $kcSrc.data "client-id") -}}
  {{- $kcClientID = index $kcSrc.data "client-id" | b64dec -}}
{{- end -}}
{{- if and $existing $existing.data (index $existing.data "kc-sa-client-secret") -}}
  {{- $kcClientSecret = index $existing.data "kc-sa-client-secret" | b64dec -}}
{{- else if (index $kcSrc.data "client-secret") -}}
  {{- $kcClientSecret = index $kcSrc.data "client-secret" | b64dec -}}
{{- end -}}
{{- /* kc-audience: source has no `audience` key — default to client-id (the
       Keycloak client itself is the audience for token-exchange in the
       PIN-auth flow). Existing target wins if set. */ -}}
{{- if and $existing $existing.data (index $existing.data "kc-audience") -}}
  {{- $kcAudience = index $existing.data "kc-audience" | b64dec -}}
{{- else -}}
  {{- $kcAudience = $kcClientID -}}
{{- end -}}
{{- /* ---- SMTP creds: SOURCE-wins lookup against sovereign-smtp-credentials ---- */ -}}
{{- /* Provisioner (issue #883, agent A5) seeds catalyst-system/sovereign-smtp-credentials
       at handover time. Keys (Phase-1 seed): smtp-user, smtp-pass.

       Phase-2 (issue #924, bp-stalwart-sovereign): the per-Sovereign
       Stalwart chart's post-install Job re-materialises the SAME
       Secret with BOTH key shapes — `smtp-user`/`smtp-pass` (Phase-1
       contract) AND `user`/`password` (legacy contract referenced
       below). Either path yields the same bytes; the lookup below
       checks `smtp-user`/`smtp-pass` FIRST (canonical Phase-1+ shape)
       and falls back to `user`/`password` for older sources.

       Why SOURCE wins over the persisted target (issue #910 Bug 3)
       =============================================================
       Pre-1.4.19 the target persistence (`existing` Secret) won over the
       source: once any value (including empty) landed in the persisted
       target, subsequent reconciles re-emitted the persisted bytes and
       NEVER rechecked the source.

       Live failure mode caught on otech105, 2026-05-05:
         t0: chart 1.4.18 install fires.
             - sovereign-smtp-credentials NOT YET seeded (A5's
               seedSovereignSMTP step runs concurrently with the chart
               reconcile, not strictly before it).
             - lookup → nil → smtp-user/smtp-pass render as "".
             - Secret created with empty SMTP creds.
         t+30s: A5 finishes, sovereign-smtp-credentials Secret exists
                with real bytes.
         t+1m: Flux reconciles bp-catalyst-platform.
             - existing target Secret has smtp-user="" (key present, value empty).
             - The OLD existing-wins guard `(index $existing.data "smtp-user")`
               returns the b64-encoded empty string `""` — that string
               IS falsy in Go templates, so the OLD code DID fall through
               to the source. BUT: an operator who hand-set smtp-user
               to a non-empty value would have it overwritten by every
               subsequent reconcile because the source ALSO won then.
               That's a footgun.

         Better contract: SOURCE always wins for SMTP creds. The
         sovereign-smtp-credentials Secret is the operator-rotatable
         seam; the catalyst-openova-kc-credentials Secret is a chart-
         derived projection. If the operator wants to override they
         edit the source (sovereign-smtp-credentials) — never the
         target. This matches docs/INVIOLABLE-PRINCIPLES.md #4 (single
         source of truth — runtime configuration, not lookup-derived
         drift).

         Fallback to existing target ONLY when source is missing —
         preserves bytes across helm-uninstall-then-reinstall when
         the operator deleted the source after first install. After
         A5's seed lands, every subsequent reconcile picks up the
         current source bytes immediately. */ -}}
{{- $smtpSrc := lookup "v1" "Secret" $namespace "sovereign-smtp-credentials" -}}
{{- $smtpUser := "" -}}
{{- $smtpPass := "" -}}
{{- /* SOURCE-wins precedence with key-shape compatibility:
       1. Phase-1+ canonical keys `smtp-user`/`smtp-pass` (A5's seed +
          bp-stalwart-sovereign #924 chart writes).
       2. Legacy keys `user`/`password` (older mothership-derived
          sources kept for back-compat).
       3. Existing-target fallback (`smtp-user`/`smtp-pass` in the
          chart-rendered Secret) — only when both source shapes are
          absent. */ -}}
{{- if and $smtpSrc $smtpSrc.data (index $smtpSrc.data "smtp-user") -}}
  {{- $smtpUser = index $smtpSrc.data "smtp-user" | b64dec -}}
{{- else if and $smtpSrc $smtpSrc.data (index $smtpSrc.data "user") -}}
  {{- $smtpUser = index $smtpSrc.data "user" | b64dec -}}
{{- else if and $existing $existing.data (index $existing.data "smtp-user") -}}
  {{- $smtpUser = index $existing.data "smtp-user" | b64dec -}}
{{- end -}}
{{- if and $smtpSrc $smtpSrc.data (index $smtpSrc.data "smtp-pass") -}}
  {{- $smtpPass = index $smtpSrc.data "smtp-pass" | b64dec -}}
{{- else if and $smtpSrc $smtpSrc.data (index $smtpSrc.data "password") -}}
  {{- $smtpPass = index $smtpSrc.data "password" | b64dec -}}
{{- else if and $existing $existing.data (index $existing.data "smtp-pass") -}}
  {{- $smtpPass = index $existing.data "smtp-pass" | b64dec -}}
{{- end -}}
{{- /* SMTP non-secret fields (smtp-host, smtp-port, smtp-from):
       SOURCE-wins lookup as well — bp-stalwart-sovereign's mirror
       Secret carries the Sovereign-local infrastructure addresses
       (`mail.<sovereignFQDN>` / `587` / `noreply@<sovereignFQDN>`).
       Pre-#924 the source carried only credentials; the chart fell
       back to `.Values.sovereign.smtp.*` defaults (`mail.openova.io`).
       Post-#924 the source carries Sovereign-local addresses too,
       and source-wins ensures Phase-2 cutover is automatic on the
       next Flux reconcile after bp-stalwart-sovereign installs.
       Existing-target stays the second fallback (back-compat for
       operator-edited targets). */ -}}
{{- $smtpHostSrc := "" -}}
{{- $smtpPortSrc := "" -}}
{{- $smtpFromSrc := "" -}}
{{- if and $smtpSrc $smtpSrc.data (index $smtpSrc.data "smtp-host") -}}
  {{- $smtpHostSrc = index $smtpSrc.data "smtp-host" | b64dec -}}
{{- end -}}
{{- if and $smtpSrc $smtpSrc.data (index $smtpSrc.data "smtp-port") -}}
  {{- $smtpPortSrc = index $smtpSrc.data "smtp-port" | b64dec -}}
{{- end -}}
{{- if and $smtpSrc $smtpSrc.data (index $smtpSrc.data "smtp-from") -}}
  {{- $smtpFromSrc = index $smtpSrc.data "smtp-from" | b64dec -}}
{{- end -}}
{{- /* ---- SMTP non-secret fields: source → target → values defaults ---- */ -}}
{{- /* Phase-2 (#924): bp-stalwart-sovereign's mirror Secret carries
       Sovereign-local `mail.<sovereignFQDN>` / `noreply@<sovereignFQDN>`.
       Source-wins so cutover is automatic. */ -}}
{{- $smtpHost := .Values.sovereign.smtp.host -}}
{{- $smtpPort := .Values.sovereign.smtp.port -}}
{{- $smtpFrom := .Values.sovereign.smtp.from -}}
{{- if $smtpHostSrc -}}
  {{- $smtpHost = $smtpHostSrc -}}
{{- else if and $existing $existing.data (index $existing.data "smtp-host") -}}
  {{- $smtpHost = index $existing.data "smtp-host" | b64dec -}}
{{- end -}}
{{- if $smtpPortSrc -}}
  {{- $smtpPort = $smtpPortSrc -}}
{{- else if and $existing $existing.data (index $existing.data "smtp-port") -}}
  {{- $smtpPort = index $existing.data "smtp-port" | b64dec -}}
{{- end -}}
{{- if $smtpFromSrc -}}
  {{- $smtpFrom = $smtpFromSrc -}}
{{- else if and $existing $existing.data (index $existing.data "smtp-from") -}}
  {{- $smtpFrom = index $existing.data "smtp-from" | b64dec -}}
{{- end -}}
apiVersion: v1
kind: Secret
metadata:
  name: {{ $secretName }}
  namespace: {{ $namespace }}
  labels:
    catalyst.openova.io/blueprint: bp-catalyst-platform
    catalyst.openova.io/component: catalyst-api
    app.kubernetes.io/part-of: catalyst
  annotations:
    # Survive helm uninstall — the Secret outlives the release. A
    # subsequent helm install picks up the bytes via lookup against the
    # source Secrets (and the persisted target if still present).
    helm.sh/resource-policy: keep
type: Opaque
data:
  kc-addr: {{ $kcAddr | b64enc | quote }}
  kc-realm: {{ $kcRealm | b64enc | quote }}
  kc-sa-client-id: {{ $kcClientID | b64enc | quote }}
  kc-sa-client-secret: {{ $kcClientSecret | b64enc | quote }}
  kc-audience: {{ $kcAudience | b64enc | quote }}
  smtp-host: {{ $smtpHost | b64enc | quote }}
  smtp-port: {{ $smtpPort | b64enc | quote }}
  smtp-from: {{ $smtpFrom | b64enc | quote }}
  smtp-user: {{ $smtpUser | b64enc | quote }}
  smtp-pass: {{ $smtpPass | b64enc | quote }}
{{- end }}
