{{- if .Values.qaFixtures.enabled }}
{{- /*
  pre-install-finalizer-strip.yaml — Fix #114 (prov #9 wedge, qa-loop iter-1).

  ROOT CAUSE chained on prov #9:
    1. bp-catalyst-platform install creates `qa-omantel` namespace +
       `qa-wp` Application CR + controller Deployments in the SAME
       install pass (no hook ordering).
    2. The `qa-cnpg-backup-s3-seed` post-install hook Job is created
       in qa-omantel; the chart waits up to 15m on its completion.
    3. If anything in the install pass fails (or the Job stalls past
       15m), `cleanupOnFail: true` triggers a rollback.
    4. Rollback deletes the controllers (catalyst-application-controller,
       catalyst-organization-controller, ...). Their CRs get
       `deletionTimestamp` but still hold the controller's finalizer
       (e.g. `application.apps.openova.io/finalizer`).
    5. The qa-omantel namespace itself enters `Terminating` because
       the Application CR has `metadata.finalizers` set.
    6. With NO controller running, the finalizer is never removed.
       Namespace is wedged in Terminating forever.
    7. Next install retry: `creating Namespace qa-omantel` no-ops
       (namespace already exists, Terminating); subsequent resource
       creates against qa-omantel are REJECTED by the apiserver
       (`unable to create new content in namespace qa-omantel because
       it is being terminated`). The seed Job's RBAC can't be created
       → Job never spawns → 15m hook timeout → rollback again.
       Infinite loop, install NEVER converges.

  TARGET-STATE FIX (per INVIOLABLE-PRINCIPLES #1, #4 — no MVP, no
  workaround):
    Run a pre-install / pre-upgrade Helm hook Job at the VERY START
    of every install attempt that:
      a. Strips finalizers from any pre-existing qa-fixture
         controller-managed CRs (Application, Organization,
         Environment, UserAccess) in the qa-omantel namespace + the
         catalyst-system namespace.
      b. If the qa-omantel namespace is in `Terminating` state, strips
         its `kubernetes` finalizer so the apiserver completes the
         deletion.
    This guarantees every install attempt starts from a clean slate,
    breaking the finalizer-deadlock chain at step 7 above. Once the
    namespace is fully deleted, the chart's regular install pass
    re-creates it cleanly and the seed Job's RBAC can be created.

  hook-weight `-100` puts this Job FIRST in the pre-install ordering
  (Helm sorts ascending; default weight is 0). It runs before the
  namespace template is applied, before controllers, before the seed
  Job — guaranteeing the cleanup completes before any other resource
  attempts to land in qa-omantel.

  This is a defense-in-depth measure; under normal operation (clean
  first install, no rollback) the Job runs, finds nothing to clean,
  and exits 0 in seconds.
*/ -}}
apiVersion: v1
kind: ServiceAccount
metadata:
  name: qa-finalizer-strip
  namespace: {{ .Release.Namespace | quote }}
  labels:
    catalyst.openova.io/managed-by: qa-fixtures
  annotations:
    helm.sh/hook: pre-install,pre-upgrade
    helm.sh/hook-weight: "-100"
    helm.sh/hook-delete-policy: before-hook-creation,hook-succeeded
---
apiVersion: rbac.authorization.k8s.io/v1
kind: ClusterRole
metadata:
  name: qa-finalizer-strip
  labels:
    catalyst.openova.io/managed-by: qa-fixtures
  annotations:
    helm.sh/hook: pre-install,pre-upgrade
    helm.sh/hook-weight: "-100"
    helm.sh/hook-delete-policy: before-hook-creation,hook-succeeded
rules:
  # Read + patch any qa-fixture CR that may carry a controller
  # finalizer. We only patch `metadata.finalizers` — never delete the
  # CR itself (the namespace deletion will reap it once the
  # finalizer is gone).
  - apiGroups: ["apps.openova.io"]
    resources: ["applications"]
    verbs: ["get", "list", "patch", "update"]
  - apiGroups: ["orgs.openova.io"]
    resources: ["organizations"]
    verbs: ["get", "list", "patch", "update"]
  - apiGroups: ["catalyst.openova.io"]
    resources: ["environments"]
    verbs: ["get", "list", "patch", "update"]
  - apiGroups: ["access.openova.io"]
    resources: ["useraccesses"]
    verbs: ["get", "list", "patch", "update"]
  # Namespace finalizer strip: only when the namespace is in
  # `Terminating` state (the Job's script checks status.phase before
  # patching). Patching `spec.finalizers` requires the
  # `finalize` subresource verb on namespaces — listed explicitly
  # so the Role is minimal-rights.
  - apiGroups: [""]
    resources: ["namespaces"]
    verbs: ["get", "list"]
  - apiGroups: [""]
    resources: ["namespaces/finalize"]
    verbs: ["update"]
---
apiVersion: rbac.authorization.k8s.io/v1
kind: ClusterRoleBinding
metadata:
  name: qa-finalizer-strip
  labels:
    catalyst.openova.io/managed-by: qa-fixtures
  annotations:
    helm.sh/hook: pre-install,pre-upgrade
    helm.sh/hook-weight: "-100"
    helm.sh/hook-delete-policy: before-hook-creation,hook-succeeded
subjects:
  - kind: ServiceAccount
    name: qa-finalizer-strip
    namespace: {{ .Release.Namespace | quote }}
roleRef:
  kind: ClusterRole
  name: qa-finalizer-strip
  apiGroup: rbac.authorization.k8s.io
---
apiVersion: batch/v1
kind: Job
metadata:
  name: qa-finalizer-strip
  namespace: {{ .Release.Namespace | quote }}
  labels:
    catalyst.openova.io/managed-by: qa-fixtures
  annotations:
    helm.sh/hook: pre-install,pre-upgrade
    helm.sh/hook-weight: "-99"
    helm.sh/hook-delete-policy: before-hook-creation,hook-succeeded
spec:
  ttlSecondsAfterFinished: 600
  backoffLimit: 4
  template:
    metadata:
      labels:
        app.kubernetes.io/name: qa-finalizer-strip
        catalyst.openova.io/managed-by: qa-fixtures
    spec:
      serviceAccountName: qa-finalizer-strip
      restartPolicy: OnFailure
      # ── Fix #185 (prov #38/#39/#41 recurrence, 2026-05-12) ────────
      # Tolerate the control-plane NoSchedule taint so this pre-install
      # hook can ALWAYS land somewhere on the cluster regardless of
      # worker-node CPU saturation. On a fresh Sovereign with
      # workerCount=0 + autoscaler-hcloud (Fix #157), the FIRST worker
      # is sized just large enough for the bootstrap-kit Pods themselves;
      # bp-catalyst-platform's umbrella install lands several Deployments
      # + StatefulSets that push that worker to 99% CPU requests before
      # the umbrella's own pre-install hooks complete. Without this
      # toleration the pre-install Job sits Pending forever (autoscaler
      # backoff after failed scale-up of a second worker), Helm pre-
      # install times out at 15m, Flux remediates 3× then gives up.
      # prov #38/#39/#41 each hit this in succession on chart pin 1.4.140.
      #
      # 4-layer trace (prov #41, omantel.biz, 2026-05-12 00:28 UTC):
      #   bp-catalyst-platform HR install.timeout=15m
      #     → Helm pre-install hook: qa-finalizer-strip Job (weight -99)
      #       → Pod requests 50m CPU + 64Mi memory (tiny)
      #         → BUT no tolerations → scheduler restricted to worker
      #           → worker cpx32 (8vCPU/16GB) at 99% CPU requests
      #             (7980m of 8000m allocated) after bootstrap-kit fan-out
      #             → FailedScheduling: Insufficient cpu (event quoted:
      #               "0/2 nodes are available: 1 Insufficient cpu, 1
      #               node(s) had untolerated taint
      #               {node-role.kubernetes.io/control-plane: true}")
      #             → autoscaler triggers scale-up worker 2→3 but goes
      #               into "1 in backoff after failed scale-up"
      #               → still Pending → 15m timeout → InstallFailed
      #               → Flux rollback → ∞ loop (installFailures: 3)
      #
      # Fix: the qa-finalizer-strip hook is a defense-in-depth cleanup
      # that completes in seconds on a clean cluster. It legitimately
      # belongs ANYWHERE there's free capacity, including the control-
      # plane node (which on prov #41 sits at 7% CPU / 9% memory —
      # 7365m CPU free vs. 50m request). Tolerating the CP taint +
      # system-cluster-critical priority is the canonical pattern for
      # short-lived chart-bootstrap Jobs that MUST not be capacity-
      # blocked.
      #
      # Why prior fixes didn't suffice:
      #   - Fix #114 introduced this hook to break a finalizer-deadlock
      #     loop on prov #9. Correct scope, never anticipated worker
      #     saturation as a scheduling failure mode.
      #   - Fix #138 (chart 1.4.138) converted the qa-cnpg-backup-s3-
      #     seed + qa-cnpg-status-seed hooks (weight 0/post-install) to
      #     regular release resources to break a circular DAG dep.
      #     Different hook surface from this one.
      #   - Fix #184 (chart 1.4.140) raised the gitea-token-mint hook
      #     (weight +10) wait budget for cold-start autoscaler. That
      #     hook runs AFTER qa-finalizer-strip (-99 < +10); if the -99
      #     hook never starts, weight-10 hook never runs.
      #
      # priorityClassName: system-cluster-critical is a stock built-in
      # k8s PriorityClass (value 2000000000) — high enough to preempt
      # user workloads if the scheduler exhausts other options. Safe
      # on every Sovereign.
      tolerations:
        - key: node-role.kubernetes.io/control-plane
          operator: Exists
          effect: NoSchedule
        - key: node-role.kubernetes.io/master
          operator: Exists
          effect: NoSchedule
      priorityClassName: system-cluster-critical
      securityContext:
        runAsNonRoot: true
        runAsUser: 65534
        runAsGroup: 65534
        fsGroup: 65534
        seccompProfile:
          type: RuntimeDefault
      containers:
        - name: strip
          # Fix #185: switch bitnamilegacy/kubectl → alpine/k8s per
          # CLAUDE.md ARCHITECT-FIRST + MIRROR-EVERYTHING rules.
          # bitnamilegacy is a Docker-Hub redirect for the deprecated
          # Bitnami images (2025-08 cutover documented in platform/
          # self-sovereign-cutover/chart/values.yaml:252). alpine/k8s
          # is the canonical alpine-based kubectl image already used
          # by the sibling pre-install hook catalyst-gitea-token-mint
          # (Fix #163). Tag 1.31.4 matches the rest of the chart family.
          image: harbor.openova.io/proxy-dockerhub/alpine/k8s:1.31.4
          imagePullPolicy: IfNotPresent
          securityContext:
            allowPrivilegeEscalation: false
            readOnlyRootFilesystem: true
            runAsNonRoot: true
            runAsUser: 65534
            runAsGroup: 65534
            capabilities:
              drop: ["ALL"]
            seccompProfile:
              type: RuntimeDefault
          resources:
            requests:
              cpu: 50m
              memory: 64Mi
            limits:
              memory: 128Mi
          command: ["sh", "-c"]
          args:
            - |
              set -eu
              QA_NS="{{ .Values.qaFixtures.namespace | default "qa-omantel" }}"
              CTRL_NS="{{ .Release.Namespace }}"

              echo "=== Fix #114: qa-fixtures finalizer strip ==="
              echo "qa-namespace=${QA_NS}  controller-namespace=${CTRL_NS}"

              # ---- Step 1: strip finalizers off any leftover
              # controller-managed CRs in qa-namespace + ctrl-namespace.
              # Each CR class is processed independently so a missing
              # CRD (chart fresh-install, no prior wedge) doesn't fail
              # the Job.
              strip_cr() {
                local kind="$1"
                local ns="$2"
                # `2>/dev/null || true` — kind may not be registered yet
                # on a fresh cluster; that's fine.
                local names
                names=$(kubectl -n "${ns}" get "${kind}" -o name 2>/dev/null || true)
                if [ -z "${names}" ]; then
                  echo "  ${kind} in ${ns}: none found, skipping"
                  return 0
                fi
                for n in ${names}; do
                  echo "  stripping finalizers on ${ns}/${n}"
                  kubectl -n "${ns}" patch "${n}" \
                    --type=merge \
                    -p '{"metadata":{"finalizers":[]}}' \
                    2>&1 || echo "  warn: patch failed (CR may be gone)"
                done
              }

              echo "--- qa-namespace CRs ---"
              strip_cr applications.apps.openova.io        "${QA_NS}"
              strip_cr environments.catalyst.openova.io    "${QA_NS}"

              echo "--- controller-namespace CRs ---"
              strip_cr useraccesses.access.openova.io      "${CTRL_NS}"

              # Cluster-scoped (Organization is cluster-scoped per the
              # CRD; useraccesses are namespaced in catalyst-system).
              strip_cr_clusterscoped() {
                local kind="$1"
                local names
                names=$(kubectl get "${kind}" -o name 2>/dev/null || true)
                if [ -z "${names}" ]; then
                  echo "  ${kind} (cluster): none found, skipping"
                  return 0
                fi
                # Only strip CRs labelled by qa-fixtures so we never
                # touch real Organizations on a production-Sovereign-
                # turned-qa-Sovereign cluster (defense-in-depth).
                for n in ${names}; do
                  local label
                  label=$(kubectl get "${n}" \
                    -o jsonpath='{.metadata.labels.catalyst\.openova\.io/managed-by}' \
                    2>/dev/null || true)
                  if [ "${label}" != "qa-fixtures" ]; then
                    echo "  ${n}: not managed-by qa-fixtures (label=${label}), skipping"
                    continue
                  fi
                  echo "  stripping finalizers on ${n}"
                  kubectl patch "${n}" \
                    --type=merge \
                    -p '{"metadata":{"finalizers":[]}}' \
                    2>&1 || echo "  warn: patch failed (CR may be gone)"
                done
              }

              echo "--- cluster-scoped CRs ---"
              strip_cr_clusterscoped organizations.orgs.openova.io

              # ---- Step 2: if qa-namespace itself is stuck Terminating,
              # strip its `kubernetes` finalizer so the apiserver
              # finishes the delete. We only do this when phase=
              # Terminating (never strip a healthy namespace's
              # finalizer, which would orphan its child resources).
              ns_phase=$(kubectl get ns "${QA_NS}" \
                -o jsonpath='{.status.phase}' 2>/dev/null || true)
              if [ "${ns_phase}" = "Terminating" ]; then
                echo "namespace ${QA_NS} is Terminating; stripping kubernetes finalizer"
                # `/finalize` subresource is the only way to mutate
                # spec.finalizers post-deletionTimestamp. kubectl's
                # `--raw` PUT against the subresource is the standard
                # recipe (works under readOnlyRootFilesystem: no temp
                # files needed; body is passed via stdin).
                BODY='{"apiVersion":"v1","kind":"Namespace","metadata":{"name":"'"${QA_NS}"'"},"spec":{"finalizers":[]}}'
                printf '%s' "${BODY}" | kubectl replace --raw \
                  "/api/v1/namespaces/${QA_NS}/finalize" \
                  -f - 2>&1 \
                  || echo "warn: namespace finalize PUT failed (apiserver may have already reaped)"
                echo "namespace finalize PUT submitted; apiserver will reap"
              else
                echo "namespace ${QA_NS} phase=${ns_phase:-<absent>}; no finalizer strip needed"
              fi

              echo "=== finalizer strip complete ==="
{{- end }}
