fix(#194): enqueue cluster inventory sync on registration and make a resync reliably triggerable - #200
fix(#194): enqueue cluster inventory sync on registration and make a resync reliably triggerable#200jgruberf5 wants to merge 4 commits into
Conversation
…t resync trigger
Cluster inventory never appeared to sync: last_synced_at stayed null forever
and a no-op PUT looked like it did nothing.
Root cause (defect 1): ClusterScanner.scan() never wrote
KubernetesCluster.last_synced_at. Every registration path (POST create, the
roks/ibm and container/opentofu/ssh auto-registration tasks) already enqueues
scan_cluster_async, and the async task runs the scan and commits -- but the
scan itself only persisted capabilities, discovered namespaces and the running
release, never a sync timestamp. So "never scanned" and "scanned and genuinely
empty" were indistinguishable from the API, and every no-op PUT (which does
enqueue a scan) left last_synced_at null. The scan now stamps last_synced_at
at the end of scan(), after all analysis has completed, so a scan that raises
early does not falsely record a sync. Because all scan paths funnel through
this one method, the fix covers registration, PUT, the /scan endpoint and
upgrade pre-checks.
Defect 2 (reliable resync trigger): relying on a no-op PUT to force a refresh
was undocumented and easy to get wrong. Added POST
/api/k8s/clusters/{id}/resync (owner/admin), which validates the cluster
exists (clean 404) and enqueues the same background scan, returning
immediately. The PUT path already enqueues a scan unconditionally; a test now
locks that a no-op PUT still triggers a rescan.
Tests (mocked K8s client; mutation-checked):
- scan stamps last_synced_at on completion and it persists across the async
task's commit; a scan that fails before completion does NOT stamp it.
- a populated fetch surfaces pod inventory -- 6 running Multus pods read as 6
and DETECTED, not 0 (the reported symptom).
- registration (POST create) enqueues the initial sync.
- a no-op PUT enqueues a rescan; the resync endpoint enqueues a scan, 404s an
unknown cluster, and is denied to viewers.
Not changed (out of scope, noted for follow-up): honouring
k8s_sync_enabled / k8s_sync_interval_seconds for periodic resync, and an
automatic re-scan after a project's modules reach applied.
Claude-Session: https://claude.ai/code/session_01UpRYiFserdBE5ESHn759N4
…dpoint
The fix added POST /api/k8s/clusters/{cluster_id}/resync but didn't refresh the
committed backend/openapi.json (openapi-check) or frontend-v2 TS types
(typecheck-frontend). Regenerated both via generate-openapi.py + openapi-typescript
7.13.0 so both CI freshness gates pass.
Claude-Session: https://claude.ai/code/session_01UpRYiFserdBE5ESHn759N4
… 404 (self-review) Self-review (MAJOR): TestPodInventoryPopulated claimed to resolve the reporter's '0 Multus pods while 18 running' ground truth, but the real fetch reads only kube-system while OpenShift's Multus lives in openshift-multus (never queried) -- so stamping last_synced_at records a fresh time over a still-0 count. The test hand-built kube_system_pods while labelling the DaemonSet openshift-multus, proving only that analyze_multus counts a handed list. Reframed the test + docstrings to lock what the fix actually does (count + stamp over a fetched namespace) and to NOT claim the OpenShift symptom is fixed; filed the pre-existing namespace-scoping gap as #202. Self-review (MINOR): removed the redundant get_cluster_details() existence check in the resync route -- require_cluster_owner already 404s a missing cluster before the body runs (test_resync_unknown_cluster_404 still green via the dependency). Verified: 22 passed (inventory-sync + routes); ruff clean. Claude-Session: https://claude.ai/code/session_01UpRYiFserdBE5ESHn759N4
Self-review (cold, adversarial) — the
|
…tring edit
The self-review fix reworded the resync route's docstring; FastAPI embeds the
docstring as the endpoint `description` in openapi.json (and it flows into the
generated TS types), so the committed spec went stale on that one field
("Schema definitions changed but names same"). Regenerated both with the exact
requirements.txt deps CI uses.
Claude-Session: https://claude.ai/code/session_01UpRYiFserdBE5ESHn759N4
Review discipline pass — verdict: BLOCK (two narrow conditions)Three independent cold audits (clean context, no prior review threads) plus an invariant sweep. Everything below verified by execution. Reviewed at To be clear up front: the one-line scanner change is the right fix at the right altitude. Putting the stamp inside Verified green
Must fix 1 — the stamp fires on a scan that fetched nothing, which defeats the reporter's stated requirementThe comment at Every fetcher swallows its exception and returns an empty default — This PR's own test demonstrates it. Failure scenario: cluster 16's bearer token expires. Every call 401s, all swallowed, analysis reports "not detected / 0 pods", the stamp is written, This matters more than a normal severity call, because of what the reporter asked for:
And the same comment notes registration deliberately precedes
Fix shape: derive a success signal from Must fix 2 — INV-4: silent merge collision with open PR #203PR #203 changes the - data["crds"], data["crd_names"], data["kube_system_pods"], data["daemonsets"]
+ data["crds"], data["crd_names"], data["multus_pods"], data["daemonsets"]This PR's new I applied #203's change and ran this PR's tests. Measured result: 3 of 4 fail with Not a one-key fix. #203 updated all five pre-existing fixtures; it simply could not see a file this PR had not created yet. This needs coordination on merge order, not a unilateral fix. Minor3. The docstring asserts "there is no silently-swallowed background no-op" ( 4. The upgrade health gate now holds an uncommitted row write across its whole window. 5. The comment overstates its own reach. 6. 7. 8. Scope against the issue — worth resolving before "Closes #194". In the follow-up above the reporter also retracted the framing this PR tests:
9. No dedup, cooldown, or queue guard on the new async trigger. Nits
Reported separatelyOne pre-existing issue outside this diff's scope was surfaced by the sweep — it concerns an unscoped, low-privilege-reachable path that this change amplifies from a read into a write. It affects released versions, so the details are being routed privately rather than posted here. Verdict: BLOCK, on must-fix 1 and 2 only. Must-fix 2 is coordination with #203. Must-fix 1 is a genuine design correction, but a small one, and the stamp's placement inside |
Summary
A registered ROKS/OpenShift cluster showed no pod inventory on the Kubernetes page and
last_synced_atwas never set, even after 70+ minutes and repeated no-opPUTs meant to force a rescan. Capability detection worked; only inventory was missing.Root cause
Defect 1 — the sync ran but was never recorded. Every registration path already enqueues
scan_cluster_async(the POST create route, and the roks/ibm, container, opentofu and ssh auto-registration tasks all callenqueue_cluster_scan), and the async task runsClusterScanner.scan()and commits. Butscan()only persisted capabilities, discovered namespaces and the running release — it never wroteKubernetesCluster.last_synced_at. So "never scanned" and "scanned and genuinely empty" were indistinguishable from the API, and every no-op PUT (which does enqueue a scan) still leftlast_synced_atnull. Nothing anywhere in the codebase ever assignedKubernetesCluster.last_synced_at.Fix:
scan()now stampslast_synced_atat the very end, after all analysis has completed, so a scan that raises early does not falsely record a sync. Because all scan paths (registration/PUT async task, the/scanendpoint, upgrade pre-checks) funnel through this one method, the fix covers them all.Defect 2 — no reliable, documented resync trigger. Forcing a refresh depended on an undocumented no-op
PUT. AddedPOST /api/k8s/clusters/{id}/resync(owner/admin) which validates the cluster exists (clean 404) and enqueues the same background scan, returning immediately. The PUT path already enqueues a scan unconditionally; a test now locks that a no-op PUT still triggers a rescan.What the tests lock (mocked K8s client, mutation-checked)
last_synced_at, and it persists across the async task's commit; a scan that fails before completion does not stamp it (reverting the stamp fails 3 tests, the negative test stays green).DETECTED, not 0 (the reported symptom).Files changed
backend/services/scanner/__init__.py— stamplast_synced_aton scan completion.backend/routes/k8s/clusters.py— addPOST /k8s/clusters/{id}/resync.backend/tests/component/test_cluster_inventory_sync.py— new scanner behaviour tests.backend/tests/integration/test_routes_k8s_clusters.py— registration/PUT/resync route tests.Out of scope (noted for follow-up)
Honouring
k8s_sync_enabled/k8s_sync_interval_secondsfor periodic resync, and an automatic re-scan after a project's modules reachapplied(issue suggestions 3 and 4). This PR closes the "never synced / can't force a resync" defects.Closes #194
https://claude.ai/code/session_01UpRYiFserdBE5ESHn759N4