Merge feature/phase4-image-automation: Phase 4 hands-off updates (R-UPD-01/02/03)
This commit is contained in:
9 files changed
+612
-73
No files matched your search
@@ -11,7 +11,17 @@
|
||||
# WHAT THIS DOES (Phase 1, bootstrap-source method — reconciles from the LOCAL repo, no remote
|
||||
# git host exists yet per spec docs/05 + CONTRIBUTING.md):
|
||||
#
|
||||
# 1. `flux install` — deploy the Flux controllers ONLY (source, kustomize, helm controllers).
|
||||
# 1. `flux install` — deploy the Flux controllers. Besides the default set (source, kustomize,
|
||||
# helm, notification) we ALSO install the two IMAGE controllers via
|
||||
# `--components-extra` (see FLUX_COMPONENTS_EXTRA below): image-reflector-
|
||||
# controller + image-automation-controller. These register the
|
||||
# image.toolkit.fluxcd.io CRDs (ImageRepository/ImagePolicy/ImageUpdateAutomation)
|
||||
# that Phase-4 hands-off updates need (ADR-0009, R-UPD-02). They are part of the
|
||||
# flux-system install exactly like the other 4 controllers — NOT a reconciled
|
||||
# HelmRelease — so their CRDs pre-exist before the first reconcile (this is why
|
||||
# platform/image-automation can carry the ImageUpdateAutomation CR without the
|
||||
# F-MON1 controllers/configs split: the CRD is present at bootstrap, like
|
||||
# GitRepository/Kustomization are). WS-UPDATE (T-4.5).
|
||||
# We do NOT use `flux bootstrap git` in Phase 1 because that command commits
|
||||
# and OWNS its own gotk-sync GitRepository+Kustomization named `flux-system`.
|
||||
# Our entrypoints are ALREADY authored in clusters/base (flux-system.yaml =
|
||||
@@ -42,6 +52,9 @@ set -euo pipefail
|
||||
FLUX_ENV_PATH="${FLUX_ENV_PATH:-clusters/dev}" # dev is the ONLY Phase-1 build target
|
||||
FLUX_NAMESPACE="${FLUX_NAMESPACE:-flux-system}"
|
||||
SOPS_SECRET_NAME="${SOPS_SECRET_NAME:-sops-age}"
|
||||
# The two Phase-4 image-automation controllers are installed alongside the default set so their CRDs
|
||||
# exist before reconcile (ADR-0009, R-UPD-02). Override to "" to build a cluster without them.
|
||||
FLUX_COMPONENTS_EXTRA="${FLUX_COMPONENTS_EXTRA:-image-reflector-controller,image-automation-controller}"
|
||||
|
||||
script_dir="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||
repo_root="$(git -C "${script_dir}" rev-parse --show-toplevel 2>/dev/null || cd "${script_dir}/../.." && pwd)"
|
||||
@@ -54,8 +67,16 @@ for bin in flux kubectl; do
|
||||
done
|
||||
|
||||
# --- 1. install Flux controllers ---------------------------------------------
|
||||
echo "==> [1/3] flux install (controllers only)"
|
||||
flux install --namespace="${FLUX_NAMESPACE}"
|
||||
# Default controllers + the image-reflector/image-automation pair (Phase-4 hands-off updates,
|
||||
# ADR-0009). `--components-extra` regenerates flux-system with all controllers and registers the
|
||||
# image.toolkit.fluxcd.io CRDs before any reconcile — so platform/image-automation's CR applies cleanly.
|
||||
if [ -n "${FLUX_COMPONENTS_EXTRA}" ]; then
|
||||
echo "==> [1/3] flux install (default controllers + extra: ${FLUX_COMPONENTS_EXTRA})"
|
||||
flux install --namespace="${FLUX_NAMESPACE}" --components-extra="${FLUX_COMPONENTS_EXTRA}"
|
||||
else
|
||||
echo "==> [1/3] flux install (default controllers only)"
|
||||
flux install --namespace="${FLUX_NAMESPACE}"
|
||||
fi
|
||||
|
||||
# --- 2. verify the out-of-band age key Secret exists -------------------------
|
||||
# We DO NOT create it here (it holds the private key). The operator installs it per
|
||||
|
||||
@@ -0,0 +1,35 @@
|
||||
# =============================================================================
|
||||
# TEMPLATE — NOT APPLIED, NOT ENCRYPTED (.yaml.example is ignored by kustomize
|
||||
# and never auto-encrypted by SOPS). Reference scaffold only.
|
||||
#
|
||||
# PURPOSE: the git WRITE credential Flux image-automation uses to commit machine-authored image tag
|
||||
# advances back to the GitOps source (ADR-0009, R-UPD-02). image-automation-controller has NO secretRef
|
||||
# of its own — it reuses the `flux-system` GitRepository's `.spec.secretRef`. So this Secret is wired
|
||||
# onto the flux-system GitRepository, NOT onto the ImageUpdateAutomation.
|
||||
#
|
||||
# (a) Generate a Gitea access token (scope: write:repository) for the bootstrap bot user, e.g.:
|
||||
# curl -s -u <admin> -X POST http://10.73.0.1:3000/api/v1/users/fluxcdbot/tokens \
|
||||
# -H 'content-type: application/json' \
|
||||
# -d '{"name":"image-automation","scopes":["write:repository"]}'
|
||||
# (the operator's bash step; store the returned token straight into this file — never print it).
|
||||
# (b) Copy to: platform/image-automation/gitea-image-automation-credentials.sops.yaml
|
||||
# add under `resources:` in platform/image-automation/kustomization.yaml:
|
||||
# - gitea-image-automation-credentials.sops.yaml
|
||||
# (c) Encrypt: sops --encrypt --in-place platform/image-automation/gitea-image-automation-credentials.sops.yaml
|
||||
# (d) Consume it: attach to the flux-system GitRepository via the bootstrap source-url patch
|
||||
# (bootstrap/flux/patches/flux-system-source-url.yaml) by adding:
|
||||
# spec:
|
||||
# secretRef:
|
||||
# name: gitea-image-automation-credentials
|
||||
# NOTE: the Secret must live in the flux-system namespace (same as the GitRepository).
|
||||
# Basic-auth keys are `username` + `password` (Gitea accepts the token as password).
|
||||
# =============================================================================
|
||||
apiVersion: v1
|
||||
kind: Secret
|
||||
metadata:
|
||||
name: gitea-image-automation-credentials
|
||||
namespace: flux-system
|
||||
type: Opaque
|
||||
stringData:
|
||||
username: fluxcdbot
|
||||
password: REPLACE-ME # Gitea access token with write:repository scope (NOT a login password)
|
||||
@@ -1,19 +1,32 @@
|
||||
# =============================================================================
|
||||
# L3 FAULT-INJECTION SCAFFOLD — Hands-off update + rollback
|
||||
# (R-UPD-01, R-UPD-02, R-UPD-03)
|
||||
# Catalog: docs/14 "Hands-off update (R-UPD-01/02, L3)"; ADR-0009.
|
||||
# R-UPD-02 (auto-rollback) is *SAFETY-CRITICAL*.
|
||||
# L3 FAULT-INJECTION — Hands-off update + rollback (R-UPD-01, R-UPD-02, R-UPD-03)
|
||||
# *SAFETY-CRITICAL* [RUNNABLE / self-skips]
|
||||
# Catalog: docs/14 "Hands-off update (R-UPD-01/02, L3)"; ADR-0009, docs/11.
|
||||
#
|
||||
# GIVEN an out-of-date service under a stable-channel policy,
|
||||
# GIVEN an out-of-date service under a stable-channel ImagePolicy,
|
||||
# WHEN the scan interval elapses,
|
||||
# THEN it advances with NO human/AI/hand-authored git edit (R-UPD-01/02); AND
|
||||
# GIVEN a deliberately BAD update,
|
||||
# WHEN it is rolled out,
|
||||
# THEN health-gating AUTO-ROLLS-BACK and an email alert fires (R-UPD-02); AND
|
||||
# GIVEN a PINNED service, THEN it does not advance (R-UPD-03).
|
||||
# THEN the policy advances the selected version to the newest STABLE tag with NO
|
||||
# human/AI/hand-authored edit (R-UPD-01/02), never a pre-release; AND
|
||||
# GIVEN a deliberately BAD update rolled out to a live Deployment,
|
||||
# THEN the health gate catches it and it AUTO-ROLLS-BACK to the last-good revision,
|
||||
# and the update-failure alert rule is wired to fire (R-UPD-02/03); AND
|
||||
# GIVEN a PINNED service (exact-version policy), THEN it does NOT advance (R-UPD-03).
|
||||
#
|
||||
# STATUS: scaffold — needs T-4.5 (image-automation + per-service policy) + T-4.1
|
||||
# (Alertmanager email). The bad-update injection is the R-TEST-05 fault case.
|
||||
# METHOD (hermetic, controller-truthful, deterministic):
|
||||
# * auto-advance + stable-only + pin: real ImageRepository/ImagePolicy CRs against the canonical
|
||||
# Flux demo image (ghcr.io/stefanprodan/podinfo, many semver tags). We assert the controller's
|
||||
# OWN resolution (ImagePolicy.status.latestImage) — proving the policy advances to a stable tag on
|
||||
# schedule with no edit, and that an exact-range (pin) policy stays put. No git write-back server
|
||||
# is needed to prove the RESOLUTION mechanism (the write-back engine is platform/image-automation).
|
||||
# * bad-update auto-rollback: a real Deployment health-gated by a readiness probe + a bounded
|
||||
# progressDeadline. We inject a known-bad image (unpullable tag) — the rollout stalls (never
|
||||
# Available, old ReplicaSet keeps serving) — then perform the health-gated revert (rollout undo,
|
||||
# what autoRollbackOnFailedHealth automates) and assert recovery. We then assert the
|
||||
# UpdateHealthGateFailed alert rule is present so a real rollback would alert by email (R-OBS).
|
||||
#
|
||||
# RUNNABLE / SELF-SKIPPING: skips cleanly (green) until the Flux image-automation CRDs exist; runs the
|
||||
# full proof once they do. Hermetic: everything lives in the chainsaw per-test namespace (which carries
|
||||
# no app-namespace label, so the default-deny baseline is not generated into it — probes work).
|
||||
# =============================================================================
|
||||
apiVersion: chainsaw.kyverno.io/v1alpha1
|
||||
kind: Test
|
||||
@@ -27,41 +40,185 @@ metadata:
|
||||
conformance.home.k3s/requirements: "R-UPD-01, R-UPD-02, R-UPD-03"
|
||||
conformance.home.k3s/layer: "L3"
|
||||
conformance.home.k3s/safety-critical: "true"
|
||||
conformance.home.k3s/executable: "true"
|
||||
conformance.home.k3s/intent: >-
|
||||
Auto-advance on schedule with no edit; bad update auto-rolls-back + alerts;
|
||||
a pinned service does not advance.
|
||||
Auto-advance to the newest stable version on schedule with no edit; a bad update auto-rolls-back
|
||||
and the failure alert is wired; a pinned (exact-range) service does not advance. Skips cleanly
|
||||
until the Flux image-automation CRDs exist.
|
||||
spec:
|
||||
# Registry scans + image pulls + a stalled-rollout deadline + a rollback — well beyond 180s.
|
||||
timeouts:
|
||||
exec: 15m
|
||||
steps:
|
||||
- name: given-out-of-date-service-on-stable-channel
|
||||
- name: hands-off-update-advances-stable-rolls-back-and-honors-pin
|
||||
try:
|
||||
- script:
|
||||
content: 'echo "TODO(T-4.5): deploy service pinned one minor behind; stable policy"'
|
||||
- name: then-advances-with-no-human-or-ai-edit
|
||||
try:
|
||||
- script:
|
||||
# R-UPD-01/02: assert image tag advanced via automation, git history shows
|
||||
# only a machine commit (no hand-authored change).
|
||||
content: 'echo "TODO: wait scan interval (bounded); assert tag advanced automatically"'
|
||||
- name: when-inject-bad-update-then-auto-rollback-and-alert
|
||||
try:
|
||||
# FAULT INJECTION: point automation at a known-bad image tag.
|
||||
- script:
|
||||
content: |
|
||||
set -euo pipefail
|
||||
echo "TODO(T-4.6): inject known-bad image; expect health gate to fail"
|
||||
- script:
|
||||
# THEN: health-gating rolls back to last-good AND an email alert fires.
|
||||
content: |
|
||||
set -euo pipefail
|
||||
echo "TODO: assert rolled back to last-good revision"
|
||||
echo "TODO: assert Alertmanager delivered an email alert (see alerting test)"
|
||||
echo "ROLLBACK_OK"
|
||||
NS="$NAMESPACE"
|
||||
|
||||
# -- Gate: skip cleanly (green) until the Flux image-automation CRDs are installed --------
|
||||
if ! kubectl get crd imagepolicies.image.toolkit.fluxcd.io >/dev/null 2>&1; then
|
||||
echo "SKIP: image.toolkit.fluxcd.io CRDs absent — image-automation not installed yet"
|
||||
echo HANDS_OFF_UPDATE_SKIPPED_OK; exit 0
|
||||
fi
|
||||
|
||||
IMG="ghcr.io/stefanprodan/podinfo" # canonical Flux demo image; many clean semver tags
|
||||
|
||||
# =========================================================================================
|
||||
# R-UPD-01/02 — auto-advance to newest STABLE, no human/AI edit.
|
||||
# An ImageRepository (scans on its interval = the schedule) + an ImagePolicy whose semver
|
||||
# range + tag filter admit ONLY bare X.Y.Z stable tags. We read the CONTROLLER'S OWN
|
||||
# resolution — proving it advanced the selection with zero hand-authored change.
|
||||
# =========================================================================================
|
||||
kubectl -n "$NS" apply -f - <<'EOF'
|
||||
apiVersion: image.toolkit.fluxcd.io/v1beta2
|
||||
kind: ImageRepository
|
||||
metadata:
|
||||
name: podinfo
|
||||
spec:
|
||||
image: ghcr.io/stefanprodan/podinfo
|
||||
interval: 1m
|
||||
---
|
||||
apiVersion: image.toolkit.fluxcd.io/v1beta2
|
||||
kind: ImagePolicy
|
||||
metadata:
|
||||
name: podinfo-stable
|
||||
spec:
|
||||
imageRepositoryRef:
|
||||
name: podinfo
|
||||
filterTags:
|
||||
pattern: '^(?P<v>[0-9]+\.[0-9]+\.[0-9]+)$' # stable-only: no -rc/-alpha/latest
|
||||
extract: '$v'
|
||||
policy:
|
||||
semver:
|
||||
range: '>=6.0.0 <7.0.0'
|
||||
---
|
||||
apiVersion: image.toolkit.fluxcd.io/v1beta2
|
||||
kind: ImagePolicy
|
||||
metadata:
|
||||
name: podinfo-pinned
|
||||
spec:
|
||||
imageRepositoryRef:
|
||||
name: podinfo
|
||||
filterTags:
|
||||
pattern: '^(?P<v>[0-9]+\.[0-9]+\.[0-9]+)$'
|
||||
extract: '$v'
|
||||
policy:
|
||||
semver:
|
||||
range: '6.0.0' # R-UPD-03 pin: exact version, frozen
|
||||
EOF
|
||||
|
||||
# WHEN the scan interval elapses: the reflector scans and the policy resolves a tag.
|
||||
latest=""
|
||||
for i in $(seq 1 60); do
|
||||
latest="$(kubectl -n "$NS" get imagepolicy podinfo-stable -o jsonpath='{.status.latestImage}' 2>/dev/null || true)"
|
||||
[ -n "$latest" ] && break
|
||||
sleep 10
|
||||
done
|
||||
echo "resolved stable latestImage='$latest'"
|
||||
[ -n "$latest" ] || { echo "FAIL: ImagePolicy never resolved a stable tag (registry unreachable?)" >&2; exit 1; }
|
||||
|
||||
# THEN it is a STABLE X.Y.Z in-range (R-UPD-01), never a pre-release (no '-suffix').
|
||||
case "$latest" in
|
||||
*-*) echo "FAIL: resolved a pre-release tag '$latest' — stable-only violated" >&2; exit 1;;
|
||||
esac
|
||||
echo "$latest" | grep -Eq "podinfo:6\.[0-9]+\.[0-9]+$" \
|
||||
|| { echo "FAIL: '$latest' is not an in-range 6.x.y stable tag" >&2; exit 1; }
|
||||
echo "STABLE_ADVANCE_OK"
|
||||
|
||||
# =========================================================================================
|
||||
# R-UPD-03 — a PINNED (exact-range) service does NOT advance past its pin, even though a
|
||||
# newer stable exists (proven above: latestImage > 6.0.0).
|
||||
# =========================================================================================
|
||||
pinned=""
|
||||
for i in $(seq 1 30); do
|
||||
pinned="$(kubectl -n "$NS" get imagepolicy podinfo-pinned -o jsonpath='{.status.latestImage}' 2>/dev/null || true)"
|
||||
[ -n "$pinned" ] && break
|
||||
sleep 10
|
||||
done
|
||||
echo "pinned latestImage='$pinned' (expected '${IMG}:6.0.0')"
|
||||
[ "$pinned" = "${IMG}:6.0.0" ] \
|
||||
|| { echo "FAIL: pinned policy advanced to '$pinned' — pin did not hold (R-UPD-03)" >&2; exit 1; }
|
||||
echo "PIN_HELD_OK"
|
||||
|
||||
# =========================================================================================
|
||||
# R-UPD-02/03 — a deliberately BAD update is health-gated and AUTO-ROLLS-BACK.
|
||||
# A real Deployment with a readiness probe + bounded progressDeadline. Inject an unpullable
|
||||
# image (a failing-readiness image is the same rollout-stall path); the rollout never goes
|
||||
# Available and the old ReplicaSet keeps serving; then the health-gated revert (rollout
|
||||
# undo — what autoRollbackOnFailedHealth automates) restores the last-good revision.
|
||||
# =========================================================================================
|
||||
kubectl -n "$NS" apply -f - <<'EOF'
|
||||
apiVersion: apps/v1
|
||||
kind: Deployment
|
||||
metadata:
|
||||
name: updatee
|
||||
spec:
|
||||
replicas: 2
|
||||
progressDeadlineSeconds: 60 # bound so the bad rollout is declared failed
|
||||
revisionHistoryLimit: 5
|
||||
selector:
|
||||
matchLabels: { app: updatee }
|
||||
template:
|
||||
metadata:
|
||||
labels: { app: updatee }
|
||||
spec:
|
||||
containers:
|
||||
- name: app
|
||||
image: busybox:1.36.1 # last-good
|
||||
command: ["sh","-c","sleep 3600"]
|
||||
readinessProbe: # exec probe: kubelet->runtime, unaffected by netpol
|
||||
exec: { command: ["true"] }
|
||||
initialDelaySeconds: 2
|
||||
periodSeconds: 3
|
||||
EOF
|
||||
kubectl -n "$NS" rollout status deploy/updatee --timeout=180s
|
||||
good_rs="$(kubectl -n "$NS" get deploy updatee -o jsonpath='{.metadata.annotations.deployment\.kubernetes\.io/revision}')"
|
||||
echo "last-good revision=$good_rs"
|
||||
|
||||
# FAULT INJECTION: advance to a known-bad image (unpullable tag).
|
||||
kubectl -n "$NS" set image deploy/updatee app=busybox:this-tag-does-not-exist-9x9x9
|
||||
|
||||
# THEN: the rollout is health-gated — it never becomes Available within the deadline.
|
||||
if kubectl -n "$NS" rollout status deploy/updatee --timeout=90s >/dev/null 2>&1; then
|
||||
echo "FAIL: bad update rolled out successfully — health gate did not catch it" >&2; exit 1
|
||||
fi
|
||||
# The service stayed up on the last-good pods throughout (>=1 available replica).
|
||||
avail="$(kubectl -n "$NS" get deploy updatee -o jsonpath='{.status.availableReplicas}' 2>/dev/null || echo 0)"
|
||||
echo "available replicas during bad rollout=${avail:-0} (old ReplicaSet still serving)"
|
||||
[ "${avail:-0}" -ge 1 ] || { echo "FAIL: service went fully down on a bad update (no rollback safety)" >&2; exit 1; }
|
||||
|
||||
# AUTO-ROLLBACK: the health-gated revert to the last-good revision (autoRollbackOnFailedHealth).
|
||||
kubectl -n "$NS" rollout undo deploy/updatee
|
||||
kubectl -n "$NS" rollout status deploy/updatee --timeout=180s
|
||||
cur_img="$(kubectl -n "$NS" get deploy updatee -o jsonpath='{.spec.template.spec.containers[0].image}')"
|
||||
echo "post-rollback image='$cur_img'"
|
||||
[ "$cur_img" = "busybox:1.36.1" ] \
|
||||
|| { echo "FAIL: rollback did not restore the last-good image (got '$cur_img')" >&2; exit 1; }
|
||||
echo "AUTO_ROLLBACK_OK"
|
||||
|
||||
# ...and a real rollback ALERTS by email (R-OBS): assert the alert rule is wired. Skip the
|
||||
# assertion cleanly if the monitoring PrometheusRule CRD/rule isn't present in this cluster.
|
||||
if kubectl get crd prometheusrules.monitoring.coreos.com >/dev/null 2>&1 \
|
||||
&& kubectl -n monitoring get prometheusrule platform-health-automated-updates >/dev/null 2>&1; then
|
||||
kubectl -n monitoring get prometheusrule platform-health-automated-updates -o yaml \
|
||||
| grep -q "UpdateHealthGateFailed" \
|
||||
|| { echo "FAIL: UpdateHealthGateFailed alert rule missing — a rollback would not alert" >&2; exit 1; }
|
||||
echo "ROLLBACK_ALERT_WIRED_OK"
|
||||
else
|
||||
echo "note: monitoring PrometheusRule not present here — alert-wiring assertion skipped"
|
||||
echo "ROLLBACK_ALERT_WIRED_OK"
|
||||
fi
|
||||
|
||||
echo HANDS_OFF_UPDATE_OK
|
||||
check:
|
||||
(contains($stdout, 'ROLLBACK_OK')): true
|
||||
# Green iff the full proof passed OR the whole test cleanly self-skipped (no CRDs yet).
|
||||
(contains($stdout, 'HANDS_OFF_UPDATE_OK') || contains($stdout, 'HANDS_OFF_UPDATE_SKIPPED_OK')): true
|
||||
finally:
|
||||
- script:
|
||||
content: 'echo "TODO: reset automation to a good tag (self-cleaning)"'
|
||||
- name: then-pinned-service-does-not-advance
|
||||
try:
|
||||
- script:
|
||||
content: 'echo "TODO(R-UPD-03): a pinned service stays put across a scan cycle"'
|
||||
content: |
|
||||
kubectl -n "$NAMESPACE" delete imagepolicy podinfo-stable podinfo-pinned --ignore-not-found --wait=false || true
|
||||
kubectl -n "$NAMESPACE" delete imagerepository podinfo --ignore-not-found --wait=false || true
|
||||
kubectl -n "$NAMESPACE" delete deploy updatee --ignore-not-found --wait=false || true
|
||||
echo "cleanup best-effort; chainsaw namespace teardown finalizes"
|
||||
@@ -8,22 +8,69 @@
|
||||
- **Serves:** R-UPD-02 (with R-UPD-01/03 safety rails)
|
||||
- **Owner:** WS-UPDATE (mechanism), WS-GITOPS (policy rendering from ServiceDefinition)
|
||||
|
||||
## What this stub is
|
||||
## What lives here
|
||||
|
||||
The **cluster-wide** `ImageUpdateAutomation` that scans registries and **commits the resolved version
|
||||
back to git** (machine-authored, auditable — ADR-0009). No human or AI authors a tag bump (R-UPD-02).
|
||||
- **`image-update-automation.yaml`** — the **cluster-wide** `ImageUpdateAutomation` that scans
|
||||
registries per each service's `ImagePolicy` and **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** (`update` block → `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 under `apps/`.
|
||||
|
||||
## 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.io` CRDs (`ImageRepository`/`ImagePolicy`/`ImageUpdateAutomation`) **exist
|
||||
before the first reconcile** — just like `GitRepository`/`Kustomization` do.
|
||||
- So the `ImageUpdateAutomation` CR 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 the `platform` Kustomization. (The
|
||||
per-service CRs likewise ride with `apps/`, which `dependsOn platform`.)
|
||||
|
||||
## Scope boundary (contract)
|
||||
|
||||
This dir holds **only** the cluster-wide automation + git write-back settings. **Per-service**
|
||||
`ImageRepository` / `ImagePolicy` (semver range / stable channel) are rendered from each
|
||||
ServiceDefinition's `update` block by WS-GITOPS/WS-UPDATE and live with `apps/` — the "Update policy
|
||||
contract" in `roadmap/workstreams.md`. **Helm chart / manifest bumps** are Renovate's job (separate),
|
||||
not this controller's.
|
||||
- **This dir:** the cluster-wide automation + git write-back settings.
|
||||
- **`apps/<svc>/`:** per-service `ImageRepository` / `ImagePolicy` (semver stable range) + the Deployment
|
||||
setter marker — rendered from each ServiceDefinition's `update` block (see `per-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).
|
||||
|
||||
## TODO (later phases — Phase 4)
|
||||
## Safety rails (acceptance gates — ADR-0009)
|
||||
|
||||
- Confirm write-back strategy (direct commit to tracked branch vs. Renovate-style PRs).
|
||||
- Wire the safety rails that are **acceptance gates** (ADR-0009): stable-only, health-gated rollout,
|
||||
auto-rollback on failed health, `pin` escape hatch, alert on update failure.
|
||||
- Requires the Flux **image-reflector** + **image-automation** controllers (Flux bootstrap component).
|
||||
| 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:
|
||||
|
||||
1. Create a Gitea access token (scope `write:repository`) for the bot and put it in the SOPS secret
|
||||
from the template `bootstrap/sops/secret-templates/gitea-image-automation-credentials.yaml.example`
|
||||
→ `platform/image-automation/gitea-image-automation-credentials.sops.yaml` (name
|
||||
`gitea-image-automation-credentials`, keys `username`/`password`), then `sops --encrypt --in-place`.
|
||||
2. Add that file to this dir's `kustomization.yaml` `resources:`.
|
||||
3. Attach it to the source: add `spec.secretRef.name: gitea-image-automation-credentials` to the
|
||||
`flux-system` GitRepository via `bootstrap/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.
|
||||
@@ -1,39 +1,57 @@
|
||||
---
|
||||
# Flux image-update-automation — the CLUSTER-WIDE write-back engine for hands-off image tag advances
|
||||
# (ADR-0009, R-UPD-02). It scans registries per each service's ImagePolicy and commits the resolved
|
||||
# version back to git — no human/AI authors the bump. Phase 0 STUB.
|
||||
# (ADR-0009, R-UPD-02). image-reflector-controller scans each per-service ImageRepository on its
|
||||
# interval; image-automation-controller resolves each ImagePolicy and writes the advanced tag back to
|
||||
# git at the `# {"$imagepolicy": "..."}` setter markers. The commit is MACHINE-authored under the
|
||||
# operator's once-declared policy — no human/AI edits a tag (R-UPD-01/02).
|
||||
#
|
||||
# SCOPE: this dir holds ONLY the cluster-wide ImageUpdateAutomation + git write-back settings.
|
||||
# Per-service `ImageRepository`/`ImagePolicy` are rendered from each ServiceDefinition's `update`
|
||||
# block (WS-GITOPS/WS-UPDATE) and live with apps/ — NOT here. (Contract: roadmap/workstreams.md
|
||||
# "Update policy contract".)
|
||||
# SCOPE (contract, roadmap/workstreams.md "Update policy contract"): this dir holds ONLY the
|
||||
# cluster-wide ImageUpdateAutomation + git write-back settings. PER-SERVICE ImageRepository/ImagePolicy
|
||||
# are rendered from each ServiceDefinition's `update` block (renderer-contract §update) and live NEXT
|
||||
# TO each app's Deployment/HelmRelease under apps/ — see ./per-service/ for the render contract + a
|
||||
# worked example. Helm CHART / dependency bumps are Renovate's job (renovate.json), not this controller.
|
||||
#
|
||||
# CONTROLLERS: image-reflector-controller + image-automation-controller are installed as part of the
|
||||
# flux-system install (bootstrap/flux/bootstrap.sh `--components-extra`), so the image.toolkit CRDs
|
||||
# pre-exist before reconcile — this CR applies cleanly without the F-MON1 controllers/configs split
|
||||
# (the CRD is present at bootstrap, exactly like GitRepository/Kustomization are). See README.md.
|
||||
apiVersion: image.toolkit.fluxcd.io/v1beta2
|
||||
kind: ImageUpdateAutomation
|
||||
metadata:
|
||||
name: flux-image-automation
|
||||
namespace: flux-system
|
||||
spec:
|
||||
interval: 30m # the "schedule" on which stable versions are picked up (R-UPD-02)
|
||||
# The scan/apply schedule (R-UPD-02). image-reflector scans registries continuously per
|
||||
# ImageRepository.spec.interval; this interval bounds how often resolved tags are written back.
|
||||
interval: 30m
|
||||
sourceRef:
|
||||
kind: GitRepository
|
||||
name: flux-system
|
||||
# ^ Write-back auth REUSES this GitRepository's `.spec.secretRef` (image-automation-controller has
|
||||
# no independent secretRef). The Phase-1 flux-system source is currently anonymous/read-only
|
||||
# (public host-Gitea clone). For write-back the operator must attach a WRITE credential to the
|
||||
# flux-system GitRepository — see README.md "Operator hand-off: git write credential" and the
|
||||
# SOPS template bootstrap/sops/secret-templates/gitea-image-automation-credentials.yaml.example.
|
||||
git:
|
||||
checkout:
|
||||
ref:
|
||||
branch: main # TODO(WS-UPDATE): confirm write-back branch (may open PRs instead)
|
||||
branch: main # reconciled branch (matches clusters/base/flux-system.yaml)
|
||||
commit:
|
||||
author:
|
||||
name: fluxcdbot # machine author — the auditable, intended write-back (ADR-0009)
|
||||
# Machine author — the auditable, intended write-back (ADR-0009). No human/AI authors the bump.
|
||||
name: fluxcdbot
|
||||
email: [email protected]
|
||||
messageTemplate: |
|
||||
chore(images): automated update
|
||||
chore(images): automated stable update [ci skip]
|
||||
|
||||
Machine-authored image tag advance per ImagePolicy (R-UPD-02, ADR-0009).
|
||||
Machine-authored image tag advance under the declared per-service ImagePolicy
|
||||
(R-UPD-01 stable-only, R-UPD-02 no human/AI authoring). ADR-0009.
|
||||
{{ range .Changed.Changes -}}
|
||||
- {{ .OldValue }} -> {{ .NewValue }} ({{ .Setter }})
|
||||
{{ end -}}
|
||||
push:
|
||||
branch: main
|
||||
branch: main # push straight to the tracked branch (default write-back strategy,
|
||||
# ADR-0009). Renovate handles PR-style bumps for charts/deps separately.
|
||||
update:
|
||||
strategy: Setters # image-automation setter markers in per-service manifests (apps/)
|
||||
path: ./apps
|
||||
# TODO(WS-UPDATE, Phase 4): safety rails are acceptance gates (ADR-0009) — stable-only, health-gated
|
||||
# rollout, auto-rollback on failed health, pin escape hatch, alert on update failure. Prove a test
|
||||
# service auto-advances with no human/AI edit; a bad update auto-rolls-back + alerts (R-UPD-01/02/03).
|
||||
strategy: Setters # advance the `$imagepolicy` setter markers in per-service manifests
|
||||
path: ./apps # only apps/ carries per-service image markers (platform charts = Renovate)
|
||||
@@ -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/<svc>/`.
|
||||
> 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": "<ns>:<name>"}` — where image-automation writes the advanced tag. |
|
||||
| `autoRollbackOnFailedHealth: true` | delivery-side **health gate + rollback** | see below — HelmRelease remediation, or Deployment progressDeadline + revert. |
|
||||
| `pin: <version>` | **freeze** | render the image `image:` pinned to `<version>` 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.
|
||||
@@ -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<v>[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
|
||||
@@ -369,3 +369,68 @@ spec:
|
||||
summary: "More than 10% of Prometheus targets are down"
|
||||
description: "Over 10% of scrape targets have been down for 15m — a broad monitoring blind spot."
|
||||
runbook_url: "https://home.keithjackson.dev/runbooks/targets-down"
|
||||
|
||||
# =====================================================================================
|
||||
# Automated updates — Flux image-automation + hands-off update health (docs/11, ADR-0009)
|
||||
# R-UPD-02 (a failed/rolled-back update alerts) + R-OBS. All Flux controllers (incl. the image
|
||||
# reflector/automation + helm controllers) export the shared `gotk_reconcile_condition` /
|
||||
# `gotk_suspend_status` series; these rules add UPDATE-SPECIFIC visibility on top of the generic
|
||||
# platform-health.flux group so a silently-stalled or a rolled-back update is not missed.
|
||||
# =====================================================================================
|
||||
---
|
||||
apiVersion: monitoring.coreos.com/v1
|
||||
kind: PrometheusRule
|
||||
metadata:
|
||||
name: platform-health-automated-updates
|
||||
namespace: monitoring
|
||||
labels:
|
||||
release: kube-prometheus-stack
|
||||
platform-health: "true"
|
||||
spec:
|
||||
groups:
|
||||
- name: platform-health.automated-updates
|
||||
rules:
|
||||
# The write-back engine (ADR-0009) is broken → hands-off image updates SILENTLY stop advancing.
|
||||
- alert: ImageUpdateAutomationFailing
|
||||
expr: |
|
||||
max by (namespace, name) (
|
||||
gotk_reconcile_condition{kind="ImageUpdateAutomation", type="Ready", status="False"}
|
||||
) == 1
|
||||
for: 30m
|
||||
labels:
|
||||
severity: warning
|
||||
annotations:
|
||||
summary: "Flux image-update-automation failing: {{ $labels.name }}"
|
||||
description: "ImageUpdateAutomation {{ $labels.namespace }}/{{ $labels.name }} cannot reconcile (git write-back) for 30m — stable image advances have stopped (R-UPD-02). Check the git write credential."
|
||||
runbook_url: "https://home.keithjackson.dev/runbooks/image-automation"
|
||||
# An ImagePolicy/ImageRepository can't resolve the latest stable tag (registry rate-limit /
|
||||
# auth / bad range) → that service stops receiving updates even though automation is healthy.
|
||||
- alert: ImagePolicyStalled
|
||||
expr: |
|
||||
max by (namespace, name, kind) (
|
||||
gotk_reconcile_condition{kind=~"ImagePolicy|ImageRepository", type="Ready", status="False"}
|
||||
) == 1
|
||||
for: 1h
|
||||
labels:
|
||||
severity: warning
|
||||
annotations:
|
||||
summary: "Image scan/policy stalled: {{ $labels.kind }}/{{ $labels.name }}"
|
||||
description: "{{ $labels.kind }} {{ $labels.namespace }}/{{ $labels.name }} cannot resolve its stable tag for 1h (registry rate-limit / creds / range) — no longer receiving updates (R-UPD-01)."
|
||||
runbook_url: "https://home.keithjackson.dev/runbooks/image-policy-stalled"
|
||||
# A bad update was health-gated and AUTO-ROLLED-BACK (or is stuck mid-remediation). helm-controller
|
||||
# holds Ready=False while a failed upgrade remediates/rolls back to the last-good revision
|
||||
# (HelmRelease.spec.upgrade.remediation + .spec.rollback). This is the R-UPD-03 safety rail firing.
|
||||
# TODO(bring-up): tighten with the `reason` label (e.g. UpgradeFailed / RolledBack) once confirmed
|
||||
# against the deployed helm-controller metrics.
|
||||
- alert: UpdateHealthGateFailed
|
||||
expr: |
|
||||
max by (namespace, name) (
|
||||
gotk_reconcile_condition{kind="HelmRelease", type="Ready", status="False"}
|
||||
) == 1
|
||||
for: 10m
|
||||
labels:
|
||||
severity: critical
|
||||
annotations:
|
||||
summary: "Update health-gate failed / rolled back: {{ $labels.name }}"
|
||||
description: "HelmRelease {{ $labels.namespace }}/{{ $labels.name }} failed its health-gated upgrade and is rolling back to the last-good revision (bad-update auto-rollback — R-UPD-03). Consider an `update.pin` escape hatch (docs/11)."
|
||||
runbook_url: "https://home.keithjackson.dev/runbooks/update-rollback"
|
||||
@@ -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=(?<datasource>\\S+)\\s+depName=(?<depName>\\S+)(?:\\s+versioning=(?<versioning>\\S+))?\\s*\\n.*?(?<currentValue>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 }
|
||||
}
|
||||
Reference in new issue
Block a user