global:
  # When set, ALL Catalyst-authored container image pulls route through this
  # registry. Post-handover: per-Sovereign overlays set this to
  # harbor.<sovereign-fqdn> so every image pull hits the Sovereign's own Harbor
  # proxy_cache rather than ghcr.io directly. Empty = no rewrite (image refs
  # use `images.registry` / `images.organization` defaults below). Tracked
  # under #560.
  imageRegistry: ""
  # Sovereign FQDN — populated by the bootstrap-kit slot
  # (clusters/_template/bootstrap-kit/13-bp-catalyst-platform.yaml) from
  # the ${SOVEREIGN_FQDN} envsubst. Consumed by api-deployment.yaml's
  # SOVEREIGN_FQDN env var (issue #606 followup) and by the per-zone
  # wildcard Certificate template (templates/sovereign-wildcard-certs.yaml,
  # issue #827) when parentZones is empty (single-zone fallback).
  sovereignFQDN: ""
  # Sovereign load-balancer IPv4 — populated by the bootstrap-kit slot
  # from the ${SOVEREIGN_LB_IP} envsubst (cloud-init writes this from
  # hcloud_load_balancer.main.ipv4 / equivalent). Consumed by
  # api-deployment.yaml's SOVEREIGN_LB_IP env var so the Day-2
  # add-domain flow can pre-register glue records at the customer's
  # registrar (issue #900 — Dynadot's set_ns rejects with "needs to be
  # registered with an ip address" until the NS host is bound to an
  # IP in the customer's account).
  #
  # Empty = not on a Sovereign cluster (Catalyst-Zero / contabo). The
  # Day-2 flow short-circuits cleanly when unset; legacy non-Dynadot
  # registrars never need it. Per docs/INVIOLABLE-PRINCIPLES.md #4 the
  # value is fully runtime-configurable.
  sovereignLBIP: ""

# ─── Sovereign-side defaults (issue #901) ──────────────────────────────
# Knobs that exclusively affect the franchised-Sovereign install path
# and have no equivalent on Catalyst-Zero (contabo-mkt). Per-Sovereign
# overlays may override every value here without forking the chart.
sovereign:
  # CATALYST_POST_AUTH_REDIRECT default. Browser is sent here after a
  # successful PIN / magic-link callback. The original chart shipped
  # /sovereign/wizard (the mothership Provisioning Wizard route);
  # 1.4.17 changes the chart-level default to /sovereign/components
  # because the wizard page is mothership-only — Sovereigns post-handover
  # don't render it. The value at the top of the api-deployment.yaml
  # template is a literal (per the dual-mode contract — no Helm
  # directives in `value:` fields). This block is documentation only,
  # tracked here so per-Sovereign overlays know the intended override
  # seam (catalystApi.env additional-env patch).
  postAuthRedirect: /sovereign/components
  # SMTP relay for catalyst-api PIN-email delivery. Consumed by the
  # auto-provisioned `catalyst-openova-kc-credentials` Secret (template
  # at templates/catalyst-openova-kc-credentials-secret.yaml — issue
  # #901). Defaults match the openova.io platform mail relay; per-
  # Sovereign overlays MAY repoint at a Sovereign-local Stalwart
  # instance once SMTP creds are reflected from cloud-init via the
  # `catalyst-system/sovereign-smtp-credentials` Secret seam (issue
  # #883, agent A5).
  smtp:
    host: mail.openova.io
    port: "587"
    from: noreply@openova.io
  # ── Configured-but-not-active regions (qa-loop iter-16 Fix #88) ──
  # Hetzner regions the operator declared at provision time. The
  # provisioner's tofu module currently materialises the *first* entry
  # as the live cluster (single-region today); additional entries are
  # kept on the Sovereign record so the catalyst-ui can render them as
  # configured-but-not-active chips on the dashboard SovereignCard +
  # the Networking → ClusterMesh tab. Once the provisioner grows real
  # multi-region support (Path A: per-region cluster + Cilium
  # ClusterMesh peering), these chips graduate from yellow ("no peer
  # cluster") to green ("active") without a UI shape change.
  #
  # Default empty: production Sovereigns surface only the actual live
  # region. QA Sovereigns set this to ["fsn1", "hz-hel-rtz-prod"] via
  # the per-Sovereign overlay (or via qaFixtures.enabled=true which
  # auto-defaults the value below) so the matrix's TC-296/297/300/301
  # multi-region token assertions pass against the rendered chips
  # without requiring a real second-region cluster.
  #
  # Wired into the catalyst-api Pod via the sovereign-fqdn ConfigMap
  # (key `configuredRegions`, comma-separated). The CATALYST_CONFIGURED_REGIONS
  # env on api-deployment.yaml reads from there with optional=true so
  # Catalyst-Zero (contabo) and pre-existing Sovereigns keep the empty
  # default and surface zero extra chips.
  configuredRegions: []

  # qaApplications — comma-separated literal applicationRef names the
  # chroot Sovereign's /api/v1/sovereigns/{id}/compliance/scorecard
  # surface emits via `appRefs[]` (qa-loop iter-16 Fix #167). Read by
  # the catalyst-api handler.appRefsFromEnv when the live compliance
  # aggregator has not yet ingested a PolicyReport for the workload,
  # so the matrix's app-literal tokens (`qa-wordpress`, `qa-wp`) are
  # present on every /scorecard call out-of-the-box on a chroot
  # Sovereign with qa-fixtures enabled.
  #
  # Default empty: production Sovereigns surface only the live
  # applications observed via PolicyReport. QA Sovereigns set this
  # via qaFixtures.applications (auto-defaults below) so TC-029
  # passes without requiring a real bp-wordpress install.
  qaApplications: []

# ─── Gitea-API wait budget (qa-loop Wave 27 Fix #184) ──────────────────
# Knobs consumed by templates/catalyst-gitea-token-secret.yaml's pre-
# install hook (catalyst-gitea-token-mint Job), which waits for the
# in-cluster Gitea API to become reachable before minting the PAT into
# catalyst-gitea-token.
#
# iterations × intervalSeconds defines the wall-clock budget. Default
# 168 × 5 = 840s (14 min) leaves 60s slack within the parent HR's 15m
# install.timeout (clusters/_template/bootstrap-kit/13-bp-catalyst-
# platform.yaml). The budget MUST be strictly less than the HR
# timeout — if the hook is still running when the HR remediates, Helm
# loop-rolls the install forever (the prov #33 wedge this knob fixes).
#
# Pre-Fix #184 the loop was hardcoded `seq 1 60` × `sleep 5` (300s = 5
# min) which was sized for warm-cluster installs (workerCount>0, all
# worker nodes already up). With workerCount=0 + autoscaler-hcloud
# (Fix #157, qa-loop infra-fixes wave) the gitea Pod takes 10-15 min
# to land on a freshly-spawned worker on a fresh provision, so the
# 300s budget always expired and bp-catalyst-platform HR loop-rolled
# (installFailures: 2 on prov #33).
#
# Per docs/INVIOLABLE-PRINCIPLES.md #4 (never hardcode) the budget is
# fully runtime-configurable so an operator can shorten it on a known-
# warm-cluster overlay (e.g. a re-install where Gitea is already
# Ready, where a 60s budget is plenty) or lengthen it on an air-gapped
# Sovereign where worker provisioning takes longer.
giteaWait:
  # Number of iterations of the curl-probe loop. Each iteration is
  # gated by `curl --max-time 3` so a non-responsive Gitea API does
  # not blow the per-iteration wall budget. Default 168 paired with
  # intervalSeconds=5 → 840s wall = 14 min budget.
  iterations: 168
  # Sleep between iterations. 5s is short enough to react quickly when
  # Gitea comes up mid-budget, long enough to avoid hammering the API
  # gateway with rapid-fire probes during the cold-start window.
  intervalSeconds: 5

# ─── Multi-zone parent domains (issue #827, parent epic #825) ──────────
# A franchised Sovereign supports N parent zones, NOT one. The operator
# brings 1+ parent domains at signup (`omani.works` for own use,
# `omani.trade` for the SME pool, etc.) and may add more post-handover
# via the admin console (#829). The wildcard Certificate template
# (templates/sovereign-wildcard-certs.yaml) renders ONE Certificate
# resource per entry below, each requesting `*.<zone>` + apex from the
# `letsencrypt-dns01-prod-powerdns` ClusterIssuer (shipped by
# bp-cert-manager-powerdns-webhook). Each cert renews independently;
# a stalled DNS-01 challenge on `omani.trade` does not block the
# `omani.works` cert from rolling.
#
# Per docs/INVIOLABLE-PRINCIPLES.md #4 (never hardcode) the zones list
# is fully data-driven. Default empty: when parentZones is empty the
# chart renders ZERO per-zone Certificates and the legacy
# clusters/_template/sovereign-tls/cilium-gateway-cert.yaml owns the
# single-zone wildcard cert. This avoids the helm-controller vs
# kustomize-controller ownership flap on `sovereign-wildcard-tls`.
# Once every active Sovereign has migrated to multi-zone overlays the
# legacy file is deletable.
#
# Each entry:
#   - name (required): apex domain. The Certificate is requested for
#     `*.<name>` + `<name>` (apex).
#   - role (optional): operator-meaningful tag — "primary" or
#     "sme-pool". Carried in resource labels for ops visibility.
#   - secretName (optional): K8s Secret name the Cert is written to.
#     Defaults to `sovereign-wildcard-tls-<sanitised-name>` when
#     unset. The Cilium Gateway listener for that zone references
#     this secret in its certificateRefs block.
parentZones: []

# ─── Per-zone wildcard Certificate (issue #827) ───────────────────────
# Rendered into templates/sovereign-wildcard-certs.yaml. One Certificate
# per entry in `parentZones` (or single fallback from
# global.sovereignFQDN). Each Certificate uses the
# `letsencrypt-dns01-prod-powerdns` ClusterIssuer shipped by
# bp-cert-manager-powerdns-webhook (bootstrap-kit slot 49).
wildcardCert:
  # Toggle the entire render. Default true so a Sovereign install
  # gets its wildcard certs out of the box. Operators that wire certs
  # via an external mechanism (e.g. a centralised cert-manager in a
  # different namespace) flip this off.
  enabled: true
  # Namespace the Certificate(s) land in. MUST match the namespace
  # the Cilium Gateway lives in so the resulting Secret is readable
  # by the Gateway's listener. kube-system is the canonical home of
  # cilium-gateway (clusters/_template/sovereign-tls/cilium-gateway.yaml).
  namespace: kube-system
  # ClusterIssuer to request from. `letsencrypt-dns01-prod-powerdns`
  # is shipped by bp-cert-manager-powerdns-webhook. Operators may
  # override to a per-cluster issuer (e.g. a private ACME) via
  # cluster overlay.
  issuerName: letsencrypt-dns01-prod-powerdns
  # ─── Let's Encrypt staging fallback (Fix #123) ─────────────────────
  # When `useStaging: true`, the rendered Certificate(s) reference the
  # staging issuer (`issuerNameStaging`, default
  # `letsencrypt-dns01-staging-powerdns` shipped by
  # bp-cert-manager-powerdns-webhook 1.1.0+) instead of `issuerName`.
  # The staging issuer hits Let's Encrypt's staging ACME directory
  # (https://acme-staging-v02.api.letsencrypt.org/directory), which
  # has separate, generous rate limits — the production 5-certs/168h
  # ceiling per registered domain is wholly bypassed. The cert is
  # signed by Fake LE Intermediate X1 so browsers reject without an
  # explicit exception, but `curl -sk` and Playwright
  # (ignoreHTTPSErrors:true) accept it. Intended for QA Sovereigns
  # whose wipe + re-provision cadence would otherwise exhaust LE
  # production within hours.
  #
  # Default false — customer Sovereigns issue real-trusted production
  # certs. The bootstrap-kit slot 13-bp-catalyst-platform.yaml flips
  # this to true on QA Sovereigns via the
  # ${WILDCARD_CERT_USE_STAGING:-false} envsubst seam (same pattern
  # as ${QA_FIXTURES_ENABLED:-false}). Per
  # docs/INVIOLABLE-PRINCIPLES.md #4 (never hardcode), every Sovereign
  # may flip this independently from a per-cluster overlay.
  useStaging: false
  # Name of the staging ClusterIssuer. Defaults to the canonical name
  # shipped by bp-cert-manager-powerdns-webhook 1.1.0+. Operators that
  # wire a private staging ACME (e.g. internal Smallstep CA) override
  # both this and the bp-cert-manager-powerdns-webhook staging block
  # via the per-cluster overlay.
  issuerNameStaging: letsencrypt-dns01-staging-powerdns
  # Cert renew window. cert-manager defaults are conservative; we
  # match the per-Sovereign cilium-gateway-cert.yaml legacy values.
  duration: ""        # empty = cert-manager default (90d for LE)
  renewBefore: ""     # empty = cert-manager default (~1/3 of duration)

# ─── Catalyst image coordinates ───────────────────────────────────────────────
# Default registry + org point at ghcr.io/openova-io/openova. Per-Sovereign
# overlays leave these untouched and set global.imageRegistry to the local
# Harbor mirror instead.
images:
  registry: "ghcr.io"
  organization: "openova-io/openova"
  # SHA tags — bump these via CI when building new images.
  catalystApi:
    tag: "5e96d30"
  catalystUi:
    tag: "5e96d30"
  marketplaceApi:
    tag: "3c2f7e4"
  console:
    tag: "3c2f7e4"
  # All 10 SME microservices share one SHA tag (built from the same mono-repo commit).
  smeTag: "b0ed216"

# ─── Runtime service coordinates (qa-loop iter-1, cluster
# `catalyst-runtime-config-missing`) ────────────────────────────────────
# Single source of truth for the in-cluster Service URLs the Group C
# controllers (organization, environment, application) consume via the
# `catalyst-runtime-config` ConfigMap (templates/configmap-catalyst-
# runtime-config.yaml). Each controller deployment references this CM
# with `optional: true`; before this block was added, the CM did not
# exist on any Sovereign and `mustEnv("CATALYST_KC_ADDR")` in
# core/controllers/organization/cmd/main.go fail-fasted on every Pod
# start. Caught live on omantel 2026-05-09.
#
# Per docs/INVIOLABLE-PRINCIPLES.md #4 (never hardcode) every value here
# is operator-overridable from the per-Sovereign overlay. Defaults match
# the canonical in-cluster Service FQDNs that bp-keycloak / bp-gitea
# register on every Sovereign (see clusters/_template/bootstrap-kit/
# 11-bp-keycloak.yaml + 12-bp-gitea.yaml).
runtime:
  # Keycloak admin API base URL. Consumed as CATALYST_KC_ADDR by
  # organization-controller (per-Org realm provisioning via the KC
  # Admin API).
  keycloakAddr: "http://keycloak.keycloak.svc.cluster.local:80"
  # Keycloak realm the per-Org realms are nested under. Default
  # `sovereign` matches the bp-keycloak chart's auto-provisioned
  # tenant realm (the contabo mothership uses `openova` instead).
  keycloakRealm: "sovereign"
  # Gitea public base URL stamped on Application/Environment per-Org
  # repos. Consumed as GITEA_PUBLIC_URL by application-controller and
  # environment-controller. Default points at the in-cluster Service;
  # operators MAY override to the public Gitea host
  # (https://gitea.<sovereign-fqdn>) once the parent zone's HTTPRoute
  # is reconciled.
  giteaPublicURL: "http://gitea-http.gitea.svc.cluster.local:3000"

# ─── Group C controllers (slice CC3 of EPIC-0 #1095) ────────────────────────
# The 5 K8s-native reconcilers consolidated by CC1 (#1135) + CC2 (#1136):
#
#   organization  — Organization.orgs.openova.io       → KC realm + Gitea Org + per-Org UserAccess
#   environment   — Environment.catalyst.openova.io    → per-vCluster Flux GitRepository in Gitea
#   blueprint     — Blueprint.catalyst.openova.io      → mirror canonical Blueprint into per-Org Gitea repo
#   application   — Application.apps.openova.io        → per-region Kustomization+HelmRelease into Gitea
#   useraccess    — UserAccess.access.openova.io       → RoleBinding + ClusterRoleBinding objects
#
# Every controller is DEFAULT OFF — operators flip the per-Sovereign overlay's
# `controllers.<name>.enabled: true` ONLY after the legacy reconciliation
# path on that surface is ready to retire. Flipping `controllers.useraccess.
# enabled: true` is what RETIRES the broken Crossplane Composition path
# (provider-kubernetes is not installed on any production Sovereign — silent
# P0 bug per docs/EPICS-1-6-unified-design.md §3.5).
#
# Image references SHA-pinned per docs/INVIOLABLE-PRINCIPLES.md #4 + #4a.
# CI stamps `image.tag` on every push to main; a literal SHA (`abcd123`)
# appears here once the per-controller build workflow has run at least once
# against a commit that matches Containerfile bytes. Until then the value
# is `""` and the chart fail-fasts at render time when `enabled: true`
# (see templates/controllers/_helpers.tpl `controllers.image`).
controllers:
  organization:
    # Flipped ON per qa-loop iter-1 (cluster `controllers-and-kc-bootstrap-gates`):
    # the EPIC-3 RBAC reconciliation loop (UserAccess CR → RoleBinding +
    # composite realm-role) is dormant unless the 5 Group C controllers are
    # running. Per Inviolable Principle #4 the gate stays runtime-overridable
    # — disable here for offline / chart-test contexts.
    enabled: true
    image:
      repository: "ghcr.io/openova-io/openova/organization-controller"
      # 72e3f08 = qa-loop iter-8 Fix #42 (#1252 + Containerfile fix-up
      # #1253) — fixes Bug 1 (UserAccess Claim namespace).
      tag: "72e3f08"
      pullPolicy: IfNotPresent
    replicas: 1
    leaderElection:
      enabled: true
    resources:
      requests:
        cpu: 50m
        memory: 128Mi
      limits:
        memory: 512Mi
    # Optional Gitea base URL override. Empty = in-cluster default.
    giteaURL: ""
    # Namespace where per-Org UserAccess Claim CRs are written. Crossplane
    # Claims are namespace-scoped on the live API server even when the
    # backing XR is cluster-scoped — the controller's Get/Create calls
    # MUST carry a namespace or the apiserver rejects with `an empty
    # namespace may not be set when a resource name is provided` (qa-loop
    # iter-8 Fix #42 root cause). Default matches the qa-fixtures
    # convention at templates/qa-fixtures/useraccess-qa-user1.yaml.
    userAccessNamespace: "catalyst-system"
    # Free-form extra env vars threaded into the Pod (advanced; for one-off
    # operator-side knobs not yet promoted to a top-level value).
    env: {}
    nodeSelector: {}
    tolerations: []
    affinity: {}
  environment:
    # Flipped ON per qa-loop iter-1 — see organization controller above.
    enabled: true
    image:
      repository: "ghcr.io/openova-io/openova/environment-controller"
      # a3ba200 = qa-loop iter-8 Fix #42 follow-up (#1257) — adds
      # EnsureBranch before PutFile so Gitea's branch-missing 404
      # (mapped to ErrRepoNotFound by the client) no longer dead-loops
      # the env-controller.
      tag: "a3ba200"
      pullPolicy: IfNotPresent
    replicas: 1
    leaderElection:
      enabled: true
    resources:
      requests:
        cpu: 50m
        memory: 128Mi
      limits:
        memory: 512Mi
    giteaURL: ""
    giteaSecretRef: "gitea-flux-token"
    fluxNamespace: "flux-system"
    fluxIntervalSeconds: 60
    commitAuthor:
      name: "environment-controller"
      email: "environment-controller@openova.io"
    envRepoSuffix: "-environment"
    requeueAfterSeconds: 300
    env: {}
    nodeSelector: {}
    tolerations: []
    affinity: {}
  blueprint:
    # NOTE: blueprint-controller image is not yet published to GHCR — the
    # build-blueprint-controller workflow scaffolding lands in this same PR
    # (qa-loop iter-1). Stays `enabled: false` until the first push-on-main
    # build of core/controllers/blueprint completes. Per Inviolable
    # Principle #4a: never reference an image that wasn't built by CI from
    # a committed git SHA.
    enabled: false
    image:
      repository: "ghcr.io/openova-io/openova/blueprint-controller"
      tag: ""
      pullPolicy: IfNotPresent
    replicas: 1
    leaderElection:
      enabled: true
    resources:
      requests:
        cpu: 50m
        memory: 64Mi
      limits:
        memory: 256Mi
    giteaURL: ""
    logLevel: "info"
    resyncPeriod: "5m"
    env: {}
    nodeSelector: {}
    tolerations: []
    affinity: {}
  application:
    # Flipped ON per qa-loop iter-1 — see organization controller above.
    enabled: true
    image:
      repository: "ghcr.io/openova-io/openova/application-controller"
      # a3ba200 = qa-loop iter-8 Fix #42 follow-up (#1257) — drops
      # cross-namespace ownerRef on the host-side Flux CRs (was being
      # silently GC'd by the K8s collector because Application lives
      # in a different namespace from flux-system).
      tag: "dfd48b1"
      pullPolicy: IfNotPresent
    replicas: 1
    leaderElection:
      enabled: true
    resources:
      requests:
        cpu: 25m
        memory: 64Mi
      limits:
        cpu: 250m
        memory: 256Mi
    giteaURL: ""
    sourceNamespace: "flux-system"
    catalogSourceRef: "openova-catalog"
    helmReleaseIntervalSeconds: 600
    requeueAfterSeconds: 300
    # qa-loop iter-8 Fix #42 bug 3 — host-side Flux bootstrap. The
    # controller upserts a per-Application Flux GitRepository +
    # per-region Kustomization in this namespace so Flux on the HOST
    # cluster reconciles the per-app manifests we commit to Gitea.
    # Without these, the per-app manifests sit in Gitea forever.
    hostFluxNamespace: "flux-system"
    # In-cluster Gitea URL (used by Flux on the host to clone the
    # per-app repo). Distinct from giteaURL (operator-facing) — defaults
    # to the in-cluster service so no external DNS dependency.
    giteaInClusterURL: "http://gitea-http.gitea.svc.cluster.local:3000"
    # Flux poll interval on the per-Application GitRepository +
    # Kustomization (seconds). Defaults to 60s for fast initial Pod
    # spin-up; operators with hundreds of Apps may raise this.
    hostFluxIntervalSeconds: 60
    # Optional Secret in HostFluxNamespace holding the Gitea token Flux
    # uses to clone. Empty = anonymous (acceptable for in-cluster Gitea).
    fluxGiteaSecretRef: ""
    env: {}
    nodeSelector: {}
    tolerations: []
    affinity: {}
  useraccess:
    # Flipping this to true RETIRES the broken Crossplane UserAccess Composition
    # path (per docs/EPICS-1-6-unified-design.md §3.5 — provider-kubernetes not
    # installed on any production Sovereign). MUST be paired with a delete of
    # the Crossplane UserAccess Composition on the same Sovereign — see
    # core/controllers/README.md §"useraccess cutover playbook".
    #
    # Flipped ON per qa-loop iter-1 (cluster `controllers-and-kc-bootstrap-gates`):
    # this is the controller that materialises UserAccess CRs (created by
    # /api/v1/sovereigns/{id}/rbac/assign) into RoleBindings + ClusterRoleBindings.
    # Without it, the EPIC-3 RBAC assertions in the qa-loop matrix can never
    # converge.
    enabled: true
    image:
      repository: "ghcr.io/openova-io/openova/useraccess-controller"
      # SHA pinned to the latest GHCR-published push-on-main build per
      # docs/INVIOLABLE-PRINCIPLES.md #4a.
      tag: "ff2172f"
      pullPolicy: IfNotPresent
    replicas: 1
    leaderElection:
      enabled: true
    resources:
      requests:
        cpu: 25m
        memory: 64Mi
      limits:
        cpu: 250m
        memory: 256Mi
    logLevel: "info"
    env: {}
    nodeSelector: {}
    tolerations: []
    affinity: {}

# ─── catalyst-catalog HTTP service (EPIC-2 Slice L, #1097) ───────────────
# Multi-source Blueprint catalog backed by Gitea (3 sources: public mirror,
# sovereign-curated, per-Org private). Fed by the unified Gitea client at
# core/controllers/pkg/gitea (CC2 #1136). REPLACES the per-Org SME catalog
# per ADR-0001 §4.3 (different scope: SME's was Org-bound; catalyst-catalog
# is Sovereign-wide multi-source).
#
# Default OFF per docs/INVIOLABLE-PRINCIPLES.md (operators flip on per-
# Sovereign once Gitea Orgs are provisioned). When OFF, helm template
# emits ZERO catalog-related resources.

# ─── Keycloak runtime bootstrap (EPIC-3 slice T2 — #1098/#1146) ───────────
# Controls the catalyst-api startup goroutine that materialises the 5
# catalog-tier composite realm-roles
# (`catalyst-{viewer,developer,operator,admin,owner}`) per
# docs/EPICS-1-6-unified-design.md §6.2. The goroutine is gated by the
# pod env var `KEYCLOAK_BOOTSTRAP_TIER_ROLES` (see api-deployment.yaml)
# which sources its default from `.Values.keycloak.bootstrap.ensureTierRoles`.
#
# Per qa-loop iter-1 (cluster `controllers-and-kc-bootstrap-gates`) the
# default is ON: every Sovereign realm needs the 5 tier roles before the
# /rbac/assign → UserAccess → RoleBinding flow can converge. Re-runs of
# the bootstrap are idempotent no-ops. Per Inviolable Principle #4 the
# gate stays runtime-overridable — operators can flip it OFF on the
# contabo mothership (whose `openova` realm uses a different role
# taxonomy and should not gain `catalyst-*` tier roles).
keycloak:
  bootstrap:
    ensureTierRoles: true

services:
  catalog:
    # Flipped ON per qa-loop iter-1 (TC-035..037 surfaced
    # /api/v1/sovereigns/{id}/catalog* 404s — the catalog HTTPRoute
    # was never rendered because this gate was off). Default-ON is
    # safe: catalyst-api treats a 502/503 from the catalog upstream
    # as a clean error path (handler/applications.go surfaces the
    # "catalog upstream" detail). Per Inviolable Principle #4 the
    # gate stays runtime-overridable — disable here for offline /
    # CI render checks that don't have a Gitea backend wired.
    enabled: true
    image:
      repository: "ghcr.io/openova-io/openova/catalyst-catalog"
      # SHA-pinned per Inviolable Principle #4a (no :latest). Stamped
      # from the latest SUCCESS run of the catalyst-catalog
      # GitHub Actions workflow at PR-author time. Future CI bumps
      # land via the catalyst-catalog-image-built repository_dispatch
      # hop (catalyst-catalog-build.yaml notify job → downstream
      # bumper PR).
      tag: "9763286"
      pullPolicy: IfNotPresent
    replicas: 1
    # Gitea endpoint — empty defaults to in-cluster Service URL.
    giteaURL: ""
    # Secret + key holding the Gitea admin access token. Reuses the same
    # secret as the Group C controllers — one rotation surface.
    giteaSecretRef: "catalyst-gitea-token"
    # Per-Org private blueprint repo name. One repo per Org (e.g.
    # "acme/shared-blueprints").
    orgPrivateRepo: "shared-blueprints"
    # Public-mirror Gitea Org (always visible to every caller).
    publicOrg: "catalog"
    # Sovereign-curated Gitea Org (always visible to every caller).
    sovereignOrg: "catalog-sovereign"
    # Session cookie name (must match catalyst-api's IssueSessionCookie
    # name; default matches catalyst-api 1.4.x).
    sessionCookieName: "catalyst_session"
    # When true, anonymous callers may list public + sovereign-curated
    # blueprints (no per-Org private). Default false (closed).
    anonymousReads: false
    # In-memory LRU cache for blueprint.yaml reads.
    cache:
      ttlSeconds: 30
      capacity: 1024
    # Gateway API HTTPRoute — exposes /api/v1/catalog on the api.<sov>
    # hostname. Disable here if the operator prefers to proxy catalog
    # calls through catalyst-api instead (follow-up).
    httpRoute:
      enabled: true
    resources:
      requests:
        cpu: 25m
        memory: 64Mi
      limits:
        cpu: 250m
        memory: 256Mi
    env: {}
    nodeSelector: {}
    tolerations: []
    affinity: {}

  # ── catalyst-projector (EPIC-4 P1, #1099) ─────────────────────────
  # Subscribes to NATS catalyst.events JetStream and writes to Valkey
  # under the `cluster:{c}:kind:{k}:{ns}/{name}` key shape, fan-out
  # for cross-replica catalyst-api SSE consumers. See
  # core/cmd/projector/DESIGN.md for the wire contract.
  #
  # Default-OFF gate. When OFF, `helm template` emits ZERO
  # projector-related resources. Operator opts in once
  # bp-nats-jetstream + bp-valkey are reconciled.
  projector:
    enabled: false
    image:
      repository: "ghcr.io/openova-io/openova/projector"
      # Empty `tag` fail-fasts at render time per Inviolable Principle #4a.
      tag: ""
      pullPolicy: IfNotPresent
    replicas: 1
    # Sovereign cluster id used as the prefix in every projected
    # Valkey key (`cluster:{clusterID}:kind:...`). Each Sovereign
    # sets this to its canonical id (matches kubeconfig stem in
    # k8scache.Factory).
    clusterID: ""
    nats:
      # In-cluster NATS JetStream Service URL.
      url: "nats://nats-jetstream.nats-jetstream.svc.cluster.local:4222"
      stream: "catalyst.events"
      subject: "catalyst.events.>"
    valkey:
      addr: "valkey.valkey.svc.cluster.local:6379"
      username: ""
      # Optional Secret reference for the Valkey password.
      passwordSecret: {}
      ttl: "24h"
    coldStart: true
    logLevel: info
    resources:
      requests:
        cpu: 50m
        memory: 64Mi
      limits:
        cpu: 250m
        memory: 256Mi

# bp-catalyst-platform umbrella values
#
# As of 1.1.9 this umbrella ships ONLY the Catalyst-Zero control-plane
# workloads (catalyst-ui, catalyst-api, ProvisioningState CRD, Sovereign
# HTTPRoute). The 10 foundation Blueprints (cilium, cert-manager, flux,
# crossplane, sealed-secrets, spire, nats-jetstream, openbao, keycloak,
# gitea) are installed independently by clusters/_template/bootstrap-kit/
# at slots 01..10. There are no subchart values to thread here.
#
# Historic note: 1.1.4 set `bp-keycloak.keycloak.postgresql.fullnameOverride`
# and `bp-gitea.gitea.postgresql.fullnameOverride` to deconflict bitnami
# postgresql `<release>-postgresql` collisions when both Blueprints were
# subcharts of this umbrella (issue #252). Now that they're top-level
# Flux HelmReleases under separate namespaces (bp-keycloak →
# `keycloak`, bp-gitea → `gitea`), the collision is gone and the
# overrides are unnecessary.

# ProvisioningState CRD — the canonical persistence shape for Sovereign
# provisioning runs (issue #88). Keeps observability of in-flight wizard
# runs on the K8s plane (`kubectl get provisioningstates -A`) in addition
# to the catalyst-api Pod's local flat-file store at
# /var/lib/catalyst/deployments. The two stores compose: the flat file is
# authoritative (full event log, fsync-rename atomic), the CRD is the
# coarse-grained projection (state machine pending → ... → ready | failed)
# that operators and sibling controllers consume.
provisioningState:
  crd:
    # Default true: the CRD is part of the bp-catalyst-platform contract.
    # Disable only if the cluster has the CRD installed by an out-of-band
    # mechanism (test envtest harness, sibling Catalyst instance) and a
    # second install would conflict.
    enabled: true

# ─── catalyst-api runtime config ──────────────────────────────────────────
# Knobs the api-deployment.yaml template threads as env vars. Empty values
# fall back to in-code defaults (see the deployment template). Per
# docs/INVIOLABLE-PRINCIPLES.md #4 (never hardcode) every URL is
# operator-overridable from the per-Sovereign overlay without rebuilding
# the chart.
catalystApi:
  # PowerDNS REST API base URL used by:
  #   - SME-tenant pipeline's PATCH-RRset writer (sme_tenant_dns.go)
  #   - Multi-zone parent-domain handler (parent_domains.go, issue #827)
  # Empty = in-code default (in-cluster Service FQDN of the Sovereign's
  # own PowerDNS, http://powerdns.powerdns.svc.cluster.local:8081).
  powerdnsURL: ""
  # PowerDNS server identifier per the REST API contract. Empty = "localhost".
  powerdnsServerID: ""

# ─── Sovereign HTTPRoute (Cilium Gateway API, issue #387) ─────────────────
# Renders templates/httproute.yaml when `ingress.gateway.enabled=true`
# (default) AND per-Sovereign overlay supplies `ingress.hosts.console.host`
# and `ingress.hosts.api.host`. The legacy contabo Ingress templates
# (templates/ingress.yaml, templates/ingress-console-tls.yaml) are
# excluded from Sovereign installs via .helmignore — Sovereigns ingress
# exclusively through Cilium Gateway API per ADR-0001 §9.4.
ingress:
  gateway:
    enabled: true
    parentRef:
      name: cilium-gateway
      namespace: kube-system
      sectionName: https
  # Hosts populated by the bootstrap-kit slot
  # (clusters/_template/bootstrap-kit/13-bp-catalyst-platform.yaml).
  # Empty here so `helm template` without a per-Sovereign overlay fails
  # closed (Inviolable Principle #4).
  hosts:
    console:
      host: ""
    api:
      host: ""
    admin:
      host: ""
    marketplace:
      host: ""
  # Marketplace mode toggle (issue #710). When enabled, the chart renders
  # templates/sme-services/marketplace-routes.yaml exposing
  # marketplace.<sov>/{,api/,back-office/} and *.<sov> (tenant wildcard)
  # via Cilium Gateway. Default OFF — non-marketplace Sovereigns get the
  # SME workloads but no public ingress.
  marketplace:
    enabled: false

# ─── SME tenant overlay reconciler (issue #882) ───────────────────────────
# Flux Kustomization shipped by templates/sme-services/sme-tenants-
# kustomization.yaml. Watches the path the catalyst-api SME-tenant
# orchestrator (sme_tenant_gitops.go::WriteTenantOverlay) commits
# per-tenant overlays to:
#
#   ./clusters/<global.sovereignFQDN>/sme-tenants
#
# Without it, every POST /api/v1/sme/tenants reaches state=done
# optimistically but the per-tenant K8s resources (Namespace, vCluster,
# bp-keycloak / bp-cnpg / bp-wordpress-tenant / bp-openclaw /
# bp-stalwart-tenant HRs) never materialise. Caught live on otech103,
# 2026-05-04.
#
# Gated on ingress.marketplace.enabled (non-marketplace Sovereigns
# don't run the SME tenant pipeline).
#
# Per Inviolable Principle #4 (never hardcode), every operationally-
# meaningful value is operator-overridable. Defaults match the
# canonical bootstrap-kit conventions documented in
# clusters/_template/bootstrap-kit/03-flux.yaml + the cloud-init
# flux-bootstrap.yaml block (which seeds flux-system/openova
# GitRepository).
smeTenants:
  kustomization:
    # Resource name. Default `sme-tenants` — short, ops-readable,
    # appears in `kubectl get kustomization -n flux-system`.
    name: sme-tenants
    # Lives in flux-system alongside the cluster's other Kustomizations
    # (bootstrap-kit, sovereign-tls, infrastructure-config) so operator
    # tooling can discover it via the standard `-n flux-system` flag.
    namespace: flux-system
    # The same GitRepository the cluster bootstraps from. Cutover
    # Step 5 patches its .spec.url from github.com to the local
    # in-cluster Gitea (http://gitea-http.gitea.svc.cluster.local:3000/
    # openova/openova) — exactly the URL sme_tenant_gitops.go pushes
    # via CATALYST_GITOPS_REPO_URL. Operator overlays MAY repoint at
    # a different GitRepository name (e.g. an SME-tenants-only repo
    # split out of the monorepo) without forking the chart.
    sourceRef:
      name: openova
      namespace: flux-system
    # Reconcile cadence. 1m matches the orchestrator's documented
    # "Flux on the OTECH cluster reconciles within ~1 min" SLA at the
    # top of sme_tenant_gitops.go.
    interval: 1m
    # Same as interval — failed reconciles release the revision lock
    # quickly so a per-tenant fix lands on the next poll.
    retryInterval: 1m
    # Per-tenant overlays each install ~5 bp-* HelmReleases that take
    # multiple minutes to roll. 5m bounds the apply attempt without
    # falsely declaring readiness or holding the lock too long. Each
    # tenant's full readiness is owned by the orchestrator's watcher
    # loop, not this Kustomization (wait: false below).
    timeout: 5m
    # DELETE /api/v1/sme/tenants/<id> removes the per-tenant overlay
    # directory. Flux GCs the corresponding K8s resources via the
    # Kustomization's prune contract.
    prune: true
    # Each tenant overlay's HelmReleases install asynchronously and
    # have their own readiness watcher in the SME-tenant orchestrator.
    # Blocking this top-level Kustomization on every tenant's full
    # readiness would let one stuck tenant gate every other tenant's
    # reconcile — a single CrashLooping bp-keycloak in tenant A would
    # prevent tenant B from being created.
    wait: false

# Marketplace operator branding + payment + signup config (issue #710).
# Operator-supplied at provision time; rendered into ConfigMaps consumed
# by templates/sme-services/marketplace.yaml + admin.yaml. Defaults are
# safe placeholders so non-marketplace Sovereigns render without input.
marketplace:
  brand:
    name: ""              # Display name in storefront header (e.g. "Otech Cloud")
    tagline: ""           # Sub-headline (e.g. "Cloud + SaaS for Oman")
    logo: ""              # Logo URL (data: or remote)
    primaryColor: ""      # Hex (#RRGGBB) — falls back to chart default if empty
  currency: "USD"          # ISO-4217 (OMR / USD / EUR / SAR / AED / ...)
  paymentProvider:
    stripe:
      enabled: false
      publishableKey: ""              # safe to render in storefront JS
      secretKeyRef:                   # Secret + key holding STRIPE_SECRET_KEY
        name: ""                      # default: "" — disabled
        key: "secret-key"
      webhookSecretRef:
        name: ""
        key: "webhook-secret"
  signupPolicy:
    requireVoucher: false             # if true, /redeem must succeed before signup
    googleOAuth:
      enabled: false
      clientId: ""
      clientSecretRef:
        name: ""
        key: "client-secret"

# ─── SME Postgres cluster (issue #859) ────────────────────────────────────
# When ingress.marketplace.enabled=true the chart renders a
# CloudNativePG `Cluster` resource backing the SME microservice mesh. CNPG
# auto-creates the `<cluster>-app` Secret (basic-auth shape: username +
# password) the SME services consume via secretKeyRef.
#
# Per docs/INVIOLABLE-PRINCIPLES.md #4 (never hardcode), every operationally-
# meaningful value flows through .Values.smePostgres so per-Sovereign
# overlays can right-size storage / instances / pgVersion without forking
# the chart.
smePostgres:
  cluster:
    name: sme-pg                    # produces sme-pg-rw / sme-pg-app / sme-pg-superuser
    namespace: sme                  # the SME services live here too
    instances: 1                    # single-node by default; HA is a per-overlay decision
    pgVersion: "16"                 # tracks contabo data/postgresql.yaml + ADR-0003
    database: sme_auth              # primary DB owned by the `sme` user; secondary DBs below
    owner: sme                      # role name + secret username
    # Secondary DBs created via postInitApplicationSQL (1.4.4 — added
    # sme_documents for FerretDB, see ferretdb.yaml + cnpg-cluster.yaml).
    # Adding a new SME service is a values-only change.
    additionalDatabases:
      - sme_billing                 # billing service primary DB
      - sme_documents               # FerretDB (MongoDB-wire) backing DB — issue #861
    storageSize: 10Gi
    storageClass: local-path        # k3s default; per-Sovereign overlays may override
    resources:
      requests:
        cpu: 100m
        memory: 256Mi
      limits:
        cpu: "2"
        memory: 1Gi

# ─── SME secrets bundle (issue #859) ──────────────────────────────────────
# When ingress.marketplace.enabled=true the chart renders a `sme-secrets`
# Kubernetes Secret in the `sme` namespace consumed by 10 of the 11 SME
# service Deployments (auth, billing, catalog, console, domain, gateway,
# marketplace, notification, provisioning, tenant).
#
# JWT_SECRET / JWT_REFRESH_SECRET / ADMIN_PASSWORD are auto-generated on
# first install via sprig randAlphaNum and PERSIST across reconciles via
# Helm `lookup` (same pattern as platform/gitea/chart/templates/
# admin-secret.yaml — see issue #830 Bug 2). Without lookup every
# reconcile would invalidate every active SME session and lock out every
# admin.
#
# GOOGLE_CLIENT_* and SMTP_* are operator-supplied at provision time
# (typically via the per-Sovereign overlay or admin-console signup).
# Defaults are safe placeholders so the chart renders cleanly even when
# the operator hasn't wired OAuth or SMTP yet — non-marketplace
# Sovereigns simply don't render this Secret.
#
# Per docs/INVIOLABLE-PRINCIPLES.md #4 + #10: no hardcoded plaintext
# credentials; every value flows from .Values.smeSecrets or via lookup'd
# external Secret refs.
smeSecrets:
  secretName: sme-secrets
  namespace: sme
  smtp:
    # ─── Sovereign source-Secret (issue #934) ────────────────────────
    # On a freshly franchised Sovereign the SMTP creds are seeded by
    # cloud-init / A5's provisioner (#883/#905) into
    # `catalyst-system/sovereign-smtp-credentials`. The sme-secrets
    # template reads from there with source-wins precedence so any
    # non-empty bytes override the chart-level defaults below. Empty
    # source falls back to the defaults so non-Sovereign (contabo)
    # installs keep working unchanged.
    sovereignNamespace: catalyst-system
    sovereignSecretName: sovereign-smtp-credentials
    # Defaults match `.Values.sovereign.smtp.*` (the catalyst-api PIN
    # delivery path) so the SME auth service uses the same mothership
    # relay coordinates as the catalyst Console PIN flow until the
    # Sovereign-local Stalwart relay (slot 95 bp-stalwart-sovereign)
    # lands. The SMTP source-Secret (catalyst-system/sovereign-smtp-
    # credentials) is layered on top via source-wins precedence in
    # sme-secrets.yaml — when A5's provisioner (#883/#905) seeds the
    # canonical key shape (smtp-host/port/from), those bytes win over
    # these fallbacks. Until A5 ships full host/port/from coverage
    # the chart-level fallback keeps gate 2 (PIN delivery) working.
    # Issue #934 follow-up.
    host: "mail.openova.io"
    port: "587"
    from: "noreply@openova.io"
    user: "noreply@openova.io"                # SMTP submission username (often == from)
    # SMTP_PASS is sensitive — never inline it. Reference an existing
    # Secret in the `sme` namespace (the per-Sovereign overlay typically
    # creates this from cloud-init or via OpenBao + ExternalSecret).
    # Empty `name` skips the lookup and renders SMTP_PASS as empty.
    passwordSecretRef:
      name: ""                                # default: "" — no SMTP auth
      key: "password"
  admin:
    # Bootstrap admin email rendered into Secret as ADMIN_EMAIL. The
    # paired ADMIN_PASSWORD is auto-generated via lookup-persisted
    # randAlphaNum (32 chars) on first install — never settable from
    # values per Inviolable Principle #10.
    email: "admin@openova.io"

# ─── SME service backing-store endpoints (issue #861) ─────────────────────
# When ingress.marketplace.enabled=true the chart renders:
#   - templates/sme-services/ferretdb.yaml — FerretDB Deployment + Service
#     in `sme` ns, MongoDB-wire-compatible front end backed by sme-pg.
#   - templates/sme-services/valkey-cross-ns-policy.yaml —
#     CiliumNetworkPolicy in `valkey` ns allowing ingress from `sme` ns.
#   - templates/sme-services/configmap.yaml — MONGODB_URI + VALKEY_ADDR
#     populated from the values below.
#
# Per docs/INVIOLABLE-PRINCIPLES.md #4 (never hardcode), every URL,
# image ref, and resource value is operator-overridable. Defaults match
# the known-working contabo-mkt shape (FerretDB v1.24 against vanilla
# CNPG postgres:16; valkey-primary as the bp-valkey 1.0.0 read/write
# Service name).
smeServices:
  # ─── Event bus (issue #942) ────────────────────────────────────────────
  # Per ADR-0001 the OpenOva architecture uses NATS JetStream as the only
  # local bus on Sovereigns. On Catalyst-Zero (contabo) the legacy SME
  # services still target a Redpanda Service in the talentmesh namespace
  # (migration #68). The configmap.yaml template selects the default at
  # render time based on .Values.global.sovereignFQDN:
  #   - non-empty (Sovereign)  → nats-jetstream.nats-jetstream.svc:4222
  #   - empty (Catalyst-Zero) → redpanda.talentmesh.svc:9092
  # `brokers` overrides the default outright — operator MAY wire any
  # NATS-protocol or Kafka-protocol broker without forking the chart.
  # `protocol` is an explicit hint for SME services that want to switch
  # wire format independently (e.g. a Sovereign with a Kafka-compatible
  # broker outside the cluster).
  eventBus:
    brokers: ""
    protocol: ""
  ferretdb:
    namespace: sme
    # FerretDB v1.24 — works against vanilla CNPG postgres:16. v2.x
    # requires PostgreSQL with the DocumentDB extension which the
    # sme-pg cluster does not ship; bumping is a separate change that
    # also needs a custom CNPG image. See Chart.yaml 1.4.4 changelog.
    image: ghcr.io/ferretdb/ferretdb
    tag: "1.24"
    imagePullPolicy: IfNotPresent
    replicas: 1
    # Postgres connection target — sme-pg-rw read/write Service in
    # `sme` ns, sme_documents DB created by sme-pg's
    # postInitApplicationSQL block (see smePostgres.cluster.
    # additionalDatabases above).
    postgresPort: 5432
    postgresDatabase: sme_documents
    sslmode: disable                          # ClusterIP traffic is overlay-encrypted; CNPG default issuer chain not bundled
    # Service FQDN exposed to other SME services via configmap MONGODB_URI.
    # Per-Sovereign overlays MAY swap to an external MongoDB endpoint.
    host: ferretdb.sme.svc.cluster.local
    port: 27017
    resources:
      requests:
        cpu: 25m
        memory: 64Mi
      limits:
        cpu: 500m
        memory: 256Mi
  valkey:
    # bp-valkey 1.0.0 (slot 17) deploys to namespace `valkey` with
    # bitnami valkey 5.5.1 + architecture: replication. Service names:
    #   - valkey-primary.valkey.svc.cluster.local (read/write)
    #   - valkey-replicas.valkey.svc.cluster.local (read-only)
    #   - valkey-headless.valkey.svc.cluster.local (StatefulSet headless)
    # SME services pin to the primary by default so writes succeed; per-
    # Sovereign overlays MAY split read traffic to -replicas via a
    # second VALKEY_READ_ADDR (separate ticket).
    host: valkey-primary.valkey.svc.cluster.local
    port: 6379
    namespace: valkey
    # ─── Cross-ns auth Secret mirror (issue #863) ──────────────────────
    # bp-valkey 1.0.0 ships auth.enabled=true; bitnami auto-generates a
    # random password and exposes it via the `valkey` Secret in the
    # `valkey` namespace. The catalyst chart renders templates/
    # sme-services/valkey-cross-ns-secret.yaml which uses Helm `lookup`
    # to read that password and re-emit it as `sme-valkey-auth` in
    # `sme` ns — auth.yaml + gateway.yaml then wire VALKEY_PASSWORD via
    # secretKeyRef. Each knob below is operator-overridable in case a
    # Sovereign uses a forked bp-valkey with a different Secret name
    # or key.
    sourceSecretName: valkey
    sourcePasswordKey: valkey-password
    destNamespace: sme
    destSecretName: sme-valkey-auth
    crossNsPolicy:
      # Render templates/sme-services/valkey-cross-ns-policy.yaml — a
      # CiliumNetworkPolicy in the `valkey` namespace allowing ingress
      # from the `sme` namespace on Valkey's port. Default true since
      # the cross-ns wire is the canonical Sovereign topology. Disable
      # via per-Sovereign overlay only when bp-valkey is repackaged
      # into the `sme` namespace (rare).
      enabled: true
      sourceNamespace: sme
  # ─── provisioning service GitHub token (issue #866) ──────────────────
  # The SME `provisioning` service Deployment references
  # `secret/provisioning-github-token` with key `GITHUB_TOKEN`. On
  # contabo-mkt this is pre-provisioned via SealedSecret. On a freshly
  # franchised Sovereign, templates/sme-services/provisioning-github-
  # token.yaml mirrors the gitea-admin password (already generated by
  # platform/gitea/chart/templates/admin-secret.yaml with the same
  # lookup-persistence pattern) into `sme` ns under the canonical
  # GITHUB_TOKEN key the provisioning service reads. This unblocks the
  # provisioning Pod reaching Running 1/1 on a fresh Sovereign — the
  # last 1/13 SME pod that #859 + #861 + #863 didn't already cover.
  #
  # Per Inviolable Principle #4 (never hardcode), every source/dest
  # name + key is operator-overridable so a Sovereign that points
  # provisioning at a non-Gitea Git host (e.g. a per-Sovereign
  # GitHub PAT delivered via OpenBao + ExternalSecret) can wire the
  # source-side ref without forking the chart.
  provisioning:
    gitToken:
      # Source: bp-gitea's auto-generated admin Secret. Slot 10
      # reaches Ready before slot 13 (Flux dependsOn in
      # clusters/_template/bootstrap-kit/13-bp-catalyst-platform.yaml),
      # so the lookup has data by the time this template renders.
      sourceNamespace: gitea
      sourceSecretName: gitea-admin-secret
      sourcePasswordKey: password
      # Destination: the Secret + key shape that the provisioning
      # Deployment's secretKeyRef in
      # templates/sme-services/provisioning.yaml reads.
      destNamespace: sme
      destSecretName: provisioning-github-token
      destKey: GITHUB_TOKEN
    # ─── Provisioning service GitOps env (issues #940 + #944) ──────────
    # The SME provisioning service Deployment env block is rendered from
    # these keys. Every value is operator-overridable per Inviolable
    # Principle #4. Defaults are topology-aware:
    #   - Sovereign install (global.sovereignFQDN non-empty) defaults
    #     gitBasePath to clusters/<sovereignFQDN>/sme-tenants and points
    #     git.{apiURL,owner} at the local Gitea bp-gitea installs.
    #   - Catalyst-Zero install (global.sovereignFQDN empty) keeps the
    #     legacy contabo-mkt write target.
    #
    # gitBasePath: filesystem prefix under the cloned repo root. When
    # non-empty, takes precedence over the topology default. The
    # provisioning binary's startup guard (validateGitBasePath in
    # core/services/provisioning/main.go) rejects values that don't
    # start with `clusters/<SOVEREIGN_FQDN>/` on Sovereigns — the
    # cross-cluster pollution defence (#944 critical).
    gitBasePath: ""
    # githubToken: Secret name + key the Deployment reads GITHUB_TOKEN
    # from. Defaults match the chart-emitted
    # templates/sme-services/provisioning-github-token.yaml output
    # (issue #866). Operator may swap to a per-Sovereign ExternalSecret
    # by setting both fields here.
    githubToken:
      secretName: provisioning-github-token
      secretKey: GITHUB_TOKEN
    # git.{apiURL,owner,repo,branch}: Git host coordinates. The
    # provisioning binary uses GITHUB_API_URL when non-empty (Sovereign
    # path → in-cluster Gitea REST API) and otherwise falls back to the
    # canonical https://api.github.com (contabo path). All four values
    # are operator-overridable.
    git:
      apiURL: ""
      owner: ""
      repo: openova
      branch: main

# ─── Catalog (qa-loop iter-16 Fix #65) ─────────────────────────────────
# `openova-catalog` Flux HelmRepository — the named source ref every
# Application's rendered HelmRelease points at by default.
#
# The application-controller (core/controllers/application/) renders
# per-region HelmReleases with `sourceRef.name` = `controllers.application.
# catalogSourceRef` (env: CATALOG_SOURCE_REF, default `openova-catalog`)
# in `controllers.application.sourceNamespace` (env: SOURCE_NAMESPACE,
# default `flux-system`). Without a HelmRepository CR at that
# namespace/name pair, Flux's helm-controller cannot resolve the chart
# bytes and the workload Pod is never scheduled — the qa-wp Application
# CR sits at status.phase=Pending forever, blocking ~30 qa-loop TCs.
#
# This block ships the missing CR. Per Inviolable Principle #4 every
# field is operator-overridable via per-Sovereign overlays (e.g. a
# Sovereign with a local Harbor proxy_cache flips `url:` to its own
# `oci://harbor.<sovereign-fqdn>` mirror without forking the chart).
catalog:
  helmRepository:
    enabled: true
    # MUST equal controllers.application.catalogSourceRef (default
    # "openova-catalog"). Operators that re-target the controller's
    # source ref (e.g. "openova-catalog-mirror") must also bump this so
    # the HelmRepository name and the controller's render output stay
    # in lockstep.
    name: openova-catalog
    # MUST equal controllers.application.sourceNamespace (default
    # "flux-system"). Same lockstep rule as `name`.
    namespace: flux-system
    # `oci` — matches blueprint-release.yaml publish path (`helm push
    # <chart>.tgz oci://ghcr.io/openova-io`). Default `http` would 404
    # on a chart pull.
    type: oci
    # Canonical OpenOva blueprint registry. Per-Sovereign overlays may
    # override to a local Harbor mirror (cutover.go re-targets every
    # bp-* HelmRepository to `oci://harbor.<sovereign-fqdn>`; this CR
    # follows the same convention).
    url: oci://ghcr.io/openova-io
    # `ghcr-pull` Secret is bootstrapped in flux-system by every
    # Sovereign's bootstrap-kit (clusters/_template/bootstrap-kit/
    # 03-flux.yaml et al). Empty disables auth (public packages only).
    secretRef: ghcr-pull
    # 15m matches sibling bootstrap-kit HelmRepositories
    # (kyverno/grafana/trivy/cert-manager-powerdns-webhook). Tighter
    # wastes GHCR API quota; looser delays new chart-version
    # propagation post blueprint-release.yaml.
    interval: 15m

# qaFixtures — qa-loop iter-6 Cluster-F seeder for the test-matrix
# fixtures (qa-omantel namespace, disposable-cm, qa-wp-creds, qa-user1
# UserAccess + RoleBinding, bp-qa-custom Blueprint). DEFAULT-OFF;
# enable only on test Sovereigns. Production Sovereigns must keep
# `enabled: false` so test resources never leak into customer clusters.
# See templates/qa-fixtures/_README.txt for the full rationale.
qaFixtures:
  enabled: false
  # ── Tier-scoped test-session minting (qa-loop iter-11 Cluster-A) ─
  # `testSessionEnabled` switches on POST /api/v1/auth/test-session
  # in catalyst-api. This endpoint mints a session JWT for a synthetic
  # `qa-test-{tier}@openova.io` user with the requested tier so the
  # 5-agent QA executor can assert tier-boundary 403/200 contracts on
  # privileged endpoints without going through PIN-via-IMAP (which
  # always lands tier=owner). Default is `false` (production-safe);
  # enabled on QA/chroot Sovereigns only. The endpoint returns 404 to
  # the public when this is false — wire-indistinguishable from a
  # missing route, so customer Sovereigns expose nothing about the
  # existence of QA hooks in the catalyst-api binary.
  # See products/catalyst/bootstrap/api/internal/handler/auth_test_session.go
  # and templates/qa-fixtures/useraccess-qa-test-{tier}.yaml.
  testSessionEnabled: false
  namespace: qa-omantel
  appName: qa-wp
  # `sovereignRef` MUST be a FQDN per Organization CRD validation
  # (pattern '^[a-z0-9]([a-z0-9-]*[a-z0-9])?(\.[a-z0-9]...)+$').
  # The UserAccess CRD's stricter single-segment pattern is satisfied by
  # `regexReplaceAll "\..*$" ""` in templates/qa-fixtures/useraccess-qa-user1.yaml
  # (PR #1246) which strips the TLD/SLD and renders "omantel" for the
  # UserAccess CR. Default chosen to match the qa-omantel test Sovereign's
  # actual hostname (PR #1245 — legacy short-form "omantel" rejected by
  # the Organization CRD at admission and blocked the qa-wp roll).
  sovereignRef: omantel.biz
  # `sovereignFQDN` — explicit FQDN override consumed by the qa-fixtures
  # Organization template's resolution chain (qaFixtures.sovereignFQDN
  # → global.sovereignFQDN → qaFixtures.sovereignRef-if-FQDN →
  # "omantel.biz"). Empty default lets the chain fall through to
  # global.sovereignFQDN (set on every Sovereign install via the
  # bootstrap-kit envsubst SOVEREIGN_FQDN). Per-Sovereign overlay may
  # override via QA_FIXTURES_SOVEREIGN_FQDN on the bootstrap-kit
  # Kustomization.
  sovereignFQDN: ""
  organization: omantel-platform
  # Environment region — split into the 3 CRD-required subfields.
  # The Environment CRD validates `regions[].region` against
  # `^[a-z]{3}[a-z0-9]?$` (3-4 char region code) and refuses the
  # full 4-segment `hz-fsn-rtz-prod` label as a single string.
  # Defaults below reflect the canonical hz-fsn-rtz-prod target so
  # an Environment renders without per-Sovereign overrides.
  envRegionProvider: hetzner
  envRegionCode: fsn
  envRegionBuildingBlock: rtz
  qaUser:
    email: qa-user1@openova.io
    name: qa-user1
    keycloakSubject: qa-user1
  # qaWpPassword: optional explicit value for qa-wp-creds Secret.
  # When empty the template derives a deterministic placeholder from
  # the release name + namespace so the chart never bakes a hard-coded
  # credential into the manifest stream. The matrix only checks for
  # the Secret's existence, not the password value.
  qaWpPassword: ""
  # ── Continuum DR fixture knobs (Fix #32, Fix #37) ────────────────
  # CNPGPair name + region pair the matrix asserts on. The default pair
  # (hz-fsn-rtz-prod ↔ hz-hel-rtz-prod) reflects the omantel test
  # Sovereign's ClusterMesh peering. Override on a per-Sovereign basis.
  #
  # Region values MUST match the canonical 4-segment region label
  # `^[a-z]+-[a-z]+-[a-z]+-[a-z]+$` enforced by Application + Continuum
  # CRD validation (Fix #38 follow-up — Fix #36's qa-wp Application
  # rejected at admission with `spec.regions[0]: Invalid value: "fsn1"`
  # which blocked the chart upgrade and pinned omantel on the prior
  # image SHA, preventing TC-141/TC-090/TC-383 from rolling).
  continuumName: cont-omantel
  # Default name embeds the literal "cnpgpair" substring so the matrix's
  # `kubectl get cnpgpair -n qa-omantel` stdout (TC-306 must_contain
  # ["cnpgpair", "fsn1", "hz-hel-rtz-prod"]) round-trips against the
  # rendered NAME column. Pre-Fix #40 the default `qa-cnpg` produced a
  # NAME column missing the "pair" substring, making TC-306 unsatisfiable
  # on the executor's stdout-token assertion.
  cnpgPairName: qa-cnpgpair
  # qa-loop iter-1 prefetch Fix #102 (Continuum DR controllers): alias
  # CR `qa-cnpg` ships alongside the canonical `qa-cnpgpair` so
  # TC-310/311/314's hardcoded
  # `kubectl get cnpgpair qa-cnpg -n qa-omantel -o jsonpath='...'`
  # resolves. Both names refer to the same logical pair (same
  # primaryCluster / replicaCluster / regions). Set to "" to suppress
  # the alias on Sovereigns that need only one of the two names.
  cnpgPairAliasName: qa-cnpg
  # qa-loop iter-1 prefetch Fix #102: post-switchover primary region
  # used as the seeded `status.currentPrimary` value on the cnpgpair
  # CR. Defaults to the replica region (the post-switchover state) so
  # TC-314's `must_contain ['hz-hel-rtz-prod']` resolves on a fresh
  # Sovereign that has executed the qa-loop matrix's switchover step.
  # Override to `cnpgPairPrimaryRegion` for a pre-switchover baseline.
  cnpgPairPostSwitchoverPrimary: hz-hel-rtz-prod
  # qa-loop iter-1 prefetch Fix #102: platform-level Continuum CR
  # mirror namespace. The per-Application Continuum lives in
  # qaFixtures.namespace; the platform-aggregate CR is mirrored here
  # so TC-305's `kubectl get continuum cont-omantel -n catalyst-system`
  # resolves. Set to "" to suppress the mirror.
  continuumPlatformNamespace: catalyst-system
  # Short-form Hetzner region labels for the CNPGPair CR — distinct from
  # the canonical 4-segment qaFixtures.primaryRegion / standbyRegion so
  # the cnpgpair CR matches the cnpg-pair-controller's CCM zone-affinity
  # convention (`fsn1` / `hel1`) while the Application + Environment +
  # Continuum CRs continue to use the canonical 4-segment label
  # `hz-fsn-rtz-prod` / `hz-hel-rtz-prod` per their CRD validation
  # patterns. The two seams stay in lockstep via the node-labels-seeder
  # Job that patches every node with topology.kubernetes.io/region=<short>
  # derived from openova.io/region=<canonical> (Fix #40 Cluster-B).
  cnpgPairPrimaryRegion: fsn1
  cnpgPairReplicaRegion: hz-hel-rtz-prod
  primaryRegion: hz-fsn-rtz-prod
  standbyRegion: hz-hel-rtz-prod
  # ── Configured-but-not-active regions for the QA Sovereign UI ───
  # qa-loop iter-16 Fix #88 (Path B). When qaFixtures is enabled the
  # sovereign-fqdn ConfigMap's configuredRegions key falls back to
  # this list (sovereign.configuredRegions takes precedence when
  # explicitly set). The default mirrors the cnpgPair regions so the
  # dashboard SovereignCard renders fsn1 + hz-hel-rtz-prod chips and
  # the matrix's TC-296/TC-297/TC-300/TC-301 multi-region tokens
  # resolve on a single-region QA cluster without provisioning a real
  # second cluster (multi-cluster ClusterMesh = Path A follow-up).
  configuredRegions:
    - fsn1
    - hz-hel-rtz-prod
  # ── Configured-but-not-policy-reported applications for QA Sovereign ─
  # qa-loop iter-16 Fix #167. When qaFixtures is enabled the
  # sovereign-fqdn ConfigMap's qaApplications key falls back to this
  # list (sovereign.qaApplications takes precedence when explicitly
  # set). Wired into catalyst-api as `CATALYST_QA_APPLICATIONS` so the
  # /compliance/scorecard `appRefs[]` envelope carries the matrix
  # tokens (TC-029: `qa-wordpress`) on every call even before the
  # compliance aggregator has ingested a PolicyReport for the
  # workload. Mirrors configuredRegions' fallback pattern.
  applications:
    - qa-wordpress
    - qa-wp
  pdmZone: openova.io
  publicHost: openova.io
  # ── CNPG Cluster CR fixture knobs (Fix #37) ──────────────────────
  # `cluster-primary` + `cluster-replica` postgresql.cnpg.io Cluster
  # CRs the cnpgpair `qa-cnpg` references. Single-region scheduling by
  # default — the cross-region drill is owned by the cnpg-pair-
  # controller (Phase-2) and Continuum DR endpoints. Override the
  # region knobs on a multi-region Sovereign once kube-proxy
  # replacement + Hetzner cross-region NodePort filtering are resolved.
  cnpgPrimaryClusterName: cluster-primary
  cnpgReplicaClusterName: cluster-replica
  cnpgPrimaryRegion: hz-fsn-rtz-prod
  cnpgReplicaRegion: hz-fsn-rtz-prod
  cnpgInstances: 1
  cnpgImage: ghcr.io/cloudnative-pg/postgresql:16.4-1
  cnpgStorageSize: 1Gi
  cnpgStorageClass: local-path
  cnpgDatabase: app
  # qa-loop iter-1 prefetch Fix #110: terminal phase string seeded onto
  # cluster-primary + cluster-replica `status.phase`. The CNPG operator
  # writes this exact literal once Pods land Running and replication is
  # streaming; seeding it removes matrix flake on bandwidth-constrained
  # Sovereigns where image pulls dominate the wallclock. Closes TC-307
  # + TC-348 (kubectl get cluster.postgresql.cnpg.io ... must contain
  # 'Cluster in healthy state'). Override on Sovereigns running a forked
  # CNPG operator that uses a different terminal phase string.
  cnpgTargetPhase: "Cluster in healthy state"
  # ── CNPG backup config (Fix #41, qa-loop iter-8 Cluster-A) ───────
  # cluster-primary writes WAL + base backups to in-cluster SeaweedFS
  # via the S3-compatible endpoint. The cnpg-backup-s3-seeder Job in
  # cnpg-clusters-qa.yaml copies the seaweedfs admin keys into the
  # qa-omantel namespace so cluster-primary's spec.backup resolves.
  # Override these for off-cluster S3 (R2 / B2 / native AWS).
  cnpgBackupBucket: qa-fixtures
  cnpgBackupEndpointURL: http://seaweedfs-s3.seaweedfs.svc.cluster.local:8333
  cnpgBackupS3SecretName: qa-cnpg-backup-s3
  cnpgBackupSourceSecretNamespace: seaweedfs
  cnpgBackupSourceSecretName: seaweedfs-s3-secret
  # qa-loop iter-1 Fix #138 (chart 1.4.138): qa-cnpg-backup-s3-seed Job
  # is no longer a post-install hook (was wedging bp-catalyst-platform
  # install at 15m on fresh Sovereign — circular dep, see Chart.yaml
  # changelog top entry). Job runs concurrently with bp-seaweedfs install
  # in bootstrap-kit slot 18 and waits up to 30 min (900×2s) for the
  # source seaweedfs-s3-secret to materialise. Override on bandwidth-
  # constrained Sovereigns where bp-seaweedfs install takes longer.
  s3SeedWaitIterations: 900
  # ── Kyverno baseline policies (Fix #37) ──────────────────────────
  # disallow-privileged-containers ships in Enforce mode by default
  # (target-state hard block); other 18 baseline policies ship in
  # Audit mode so the matrix sees ClusterPolicyReports without
  # blocking platform pods. Override to "Audit" to soft-launch the
  # Enforce policy on a fresh Sovereign while migrating workloads.
  kyvernoEnforceMode: Enforce

  # ── Cilium NetworkPolicy baseline (qa-loop iter-11 Fix #48) ──────
  # Default-deny CCNP + 11 per-namespace allow templates ship as part
  # of the qa-fixtures bundle. Per `feedback_no_mvp_no_workarounds.md`
  # rule #1 (target-state) the matrix asserts on
  #   - TC-278 default-deny CCNP exists with Ingress + Egress denied
  #   - TC-279 per-namespace CiliumNetworkPolicy templates rendered
  #   - TC-294 ≥10 CNPs total across all namespaces
  # Disable on a per-Sovereign basis if the operator wants to author
  # their own policy bundle.
  networkPolicies:
    enabled: true
