Map a ServiceDefinition `update` block to ImageRepository/ImagePolicy + the $imagepolicy setter marker: stable-only (semver range + bare-X.Y.Z filterTags, pre-release excluded, R-UPD-01), pin -> fixed image with no marker / suspended policy (R-UPD-03). Worked pihole example under per-service/ (reference only, not deployed; WS-GITOPS emits the equivalent into apps/<svc>/). Add renovate.json for Helm chart / dependency / operator-manifest bumps: auto-merge minor/patch/digest on green CI, major needs review, image managers disabled so Renovate never fights Flux over a tag. Co-Authored-By: Claude Opus 4.8 <[email protected]>
platform/image-automation
Flux image-update-automation — hands-off container image tag advances driven by declared policy.
- Governing ADR: ADR-0009 — Automated updates: Flux image automation + Renovate
- Spec: docs/11 — Automated Updates, docs/05 §Automated updates interplay
- Serves: R-UPD-02 (with R-UPD-01/03 safety rails)
- Owner: WS-UPDATE (mechanism), WS-GITOPS (policy rendering from ServiceDefinition)
What lives here
image-update-automation.yaml— the cluster-wideImageUpdateAutomationthat scans registries per each service'sImagePolicyand commits the resolved stable version back to git (machine-authored, auditable — ADR-0009). No human or AI authors a tag bump (R-UPD-02).per-service/— the render contract (updateblock →ImageRepository/ImagePolicy/setter marker) plus a worked example (pihole.example.yaml). Reference only, not deployed — WS-GITOPS emits the equivalent next to each real app's Deployment/HelmRelease underapps/.
Controllers (how the CRDs get here)
The two controllers — image-reflector-controller + image-automation-controller — are installed
as part of the flux-system install by bootstrap/flux/bootstrap.sh via
flux install --components-extra=image-reflector-controller,image-automation-controller, exactly like
the other four Flux controllers. Consequences:
- The
image.toolkit.fluxcd.ioCRDs (ImageRepository/ImagePolicy/ImageUpdateAutomation) exist before the first reconcile — just likeGitRepository/Kustomizationdo. - So the
ImageUpdateAutomationCR here needs no F-MON1 controllers/configs split: F-MON1 bites only when a CR is applied in the same Flux pass that installs its CRD (via a HelmRelease). Here the CRD is a bootstrap artifact, so the CR applies cleanly from theplatformKustomization. (The per-service CRs likewise ride withapps/, whichdependsOn platform.)
Scope boundary (contract)
- This dir: the cluster-wide automation + git write-back settings.
apps/<svc>/: per-serviceImageRepository/ImagePolicy(semver stable range) + the Deployment setter marker — rendered from each ServiceDefinition'supdateblock (seeper-service/README.md).- Renovate (
/renovate.json): Helm chart / dependency / operator-manifest bumps (opens PRs, auto-merges allowed classes on green CI). Not this controller's job, and deliberately non-overlapping (Renovate's docker/kubernetes image managers are disabled so it never fights Flux over a tag).
Safety rails (acceptance gates — ADR-0009)
| Rail | Where enforced |
|---|---|
| Stable-only (R-UPD-01) | ImagePolicy semver range + a filterTags pattern that admits only bare X.Y.Z (excludes -rc/latest). |
| No human/AI authoring (R-UPD-02) | image-automation writes the tag at the $imagepolicy setter; the commit is machine-authored (fluxcdbot). |
| Health-gated auto-rollback (R-UPD-03) | HelmRelease upgrade.remediation + rollback (preferred), or Deployment progressDeadlineSeconds + readiness probe. |
| Pin escape hatch (R-UPD-03) | update.pin renders a fixed image with no setter marker and no/suspended ImagePolicy — automation is frozen. |
| Alert on failure (R-UPD-02 + R-OBS) | platform/monitoring/alerts.yaml group platform-health.automated-updates (ImageUpdateAutomationFailing, ImagePolicyStalled, UpdateHealthGateFailed). |
Proven by the L3 conformance test
conformance/tests/l3-fault-injection/hands-off-update/ (auto-advance stable, bad-update
auto-rollback + alert-wired, pin holds).
Operator hand-off: git write credential (required before enabling)
image-automation-controller reuses the flux-system GitRepository's .spec.secretRef for push auth
(it has no independent secretRef). The Phase-1 source is anonymous/read-only, so before write-back works
the operator must:
- Create a Gitea access token (scope
write:repository) for the bot and put it in the SOPS secret from the templatebootstrap/sops/secret-templates/gitea-image-automation-credentials.yaml.example→platform/image-automation/gitea-image-automation-credentials.sops.yaml(namegitea-image-automation-credentials, keysusername/password), thensops --encrypt --in-place. - Add that file to this dir's
kustomization.yamlresources:. - Attach it to the source: add
spec.secretRef.name: gitea-image-automation-credentialsto theflux-systemGitRepository viabootstrap/flux/patches/flux-system-source-url.yaml.
Enabling (deferred — like velero/oauth2-proxy)
image-automation is intentionally commented out in platform/kustomization.yaml. Enable it only
after the write credential above is provisioned — otherwise the ImageUpdateAutomation reconcile errors
on push and, under the unified graph's platform wait, could stall the downstream graph. Uncomment the
- image-automation line once the hand-off is done.