fix(triage): gate image cluster_count on cluster liveness #18

Merged
jonas merged 1 commit from fix/triage-cluster-count-liveness into main 2026-06-26 14:44:10 +02:00
Owner

Problem

Triage's "X / Y clusters" count (asset_risk.cluster_count) is computed in the asset_risk_src view from cluster_digests / publicly_exposed_digests, without the cluster_sessions liveness gate that the image-detail Workloads tab (uiapi.ImageDetailHandler, via scam.LiveRecordFilter) applies.

When a cluster's agent stops heartbeating (crash, network, decommission) no DELETE event arrives, so its pods stay pod_phase='Running', msg<>'DELETE'. Triage keeps counting them; the Workloads tab correctly drops them. → count shows in triage but the Workloads tab is empty. For admins, the hidden-namespace and ACL filters are no-ops, so liveness is the sole divergence.

This is the invariant cluster_summary already documents: maintained tables can't bake a NOW()-relative gate, so the reader applies it — the triage path just never did.

Fix

New migration 20260630a_asset_risk_live_cluster_spread.sqlCREATE OR REPLACE VIEW asset_risk_src adding a cluster_sessions liveness EXISTS gate to the two image-spread CTEs (image_cluster_spreadcluster_count, image_exposed_spreadexposed_cluster_count, gated identically so "X/Y" stays consistent). Registered in cmd/server/main.go after 20260630.

Surgical scope: the existence JOIN and the cluster-type triage rows stay on raw cluster_digests, so an image deployed only in a dead cluster still appears in triage (ranked by its other signals) with cluster_count = 0 — the frontend hides the chip at 0, matching the empty Workloads tab. (Gating cluster_digests itself, which would also drop such images/dead-cluster rows from triage, is a possible follow-up.)

The 24h window hardcodes SPAM_CLUSTER_LIVE_WINDOW's default (Postgres can't read the env var); documented in the migration header. Takes effect on the next asset_risk rebuild (~hourly debounce / daily backstop), bounding staleness like the existing baked pod_phase gate.

Verification

  • Full view applied via CREATE OR REPLACE in a rolled-back transaction against the lab DB → parses, column-compatible.
  • No regression in lab: gated vs un-gated counts identical (63/63 digests, 0 changed).
  • go vet ./cmd/server/... clean, gofmt clean. No web changes.
  • Note: symptom is prod-only — the lab has a single always-live cluster, so liveness gating is a no-op there; mechanism verified by code + recomputed counts.

🤖 Generated with Claude Code

## Problem Triage's "X / Y clusters" count (`asset_risk.cluster_count`) is computed in the `asset_risk_src` view from `cluster_digests` / `publicly_exposed_digests`, **without** the `cluster_sessions` liveness gate that the image-detail **Workloads** tab (`uiapi.ImageDetailHandler`, via `scam.LiveRecordFilter`) applies. When a cluster's agent stops heartbeating (crash, network, decommission) no `DELETE` event arrives, so its pods stay `pod_phase='Running'`, `msg<>'DELETE'`. Triage keeps counting them; the Workloads tab correctly drops them. → **count shows in triage but the Workloads tab is empty.** For admins, the hidden-namespace and ACL filters are no-ops, so liveness is the sole divergence. This is the invariant `cluster_summary` already documents: maintained tables can't bake a `NOW()`-relative gate, so the reader applies it — the triage path just never did. ## Fix New migration `20260630a_asset_risk_live_cluster_spread.sql` — `CREATE OR REPLACE VIEW asset_risk_src` adding a `cluster_sessions` liveness `EXISTS` gate to the two image-spread CTEs (`image_cluster_spread` → `cluster_count`, `image_exposed_spread` → `exposed_cluster_count`, gated identically so "X/Y" stays consistent). Registered in `cmd/server/main.go` after `20260630`. **Surgical scope:** the existence JOIN and the cluster-type triage rows stay on raw `cluster_digests`, so an image deployed only in a dead cluster still appears in triage (ranked by its other signals) with `cluster_count = 0` — the frontend hides the chip at 0, matching the empty Workloads tab. (Gating `cluster_digests` itself, which would also drop such images/dead-cluster rows from triage, is a possible follow-up.) The 24h window hardcodes `SPAM_CLUSTER_LIVE_WINDOW`'s default (Postgres can't read the env var); documented in the migration header. Takes effect on the next `asset_risk` rebuild (~hourly debounce / daily backstop), bounding staleness like the existing baked `pod_phase` gate. ## Verification - Full view applied via `CREATE OR REPLACE` in a rolled-back transaction against the lab DB → parses, column-compatible. - No regression in lab: gated vs un-gated counts identical (63/63 digests, 0 changed). - `go vet ./cmd/server/...` clean, `gofmt` clean. No web changes. - Note: symptom is **prod-only** — the lab has a single always-live cluster, so liveness gating is a no-op there; mechanism verified by code + recomputed counts. 🤖 Generated with [Claude Code](https://claude.com/claude-code)
fix(triage): gate image cluster_count on cluster liveness
Some checks failed
Build and Deploy / build-sbom-scanner (push) Successful in 1m9s
Build and Deploy / build-image-scanner (push) Successful in 1m12s
Build and Deploy / build-repo-runner (push) Successful in 1m18s
Build and Deploy / build-app (push) Successful in 3m51s
Build and Deploy / deploy-helm (push) Has been cancelled
027252c754
asset_risk's cluster_count / exposed_cluster_count were computed off
cluster_digests / publicly_exposed_digests without the cluster_sessions
liveness gate the image-detail Workloads tab applies. A digest whose only
running pods live in a now-stale cluster (agent stopped heartbeating, so no
DELETE event arrives and the pods stay pod_phase='Running') showed "Y
clusters" in triage but an empty Workloads tab.

Add a liveness EXISTS gate to the two image-spread CTEs in asset_risk_src
(24h window, matching SPAM_CLUSTER_LIVE_WINDOW's default) so triage agrees
with the detail view. Surgical: the existence JOIN and cluster_signals stay
on raw cluster_digests, so an image deployed only in a dead cluster still
appears in triage (ranked by its other signals) with cluster_count = 0 and
the chip hidden, matching the empty tab.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
jonas merged commit f7810208bb into main 2026-06-26 14:44:10 +02:00
jonas deleted branch fix/triage-cluster-count-liveness 2026-06-26 14:44:11 +02:00
Sign in to join this conversation.
No reviewers
No milestone
No project
No assignees
1 participant
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set

Reference
Motstandskraft/spam!18
No description provided.