openova/products/catalyst/chart/values.yaml
2026-05-09 23:22:47 +00:00

1038 lines
50 KiB
YAML

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
# ─── 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
# 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: "361337b"
catalystUi:
tag: "361337b"
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"
# SHA pinned to the latest GHCR-published push-on-main build per
# docs/INVIOLABLE-PRINCIPLES.md #4a. CI bumps this on every push.
tag: "1b29c71"
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: ""
# 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"
# SHA pinned to the latest GHCR-published push-on-main build.
tag: "1b29c71"
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"
# SHA pinned to the latest GHCR-published push-on-main build.
# 3d1deef = qa-loop iter-1 application-controller-flag-mismatch fix
# (PR #1196 main.go + PR #1199 Containerfile pkg/ COPY).
tag: "3d1deef"
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
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
# 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
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
# 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
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
# ── 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
# ── 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