# Per-service image-automation — the render contract (`update` block → Flux CRs) **Owner:** WS-UPDATE (this contract) + WS-GITOPS (emits these next to each app's Deployment/HelmRelease under `apps/`). **Serves:** R-UPD-01 (stable-only), R-UPD-02 (no human/AI authoring), R-UPD-03 (pin / auto-rollback). **Source of truth:** [`renderer-contract.md` §`update`](../../../schemas/renderer-contract.md), [ADR-0009](../../../../home-network-k3s/decisions/adr-0009-automated-image-updates.md), [docs/11](../../../../home-network-k3s/docs/11-automated-updates.md). > These files are **reference examples**, deliberately **not** wired into any `kustomization.yaml`, so > they never deploy. WS-GITOPS renders the equivalent per real container service into `apps//`. > The cluster-wide write-back engine that consumes the setter markers is `../image-update-automation.yaml`. ## What a container `kind` ServiceDefinition `update` block renders For every `kind: container` service whose `update.pin` is **unset**, render THREE things: | `update` field | Renders to | Notes | |---|---|---| | (implicit — `source.image`) | **`ImageRepository`** | scans the registry repo on `interval`. | | `channel: stable` + `range` | **`ImagePolicy`** `.spec.policy.semver.range` | **stable-only**: the range plus a filter that **excludes pre-release** tags (`-alpha/-rc/…`). | | `cadence` | `ImageRepository.spec.interval` (scan schedule) | the "on a schedule" of R-UPD-02. Cron `cadence` → nearest Flux duration (Flux scans on a duration, not cron). | | (image ref in the Deployment) | a **setter marker** comment on the `image:` field | `# {"$imagepolicy": ":"}` — where image-automation writes the advanced tag. | | `autoRollbackOnFailedHealth: true` | delivery-side **health gate + rollback** | see below — HelmRelease remediation, or Deployment progressDeadline + revert. | | `pin: ` | **freeze** | render the image `image:` pinned to `` with **NO** setter marker, and **suspend** (or omit) the ImagePolicy. Automation cannot advance a pinned service (R-UPD-03). | `kind: vm` services do **not** render these — they update in-guest (dnf-automatic/unattended-upgrades on `cadence`, cloud-init), per docs/11 §3. See `catalog/vm-demo.yaml`. ## Stable-only (R-UPD-01) — the two guardrails 1. **semver range** (`.spec.policy.semver.range`) — only versions matching the operator's `range`. 2. **pre-release exclusion** — either a `filterTags` pattern that only admits `X.Y.Z` (no `-suffix`), or rely on semver's rule that pre-releases (`1.2.3-rc1`) sort **below** their release and are only selected if the range explicitly asks for them. We do BOTH (belt-and-braces) so an upstream `latest` or `-rc` tag can never be picked. ## Auto-rollback (R-UPD-03) — where the health gate lives The setter write only changes a tag in git; the **rollback** is enforced at delivery: - **HelmRelease-delivered services (preferred):** Flux `HelmRelease.spec.upgrade.remediation.retries` + `.spec.rollback` — a failed, health-gated upgrade auto-rolls-back to the last-good Helm revision. The image tag lives in the HelmRelease `values` with the setter marker, so a bad advance is caught and reverted by Flux with no human step. This is the mechanism the conformance test exercises. - **Plain-Deployment services:** the Deployment `progressDeadlineSeconds` + readiness probe make a bad image a stalled (never-Available) rollout; the old ReplicaSet keeps serving, and the `UpdateHealthGateFailed` alert fires (see `platform/monitoring/alerts.yaml`). Recovery is the next good advance or the operator `pin` escape hatch. Either way a failed/rolled-back update **alerts by email** (R-UPD-02 + R-OBS) via the `automated-updates` PrometheusRule group.