fix(#202): count Multus pods in the DaemonSet's namespace so OpenShift (openshift-multus) reports correctly - #203
Conversation
…t reports correctly The scanner fetched pods only from a hardcoded namespace set (kube-system, cert-manager, kamaji-system, + BNK/F5 discovery) and analyze_multus derived running_pods from the kube-system list alone. On ROKS/OpenShift, Multus runs in openshift-multus, which was never queried, so running_pods read 0 even when Multus was healthy (the DaemonSet is still found via list_daemon_set_for_all_namespaces, so status showed DETECTED with 0 pods — exactly the reported "0 Multus pods while N running"). Fix (DaemonSet-namespace-driven): fetch the Multus pods from whatever namespace the discovered DaemonSet actually lives in. Vanilla k8s (kube-system) reuses the already-fetched pod list — no extra API call; OpenShift (openshift-multus) and any future layout fetch that namespace. No new hardcode. - fetch.py: add _multus_daemonset_namespace + _fetch_multus_pods; expose a new namespace-scoped "multus_pods" in the fetch dict. - __init__.py: feed analyze_multus the scoped multus_pods. - prereqs.py: rename the analyze_multus param to multus_pods; count running pods from it. Tests exercise the REAL fetch path: fetch_scan_data runs with the k8s API mocked so list_daemon_set_for_all_namespaces reports Multus in openshift-multus and list_namespaced_pod returns Multus pods ONLY there. The scan then queries openshift-multus and counts 3 running pods (was 0 before). A vanilla-k8s test (Multus in kube-system) still counts. Reverting the fix reds the OpenShift tests (mutation-verified). Stub fetch dicts gain the new multus_pods key. Claude-Session: https://claude.ai/code/session_01UpRYiFserdBE5ESHn759N4
Self-review (cold, adversarial) — no blocker, no major; correct for the real OpenShift scenarioAn independent cold auditor reviewed this PR, executing against a mocked k8s API (326 tests green across the scanner/fetch/prereqs/recommendations/inventory suites). Held under attack (verified):
Two MINORs (cosmetic, non-blocking):
Addressing both below for robustness. |
…d-count scope (self-review) Self-review MINOR 2: _multus_daemonset_namespace (fetch) and analyze_multus (prereqs) picked the first DaemonSet merely CONTAINING "multus", so a sibling like "multus-additional-cni-plugins" could be chosen ahead of the primary if it sorted first. Both now prefer the DaemonSet named EXACTLY "multus" (falling back to the first containing it), so the fetched namespace and the reported DaemonSet are consistent regardless of list order. Locked by two new tests (sibling-first ordering). Self-review MINOR 1: running_pods intentionally counts the broad Multus- networking pod set (any Running pod whose name carries "multus"). Precise per-DaemonSet attribution would need pod ownerReferences we do not fetch; a name-prefix heuristic mis-handles real-world names (a "multus" DaemonSet whose pods are "kube-multus-ds-*", as the vanilla test shows), so guessing would regress that case. Kept the tolerant match and documented the scope — it is a display metric and still far better than the 0-count bug. Verified: scanner/prereqs/multus + recommendations/proxy-inventory/discovery/ scan-task suites = 129 passed; ruff clean. Claude-Session: https://claude.ai/code/session_01UpRYiFserdBE5ESHn759N4
Review discipline — round 1 @
|
Review —
|
| Input | _multus_daemonset_namespace |
analyze_multus |
|---|---|---|
{"name": None, ...} |
tolerates | AttributeError: 'NoneType' object has no attribute 'lower' |
{"name": "multus"}, no namespace |
returns None |
KeyError: 'namespace' |
Neither is reachable today (_fetch_daemonsets always populates both), so this is a drift hazard, not a live crash — but the diff introduced the divergence while holding the safe idiom in the other file. One of the two hardenings is wrong: either None is reachable and analyze_multus crashes the scan, or it isn't and the or "" is dead. Extract one shared pick_primary_multus_daemonset(daemonsets); no test asserts the two pickers agree.
m-3 · The comment describing running_pods contradicts what the code now computes. prereqs.py:198-205 claims the metric is "the broad Multus-networking pod set… on a cluster with a separate multus-additional-cni-plugins DaemonSet this includes those pods too." That was true pre-fix; it is now false whenever the sibling lives elsewhere, because only the primary DaemonSet's namespace is fetched. Verified end-to-end with multus@openshift-multus (3 pods) + sibling@sib-ns (6 pods) → running_pods = 3, sibling excluded. The metric is now a topology-dependent hybrid: neither the broad set nor the DaemonSet's ready count.
The flip side matters more in practice: real OpenShift co-locates both DaemonSets in openshift-multus, so on a 3-node cluster this now reports running_pods: 6 beside a DaemonSet reading 3/3. The issue's symptom "0 Multus pods while N running" becomes "6 while 3 running". Still wrong, in the other direction.
Also, the stated reason for accepting that — "precise per-DaemonSet attribution needs pod ownerReferences we do not fetch" — understates what is available: _fetch_pods_in_ns already captures pod labels (fetch.py:266). The DaemonSet's spec.selector.matchLabels is the canonical mechanism and is a one-line addition to an already-fetched object (_fetch_daemonsets captures metadata.labels but not spec.selector) — no extra API call. The design choice may still be right; the justification for it isn't accurate.
m-4 · An RBAC denial reproduces the exact #202 symptom with zero diagnostic. _fetch_pods_in_ns swallows every exception and returns [] with no logging — unlike sibling _fetch_daemonsets, which logs at fetch.py:252. On a ROKS cluster whose scan credential can list DaemonSets cluster-wide but not pods in openshift-multus, the panel shows the identical wrong running_pods: 0 this PR fixes, and nothing says why. Verified: no exception raised, running_pods = 0, status = detected. One logger.warning(f"Failed to fetch pods in {namespace}: {e}") makes the fix's own failure mode observable.
m-5 · The new fetch is serial, on the critical path. fetch.py:948 sits outside the with pool: block (closes at :936), so it runs after the entire parallel burst, adding up to _request_timeout=10 serially to every OpenShift scan. It correctly adds no call where none is needed (vanilla queries ['cert-manager','kube-system']; OpenShift adds openshift-multus). Resolving daemonsets_f early and submitting the dependent fetch inside the pool keeps it off the critical path — or it disappears entirely under M-1's sweep.
m-6 · Coverage gaps. Untested: empty DaemonSet list; DaemonSet with no namespace key; {"name": None}; and sibling-before-primary end-to-end (_run_fetch hardcodes a single DS named multus, so fetch→analyze agreement under a cross-namespace sibling list is never exercised — only the two unit-level pickers). test_non_running_openshift_multus_pods_not_counted fails pre-fix only via KeyError: 'multus_pods', so it proves the dict contract, not the phase filter it names.
Mock fidelity is acceptable — _v1_pod/_v1_daemonset set every field the parsers read, so no auto-attribute leaks into an assertion. But the docstring's "exercise the REAL fetch path" is generous: with only CoreV1Api/AppsV1Api patched, every other key degrades to empty through swallowed exceptions and version_info comes back as raw MagicMock objects. The multus assertions are sound; the framing overstates.
Nits
scanner/__init__.py:107still threadsdata["kube_system_pods"]intoanalyze_sriov, whose third parameter now appears only in its signature — I grepped the whole function body, one occurrence. Multus was its last real consumer. Drop the dead parameter, or the plumbing.analyze_multusis in__all__and its 3rd parameter was renamedkube_system_pods→multus_pods. All in-repo calls are positional so nothing breaks; keyword callers outside the repo would.
Review Assessment
- Verdict: REVISE
- Audit SHA:
1e575d09db32e443020e42811b98d7a9ee16d72a - Cold Audit Performed: Yes — independent agent, no session context, full diff, verified against the repo
- Invariants Verified: INV-4 (cross-writer namespace/file collision — clean vs open fix(#191): validate credential-template provider so an unknown value can't silently inject nothing #199/fix(#194): enqueue cluster inventory sync on registration and make a resync reliably triggerable #200/fix(#195): stream opentofu task logs incrementally and expose log fields in the task list #201/Surface L4Route service-settings faithfully (groundwork for #8; on-screen fix needs cluster data) #172); INV-6 analog (frontend
?? 0fails closed tomissing, acceptable for a display metric); INV-1/2/3/5/7/8/9 N/A (no DB queries, FK writes, schema enums, delete paths, migrations, locks, or shell scripts in this diff) - Git & Harness Cleanliness: Clean for PR content; the local checkout carries unrelated untracked harness drift (
.gitignore.maf.new,bin/hooks/)
Findings & Action Items
- Major (Blockers):
-
fetch.py:833/:895(+prereqs.pycert-manager & kamaji gates): hardcoded-namespace pod fetch is a 3-instance class; only the multus consumer is fixed. cert-manager additionally downgrades status and emits a wrong recommendation. Class fix: onelist_pod_for_all_namespacessweep per the existingbnk_pod_discovery.py:272pattern — or file the follow-ups explicitly.
-
- Minor (Non-blocking):
-
fetch.py:295/prereqs.py:190: exact-multusmatch never fires onkube-multus-ds; selection falls back to list order. -
fetch.py:292vsprereqs.py:188: duplicated picker, divergent null-handling — extract one shared helper. -
prereqs.py:198-205: comment contradicts the implementation; count reads ~2× the DaemonSet on co-located OpenShift; the ownerReferences rationale ignores already-fetched pod labels. -
fetch.py:281: add alogger.warningso an RBAC denial is distinguishable from "Multus absent". -
fetch.py:948: serial fetch outside the pool; submit it inside. -
tests/unit/test_scanner_multus_namespace.py: add empty-DS-list, missing-namespace, and end-to-end sibling-first cases.
-
- Nits:
scanner/__init__.py:107:analyze_sriov'skube_system_podsparameter is dead.analyze_multuspublic parameter renamed; positional-only in-repo, safe.
Summary
ClusterScannerreported 0 running Multus pods on ROKS/OpenShift even when Multus was healthy. This fixes the namespace-scoping so the count reflects reality on any cluster layout.Root cause
The pod fetch (
backend/services/scanner/fetch.py) enumerated pods only from a hardcoded namespace set (cert-manager,kube-system,kamaji-system, + BNK/F5 discovery), andanalyze_multus(prereqs.py) derivedrunning_podsfrom thekube-systemlist alone. On OpenShift/ROKS, Multus runs inopenshift-multus, which was never queried — sorunning_podsread 0. The DaemonSet is still discovered cluster-wide vialist_daemon_set_for_all_namespaces, so status showedDETECTEDwith 0 pods: exactly the reported "0 Multus pods while N running".Fix (DaemonSet-namespace-driven — preferred option)
Fetch the Multus pods from whatever namespace the discovered DaemonSet actually lives in, rather than a hardcoded
kube-system:kube-system) reuses the already-fetched pod list — no extra API call.openshift-multus) and any future layout fetch that namespace.Changes:
fetch.py:_multus_daemonset_namespace+_fetch_multus_pods; new namespace-scopedmultus_podsin the fetch dict.__init__.py: feedanalyze_multusthe scopedmultus_pods.prereqs.py: rename theanalyze_multusparam tomultus_pods; count running pods from it.How the tests exercise the REAL fetch (not a handed list)
tests/unit/test_scanner_multus_namespace.pyruns the actualfetch_scan_datawith the k8s API mocked, not a pre-built pod list:list_daemon_set_for_all_namespacesreports the Multus DaemonSet inopenshift-multus.list_namespaced_pod(namespace=...)returns Multus pods only foropenshift-multus(empty forkube-system) — the exact shape of a real OpenShift cluster.The test asserts the fetch actually queried
openshift-multus(viacall_args_list) and thatrunning_pods == 3. A vanilla-k8s test (Multus inkube-system) still counts (no regression), plus a non-Running-phase guard.Reproduce + verify evidence
_fetch_multus_pods(...)back tomultus_pods = kube_system_podsreds the OpenShift tests —running_pods == 0andopenshift-multusnever appears in the queried namespaces (assert 'openshift-multus' in {'cert-manager', 'kube-system'}fails). The vanilla-k8s test stays green.test_scanner_prereqs,test_scanner_recommendations,test_proxy_inventory,test_running_release_discovery,test_bnk_pod_discovery) = 172 passed.ruff checkclean on all changed files.Not verifiable without a live cluster
The exact Multus DaemonSet/pod naming and the
openshift-multusnamespace on a real ROKS/OpenShift cluster are assumed from the issue report; the logic keys off"multus"in the DaemonSet name and its reported namespace, so it adapts to whatever a real cluster presents.Closes #202