Skip to content
Back to skills

Flux Reconcile Audit

ASecurity

Audit every Flux Kustomization and HelmRelease over a window (default 7d): classify broken / slow / miswired / propagating / suspended / healthy with per-item culprit, failure counts, and p99 reconcile duration. Use for "what's slow / stuck / been broken this week".

  • 11 stars
  • 0 votes
  • 0 copies
  • 2 views
  • Added September 9, 2026
devopspythongobashapiperformance

Works with

  • api

Security analysis

A100/100

Pro scans all 3 files and shows the line behind each finding

Scanned September 27, 2026

npx -y skills add agentydragon/ducktape --skill flux_reconcile_audit --agent claude-code

Installs into .claude/skills of the current project.

Are you the author of Flux Reconcile Audit?

Add the live security badge to your README. It updates with every re-scan.

Security grade badge for Flux Reconcile Audit
[![Security: A — Skills Directory](https://www.skillsdirectory.com/api/skills/agentydragon-flux-reconcile-audit/badge)](https://www.skillsdirectory.com/skills/agentydragon-flux-reconcile-audit)

More formats (shields.io, HTML) on the badges page. Keep it an A: scan every change in CI with Pro.

Download with Pro
SKILL.md
---
name: flux_reconcile_audit
description: >-
  Audit every Flux Kustomization and HelmRelease over a window (default 7d):
  classify broken / slow / miswired / propagating / suspended / healthy with
  per-item culprit, failure counts, and p99 reconcile duration. Use for
  "what's slow / stuck / been broken this week".
---

# Flux Reconcile Audit

Run `audit.py` from this skill directory. It starts a `kubectl proxy`,
queries K8s API + Mimir + Loki concurrently, and prints a single
Markdown report.

```bash
# Default — 7-day window, all data sources on, ~25 s
python3 skills/flux_reconcile_audit/audit.py

# "What's broken right now?" — 30 m window, no Loki, ~3 s
python3 skills/flux_reconcile_audit/audit.py --window 30m --no-loki

# Drill into one resource
python3 skills/flux_reconcile_audit/audit.py --window 1h --name forgejo
```

## Buckets

| Bucket                     | Definition                                                                            | Promoter                                                                                                        |
| -------------------------- | ------------------------------------------------------------------------------------- | --------------------------------------------------------------------------------------------------------------- |
| **Broken**                 | Currently failing; last event was a failure.                                          | `last_was_failure` AND (`real_fails > finishes` OR `real_fails ≥ 2`).                                           |
| **Miswired-but-converges** | Eventually `Ready=True` but only after retries.                                       | `real_fails > 0` AND `finishes > 0` AND not Broken.                                                             |
| **Slow-but-converges**     | Reaches Ready=True but takes too long per push.                                       | p99 reconcile duration ≥ 60 s (Kustomization) / 300 s (HelmRelease) AND `finishes > 0` AND not Broken/Miswired. |
| **Propagating**            | Currently Ready=False because a dependsOn is catching up; no real failures in window. | `Ready=False, reason=DependencyNotReady, real_fails=0`.                                                         |
| **Suspended**              | `spec.suspend=true`. Informational only.                                              | —                                                                                                               |
| **Healthy**                | Everything else.                                                                      | —                                                                                                               |

First rule that matches wins. The `last_was_failure` gate on Broken is
load-bearing: a Kustomization currently `Ready=True
ReconciliationSucceeded` with hundreds of historical failures is
Miswired-but-recovered, not Broken.

## Data sources

In rough order of preference (cheapest + most authoritative first):

1. **K8s API via `kubectl proxy`** — the script starts a `kubectl proxy
--port=8001` for its lifetime. All Kustomization / HelmRelease /
   Event / probe-kind list calls hit `http://localhost:8001`. One TCP
   connection, no per-call kubectl subprocess fork, ~30 ms per call vs
   ~300 ms for `kubectl get … -o json`.
2. **K8s Events** (`/api/v1/events?fieldSelector=involvedObject.apiVersion=…`).
   Structured `reason` / `message` / `count` / `lastTimestamp`. One
   request per controller. Default event retention is ~1 h.
3. **Kustomization `status.conditions[Ready].message`** parsed for the
   bracketed `[Kind/namespace/name status: 'X']` reference — primary
   attribution for "which underlying object is the problem".
4. **Label-selector probe** — at startup we fetch every condition-bearing
   probe kind (`Deployment`, `Cluster` from CNPG, `Bucket` from
   SeaweedFS, …) cluster-wide via `/apis/<g>/<v>/<r>` in parallel, then
   in-memory filter by `kustomize.toolkit.fluxcd.io/name`. One HTTP
   call per kind, never per resource.
5. **Mimir** — one batched query `histogram_quantile by (name, kind)`
   for p99 reconcile duration. Skipped with `--no-mimir`.
6. **Loki** — two batched queries per controller (`Reconciliation
finished` count + `Reconciliation failed` count) by
   `Kustomization_name` / `HelmRelease_name`. Used as the supplement
   for windows past event retention. Only consulted when `--window >
1h`. Skipped with `--no-loki`.

## Prereqs

### Mimir scraping the Flux controllers

The PodMonitor in `cluster/cdk8s/flux_monitoring.py` points Prometheus at every
controller's `http-prom` port. Verify:

```bash
kubectl get podmonitor -n flux-system flux-system
```

Without it, `gotk_reconcile_duration_seconds_*` is empty and the Slow
bucket can never fire.

### Port-forwards

The script reads Mimir at `localhost:8080` and Loki at `localhost:3100`:

```bash
kubectl port-forward -n monitoring svc/mimir-querier 8080:8080 >/tmp/mimir-pf.log 2>&1 &
kubectl port-forward -n loki svc/loki-read 3100:3100 >/tmp/loki-pf.log 2>&1 &
# wait until both print "Forwarding from" — at least ~5 s — before running.
```

The K8s API does **not** need its own port-forward; the script starts
its own `kubectl proxy` internally.

## What the report contains

Per non-healthy resource:

- **Current status** — Ready + reason from the live K8s API.
- **p99 reconcile duration** — Mimir, when enabled and non-zero.
- **Failures / successes in window** — counted from K8s Events; with
  `--window > 1h` both counts are supplemented from Loki and the
  larger value used. Transient errors (etcd timeouts etc.) are split
  off and reported as `(+N transient)`. Loki supplements show as
  `(loki-fail=N, loki-ok=N)`.
- **Underlying culprit** — `kind/name (namespace)` parsed from Flux's
  own message, plus the cluster-reported status of that object. This
  is the killer feature: Flux already says which Deployment / CNPG
  Cluster / etc. is the cause; the script just extracts it.
- **Last error** — verbatim event message text, truncated to 400 chars.
- **Label-selector probe** — only when the managed object's own
  condition is False or its phase isn't Running/Succeeded/Bound. Capped
  at 8 entries.

## Gotchas

- **Event retention defaults to ~1 h.** For "what's been wrong this
  week" you need Loki. For "what's wrong now" events are sufficient
  and faster — pass `--no-loki`.
- **`Ready=Unknown reason=Progressing` is a Broken signal too** when
  the last event was a failure. The rule is on event ordering, not on
  the current Ready field.
- **Flux's culprit regex is `[Kind/namespace/name status: '<state>']`.**
  Most `HealthCheckFailed` messages carry this; `BuildFailed` /
  `ApplyFailed` / `PostBuildFailed` do not. Those buckets get the
  verbatim error message under "Last error" but no structured culprit
  line.
- **Deployment condition gotcha.** A Deployment with `Available=True`
  but `Progressing=False ProgressDeadlineExceeded` is effectively
  stuck. The probe surfaces both conditions so this isn't hidden
  behind the happier Available.
- **The `flux-system` Kustomization usually shows as Slow** with p99
  ≈ 8 min because reconciling all of gotk-components takes a while.
  Expected, not a finding.
- **Wait long enough for the Mimir/Loki port-forwards to come up.**
  ~5 s minimum. If a port-forward is still booting when the audit
  starts, those queries silently return empty and the report quietly
  miscategorizes.
- **Adding a new stateful CRD?** Append it to `DEFAULT_PROBE_KINDS` in
  `audit.py` so the label-selector probe covers it. The list is
  intentionally curated rather than auto-discovered — discovery would
  add several seconds for no real value on a well-known cluster.

## Performance

Roughly:

- `kubectl proxy` startup: ~1 s
- Universe + Events + 14 probe-kind lists, all parallel: ~1-2 s
- Mimir `histogram_quantile(…[7d])`: ~15-25 s (the irreducible floor
  for week-long histogram queries on this scale)
- Loki two batched queries: ~5 s each, run in parallel: ~5 s
- Classification + report: ~0.1 s

Wall: **~3 s** for `--window 30m --no-loki`, **~25 s** for `--window 7d`
with the full pipeline. Down from ~7 min in the per-resource-kubectl-call
predecessor.

## Test protocol (for skill maintainers)

When changing classification rules or queries, validate against the
live cluster in this order:

1. `--window 30m --no-loki --name <known-broken>` — single resource,
   events only. Confirm culprit attribution lands.
2. `--window 30m --no-loki` — full universe, event-only path. Confirm
   bucket counts are sensible.
3. `--window 7d` — full universe with Loki supplements. Confirm
   Miswired bucket is populated (longer window catches
   retry-then-converge patterns).
4. Always wait ≥5 s after starting the Mimir + Loki port-forwards
   before invoking the script.

See `cluster/cdk8s/flux_monitoring.py` for the
metric-scraping wiring this depends on.

Files in this skill

  • BUILD.bazel478 B
  • SKILL.md8.8 KB
  • audit.py20.4 KB

Attribution

Is this your skill, or is something wrong with this listing? Request removal or report an issue. Author removals are honored within 72 hours.

Comments

Loading comments…

Newsletter

Get the weekly list of safe new skills, and the ones we pulled.