PlatformConfig
Scope: cluster · Singleton (name cluster)
PlatformConfig configures the operator itself — how it deploys and manages the platform. Today it configures the container registry: point every image the operator deploys at a custom (e.g. air-gapped mirror) registry, with pull credentials, in one place.
apiVersion: core.navique.com/v1alpha1
kind: PlatformConfig
metadata:
name: cluster # singleton — only "cluster" is honoured
spec:
registry:
# Mirror host, optionally with a path prefix.
host: registry.example.com/mirror
auth:
secretRef:
name: registry-pull-secret # a kubernetes.io/dockerconfigjson Secret
namespace: navique-system # defaults to the operator system namespaceWhat it covers
When spec.registry.host is set, the operator host-mirrors every image it deploys — it swaps the registry host while preserving the image path and tag, so ghcr.io/cloudnative-pg/cloudnative-pg:1.29 becomes registry.example.com/mirror/cloudnative-pg/cloudnative-pg:1.29. This applies to:
- The bundled capability operators — CloudNativePG, cert-manager, External Secrets, Sealed Secrets, the ClickHouse and Redis operators, MongoDB (MCK), the LiteLLM and Langfuse operators, Meilisearch, Sail/Istio. Each chart's image values are rewritten to the mirror.
- The datastore & workload instances the operators manage — Redis, LiteLLM (gateway + migration job), Langfuse (web + worker), LibreChat, Meilisearch, the Istio control plane (istiod / ztunnel / CNI).
- The management-plane console — its only registry override (it has no separate setting).
The pull Secret referenced by spec.registry.auth.secretRef is propagated: the operator copies it into every namespace it deploys into (the capability operators' system namespaces and each workload namespace) and wires it as imagePullSecrets on the charts, the emitted instance CRs, and operator-managed pods.
What it does NOT cover
- The operator's own image. The operator is already running, so it cannot use a CR to decide where to pull itself from. Set that at install time via the operator Helm chart —
image.repositoryandimagePullSecrets— which is also where you provide the bootstrap registry for an air-gapped install. - CloudNativePG and ClickHouse operand images (the PostgreSQL / ClickHouse server images the upstream operator selects on its own) are given the pull Secret but keep the operator's default image reference — there is no stable tag to re-pin safely. Mirror those at the same repository path; the pull Secret lets the instance pods authenticate to the mirror.
- A couple of deep
busyboxinit images inside the Meilisearch / LibreChat charts are not exposed as values and are not rewritten.
Install-time seeding (recommended)
For a GitOps or air-gapped install, seed the PlatformConfig with the operator Helm chart so the very first capability-operator install is already mirrored:
helm install navique deploy/helm \
--set image.repository=registry.example.com/mirror/scigility/navique-ai-core-operator \
--set imagePullSecrets[0].name=registry-pull-secret \
--set registry.host=registry.example.com/mirror \
--set registry.pullSecret.name=registry-pull-secretregistry.pullSecret.dockerConfigJson can also be set to have the chart create the Secret; otherwise it must already exist in the system namespace.
Operator-wide defaults
Beyond the registry, spec.defaults and spec.lifecycle configure how the operator deploys and manages everything. A component's own spec always overrides the matching default.
spec:
defaults:
clusterDomain: cluster.local # custom cluster DNS domain (computed service URLs)
storageClass: fast-ssd # default for managed-datastore PVCs
commonLabels: { team: platform } # stamped on every operator-created object
commonAnnotations: { owner: ai }
proxy: # injected as HTTP(S)_PROXY / NO_PROXY env
httpProxy: http://proxy:3128
httpsProxy: http://proxy:3128
noProxy: .svc,.cluster.local,10.0.0.0/8
scheduling: # default pod placement
nodeSelector: { workload: platform }
tolerations: [{ key: platform, operator: Exists }]
priorityClassName: system-cluster-critical
securityContext: { runAsNonRoot: true, seccompProfile: { type: RuntimeDefault } }
imagePullPolicy: IfNotPresent
registry:
operandImages: # pin operator-chosen operand images (host-mirrored)
postgres: ghcr.io/cloudnative-pg/postgresql:17.2
clickhouse: clickhouse/clickhouse-server:25.3
lifecycle:
pauseUpgrades: false # stop version-driven capability-operator re-applies
retainOnUninstall: false # keep a shared operator's release when its last consumer is removed
maintenanceUntil: "2026-07-01T02:00:00Z" # pause capability-operator upgrades until this time, then auto-resume
runtime:
logLevel: info # debug|info|warn|error — changes the operator's verbosity LIVEHow they're applied. clusterDomain, storageClass, commonLabels/Annotations and operandImages are applied directly to the resources the operator emits. The pod-level defaults — proxy, scheduling, securityContext, imagePullPolicy, and the common labels/annotations — are applied to every object the bundled charts render through a single Helm post-renderer, so they reach the capability operators and the workload charts uniformly without per-chart configuration.
scheduling reaches every platform pod, not just chart-rendered ones. Most application pods (the LiteLLM gateway, Langfuse, and the datastores) are created by upstream operators from the CRs this operator emits — the Helm post-renderer never sees them. So scheduling (nodeSelector / tolerations / affinity / priorityClassName) is additionally translated into each emitted CR's own placement schema: CloudNativePG's spec.affinity block, the ClickHouse/Keeper spec.podTemplate, the OT-Redis spec, the Langfuse web/worker components, the MongoDB StatefulSet overlay, and the LiteLLM spec.podScheduling block — plus the operator's hand-built Deployments (management plane, guardrail proxy) and backup CronJobs. Each field is set only when the workload does not already define its own, so an explicit value wins. A default value already set by the workload always takes precedence.
One placement caveat: priorityClassName. Two upstream CRDs model no such field, so that single default is skipped for them while their nodeSelector/tolerations/affinity are applied normally — Langfuse, and the LiteLLM gateway (whose spec.podScheduling covers the other three). The Gateway raises a PlacementUnsupported warning event naming priorityClassName when it is set, rather than writing a field the API server would prune.
LiteLLM placement needs litellm-operator 0.24.0+
spec.podScheduling was added upstream in litellm-operator 0.24.0 (bundled since operator release 0.21.0). It applies to both the proxy Deployment and the database migration Job, so a migration can never be scheduled somewhere the proxy is not allowed to run. On an older, user-installed litellm-operator that the platform adopted, the field is pruned and the gateway keeps default placement.
proxy is not the registry. A private registry (Artifactory, Harbor, …) is configured via spec.registry.host + auth — that's the air-gap image mechanism. spec.defaults.proxy injects the standard outbound HTTP_PROXY/HTTPS_PROXY/NO_PROXY env vars for restricted-egress clusters where workloads reach external services (e.g. an external LLM API) through a corporate forward proxy. HTTP_PROXY vs HTTPS_PROXY select the proxy by the scheme of the target URL (usually the same proxy value) — they are not two registries. Leave proxy unset on fully air-gapped clusters that have no egress.
lifecycle.maintenanceUntil is a maintenance window: while the current time is before it, version-driven capability-operator upgrades/re-applies are paused (equivalent to pauseUpgrades), then resume automatically when the window passes (the operator requeues at the deadline). An unhealthy release is still re-applied so nothing stays broken.
runtime.logLevel changes the operator's log verbosity live (no restart) on the next reconcile. The operator's own image is still an install-time concern set via the operator Helm chart (image.repository / imagePullSecrets), not this CR — the operator is already running when it reads PlatformConfig. The operator emits no usage telemetry, so there is nothing to opt out of.
Operator install namespaces
By default the operator installs each bundled capability operator into its own dedicated namespace (cnpg-system, litellm-system, cert-manager, external-secrets, sealed-secrets, langfuse-system, redis-system, mongodb-system, clickhouse-system, sail-operator). spec.operators lets you change where they land — most commonly to consolidate all capability operators into a single namespace.
spec:
operators:
namespace: navique-operators # install ALL capability operators here
namespaces: # optional per-operator overrides (win over `namespace`)
cloudnative-pg: data-systemResolution (per operator). A per-operator entry in namespaces wins → else the shared spec.operators.namespace → else the operator's built-in default namespace. Leaving spec.operators unset entirely preserves today's behaviour: each operator in its own dedicated namespace.
Valid operator keys for the namespaces map:
| Key | Default namespace |
|---|---|
external-secrets | external-secrets |
sealed-secrets | sealed-secrets |
cloudnative-pg | cnpg-system |
cert-manager | cert-manager |
litellm-operator | litellm-system |
langfuse-operator | langfuse-system |
redis-operator | redis-system |
mongodb-kubernetes | mongodb-system |
clickhouse-operator | clickhouse-system |
sail-operator | sail-operator |
The chosen namespace is created if it does not exist when the operator is first installed.
Note —
spec.operatorsis applied at install time. Changing it after a capability operator is already installed does not migrate the existing release to the new namespace; the operator must be torn down and redeployed to move it.
Bundled MCP catalog footprint
spec.mcp tunes the cluster-shared bundled MCP catalog release (see MCPServer). Because a catalog server (e.g. web search) is deployed as one shared, ref-counted release for the whole cluster, its footprint is set here rather than per-ChatUI.
| Field | Description |
|---|---|
footprint | full (SearXNG + Presidio + Playwright; default) or minimal (search only — PII/JS-render off) |
values | Free-form Helm-values overlay merged into the shared catalog release |
Like spec.operators, a footprint change does not migrate an already-deployed release.
Routing — Ingress or Gateway API
spec.routing selects how workload ingress is exposed: classic Ingress (default) or the Gateway API (HTTPRoutes).
spec:
routing:
mode: Gateway # Ingress (default) | Gateway
gatewayClassName: istio # required for standalone; default "istio" under a mesh
gateway:
mode: managed # managed (operator owns it) | adopt | external
name: navique
namespace: navique-system
clusterIssuer: letsencrypt-prod # cert-manager gateway-shim for HTTPS listenersWhen Gateway mode is used (resolved per workload): the workload's spec.ingress.api wins (Gateway/Ingress), else routing.mode == Gateway, else an active ServiceMesh (Istio is a Gateway API implementation, so enabling the mesh flips routing automatically). The default is Ingress — zero behaviour change until you opt in.
What the operator does in Gateway mode: it suppresses the workload's upstream Ingress and emits an HTTPRoute (hostnames=[your host], backend = the workload Service) attached to a shared Gateway it manages in the system namespace — one HTTP listener for all workloads, plus a per-host HTTPS listener (cert-manager gateway-shim, mode: Terminate) when a workload sets ingress.tls and a clusterIssuer is configured. The management-plane console, which has no classic Ingress, finally gets real routing this way.
A workload forces classic Ingress even under a mesh with its own escape hatch:
# on a Gateway / Langfuse / ChatUI / ManagementPlane
spec:
ingress:
enabled: true
host: legacy.example.com
api: Ingress # override: keep classic Ingress for this workloadGateway mode requires the Gateway API CRDs (gateway.networking.k8s.io) to be installed — the ServiceMesh (Sail/Istio) installs them, or a standalone controller (Envoy Gateway, NGINX Gateway Fabric) does; the operator gates on them and reports a waiting status if they are absent. Basic-auth ingress annotations have no Gateway API equivalent and do not carry over (use a mesh AuthorizationPolicy).
Adopt an existing Gateway (don't create a second one)
If your cluster already has a Gateway for the same gatewayClassName — for example one provisioned by Terraform or Helm that an external load balancer (Azure Application Gateway, an ALB, …) already routes to — you must tell the operator to use it via routing.gateway:
spec:
routing:
mode: Gateway
gatewayClassName: nginx
gateway:
mode: external # reference it; the operator never modifies it (use "adopt" to manage labels)
name: nginx-gateway
namespace: nginx-gatewayIf you leave routing.gateway unset, the operator falls back to creating and owning its own Gateway (navique in the system namespace). When another Gateway for the same class already exists, that second Gateway spins up a parallel data-plane LoadBalancer that collides with the existing one on the cloud's internal load balancer (e.g. both claim port 80), so the new LoadBalancer never gets an address and your external proxy keeps routing to a Gateway that now has no routes — a platform-wide 502. To prevent this, the operator refuses to create a duplicate: it leaves the workload Pending with an InfrastructureBlocked status/event naming the existing Gateway and asking you to set routing.gateway. Setting it (as above) resolves the block.
The operator also surfaces a managed Gateway whose data-plane LoadBalancer is stuck without an address (e.g. a cloud SyncLoadBalancerFailed): the workload reports InfrastructureBlocked with the underlying detail instead of looking healthy in-cluster while being unreachable from outside.
Layering & precedence
- Install-time (Helm/flags): the operator's own image + its pull Secret.
- Runtime (this CR): everything the operator deploys downstream.
- Per-component: a
Gateway/Observabilitymay still set its ownspec.image.repository; an explicit value is host-mirrored, not replaced.
Changing the registry after the platform is deployed takes effect as each component next reconciles. Removing the PlatformConfig stops rewriting (images revert to their upstream registries on the next reconcile).
Status
| Field | Meaning |
|---|---|
status.active | Host-mirror rewriting is in effect. |
status.registry | The active mirror host. |
status.pullSecret | The resolved source pull Secret (namespace/name). |
status.propagatedNamespaces | Namespaces the pull Secret was copied into. |
A PlatformConfig with any name other than cluster is rejected with a NotSingleton condition and ignored.