diff --git a/platform/image-automation/per-service/README.md b/platform/image-automation/per-service/README.md new file mode 100644 index 0000000..9a2f03b --- /dev/null +++ b/platform/image-automation/per-service/README.md @@ -0,0 +1,48 @@ +# 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. diff --git a/platform/image-automation/per-service/pihole.example.yaml b/platform/image-automation/per-service/pihole.example.yaml new file mode 100644 index 0000000..538d872 --- /dev/null +++ b/platform/image-automation/per-service/pihole.example.yaml @@ -0,0 +1,75 @@ +--- +# WORKED EXAMPLE (reference only — NOT wired into a kustomization, never deployed). +# Rendered from catalog/pihole.yaml's `update` block by WS-GITOSS into apps/pihole/. Shows the exact +# ImageRepository + ImagePolicy + Deployment setter-marker shape for a container ServiceDefinition. +# +# update: +# channel: stable +# range: ">=2024.0.0" +# cadence: "0 4 * * *" +# autoRollbackOnFailedHealth: true +# +# See ./README.md for the field-by-field contract. +# --------------------------------------------------------------------------------------------------- +apiVersion: image.toolkit.fluxcd.io/v1beta2 +kind: ImageRepository +metadata: + name: pihole + namespace: pihole +spec: + image: pihole/pihole # from source.image + interval: 6h # scan schedule; nearest Flux duration to cadence "0 4 * * *" (daily-ish) + # provider: generic # dockerhub anon; a pull secret goes here if registry rate-limits bite +--- +apiVersion: image.toolkit.fluxcd.io/v1beta2 +kind: ImagePolicy +metadata: + name: pihole + namespace: pihole +spec: + imageRepositoryRef: + name: pihole + # STABLE-ONLY (R-UPD-01): (1) semver range from `update.range`; (2) a tag filter that admits only + # bare X.Y.Z releases so a pre-release / `latest` / date-nightly tag can never be selected. + filterTags: + pattern: '^v?(?P[0-9]+\.[0-9]+\.[0-9]+)$' + extract: '$v' + policy: + semver: + range: '>=2024.0.0' +--- +apiVersion: apps/v1 +kind: Deployment +metadata: + name: pihole + namespace: pihole +spec: + replicas: 1 + # progressDeadlineSeconds bounds a bad rollout so the health gate (readiness probe) can catch it and + # the old ReplicaSet keeps serving — the plain-Deployment auto-rollback path (R-UPD-03). A + # HelmRelease-delivered service would instead use spec.upgrade.remediation + spec.rollback. + progressDeadlineSeconds: 120 + selector: + matchLabels: { app: pihole } + template: + metadata: + labels: { app: pihole } + spec: + containers: + - name: pihole + # THE SETTER MARKER: image-automation writes the ImagePolicy-resolved stable tag here. + # No human/AI ever edits this tag — the marker binds it to the pihole ImagePolicy above. + image: pihole/pihole:2024.7.0 # {"$imagepolicy": "pihole:pihole"} + readinessProbe: # the health gate: a bad image never becomes Ready + httpGet: { path: /admin/, port: 80 } + initialDelaySeconds: 10 + periodSeconds: 5 +--- +# PINNED VARIANT (R-UPD-03 escape hatch) — what the SAME service renders when `update.pin: "2024.7.0"` +# is set: the image is fixed, the setter marker is ABSENT, and NO ImagePolicy/ImageRepository is +# rendered (or it is suspended). Automation cannot advance it. This is the one sanctioned human touch, +# and it STOPS automation rather than performing an update. +# +# containers: +# - name: pihole +# image: pihole/pihole:2024.7.0 # pinned — no $imagepolicy marker; automation is frozen diff --git a/renovate.json b/renovate.json new file mode 100644 index 0000000..ac9e243 --- /dev/null +++ b/renovate.json @@ -0,0 +1,73 @@ +{ + "$schema": "https://docs.renovatebot.com/renovate-schema.json", + "_comment": "Renovate config — Phase 4 automated updates (ADR-0009, R-UPD-01/02). Renovate owns HELM CHART + DEPENDENCY bumps (opens PRs; allowed classes auto-merge on green CI). Flux image-automation owns per-service container IMAGE TAGS (the $imagepolicy setters). The two do NOT overlap: this config DISABLES image-tag updates so Renovate never fights Flux over a running version. Self-hosted against the bootstrap Gitea; platform: gitea set at runtime by the operator's renovate runner.", + "extends": [ + "config:recommended", + ":dependencyDashboard", + ":semanticCommits", + "helpers:pinGitHubActionDigests" + ], + "timezone": "America/New_York", + "schedule": ["after 3am and before 6am"], + "prConcurrentLimit": 5, + "prHourlyLimit": 2, + "commitMessagePrefix": "chore(deps):", + "labels": ["dependencies", "automated-update"], + "ignoreUnstable": true, + "respectLatest": true, + "enabledManagers": ["flux", "kustomize", "helmv3", "regex", "github-actions"], + "flux": { + "managerFilePatterns": ["/(^|/)platform/.+\\.ya?ml$/", "/(^|/)apps/.+\\.ya?ml$/"] + }, + "kustomize": { + "managerFilePatterns": ["/(^|/)kustomization\\.ya?ml$/"] + }, + "customManagers": [ + { + "customType": "regex", + "description": "Inline `# renovate: datasource=... depName=...` markers on pinned URLs / version literals (e.g. platform/kubevirt/kustomization.yaml release URLs, ci/versions.env, infra/devcontainer/versions.env).", + "managerFilePatterns": [ + "/(^|/)kustomization\\.ya?ml$/", + "/(^|/)versions\\.env$/" + ], + "matchStrings": [ + "#\\s*renovate:\\s*datasource=(?\\S+)\\s+depName=(?\\S+)(?:\\s+versioning=(?\\S+))?\\s*\\n.*?(?v?[0-9]+\\.[0-9]+\\.[0-9]+)" + ], + "versioningTemplate": "{{#if versioning}}{{{versioning}}}{{else}}semver{{/if}}" + } + ], + "packageRules": [ + { + "description": "Auto-merge the allowed low-risk classes on green CI — no human authoring (R-UPD-02). The Gitea merge gate (make -C ci gate) is the required check.", + "matchUpdateTypes": ["minor", "patch", "digest"], + "automerge": true, + "automergeType": "pr", + "platformAutomerge": true + }, + { + "description": "MAJOR / potentially-breaking bumps require human review (docs/11 safety rail). Never auto-merged.", + "matchUpdateTypes": ["major"], + "automerge": false, + "addLabels": ["needs-review", "breaking"] + }, + { + "description": "Keep the KubeVirt + CDI operator pair on a compatible track — grouped so they bump together (platform/kubevirt/kustomization.yaml).", + "matchDepNames": ["kubevirt/kubevirt", "kubevirt/containerized-data-importer"], + "groupName": "kubevirt-cdi" + }, + { + "description": "Pinned tool versions (ci/versions.env, infra/devcontainer/versions.env) are a deliberate change surface — label but do not auto-merge; a version bump there changes CI reproducibility.", + "matchFileNames": ["ci/versions.env", "infra/devcontainer/versions.env"], + "automerge": false, + "addLabels": ["ci-tooling"] + } + ], + "ignorePaths": [ + "**/*.example", + "**/*.sops.yaml", + "conformance/**", + "platform/image-automation/per-service/**" + ], + "docker": { "enabled": false }, + "kubernetes": { "enabled": false } +}