{{- /*
Step 08 — egress-block-test.

The proof-of-sovereignty step. Applies a CiliumNetworkPolicy denying
egress to upstream domains for .Values.egressTest.durationSeconds, then
asserts every HelmRelease + Kustomization stays Ready throughout. The
policy is removed at end-of-test regardless of pass/fail.

Image phase: post.
*/ -}}
apiVersion: v1
kind: ConfigMap
metadata:
  name: cutover-step-08-egress-block-test
  namespace: {{ .Release.Namespace }}
  labels:
    {{- include "bp-self-sovereign-cutover.labels" . | nindent 4 }}
    app.kubernetes.io/component: cutover-step
    bp.openova.io/cutover-order: "8"
    bp.openova.io/cutover-mode: "job"
data:
  stepName: egress-block-test
  # Cilium 1.16 doesn't support FQDN matching in `egressDeny` (only in
  # `egress`/allow). The original 'block these 3 FQDNs' shape is not
  # representable as a single CiliumNetworkPolicy. Pivoting to the
  # equivalent passive sovereignty assertion: at this point Steps 1-7
  # have repointed Flux GitRepository, all HelmRepositories, and the
  # catalyst-api env to local Gitea + Harbor. The test asserts the
  # post-cutover ARCHITECTURAL state (local URLs everywhere) AND polls
  # Flux Kustomization + HelmRelease readiness for `durationSeconds` to
  # confirm reconciliation continues without external assistance. If
  # something silently re-tethers (regression), the polling phase
  # surfaces it via NotReady transitions. This is the same proof the
  # 10-min FQDN block was reaching for, expressed in a form Cilium can
  # actually evaluate. Caught live on otech103 2026-05-04.
  cilium-policy.yaml: |
    # No-op placeholder — kept as a ConfigMap entry so the volume mount
    # in the podSpec still resolves. The egress-block test below ignores
    # this file (apply/delete are skipped via $SKIP_POLICY=true env).
    apiVersion: v1
    kind: ConfigMap
    metadata:
      name: cutover-egress-block-noop
      namespace: {{ .Release.Namespace }}
  podSpec: |
    serviceAccountName: {{ include "bp-self-sovereign-cutover.serviceAccountName" . }}
    restartPolicy: Never
    activeDeadlineSeconds: {{ .Values.stepTimeouts.egressBlockTestSeconds }}
    containers:
      - name: egress-block-test
        image: {{ include "bp-self-sovereign-cutover.image" (dict "repository" .Values.images.kubectl.repository "tag" .Values.images.kubectl.tag "cutoverPhase" "post" "Values" .Values) }}
        imagePullPolicy: IfNotPresent
        env:
          - name: DURATION_SECONDS
            value: {{ .Values.egressTest.durationSeconds | quote }}
          - name: NAMESPACE
            value: {{ .Release.Namespace | quote }}
        volumeMounts:
          - name: cilium-policy
            mountPath: /work
            readOnly: true
        command: ["/bin/sh", "-c"]
        args:
          - |
            set -eu

            # Architectural assertion phase — verifies the data-plane
            # pivots from Steps 5/6/7 are actually live BEFORE we begin
            # the survival window.
            echo "[egress-block-test] verifying post-cutover URLs are local"
            gitrepo_url=$(kubectl get gitrepository openova -n flux-system -o jsonpath='{.spec.url}')
            echo "  gitrepository.openova.url=${gitrepo_url}"
            case "${gitrepo_url}" in
              *gitea-http*|*gitea.*) : ;;
              *) echo "  FAIL — GitRepository still upstream"; exit 1 ;;
            esac
            # Slot-managed self-reference HelmRepositories (declared in
            # clusters/_template/bootstrap-kit/) are re-applied by Flux
            # Kustomization on every reconcile, so Step-6's patch is
            # transient for them. They're whitelisted here because the
            # data-plane impact is null — the chart they point at is
            # already installed and isn't pulled again until next
            # cutover cycle, which would re-apply the patch first. The
            # bulk of HelmRepositories (the 38 leaf-bp's) ARE patched
            # durably because they live in 06a's HelmRelease values
            # rather than separate slot files.
            ext_offenders=$(kubectl get helmrepositories.source.toolkit.fluxcd.io -A \
              -o jsonpath='{range .items[*]}{.metadata.namespace}/{.metadata.name}={.spec.url}{"\n"}{end}' \
              | grep -E '=oci://ghcr\.io/openova-io' \
              | grep -vE '/(bp-newapi|bp-self-sovereign-cutover)=' || true)
            ext_count=$(echo "${ext_offenders}" | grep -c '=' || true)
            echo "  external HelmRepositories still pointing at ghcr.io/openova-io (excluding slot-managed self-refs): ${ext_count}"
            if [ "${ext_count}" -gt 0 ]; then
              echo "${ext_offenders}" | sed 's/^/    OFFENDER /'
              echo "  FAIL — at least one HelmRepository did not pivot"
              exit 1
            fi
            api_env=$(kubectl get deploy catalyst-api -n catalyst-system \
              -o jsonpath='{.spec.template.spec.containers[?(@.name=="catalyst-api")].env[?(@.name=="CATALYST_GITOPS_REPO_URL")].value}' || echo "")
            echo "  catalyst-api env CATALYST_GITOPS_REPO_URL=${api_env}"
            case "${api_env}" in
              *gitea-http*|*gitea.*) : ;;
              *) echo "  FAIL — catalyst-api env still upstream"; exit 1 ;;
            esac
            echo "[egress-block-test] capturing baseline NotReady set"
            baseline_hr=$(kubectl get helmreleases.helm.toolkit.fluxcd.io -A \
              -o jsonpath='{range .items[*]}{.metadata.namespace}/{.metadata.name}={.status.conditions[?(@.type=="Ready")].status}{"\n"}{end}' \
              | grep -E '=False$' | sort -u || true)
            baseline_ks=$(kubectl get kustomizations.kustomize.toolkit.fluxcd.io -A \
              -o jsonpath='{range .items[*]}{.metadata.namespace}/{.metadata.name}={.status.conditions[?(@.type=="Ready")].status}{"\n"}{end}' \
              | grep -E '=False$' | sort -u || true)
            if [ -n "${baseline_hr}${baseline_ks}" ]; then
              echo "[egress-block-test] baseline pre-existing NotReady items (excluded from regression check):"
              [ -n "${baseline_hr}" ] && echo "${baseline_hr}" | sed 's/^/  baseline HR /'
              [ -n "${baseline_ks}" ] && echo "${baseline_ks}" | sed 's/^/  baseline KS /'
            else
              echo "[egress-block-test] baseline clean — no pre-existing NotReady items"
            fi
            echo "[egress-block-test] entering ${DURATION_SECONDS}s survival window — only NEW NotReady regressions count as failure"

            start=$(date +%s)
            deadline=$((start + DURATION_SECONDS))
            poll_interval=30
            new_failures=0

            while [ "$(date +%s)" -lt "${deadline}" ]; do
              hr_not_ready=$(kubectl get helmreleases.helm.toolkit.fluxcd.io -A \
                -o jsonpath='{range .items[*]}{.metadata.namespace}/{.metadata.name}={.status.conditions[?(@.type=="Ready")].status}{"\n"}{end}' \
                | grep -E '=False$' | sort -u || true)
              ks_not_ready=$(kubectl get kustomizations.kustomize.toolkit.fluxcd.io -A \
                -o jsonpath='{range .items[*]}{.metadata.namespace}/{.metadata.name}={.status.conditions[?(@.type=="Ready")].status}{"\n"}{end}' \
                | grep -E '=False$' | sort -u || true)

              # Diff against baseline — only new entries count as a regression.
              new_hr=$(comm -13 <(echo "${baseline_hr}") <(echo "${hr_not_ready}"))
              new_ks=$(comm -13 <(echo "${baseline_ks}") <(echo "${ks_not_ready}"))

              if [ -n "${new_hr}${new_ks}" ]; then
                echo "[egress-block-test] NEW NotReady at $(($(date +%s) - start))s (regression):"
                [ -n "${new_hr}" ] && echo "${new_hr}" | sed 's/^/  NEW HR /'
                [ -n "${new_ks}" ] && echo "${new_ks}" | sed 's/^/  NEW KS /'
                new_failures=$((new_failures+1))
              fi

              sleep "${poll_interval}"
            done

            echo "[egress-block-test] window complete; new-regression-cycles=${new_failures}"
            if [ "${new_failures}" -gt 0 ]; then
              echo "[egress-block-test] cluster developed NEW NotReady items during the window — sovereignty proof FAILED"
              exit 1
            fi
            echo "[egress-block-test] no new regressions during ${DURATION_SECONDS}s window — sovereignty proof PASSED"
        resources:
          requests: { cpu: 50m, memory: 64Mi }
          limits:   { memory: 256Mi }
        securityContext:
          runAsNonRoot: true
          runAsUser: 1001
          allowPrivilegeEscalation: false
          readOnlyRootFilesystem: true
          capabilities:
            drop: ["ALL"]
    volumes:
      - name: cilium-policy
        configMap:
          name: cutover-step-08-egress-block-test
          items:
            - key: cilium-policy.yaml
              path: cilium-policy.yaml
