Skip to content

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.

yaml
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 namespace

What 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.repository and imagePullSecrets — 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 busybox init images inside the Meilisearch / LibreChat charts are not exposed as values and are not rewritten.

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:

bash
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-secret

registry.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.

yaml
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 LIVE

How 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.

yaml
spec:
  operators:
    namespace: navique-operators       # install ALL capability operators here
    namespaces:                        # optional per-operator overrides (win over `namespace`)
      cloudnative-pg: data-system

Resolution (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:

KeyDefault namespace
external-secretsexternal-secrets
sealed-secretssealed-secrets
cloudnative-pgcnpg-system
cert-managercert-manager
litellm-operatorlitellm-system
langfuse-operatorlangfuse-system
redis-operatorredis-system
mongodb-kubernetesmongodb-system
clickhouse-operatorclickhouse-system
sail-operatorsail-operator

The chosen namespace is created if it does not exist when the operator is first installed.

Note — spec.operators is 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.

FieldDescription
footprintfull (SearXNG + Presidio + Playwright; default) or minimal (search only — PII/JS-render off)
valuesFree-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).

yaml
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 listeners

When 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:

yaml
# on a Gateway / Langfuse / ChatUI / ManagementPlane
spec:
  ingress:
    enabled: true
    host: legacy.example.com
    api: Ingress        # override: keep classic Ingress for this workload

Gateway 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:

yaml
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-gateway

If 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 ​

  1. Install-time (Helm/flags): the operator's own image + its pull Secret.
  2. Runtime (this CR): everything the operator deploys downstream.
  3. Per-component: a Gateway/Observability may still set its own spec.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 ​

FieldMeaning
status.activeHost-mirror rewriting is in effect.
status.registryThe active mirror host.
status.pullSecretThe resolved source pull Secret (namespace/name).
status.propagatedNamespacesNamespaces the pull Secret was copied into.

A PlatformConfig with any name other than cluster is rejected with a NotSingleton condition and ignored.

Open core under AGPL-3.0. Enterprise components are proprietary and license-gated.