Enabling the operator β the executed runbook
Steps 1β5 of this page were executed live on 2026-08-22 against the production cluster and
committed back the same session. It exists so the next enablement is a checklist, not an
archaeology dig. The generic halves live in the platform repo
(deploy/aks/INSTANCE-LIFECYCLE.md, deploy/aks/manifests/hosting-operator/README.md); this page
records what an ACTUAL enablement looks like, with the real names, and what only doing it reveals.
π¨ A merged enablement is not an enabled operator β step 6 is the enablement. The values PR (Memex#86) merged 2026-08-22 13:24Z; the last
Helm releaserun was 05:21Z the same day β eight hours EARLIER. Nothing red anywhere, five green checklist items, and on 2026-08-23 aSuspendaction still failed at Launch operator job with "instance actions are disabled on this installation", because theHosting__Operator__*env had never reached a running container. Config that is merged but not rolled is the same silent state as a gate that skipped: the evidence it did not happen is the absence of evidence. Do not tick this runbook off until the verification probe below passes.
Why an operator at all
The mesh half of instance lifecycle (records, requests, plans, phases) is complete and unit-tested
β but a lifecycle run legitimately needs az, kubectl, helm and pg_dump, and the portal
image carries none of them, on purpose. So every run executes as a short-lived Kubernetes Job
in memex-ops under its own identity. Three principals, deliberately separate:
| Principal | Lives | Can | Cannot |
|---|---|---|---|
| the portal's own SA | portal ns | patch its own Deployment (self-update) | anything else |
hosting-jobrunner |
the portal's namespace (see defect 2) | create/watch/delete Jobs and read logs in memex-ops |
touch namespaces, databases, cloud resources |
hosting-operator |
memex-ops, exists only while a Job runs |
namespaces, Helm releases, DNS, Key Vault, Postgres β via Workload Identity | outlive the run |
The checklist (as executed)
- Identity + backup store β
backups.bicepdeployed tomemex-aks-rg: storagememexdbbackups(ZRS, 365d lifecycle), identityhosting-operator(client280fd351-β¦), federated tosystem:serviceaccount:memex-ops:hosting-operator. - Grants β for BOTH instances' resources, not just the control instance's:
DNS Zone Contributor on
meshweaver.cloudandsystemorph.com; Key Vault Secrets Officer onSystemorphandmemex; plus the bicep's own Postgres Contributor and backup-account Blob Data Contributor. - RBAC β
kubectl applyofmanifests/hosting-operator/(client-id annotation edited first). - Image β
az acr buildofhosting-operator:1.0.0with build contextdeploy/(the image carries the chart; building fromoperator/produces one that fails at "Deploy release"). - Chart values β the
hostingOperator:block in the control instance's PUBLIC overlay (values.memex.public.yaml, Memex#86). π¨ These render into the ConfigMap (Hosting__Operator__*) β they are NOT free-form config entries; a key set anywhere else reaches no container and every action refuses as "disabled on this installation". π¨ Since 2026-09-08 the configuration is the FALLBACK, not the input. Every run reads the Deployment record'soperatorblock first β image, namespace, service account, environment β and theHosting__Operator__*configuration only where the record is silent (HostingOperator.ResolveIdentity). Measured the other way round the same day: Memex#255 movedoperator.imagetohosting-operator:00dc0e8(record v50) and the next reconcile (memex-reconcile-20260908-ci8131, 20:23Z) still ran the old image and failed at the exact step the new image fixes, because a ConfigMap reaches a pod only at a helm deploy. An operator fix is now a record edit and the next action.Hosting__Operator__Enabledstays configuration-only: whether this installation may run actions at all is not any record's call. - Token mount + THE ROLL β the jobrunner token volume in
portal-patch.json, then the Helm release workflow (deploy) rolls the portal. This step is the enablement: everything before it edits repos and cloud resources; only this run putsHosting__Operator__Enabled=trueand the token mount into a running pod. It can itself be triggered from the mesh βrequestedAction: HelmRelease+helmAction: deployon the control instance's own record β because a HelmRelease action dispatches the config repo's workflow under the fleet's GitHub App credential and does not need the operator that is being enabled.
Verify it is actually live
The checklist having been executed is not evidence the config reached a container β values land in
a ConfigMap only when the release rolls, and a portal that later rolled by self-update keeps
whatever env its Deployment spec had. The only honest probe is a real action β β οΈ NOT a dry
run: a dryRun: true plan deliberately ends at "Dry run β nothing changed" BEFORE the Launch
operator job phase, so it proves nothing about the operator. A real Backup is the harmless
probe β its worst case is that it works and you gain a verified dump:
{ "id": "operator-probe", "namespace": "Deployments", "name": "Operator probe",
"nodeType": "Hosting/InstanceAction",
"content": { "$type": "InstanceActionContent",
"deployment": "Deployments/<any-recorded-instance>",
"requestedAction": "Backup", "backupStore": "<a recorded store>",
"reason": "probe: is the operator live?" } }
Read the Launch operator job phase of its log. Started and beyond β live. "instance actions
are disabled on this installation" β step 6 has not landed, whatever the repos say. (That is
exactly how the gap above surfaced: a real Suspend on 2026-08-23 refused at that phase while
every repo read as done.)
The two defects only a live enablement found
- The jobrunner Role had no RoleBinding (MeshWeaver#2070). A Role without a binding is furniture: the portal's token 403s on its first CreateJob, and from the mesh that reads as a misconfigured operator, not a missing binding.
- The jobrunner SA + token Secret were unmountable by construction. They shipped in
memex-ops, but a pod can only mount Secrets from its OWN namespace. They belong in the portal's namespace; the RoleBinding inmemex-opsnames the foreign SA β which is exactly what scopes the portal-held credential to jobs-only power there while staying mountable here. Verified with SubjectAccessReviews: create-jobstrue, delete-namespacesfalse.
Both are the same lesson as the CI invariants: a control surface nobody has exercised is not a control surface. Every green unit test passed while the whole thing was unlaunchable.
Who owns a run β the attribution rule
Every Hosting/InstanceAction run has an OWNER, and it is never a guess:
- The owner is the node's framework-stamped
lastModifiedBy(elsecreatedBy) β never a field on the content, which a requester could simply type. - A destructive action (teardown, restore) with no named owner is refused β deliberately stricter than package provisioning, where an unattributed write is trusted infrastructure context. Installing a package unattributed is fine; dropping a database unattributed is not.
- A system-initiated run (the idle lifecycle: suspend after
idleSuspendDays, teardown afteridleTeardownDays) is owned by the System principal β never attributed to a human, for the same reason provisioning never grants its invoker ownership: attribution must not become entitlement, in either direction.
After enablement: the mesh IS the admin surface
From here, cluster administration goes through nodes on the control instance β Hosting/Deployment
records as intent, Hosting/InstanceAction runs as execution (including HelmRelease with
capture/adopt/deploy), Hosting/InstanceRequest for self-service. Raw kubectl/az remains
a break-glass stopgap whose every use must land back in a repo the same session.