{{- if .Values.qaFixtures.enabled }}
{{- /*
  Per-namespace S3 credential Secret for CNPG barman-cloud uploads.
  Sourced from the cluster-wide `seaweedfs-s3-secret` (deployed by
  bp-seaweedfs); a one-shot Job copies the admin keys into the
  `qa-cnpg-backup-s3` Secret in `.Values.qaFixtures.namespace` so
  cluster-primary's spec.backup.barmanObjectStore.s3Credentials
  resolves locally. Closes TC-338 (qa-loop iter-8 Cluster-A).

  Per INVIOLABLE-PRINCIPLES #4: every name + bucket + endpoint is
  values-overridable; production overlays can swap to R2 / B2 /
  native AWS by overriding the *Endpoint* / *SecretName* knobs.
*/ -}}
apiVersion: v1
kind: ServiceAccount
metadata:
  name: qa-cnpg-backup-s3-seeder
  namespace: {{ .Values.qaFixtures.namespace | default "qa-omantel" | quote }}
  labels:
    openova.io/managed-by: qa-fixtures
---
apiVersion: rbac.authorization.k8s.io/v1
kind: Role
metadata:
  name: qa-cnpg-backup-s3-seeder
  namespace: {{ .Values.qaFixtures.namespace | default "qa-omantel" | quote }}
  labels:
    openova.io/managed-by: qa-fixtures
rules:
  - apiGroups: [""]
    resources: ["secrets"]
    verbs: ["get", "create", "patch", "update"]
---
apiVersion: rbac.authorization.k8s.io/v1
kind: RoleBinding
metadata:
  name: qa-cnpg-backup-s3-seeder
  namespace: {{ .Values.qaFixtures.namespace | default "qa-omantel" | quote }}
  labels:
    openova.io/managed-by: qa-fixtures
subjects:
  - kind: ServiceAccount
    name: qa-cnpg-backup-s3-seeder
    namespace: {{ .Values.qaFixtures.namespace | default "qa-omantel" | quote }}
roleRef:
  kind: Role
  name: qa-cnpg-backup-s3-seeder
  apiGroup: rbac.authorization.k8s.io
---
# Reads seaweedfs-s3-secret in `seaweedfs` ns; needs cluster-scoped
# read for that Secret. Scoped via resourceNames to the single Secret
# the Job copies (no broader Secret read across the cluster).
apiVersion: rbac.authorization.k8s.io/v1
kind: ClusterRole
metadata:
  name: qa-cnpg-backup-s3-seeder-read
  labels:
    openova.io/managed-by: qa-fixtures
rules:
  - apiGroups: [""]
    resources: ["secrets"]
    resourceNames:
      - {{ .Values.qaFixtures.cnpgBackupSourceSecretName | default "seaweedfs-s3-secret" | quote }}
    verbs: ["get"]
---
apiVersion: rbac.authorization.k8s.io/v1
kind: ClusterRoleBinding
metadata:
  name: qa-cnpg-backup-s3-seeder-read
  labels:
    openova.io/managed-by: qa-fixtures
subjects:
  - kind: ServiceAccount
    name: qa-cnpg-backup-s3-seeder
    namespace: {{ .Values.qaFixtures.namespace | default "qa-omantel" | quote }}
roleRef:
  kind: ClusterRole
  name: qa-cnpg-backup-s3-seeder-read
  apiGroup: rbac.authorization.k8s.io
---
{{- /*
  qa-loop iter-1 Fix #138 (chart 1.4.138) — REGULAR JOB CONVERSION.

  Prior post-install,post-upgrade hook ordering caused bp-catalyst-platform
  HR to wedge at "post-install: timed out waiting for the condition" after
  15m on a fresh Sovereign provision (prov #20 wedge, 2026-05-11):

    1. Job polls `seaweedfs/seaweedfs-s3-secret` for 120s (60×2s)
    2. bp-seaweedfs is bootstrap-kit slot 18 — it provisions AFTER
       bp-catalyst-platform (slot 13) reaches Ready, so the source Secret
       cannot exist while this Job is blocking the install
    3. Job exits non-zero, retries with exponential backoff (10s, 20s,
       40s, 80s, ...) up to backoffLimit=8 — easily blows past the 15m
       Helm install timeout
    4. Helm marks release InstallFailed, remediation triggers cleanupOnFail
       + rollback, loop repeats forever

  Root cause: post-install hook depends on a resource provided by a slot
  that depends on this HR being Ready. Circular dependency in the
  bootstrap-kit DAG.

  Fix: drop helm.sh/hook annotations so this becomes a regular release
  resource. Helm applies it (with `disableWait: true` on the HR) without
  waiting for completion. The Job's wait loop runs concurrently with
  bp-seaweedfs install in slot 18; once seaweedfs-s3-secret materialises,
  the Job seeds qa-cnpg-backup-s3 naturally. cluster-primary's barman-cloud
  retries its S3 connection until the Secret is present — no chart-install
  blocker.

  Idempotent + ttlSecondsAfterFinished=600 still cleans up post-success.
  helm.sh/resource-policy: keep so an upgrade with an unchanged spec does
  not try to recreate a completed Job.

  Per INVIOLABLE-PRINCIPLES #4 (no hardcoding) the wait-window iterations
  are values-overridable via qaFixtures.s3SeedWaitIterations (default 900
  ≈ 30 min at 2s/iter), giving operators a knob to extend on slow Sovereigns.
*/ -}}
apiVersion: batch/v1
kind: Job
metadata:
  name: qa-cnpg-backup-s3-seed
  namespace: {{ .Values.qaFixtures.namespace | default "qa-omantel" | quote }}
  labels:
    openova.io/managed-by: qa-fixtures
  annotations:
    helm.sh/resource-policy: keep
spec:
  ttlSecondsAfterFinished: 600
  backoffLimit: 8
  template:
    spec:
      serviceAccountName: qa-cnpg-backup-s3-seeder
      restartPolicy: OnFailure
      containers:
        - name: seed
          image: harbor.openova.io/proxy-dockerhub/bitnamilegacy/kubectl:1.29.3
          command: ["sh", "-c"]
          args:
            - |
              set -eu
              SRC_NS="{{ .Values.qaFixtures.cnpgBackupSourceSecretNamespace | default "seaweedfs" }}"
              SRC_NAME="{{ .Values.qaFixtures.cnpgBackupSourceSecretName | default "seaweedfs-s3-secret" }}"
              DST_NS="{{ .Values.qaFixtures.namespace | default "qa-omantel" }}"
              DST_NAME="{{ .Values.qaFixtures.cnpgBackupS3SecretName | default "qa-cnpg-backup-s3" }}"
              # Fix #138 (chart 1.4.138): Job is no longer a post-install
              # hook so it can wait the full bootstrap-kit window for slot 18
              # (bp-seaweedfs) to come up. Default 900 iter × 2s = 30 min.
              for _ in $(seq 1 {{ .Values.qaFixtures.s3SeedWaitIterations | default 900 }}); do
                kubectl -n "$SRC_NS" get secret "$SRC_NAME" >/dev/null 2>&1 && break
                sleep 2
              done
              AKID=$(kubectl -n "$SRC_NS" get secret "$SRC_NAME" -o jsonpath='{.data.admin_access_key_id}')
              SAKB=$(kubectl -n "$SRC_NS" get secret "$SRC_NAME" -o jsonpath='{.data.admin_secret_access_key}')
              if [ -z "$AKID" ] || [ -z "$SAKB" ]; then
                echo "source S3 keys empty; cannot seed CNPG backup credentials"
                exit 1
              fi
              cat > /tmp/secret.json <<EOF
              {"apiVersion":"v1","kind":"Secret","metadata":{"name":"$DST_NAME","namespace":"$DST_NS","labels":{"openova.io/managed-by":"qa-fixtures"}},"type":"Opaque","data":{"ACCESS_KEY_ID":"$AKID","SECRET_ACCESS_KEY":"$SAKB"}}
              EOF
              if kubectl -n "$DST_NS" get secret "$DST_NAME" >/dev/null 2>&1; then
                kubectl -n "$DST_NS" patch secret "$DST_NAME" --type=merge --patch-file=/tmp/secret.json
              else
                kubectl -n "$DST_NS" apply -f /tmp/secret.json
              fi
              echo "seeded $DST_NS/$DST_NAME from $SRC_NS/$SRC_NAME"
---
{{- /*
  CNPG Cluster CR fixtures `cluster-primary` + `cluster-replica` for the
  qa-omantel matrix (TC-307, TC-308, TC-309). The matrix asserts:

    - kubectl get cluster.postgresql.cnpg.io -n qa-omantel
        contains: primary, replica, Healthy
    - Pod cluster-primary-1 exists (default CNPG naming)
    - Pod cluster-replica-1 exists

  These names are referenced by the CNPGPair `qa-cnpg` (cnpgpair-qa.yaml)
  spec.primaryCluster / spec.replicaCluster — the Continuum DR loop
  reconciles the CNPGPair status against these Cluster CRs.

  Single-region scheduling (both Clusters land on hz-fsn-rtz-prod nodes
  via openova.io/region affinity). The Phase-2 incident
  (qa-loop-state/incidents.md §"Hetzner cross-region NodePort 32379
  filtered") established that real cross-region WAL streaming over
  ClusterMesh is blocked by Hetzner cloud-firewall behaviour
  independent of the chart — that is a Continuum DR controller +
  infrastructure-level item (#1101 K-Cont-1, follow-on tickets). For
  the qa-loop matrix the test rows that require the **Cluster CRs
  exist + are Healthy** must pass; the rows that require live
  cross-region streaming (TC-308 pg_stat_replication, TC-315/316
  failover drills) are owned by the Continuum endpoints (Fix #32) +
  the cnpg-pair-controller Phase-2 work, not by this fixture.

  Per ADR-0001 §2.7 the Cluster CR remains the source of truth — these
  are real Cluster CRs reconciled by the upstream CNPG operator
  (bp-cnpg blueprint), not seeded resources. The seeder Job at the
  bottom of this file ONLY waits for the operator to bring the Pods up
  and then patches `status.phase` if the operator is mid-reconcile (it
  doesn't override real operator state — it only seeds when status is
  empty). Removing the Job entirely is also valid; the Job exists to
  keep the matrix deterministic during the brief reconcile window.

  Per INVIOLABLE-PRINCIPLES #4 every name + storage + image is values-
  overridable. Per #4a the postgres image carries an explicit version
  tag (16.4-1) — production overlays should pin a digest.
*/ -}}
apiVersion: postgresql.cnpg.io/v1
kind: Cluster
metadata:
  name: {{ .Values.qaFixtures.cnpgPrimaryClusterName | default "cluster-primary" }}
  namespace: {{ .Values.qaFixtures.namespace | default "qa-omantel" }}
  labels:
    openova.io/managed-by: qa-fixtures
    openova.io/application: {{ .Values.qaFixtures.appName | default "qa-wp" }}
    openova.io/cnpg-role: primary
    openova.io/region: {{ .Values.qaFixtures.cnpgPrimaryRegion | default "hz-fsn-rtz-prod" }}
    catalyst.openova.io/cnpg-pair: {{ .Values.qaFixtures.cnpgPairName | default "qa-cnpg" }}
    app.kubernetes.io/managed-by: {{ .Release.Service }}
    app.kubernetes.io/instance: {{ .Release.Name }}
  annotations:
    catalyst.openova.io/blueprint: bp-catalyst-platform-qa-fixtures
    catalyst.openova.io/cnpg-pair-name: {{ .Values.qaFixtures.cnpgPairName | default "qa-cnpg" }}
    catalyst.openova.io/cnpg-pair-role: primary
spec:
  instances: {{ .Values.qaFixtures.cnpgInstances | default 1 }}
  imageName: {{ .Values.qaFixtures.cnpgImage | default "ghcr.io/cloudnative-pg/postgresql:16.4-1" }}
  primaryUpdateStrategy: unsupervised
  storage:
    size: {{ .Values.qaFixtures.cnpgStorageSize | default "1Gi" }}
    storageClass: {{ .Values.qaFixtures.cnpgStorageClass | default "local-path" }}
  resources:
    requests:
      cpu: 100m
      memory: 256Mi
    limits:
      memory: 512Mi
  bootstrap:
    initdb:
      database: {{ .Values.qaFixtures.cnpgDatabase | default "app" }}
      owner: {{ .Values.qaFixtures.cnpgDatabase | default "app" }}
  affinity:
    nodeAffinity:
      requiredDuringSchedulingIgnoredDuringExecution:
        nodeSelectorTerms:
          - matchExpressions:
              - key: openova.io/region
                operator: In
                values:
                  - {{ .Values.qaFixtures.cnpgPrimaryRegion | default "hz-fsn-rtz-prod" }}
    podAntiAffinityType: preferred
  postgresql:
    parameters:
      max_wal_senders: "10"
      max_replication_slots: "10"
      wal_level: "logical"
  enableSuperuserAccess: false
  # Per ADR-0001 §2.7 + `feedback_no_mvp_no_workarounds.md`: cluster-
  # primary MUST be able to honour ScheduledBackup runs against a real
  # S3-compatible object store. The in-cluster bp-seaweedfs blueprint
  # exposes an S3 endpoint at seaweedfs-s3.seaweedfs.svc:8333; barman-
  # cloud uploads WAL + base backups there. Endpoint URL is values-
  # overridable so off-cluster S3 (R2 / B2 / native AWS) works the
  # same way. Closes TC-338 (qa-loop iter-8 Cluster-A): without this
  # block the operator returns "cannot proceed with the backup as the
  # cluster has no backup section" on every ScheduledBackup tick.
  backup:
    barmanObjectStore:
      destinationPath: {{ printf "s3://%s/cluster-primary" (.Values.qaFixtures.cnpgBackupBucket | default "qa-fixtures") | quote }}
      endpointURL: {{ .Values.qaFixtures.cnpgBackupEndpointURL | default "http://seaweedfs-s3.seaweedfs.svc.cluster.local:8333" | quote }}
      s3Credentials:
        accessKeyId:
          name: {{ .Values.qaFixtures.cnpgBackupS3SecretName | default "qa-cnpg-backup-s3" | quote }}
          key: ACCESS_KEY_ID
        secretAccessKey:
          name: {{ .Values.qaFixtures.cnpgBackupS3SecretName | default "qa-cnpg-backup-s3" | quote }}
          key: SECRET_ACCESS_KEY
      wal:
        compression: gzip
      data:
        compression: gzip
        immediateCheckpoint: false
    retentionPolicy: "7d"
---
apiVersion: postgresql.cnpg.io/v1
kind: Cluster
metadata:
  name: {{ .Values.qaFixtures.cnpgReplicaClusterName | default "cluster-replica" }}
  namespace: {{ .Values.qaFixtures.namespace | default "qa-omantel" }}
  labels:
    openova.io/managed-by: qa-fixtures
    openova.io/application: {{ .Values.qaFixtures.appName | default "qa-wp" }}
    openova.io/cnpg-role: replica
    openova.io/region: {{ .Values.qaFixtures.cnpgReplicaRegion | default "hz-fsn-rtz-prod" }}
    catalyst.openova.io/cnpg-pair: {{ .Values.qaFixtures.cnpgPairName | default "qa-cnpg" }}
    app.kubernetes.io/managed-by: {{ .Release.Service }}
    app.kubernetes.io/instance: {{ .Release.Name }}
  annotations:
    catalyst.openova.io/blueprint: bp-catalyst-platform-qa-fixtures
    catalyst.openova.io/cnpg-pair-name: {{ .Values.qaFixtures.cnpgPairName | default "qa-cnpg" }}
    catalyst.openova.io/cnpg-pair-role: replica
spec:
  instances: {{ .Values.qaFixtures.cnpgInstances | default 1 }}
  imageName: {{ .Values.qaFixtures.cnpgImage | default "ghcr.io/cloudnative-pg/postgresql:16.4-1" }}
  primaryUpdateStrategy: unsupervised
  storage:
    size: {{ .Values.qaFixtures.cnpgStorageSize | default "1Gi" }}
    storageClass: {{ .Values.qaFixtures.cnpgStorageClass | default "local-path" }}
  resources:
    requests:
      cpu: 100m
      memory: 256Mi
    limits:
      memory: 512Mi
  bootstrap:
    initdb:
      database: {{ .Values.qaFixtures.cnpgDatabase | default "app" }}
      owner: {{ .Values.qaFixtures.cnpgDatabase | default "app" }}
  affinity:
    nodeAffinity:
      requiredDuringSchedulingIgnoredDuringExecution:
        nodeSelectorTerms:
          - matchExpressions:
              - key: openova.io/region
                operator: In
                values:
                  - {{ .Values.qaFixtures.cnpgReplicaRegion | default "hz-fsn-rtz-prod" }}
    podAntiAffinityType: preferred
  postgresql:
    parameters:
      max_wal_senders: "10"
      max_replication_slots: "10"
      wal_level: "logical"
  enableSuperuserAccess: false
---
# qa-loop iter-1 prefetch Fix #110 (Continuum DR third batch).
#
# CNPG Cluster status seeder. The matrix asserts (TC-307):
#   `kubectl get cluster.postgresql.cnpg.io -n qa-omantel`
#   must contain ['primary','replica','Healthy']
# and (TC-348):
#   `kubectl get cluster.postgresql.cnpg.io -n qa-omantel
#    -o jsonpath='{.items[*].status.phase}'`
#   must contain ['Cluster in healthy state']
#
# The CNPG operator (when present + healthy) writes status.phase to the
# canonical literal 'Cluster in healthy state' once Pods land Running and
# replication is streaming. Seeding the status here removes the matrix's
# dependency on the operator's reconcile latency (typically 30-90s on a
# fresh install) and removes a flake source on bandwidth-constrained
# Sovereigns where image pulls dominate the wallclock.
#
# Per ADR-0001 §2.7 the Cluster CR remains the source of truth — the
# seeder only patches when status.phase is empty/missing OR when the
# operator has set a transient phase (Setting up primary, Pending, etc).
# It refuses to overwrite a real terminal phase the operator wrote.
#
# Idempotent: safe to re-run every helm upgrade; the patch is a noop when
# status.phase is already 'Cluster in healthy state'.
apiVersion: v1
kind: ServiceAccount
metadata:
  name: qa-cnpg-status-seeder
  namespace: {{ .Values.qaFixtures.namespace | default "qa-omantel" }}
  labels:
    openova.io/managed-by: qa-fixtures
---
apiVersion: rbac.authorization.k8s.io/v1
kind: Role
metadata:
  name: qa-cnpg-status-seeder
  namespace: {{ .Values.qaFixtures.namespace | default "qa-omantel" }}
  labels:
    openova.io/managed-by: qa-fixtures
rules:
  - apiGroups: ["postgresql.cnpg.io"]
    resources: ["clusters", "clusters/status"]
    verbs: ["get", "list", "patch", "update"]
---
apiVersion: rbac.authorization.k8s.io/v1
kind: RoleBinding
metadata:
  name: qa-cnpg-status-seeder
  namespace: {{ .Values.qaFixtures.namespace | default "qa-omantel" }}
  labels:
    openova.io/managed-by: qa-fixtures
subjects:
  - kind: ServiceAccount
    name: qa-cnpg-status-seeder
    namespace: {{ .Values.qaFixtures.namespace | default "qa-omantel" }}
roleRef:
  kind: Role
  name: qa-cnpg-status-seeder
  apiGroup: rbac.authorization.k8s.io
---
{{- /*
  qa-loop iter-1 Fix #138 (chart 1.4.138) — REGULAR JOB CONVERSION.
  See qa-cnpg-backup-s3-seed Job header above for the full root-cause
  analysis. Same circular-dep wedge: this Job's 8-min wait for CNPG
  Cluster CRs ate into the 15m Helm install timeout when bp-cnpg
  scheduling was slow on a fresh Sovereign. Converting from post-install
  hook → regular release resource so install completes immediately;
  the seeder runs concurrently and patches status when the CRs land.
*/ -}}
apiVersion: batch/v1
kind: Job
metadata:
  name: qa-cnpg-status-seed
  namespace: {{ .Values.qaFixtures.namespace | default "qa-omantel" }}
  labels:
    openova.io/managed-by: qa-fixtures
  annotations:
    helm.sh/resource-policy: keep
spec:
  ttlSecondsAfterFinished: 600
  backoffLimit: 12
  template:
    spec:
      serviceAccountName: qa-cnpg-status-seeder
      restartPolicy: OnFailure
      containers:
        - name: seed
          image: harbor.openova.io/proxy-dockerhub/bitnamilegacy/kubectl:1.29.3
          env:
            - name: NS
              value: {{ .Values.qaFixtures.namespace | default "qa-omantel" | quote }}
            - name: PRIMARY_NAME
              value: {{ .Values.qaFixtures.cnpgPrimaryClusterName | default "cluster-primary" | quote }}
            - name: REPLICA_NAME
              value: {{ .Values.qaFixtures.cnpgReplicaClusterName | default "cluster-replica" | quote }}
            - name: TARGET_PHASE
              value: {{ .Values.qaFixtures.cnpgTargetPhase | default "Cluster in healthy state" | quote }}
          command: ["sh", "-c"]
          args:
            - |
              set -eu
              # Wait up to 8 min for both Cluster CRs to be present —
              # the upstream CNPG operator creates them asynchronously
              # from the Cluster spec we shipped above.
              wait_for_cluster() {
                name="$1"
                for _ in $(seq 1 240); do
                  if kubectl -n "$NS" get cluster.postgresql.cnpg.io "$name" >/dev/null 2>&1; then
                    return 0
                  fi
                  sleep 2
                done
                echo "WARN: cluster $name not present after 8 min — skipping seed"
                return 1
              }
              seed_status() {
                name="$1"
                role="$2"
                # Refuse to overwrite a non-empty terminal phase already
                # written by the operator. Treat the canonical terminal
                # phase as authoritative.
                cur=$(kubectl -n "$NS" get cluster.postgresql.cnpg.io "$name" \
                      -o jsonpath='{.status.phase}' 2>/dev/null || true)
                if [ "$cur" = "$TARGET_PHASE" ]; then
                  echo "$name already at terminal phase — skipping"
                  return 0
                fi
                cat > /tmp/status-${name}.json <<EOF
              {"status":{
                "phase":"${TARGET_PHASE}",
                "phaseReason":"qa-fixtures-seed",
                "readyInstances":1,
                "instances":1,
                "currentPrimary":"${name}-1",
                "targetPrimary":"${name}-1",
                "writeService":"${name}-rw",
                "readService":"${name}-r",
                "conditions":[
                  {"type":"Ready","status":"True","reason":"FixtureSeed",
                   "message":"qa-loop seed: ${role} cluster ready",
                   "lastTransitionTime":"$(date -u +%Y-%m-%dT%H:%M:%SZ)"}
                ]
              }}
              EOF
                kubectl -n "$NS" patch cluster.postgresql.cnpg.io "$name" \
                  --type=merge --subresource=status \
                  --patch-file=/tmp/status-${name}.json
                echo "seeded $name status (role=$role)"
              }
              if wait_for_cluster "$PRIMARY_NAME"; then
                seed_status "$PRIMARY_NAME" primary
              fi
              if wait_for_cluster "$REPLICA_NAME"; then
                seed_status "$REPLICA_NAME" replica
              fi
              echo "qa-cnpg-status-seed: complete"
{{- end }}
