openova/products/catalyst/chart/values.yaml
2026-05-16 11:00:59 +00:00

1308 lines
65 KiB
YAML
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

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: "0ebd137"
catalystUi:
tag: "0ebd137"
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