ADR-058: Images and operators are pinned exactly per release; the platform installs the operators a cluster needs as Argo CD applications
Status: Accepted (2026-10-26); the consequence for shared clusters is amended by ADR-060: A cluster repo owns what a cluster shares and lists the systems it runs Builds on: ADR-017: The GitOps repo owns every Kubernetes manifest; services ship an image (the gitops repo owns every manifest), ADR-020: Addons: a stable contract for backing services (operators were cluster prerequisites), ADR-054: The 5.0 interface: system.yaml checked by the renderer, noun plugins with tombstones, and a migration that proves itself, spec 115; extended by spec 121
Context
Section titled “Context”5.0 renders the database image as a major-only tag (postgresql:17), so every pull can bring a different minor version. Operators are installed only on the local cluster, by cluster-up, at versions from environment variables. On real clusters, the docs left them to the cluster owner. The 5.x series adds more resources and more operators, and the user decided that the platform installs what a cluster needs, unless the owner opts out.
Decision
Section titled “Decision”- One versions table per platform release, in the renderer (
templates/gitops-app/scripts/render.py:POSTGRES_IMAGES,OPERATORS). Every image and chart the renderer writes comes from it, as a full version (an image may add a digest). It is never a major-only or floating tag. The table ships with the renderer that uses it, so a gitops repo renders what its platform release pinned.cplatreads the platform’s copy when it needs the expected versions (/platform:doctor). Spec 121 adds the other rows, the CLI and the upgrade rules. - Operators are Argo CD applications that the renderer writes once per cluster, only when needed. They live outside every system’s tree, in
operators/:operators/apps/<operator>.yaml(the pinned chart) andoperators/root.yaml(an Argo application that syncsoperators/apps). Today the cluster is the repo’scluster_server, and every environment deploys there. An operator is rendered when an environment that is not opted out needs it (CloudNativePG: an in-cluster database instance). Without any,operators/does not exist. environments.<env>.operators: owneropts an environment out. The platform installs and upgrades nothing for it, and/platform:doctorcompares the installed version with the table’s for each operator that environment needs.- Operator applications carry no resources finalizer. Removing one from git (the last database is gone) stops Argo from managing it but never deletes the operator or its CRDs, so no custom resource and no data is cascaded away.
- A human applies
operators/root.yamlonce per cluster, before the system roots, exactly as withbootstrap/(devbox run bootstrap, anddevbox run cluster-uplocally). Its applications then follow git.
Consequences
Section titled “Consequences”- Each system’s tree (
applications/<system>/,bootstrap/<system>-root.yaml) stays byte-identical for a 5.0system.yaml, except the pinned database image. The operator files are cluster-level. - A cluster shared by several gitops repos gets a cluster repo (ADR-060), which installs the operators for all of them; every registered system sets
operators: ownerin every environment. Before ADR-060 one of the gitops repos had to install the operators and the others setoperators: owner; such a cluster moves to a cluster repo without reinstalling anything (the application names stayplatform-operatorsandoperator-<name>). - An existing 5.0 cluster whose owner installed CloudNativePG by hand must set
operators: ownerbefore applyingoperators/root.yaml, or skip that file. - The local cluster with Argo CD no longer installs CloudNativePG through k3s. Argo CD installs it from
operators/. Without Argo CD (the platform’s end-to-end test),cluster-upinstalls the chart version thatoperators/apps/cloudnative-pg.yamlpins.