Updating the operator
The platform updates its own images. The operator's controller does not — it is installed and upgraded outside the product, through whichever of three channels installed it. This page is what each of those looks like, how to see what you are running, and why the product shows you the version instead of offering a button.
For the platform's own updates — a new app image, a chart change, a CR value —
see updating.md. This page is only about the controller that
reconciles your Zaentrum CR.
What "the controller" means here
Section titled “What "the controller" means here”Installing the operator creates, in its own namespace zaentrum-operator-system:
the Zaentrum and ZaentrumAddon CRDs, the cluster RBAC, and one Deployment —
zaentrum-operator-controller-manager, running ghcr.io/zaentrum/operator:<tag>.
That Deployment is the controller. It watches your CR and renders the whole
platform from the Helm chart embedded in its own image
(see operator.md).
Two consequences follow, and they are the whole subject of this page:
- The chart ships inside the controller image. A chart change is a controller change — the new templates take effect only once the controller runs a new image (updating.md flow B).
- The controller is not one of the workloads it manages. It runs in another namespace, under cluster-scoped permissions, and nothing inside the platform namespace can write to it.
See what you run
Section titled “See what you run”The operator reports itself on the CR, under status.controller:
| Field | Is |
|---|---|
image | what the controller pod runs, tag or digest |
version | the tag, else the short digest, else unknown |
source | how it was installed: olm, manifest, appliance or unknown |
availableUpdate | a newer version found on the channel; "" when there is none, or nothing looks |
observedAt | when the operator last looked |
Three ways to read it, in rising order of how much access they need.
1. The CLI, through the portal — no cluster access at all:
zae platform controller --url https://media.example.orghttps://media.example.org — the operator's controller version v0.4.1 image ghcr.io/zaentrum/operator:v0.4.1 installed OLM — a subscription the cluster manages update v0.5.0 available observed 2026-09-22T08:00:00ZUpdated outside the platform: approve the update in its OLM subscription.A release's install reports its vX.Y.Z, as the example does; an install of
main's pinned manifest reports its sha-<commit> tag, and an appliance built
from main reports latest. Which release the channel serves:
releases.
zae platform status ends with the same section, and --json on either prints
what the portal reported, for a script. An instance whose operator predates the
field says so and zae platform controller exits 3 — not offered by this
instance — so a check can branch on it. See
the CLI contract.
When availableUpdate is a channel tag rather than a version, both surfaces
say the channel serves a different image instead of naming it — an install
pinned to a commit image (:sha-…) reports this every time the channel moves
ahead of it, which means you are not on what the channel serves rather than
that anything is broken; tagged releases (v0.5.0) are what make the
comparison precise.
2. The portal. The operator console (/portal/operator, admin-only) shows
an operator controller card with the same facts and the same one line
naming the path. There is no button on it, for the reasons
below.
3. The cluster, if you have access to it:
kubectl -n zaentrum get zaentrum -o jsonpath='{.items[0].status.controller}' | jqkubectl -n zaentrum-operator-system get deploy zaentrum-operator-controller-manager \ -o jsonpath='{.spec.template.spec.containers[0].image}'The three channels
Section titled “The three channels”1. OLM — a subscription the cluster manages
Section titled “1. OLM — a subscription the cluster manages”On OpenShift or any OLM cluster the operator is installed from its bundle
(package zaentrum-operator, channel stable — see
the bundle).
OLM then owns the lifecycle: it watches the catalog and creates an
InstallPlan for each new version in the channel.
The bundle supports OwnNamespace, SingleNamespace and AllNamespaces, so
the Subscription's name and namespace are whatever the install chose — find
it first:
oc get subscription -A | grep zaentrum-operatoroc -n <ns> get subscription <name> -o jsonpath='{.spec.installPlanApproval}{"\n"}' # Automatic | Manualoc -n <ns> get installplanoc -n <ns> patch installplan <plan> --type merge -p '{"spec":{"approved":true}}' # approve a Manual planoc -n <ns> get csv # Succeeded once it has rolledinstallPlanApproval: Automatic— the cluster rolls new versions on its own; there is nothing to do, andavailableUpdateis a heads-up, not a task.installPlanApproval: Manual— approve the install plan. This is the path the product names forsource: olm.
Changing the channel (spec.channel on the Subscription) is the same act one
level up, and also belongs to whoever owns the cluster's operators.
2. The pinned install manifest
Section titled “2. The pinned install manifest”Without OLM the operator is installed by applying one manifest, which carries
the namespace, both CRDs, the cluster RBAC and the controller Deployment: a
release's operator-install.yaml, attached to the operator's
GitHub release with
the controller on :vX.Y.Z, or main's
deploy/operator-install.yaml,
with the controller pinned to an immutable :sha-<gitsha>. Updating to a
release is applying the next release's manifest
(releases); updating on main is moving the pin:
either way as cluster-admin, and normally through the deployment repository that holds the manifest, so the record of what is installed stays the record:
# in the repository that holds your copy of the manifestsed -i 's|ghcr.io/zaentrum/operator:.*|ghcr.io/zaentrum/operator:sha-<newsha>|' \ deploy/operator-install.yamlgit commit -am "operator: roll the controller to sha-<newsha>" && git push# …your pipeline applies it, or, by hand:oc apply -f deploy/operator-install.yamloc -n zaentrum-operator-system rollout status deploy/zaentrum-operator-controller-managerPinning a digest or an immutable SHA tag rather than a floating one is what makes the roll deterministic and reversible: rolling back is applying the previous manifest. The reference demo does exactly this — see reference-demo.md and updating.md flow B.
3. The appliance
Section titled “3. The appliance”The appliance bakes the operator install — the CRDs, the cluster RBAC and the controller Deployment — into its image, and k3s applies it on boot. Two things follow, today:
- The appliance built from
mainruns the floatingghcr.io/zaentrum/operator:latest. It moves to a newer build whenever its pod restarts and pulls the tag again, while the CRDs and RBAC stay the ones the appliance image was built with — so a CR field newer than the image,spec.storage.postgreson an appliance from before the bundled Postgres had a claim, is not in its CRDs;status.controllerreports its version aslatest. A release's appliance (appliance:vX.Y.Z) runsoperator:vX.Y.Zwith the CRDs it was built with, and moves only to a newer appliance image. - A new appliance image means a new container. Started with the
front door's command, a new
container is a new, empty platform:
docker rmanddocker runstart a new cluster, and the platform's data does not carry over — not even with a volume at k3s's storage path. Started with the whole k3s state on a named volume and a fixed host name, a container re-created from a newer image finds its cluster and data again (persistence), but the volume keeps the operator install the first container brought, its CRDs included, until a newer one is copied into it before the re-creation — the operator repo's appliance README shows the copy.
What a controller update carries with it
Section titled “What a controller update carries with it”Not only a new binary:
- the embedded platform chart — the templates every platform workload is rendered from. Expect a reconcile after the roll, and expect workloads whose rendered spec changed to restart;
- the CRD schema — a new field is only usable once the CRD that validates it is installed. Applying a CR that uses a field the running CRD does not know is rejected by the apiserver, not ignored (adding a new CR spec field);
- the cluster RBAC — what the controller, and the permissions it hands to the platform's own components, are allowed to do.
What it does not carry: your CR, your data, and the platform version you are
pinned to. spec.version stays where you put it; a new controller re-renders
the same platform unless the chart changed.
Why the product will not do it for you
Section titled “Why the product will not do it for you”Four reasons, in the order they bite:
- Permissions.
portal-apiis scoped to the platform's namespace. The controller lives in another one, and its install also carries cluster-scoped objects — CRDs, ClusterRoles, ClusterRoleBindings. Granting the portal enough to replace them would make every admin of the platform an effective cluster-admin, which is a much larger change than an update button. - Self-surgery. The thing that would perform the update is the thing being replaced. A controller that rolls its own Deployment is terminated mid-reconcile by the rollout it started; if the new image is wrong, the component that could roll it back is the one that is now failing.
- The record. On OLM the cluster owns the operator's lifecycle, and on a pinned manifest the manifest in your repository is the record of what is installed. A button in the product would change the cluster without changing either, and the next pipeline run would quietly undo it.
- It is the ordinary shape. A cluster operator updates the workloads it manages; it is itself updated through whatever installed it. Keeping that boundary is what makes the platform's own updates safe to hand to a non-cluster-admin at all.
So the product does the half it can do honestly: it shows the version in charge,
says whether something newer exists, and names the path. The portal's operator
console and
zae platform controller
both stop exactly there.
After the roll
Section titled “After the roll”oc -n zaentrum-operator-system rollout status deploy/zaentrum-operator-controller-manageroc -n zaentrum-operator-system logs deploy/zaentrum-operator-controller-manager --tail=50kubectl -n zaentrum get zaentrum # PHASE back to Readyzae platform controller --url https://media.example.org # the version you expectzae platform status --url https://media.example.org # every workload ready againA platform that does not return to Ready, or workloads that do not come back,
are in troubleshooting.md. A controller that reports no
version at all is simply older than this field — update it by the channel that
installed it, and it will start reporting.
See also: operator.md · updating.md · self-hosting.md · the CLI contract · troubleshooting.md