The operator got no mail when Flux began applying a git change and none when it finished. Measured cause: notification-controller v1.4.0 is running and its CRDs serve v1beta3, but there are ZERO Provider and ZERO Alert objects in the cluster — nothing in git ever created any. The obvious fix does not work. Neither Flux event can be filtered into a deploy signal: ReconciliationSucceeded is a heartbeat (101 events in one hour across 16 Kustomizations, message byte-identical whether the apply changed anything), and Progressing is worse (five Kustomizations re-apply the same object on every tick — event counts 2505/2504/2501/2495/2490). gotk_* carries no revision label at all (gotk_reconcile_condition 120 series, gotk_suspend_status 40, none with one). The applied revision is the only signal that moves when and only when git moves, and it lives in object status alone. So export it via the kube-state-metrics custom-resource-state path that already serves the Velero Schedule metrics — kube_flux_kustomization_info/_suspended and kube_flux_gitrepository_info — and ask the question in PromQL: FluxRolloutInProgress (severity: info, no `for:`) fires the moment a non-suspended Kustomization's applied revision is not the revision the GitRepository fetched from main, and CLEARS when all have caught up. Fired mail = a deploy started; RESOLVED mail = it completed. The completion half is send_resolved, not a second rule. FluxRolloutStuck (severity: warning, for: 30m) is the same expression as the escalation for a rollout that started and never converged. Two guards, each with a test whose only job is to fail if the clause is removed: `and on () (count(gitrepository_info) > 0)` — without it a missing exporter makes `unless on (revision) <empty>` remove nothing and all 16 Kustomizations alert forever, the permanently-firing mode this repo shipped once already — and the suspended-Kustomization exclusion, without which a suspended tier holds the rollout open and the "applied" mail never arrives. A dedicated Alertmanager severity=info route + email-deploy receiver keeps deploy notices off the [WARNING] catch-all. podAnnotations carries a roll-counter because the chart sets no checksum over the custom-resource-state ConfigMap, so a config-only edit would leave KSM on the old collector set with the new alerts silently inert. Verified: promtool SUCCESS on 6 scenarios (both guards, plus the convergence test that proves the alert empties itself); lint/l0/render-golden/l1/decisions-log/ spec-immutability green in the pinned toolbox. NOT verified: no mail has been sent — the metrics do not exist until this merges. Full gap list, and the deliberately-cut notification-controller half, in the handoff entry. Co-Authored-By: Claude Opus 5 (1M context) <[email protected]>
7.0 KiB
flux-rollout — a git change is rolling out, or has stopped halfway
Alerts: FluxRolloutInProgress (info, no for:) and FluxRolloutStuck (warning, 30m)
File: platform/monitoring/alerts.yaml, group platform-health.flux
Metrics: kube_flux_kustomization_info / kube_flux_kustomization_suspended /
kube_flux_gitrepository_info — kube-state-metrics custom-resource-state, configured in
platform/monitoring/kube-prometheus-stack.yaml
What this means here
These two are the deploy notification, not a fault report. On this cluster merging a PR into
main IS the deploy, and until this pair existed nothing told you a deploy had begun: the Flux event
stream emits ReconciliationSucceeded on every 10-minute tick whether or not anything changed (101
such events in one hour, measured 2026-09-29), so "applying a commit" and "idle" looked identical.
The pair reads the one signal that moves only when git moves — the applied revision:
GitRepository/flux-system.status.artifact.revision— whatmainis (polled every 1m)Kustomization/*.status.lastAppliedRevision— what each tier has actually applied
FluxRolloutInProgress fires the moment any non-suspended Kustomization's applied revision is
not the revision fetched from main, and clears when every one of them has caught up. So:
| Subject | Means | |
|---|---|---|
| firing | [FLUX] applying — FluxRolloutInProgress |
a commit started rolling out |
| resolved | [FLUX] applied — FluxRolloutInProgress |
every Kustomization reached it; rollout complete |
Both are expected, routine, and not a problem. The severity: info label routes them to the
dedicated email-deploy Alertmanager receiver so they never arrive subject-lined [WARNING].
FluxRolloutStuck is the one that matters. Same expression, for: 30m, severity: warning. It
means a deploy started and did not converge — the "applying" mail arrived and the "applied" mail
never did. The 14-tier chain is dependency-ordered and revision-gated, so a single stalled tier holds
every tier below it on "dependency not ready".
First commands
kubectl does not work on the workstation — the toolchain lives only in the pinned container, and
the context must always be named explicitly.
Which tiers are behind, and on what:
./infra/devcontainer/dev.sh run bash -c '
kubectl --context k3s-prod -n flux-system get gitrepository flux-system \
-o jsonpath="WANTED: {.status.artifact.revision}{\"\n\"}"
kubectl --context k3s-prod get kustomizations -A \
-o custom-columns="NAME:.metadata.name,APPLIED:.status.lastAppliedRevision,READY:.status.conditions[?(@.type==\"Ready\")].status"'
Any row whose APPLIED differs from WANTED is a tier this alert is naming. Then ask why that
tier is not moving — its Ready condition carries the reason:
./infra/devcontainer/dev.sh run bash -c '
kubectl --context k3s-prod -n flux-system get kustomization <name> \
-o jsonpath="{range .status.conditions[*]}{.type}={.status} {.reason}: {.message}{\"\n\"}{end}"'
Confirm the alert is reading live data rather than a stale exporter:
./infra/devcontainer/dev.sh run bash -c '
POD=$(kubectl --context k3s-prod -n monitoring get pod -l app.kubernetes.io/name=prometheus -o name | head -1)
kubectl --context k3s-prod -n monitoring exec "$POD" -c prometheus -- \
/bin/promtool query instant http://localhost:9090 "count by (__name__) ({__name__=~\"kube_flux.*\"})"'
Three names must come back (kube_flux_kustomization_info, kube_flux_kustomization_suspended,
kube_flux_gitrepository_info). If that query is empty, the alerts are inert — go to the last
cause below.
Likely causes, most likely first
- Nothing is wrong — the rollout is simply in flight. Most Kustomizations reconcile on a 10m
interval and the chain is sequential, so several minutes between the "applying" and "applied"
mails is normal.
ciliumis on a 1h interval and can legitimately be the last to converge. - A gating tier is unhealthy, so everything below it is parked. Look for
DependencyNotReadyin the stuck tier's message and walk up the chain to the first tier that is not Ready; that one is the real fault. Its own alert (FluxReconciliationFailure) and runbookflux-reconcileown it. - A Kustomization is suspended. A suspended tier is excluded from this alert by design (so it
cannot hold the "applied" mail open forever), but anything that
dependsOnit still stalls and will show here.FluxResourceSuspended/ runbookflux-suspendedcovers it. - A health check is timing out. A
wait: truetier only recordslastAppliedRevisiononce its objects pass health checks, so a workload that never goes Ready keeps the tier on the old revision even though the manifests applied fine. - kube-state-metrics is not publishing the
kube_flux_*family. Then the rules are inert and you would get no mail at all rather than a stuck one — so this is the cause when the complaint is silence. The metrics come fromcustomResourceStateplus a read-only RBAC rule forkustomize.toolkit.fluxcd.io/source.toolkit.fluxcd.io; without that RBAC, KSM emits the set empty and logs a forbidden error. Check its logs:Note the chart puts no checksum annotation over that ConfigMap, so a change to the config alone does not restart the pod — that is why./infra/devcontainer/dev.sh run bash -c ' kubectl --context k3s-prod -n monitoring logs deploy/kube-prometheus-stack-kube-state-metrics \ --tail=50 | grep -i "forbidden\|custom.resource" || echo "no RBAC complaints"'podAnnotationscarries ahomelab.keithjackson.dev/crs-config-rolloutcounter that must be bumped in the same commit.
When to escalate or stop
FluxRolloutInProgressalone, cleared within ~15 minutes: do nothing. That is the feature working. Do not suspend, reconcile or apply anything in response to it.FluxRolloutStuckfiring: fix the upstream fault, not this symptom. This alert names the tier that is behind; it never names the cause. Walk up to the first non-Ready tier and useflux-reconcile.- Never
kubectl apply,editorpatchthe change through by hand. Flux reconcilesmain; a hand-applied object is drift that the next reconcile reverts or, worse, adopts — and it makes the applied-revision signal these alerts read a lie.flux reconcile(an operator action, never an agent's) is the only nudge that is safe, and it only hurries the same pull. - Escalate to a revert if a stuck rollout is degrading service. Reverting the offending commit
on
mainthrough the normal PR flow is the rollback path; see runbookupdate-rollback. - If both alerts are silent during a deploy you know happened, the notification itself is broken, not the deploy. That is cause 5 above, and it is worth fixing immediately — silence from these rules is indistinguishable from "no deploys happened", which is the exact defect they were written to close.