global: # When set, ALL Catalyst-authored container image pulls route through this # registry. Post-handover: per-Sovereign overlays set this to # harbor. 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 `*.` + 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 # `*.` + `` (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-` 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: "0fe0cac" catalystUi: tag: "0fe0cac" 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.) 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..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. # 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 `-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./{,api/,back-office/} and *. (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//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/ 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 `-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//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//` 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.` 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 # .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.`; 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= # derived from openova.io/region= (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