Repository navigation
Explain volume attach failures from incompatible disk types; audit PD volumes that can land on Hyperdisk-only nodes - #2069
Conversation
… volumes that can land on Hyperdisk-only nodes Post-bind detection kept only the latest attach event per pod. Once the CSI attacher backs off, that event is "ControllerPublish not permitted ... due to backoff condition", which hides the Compute Engine error naming the cause (e.g. "pd-ssd disk type cannot be used by n4d-standard-4 machine type"). - Show the newest informative attach error for the same pod UID and volume instead of the backoff echo, with onset from the first real failure. - Parse Compute Engine's disk/machine incompatibility into Cause/Action; when only echoes remain, derive it from the node's machine type and the volume's provisioned disk type. diagnose's startupBlockers now carry cause/action. - Reject events recorded against a predecessor pod UID (StatefulSet pods are recreated under the same name). - New Cluster Audit check pdVolumeHyperdiskOnlyNode: workloads whose PD-backed volumes can be scheduled onto nodes from Hyperdisk-only machine series, evaluated against the owner's template, volume node affinity/zone labels and taints. Danger when a pod already sits on such a node. Evidence is gated on the reader's list access to nodes, persistentvolumes and storageclasses.
PR Summary by QodoExplain incompatible disk attaches and audit risky PD placement
AI Description
Diagram
High-Level Assessment
Files changed (15)
|
Code Review by Qodo
1. Old replicas hide disk placement risks
|
The backoff fallback now resolves the volume the echo names instead of any PD the pod mounts, and a rejected Hyperdisk type no longer gets the "supports only Hyperdisk" cause or a move-to-Hyperdisk action.
Live verification (post-merge, on
|
| Event state | Before (9ae4cc6bb) |
After (7abd60120) |
|---|---|---|
| 1. Real error newest (as observed) | — | Real error shown · cause + action ✓ |
| 2. Backoff echo newest (outage shape) | Echo only · no cause/action | Real error shown · cause + action ✓ |
| 3. Real error expired, echo only | Echo only · no cause/action | Echo shown · cause derived from node + PV/StorageClass ✓ |
Cause: pd-ssd volumes can't attach to n4d-standard-2 nodes: the N4D machine series supports only Hyperdisk.
Audit (pdVolumeHyperdiskOnlyNode) on real cluster snapshots:
- Repro cluster: 1 finding out of 666,
StatefulSet k1-test/pd-on-n4d, danger. The cluster's other PD workloads were not flagged; the taint keeps them off the node. - Production-shape cluster (e2 + n4d nodes, Redis on pd-ssd pinned to e2): 0 findings out of 605.
- The same snapshot with the Redis nodeSelector removed, which is the outage configuration: 2 warnings, one for
redis-masterand one forredis-replicas, each naming the n4d node and its claims. This ran on the pre-7462bfdbabranch head.
The test pool, namespace and disk were deleted afterwards.
…claims, Released PVs) (#2055) In a real cleanup every piece of unused storage was found by hand: - about 311Gi of Loki and Mimir disks left behind after their apps were deleted - about 45Gi of 2.7-year-old PVCs in `default` - an 8Gi Postgres PVC with no workload - a PVC that survived its Helm uninstall - two PVCs Pending for about three years on a WaitForFirstConsumer storage class, which the issue detector deliberately ignores - Released PVs stuck in a `VolumeFailedDelete` retry loop Cluster Audit now reports these as **snapshot** Efficiency checks (Medium on the existing Checks and MCP `get_cluster_audit` surfaces): | Check | Reports | |---|---| | `pvcNoConsumer` | Bound PVCs older than a day with **no consumer Radar can observe**: Pods, Deployments, ReplicaSets, StatefulSets (including zero replicas and leftover ordinals after a scale-down), DaemonSets, Jobs, CronJobs, generic ephemeral volumes, and Pending PVC clones (a clone protects its source only while it is Pending). On Kubernetes 1.37+, the claim's `Unused` condition is used when present (below). Shows size, storage class, age, bound PV and reclaim policy. | | `pvcLongPending` | PVCs Pending for over 24h with no observed consumer, WaitForFirstConsumer included. Shown as "Pending since creation"; a claim never returns to Pending. A Pending claim that *has* a consumer is already an outage issue, so it isn't double-reported as waste. | | `releasedPV` | Released PVs: those kept by a `Retain` policy (after a 24h grace when release time is known, or flagged with "release time unknown" and PV age when it is not), and those whose deletion hasn't completed after an hour of known release duration. A recent `VolumeFailedDelete` warning establishes deletion failure immediately, even without a phase-transition timestamp. Delete PVs without that timestamp or a recent warning are not evaluated. **Failed** PVs (an in-tree delete or recycle failed after the claim was deleted) are reported immediately, with the latest current `VolumeFailedDelete`/`VolumeFailedRecycle` warning or else the PV's `status.message`. | **Not waste by construction:** - **Owned PVCs are skipped.** A PVC with a controller owner reference, such as a hibernated CloudNativePG cluster's volumes, a stopped KubeVirt VM's disks, or workflow `volumeClaimTemplates`, follows its owner's lifecycle, so these checks leave it to the owner. - **The check says "no consumer observed"**, never "unused" or "safe to delete". The caveat about consumers Radar can't see (CRDs mounting volumes intermittently, snapshot operations) lives in the check description (shown once per check) and the remediation (returned with every MCP finding), not on every row. Rows state facts only: size, class, age, bound PV, reclaim policy, and the `Unused` time or binding-mode evidence where present. - **Remediation says how to reclaim.** No consumer: if nothing needs the data, delete the claim; if its PV is Retain, reclaim that too. Retain PVs: deleting the PV keeps its disk; for a dynamically provisioned volume set the reclaim policy to Delete, otherwise delete the PV and then the disk in your cloud provider. Both keep the "confirm nothing needs the data" step first. **Kubernetes 1.37 `Unused` condition** (PVC protection controller, beta, on by default): - `Unused=False` (a running Pod uses the claim) counts as a consumer, even if Radar's Pod inventory missed it. - With `Unused=True`, the 24h floor counts from when the claim became unused instead of from its creation, so a claim whose workload was deleted minutes ago isn't flagged. The finding adds "Kubernetes reports it unused by any Pod since <time> (5d ago)". It reports the condition rather than promising a minimum idle time: upstream notes the time can be off in either direction because of controller delays. Right after the feature is enabled (e.g. an upgrade to 1.37), every unused claim's timestamp is the enable time, so long-unused claims are held back for a day and then reported with that shorter age. - No condition or `Unknown` keeps the creation-age behavior; absence never implies unused. `pvcLongPending` doesn't use it. **Deleting claims and volumes → `stuckTerminating`:** - PVCs and PVs are now `stuckTerminating` subjects, with the same 5m/30m ramp as other kinds. Deleting PVCs were already skipped by the storage checks; deleting PVs are now skipped by `releasedPV` too, so a deleting Bound PV no longer counts as a pass and a deleting Released PV isn't reported twice. - For `kubernetes.io/pvc-protection` and `kubernetes.io/pv-protection`, the finding explains what holds the deletion and what happens when it's released (the claim is deleted and the volume follows its reclaim policy), suggests a backup if the deletion was unintended, and says not to remove the finalizer. It never tells the user to stop Pods or delete the claim: a deletion can't be cancelled, and completing it can delete the data. - The shared `stuckTerminating` remediation (shown in Checks and MCP) now exempts storage finalizers from its "clear finalizers as a last resort" advice. **Coverage:** - Inputs are gated on cache readiness. PVs and StorageClasses are cluster-scoped and checked against the caller's exact grants per kind; PVCs, workload templates, and events follow namespace visibility. - `missingInputs` keeps its existing meaning: something was not evaluated. A storage input is recorded only when its absence left a claim or volume unevaluated, so unreadable evidence never looks like a clean pass: - an unreadable PVC inventory (`persistentvolumeclaims`) - unknown consumers (`pvc-consumers`) - a PV inventory the caller may read but Radar couldn't list (`persistentvolumes`) - a Released Delete volume still in its 1h grace period whose outcome depends on events Radar can't see (`pv-deletion-events`) - a Released Delete volume with a nil or zero phase-transition timestamp and no recent deletion warning: release duration is unknown, so PV creation age cannot establish a stalled deletion or a pass (`pv-release-time`) - Gaps that still let a finding fire are stated in the finding instead: "reclaim policy not visible", "StorageClass binding mode not visible", "Deletion warning events not fully visible". Radar's "Some checks could not run" banner and Hub's "checks skipped" copy stay accurate, so no consumer change is needed. - Without a PV grant, Released PVs aren't subjects of that caller's scan, so namespace-restricted users don't get a permanent missing input. - **Failed PVs never pass and are never skipped.** The PV controller sets `Failed` only when reclaiming failed, so the phase alone establishes the finding. Events only add detail; incomplete coverage adds the "Deletion warning events not fully visible" caveat, never `pv-deletion-events`. The controller emits the warning once, at the transition, and events expire (1h by default), so `status.message` is the fallback. - **What `releasedPV` counts:** Bound PVs pass, Released and Failed PVs are judged, and Available/Pending PVs are not counted. They have no claim to have outlived, so counting them as passes would only inflate the pass rate (for example, a local-static-provisioner pool). No Radar-kept usage history, cost column or delete action; those are follow-ups. `pkg/audit` changes are additive. **Testing** - Table tests cover Pods, Deployments, ReplicaSets, StatefulSets, DaemonSets, Jobs, CronJobs, generic ephemeral volumes, and Pending clones through both reference fields (including owned, young, and deleting destinations). Cross-namespace `dataSourceRef` references protect only the source in the referenced namespace. Bound clones, non-core references, and VolumeSnapshot references do not protect a source. Tests also cover StatefulSet replicas 0 and leftover ordinals, controller-owned (CNPG) claims, the 24h age floor at a fixed clock, WaitForFirstConsumer with and without consumers, and Retain vs Delete with and without events. Nil and zero release timestamps cover Delete skips, immediate recent-warning findings, and Retain findings with unknown release time and PV age. They also cover missing inputs only for skipped subjects (otherwise the caveat is in the finding), PV inventories that are unavailable vs. outside the caller's grant, and unreadable namespaces suppressing findings. - `Unused` condition: False with no observed consumer and with incomplete inventory, True 2h ago on a 30-day-old claim, the exact 24h boundary, future and zero timestamps, True 10 days ago with the evidence sentence, zero-replica StatefulSet and Pod consumers, absent and `Unknown` conditions, and Pending claims ignoring it. - Audit counts, MCP audit scope (including `pv-release-time` appearing only with a PV grant), and the `ChecksView` input labels. - Failed PVs: a Delete PV with a current warning (event text and age), with only `status.message`, with partial or unreadable events (caveat, evaluated, not passed, no missing input), with a warning older than the Failed transition (ignored), Recycle with `VolumeFailedRecycle`, and a policy switched to Retain after the failure. Counts: Bound passes; Failed is evaluated but not passed; Available and Pending are not counted. `internal/audit` collects events for Failed PVs. - `make test` and `make tsc` pass. The original change was checked live on a kind cluster (an unconsumed bound PVC and a Released Retain PV through REST and MCP). **Live verification** (kind, Kubernetes v1.36.1 so `lastPhaseTransitionTime` is populated; real binary in local and proxy-auth modes; REST + MCP + Checks UI) - **Earlier round** (independent live tester, head `3dc5f6a3`): - The prior proof re-run passed. - Unreadable StorageClasses produce the "binding mode not visible" caveat and no missing input. - A namespace user without a PV grant gets no `persistentvolumes` missing input. - A real skip (no Pod list) is listed as `pvc-consumers`. - A Released Delete PV past its grace period is flagged, with the "not fully visible" caveat under incomplete events. - A CSI `VolumeFailedDelete` on a Released PV is reported as "deletion is failing". - Warnings from before the current release are ignored. - The Checks view shows the "could not run" banner only on real skips. - MCP `get_cluster_audit` matches REST. - The WaitForFirstConsumer, default-class and class-not-found messages (readable StorageClasses) are covered by unit tests only. - **That round found this bug:** a hostPath Delete PV whose in-tree deletion failed went to `Failed` with a real `VolumeFailedDelete` event, and was counted as a `releasedPV` **pass**. - **After this fix** (head `4be95ee8`: the binary was built from `3dc5f6a3` plus the uncommitted fix, whose diff is identical to the `4be95ee8` commit; same repro: hostPath Delete PV at `/var/...`, claim deleted, PV `Failed` with `status.message` set and one event in `default`): - Admin view, `releasedPV` evaluated 2, passed 1 (the Bound PV passes, the Failed PV is flagged): `Failed PV: reclaiming the volume failed after its claim was deleted (reclaim policy Delete). Latest reclaim warning (2m ago): host_path deleter only supports /tmp/.+ but received provided /var/fix2055-delete-fail. Capacity 1Mi; storage class ""; former claim fix2055-f3/claim-delete-fail; Failed for 2m.` MCP shows the same text. - A proxy-auth user with a PV grant who sees only the fixture namespace, so the event in `default` is hidden: the same finding, carrying the `Status message: host_path deleter only …` text plus "Deletion warning events not fully visible". There is no `pv-deletion-events` missing input, and REST and MCP match. - The Checks UI renders the finding under "Released or Failed persistent volume". - Documentation commits and the README check-count test followed those live checks. The release-time uncertainty and Pending-clone scenarios have unit and MCP-handler coverage; their live verification remains pending. **Live verification on GKE 1.37.1** (nonprod control plane upgraded to 1.37.1; current head) - The controller set `Unused=False` on a claim with a running Pod and `Unused=True` (timestamped at Pod deletion) on two claims whose Pods were deleted. A `standard-rwo` (WaitForFirstConsumer) clone of one of them stayed Pending. - A snapshot of those live objects ran through the audit with creation and unused times shifted back to clear the 24h floor: | Variant | Result | |---|---| | As captured (claims minutes old) | No storage findings; only the pre-existing in-use claim evaluated, passing | | Created 10d ago, unused 5d | `orphan` flagged with the Unused time (5d ago); `clone-source` protected by the Pending clone; `in-use` passes; the abandoned Pending clone flagged as `pvcLongPending` | | Same, in-use Pod removed from the snapshot | `in-use` still passes on `Unused=False` | | Created 10d ago, unused since minutes ago | `orphan` and `clone-source` held back (not evaluated) | | Same as row 2, clone removed | `clone-source` flagged | - Deleting PVC/PV routing, protection-finalizer copy (including that it never advises completing the deletion) and the shared remediation's storage exception are covered in `pkg/audit` tests; all three fail on the previous head. - `make test`, `make tsc` and the audit/MCP suites pass on the current head, after merging main (which brought #2053). **Live check of stuck deletion** (GKE 1.37.1, head `9ea8f6871`): a PVC still mounted by a running Pod was deleted, then its bound PV. After 6 minutes both were reported by `stuckTerminating` as warnings: the PVC held by `kubernetes.io/pvc-protection`, the PV by `kubernetes.io/pv-protection` plus the two CSI finalizers, each with the protection explanation. `releasedPV` counted only the cluster's other (Bound) PV, so the deleting PV was neither passed nor double-reported. Deleting the namespace then removed the PVC, PV and disk. **Docs** - README states the Cluster Audit check count again (44, the size of `CheckRegistry`, which now includes the Persistent Disk placement check main added in #2069); `TestREADMECheckCountMatchesRegistry` fails when the two drift. - The storage review checks section in `docs/configuration.md` describes the consumer inventory, release-time uncertainty, thresholds, and evidence gaps; the MCP audit-access bullet links to it instead of repeating it. **Rollout and compatibility** - The MCP `get_cluster_audit` description now lists "stale PVCs/PVs" under Efficiency; the short wording keeps the tool catalog within `TestToolCatalogContextBudget` after main's growth. - The three checks are on by default at Medium, so posture counts (Medium findings, per-check pass rates) shift on upgrade for any cluster with unclaimed PVCs or Released/Failed PVs. Each check can be disabled in audit settings. - `releasedPV` counts Bound PVs as passes; Available and Pending PVs are not counted at all. - New `missingInputs` keys: `pvc-consumers`, `pv-deletion-events`, `pv-release-time`, plus `persistentvolumeclaims` / `persistentvolumes` when those inventories are unavailable. They appear only when a claim or volume went unevaluated, so Radar's "Some checks could not run" banner and Hub's "checks skipped" copy stay accurate. - `pkg/audit` changes are additive: new `CheckInput` fields and one exported predicate, `PVNeedsReclaimEvents`. Radar Hub imports `pkg/checks`, not `pkg/audit`, and keys checks by ID (titles are display-only), so an older Hub renders the new checks from the cluster's catalog. No new endpoint, so no feature flag is needed. **Known limits (product calls, not bugs)** - Findings within a check are ordered by severity, then kind, namespace and name (`pkg/checks.sortEffectiveFindings`), not by size, so a large disk can sit behind "View all". Size-first ordering needs a field on the shared `pkg/checks` finding that Hub would also carry; it is a follow-up. - `pvcLongPending` stays Medium like the other two checks. A Pending claim has no disk yet, but the detector can only emit High or Medium; Low is an org override in Hub. - StatefulSet scale-down leftovers are not flagged: every claim-template ordinal counts as consumed, so `data-kafka-4` after a 5→3 scale-down stays silent. - Consumers Radar can't see cause false positives, each worded "no consumer observed" with the CRD caveat. Examples: JupyterHub `claim-<user>` after culling, CI caches and backup targets mounted by short-lived Pods, KubeVirt VMs with direct PVC volumes, and snapshot sources or clone operations beyond the observed Pending PVC references. Ignored namespaces or disabling the check are the escape hatches. - PVC subjects follow namespace visibility, as other namespaced audit subjects do. Like those kinds, PVCs in a namespace that a namespace-scoped informer doesn't cover are not reported as a missing input.
Why
A prod Redis StatefulSet was rescheduled onto an
n4d-standard-4node after a node-pool change. N4D machines attach only Hyperdisk, and Redis's volumes arepremium-rwo(pd-ssd), so the volume never attached and the pods stayed in ContainerCreating for hours.Radar reported the attach failure, but with the wrong event.
diagnoseandissuesboth showed:That is the CSI attacher's retry notice. The event that names the cause was older and was dropped, because post-bind detection keeps only the latest event per pod:
What changes
Diagnose and issues (post-bind detection)
CauseandAction: "pd-ssd volumes can't attach to n4d-standard-4 nodes: the N4D machine series supports only Hyperdisk". The action offers two fixes: pin the workload to a compatible pool, or move it to a Hyperdisk StorageClass that every eligible pool supports.node.kubernetes.io/instance-typeand the PV's provisioned disk type.diagnose'sstartupBlockersnow carrycauseandaction. Both are optional additions.Cluster Audit: new check
pdVolumeHyperdiskOnlyNode(Reliability)pd-*) and its pods could be scheduled onto a node from a Hyperdisk-only machine series.pv.kubernetes.io/provisioned-by). A static PV that merely borrows a class name is skipped. StorageClass parameter keys are matched case-insensitively, as the PD CSI driver matches them.nodes,persistentvolumesandstorageclasses. A reader missing any of them gets no evidence, and the check is silent.New package
pkg/gkediskholds the shared knowledge.pkg/audit.CheckInputgains aPDPlacementfield. Both are additive for Hub.Decisions
internal/audit/scope.go,internal/audit/runner.go,pkg/audit/{types,registry,checks}.go. This PR's edits sit in separate hunks, andPDPlacementis its own field rather than the PV/PVC/StorageClass inputs Cluster Audit: find unclaimed storage (unconsumed PVCs, long-Pending claims, Released PVs) #2055 adds. Expect at most a mechanical rebase.Scenario ledger
TestDetectPostBindProblems_AttachBackoffShowsRealCause(fails on main)…AttachBackoffOnlyDerivesCause(fails on main)…AttachBackoffOnCompatibleNode…AttachIgnoresPredecessorPodEvents…AttachEchoMatchesItsOwnVolume…UnrelatedAttachFailureHasNoDiskCauseTestPDPlacement_UnpinnedStatefulSetReachesHyperdiskOnlyNode,TestRunFromCacheFlagsPDVolumeReachingHyperdiskOnlyNode…PodAlreadyOnHyperdiskOnlyNodeIsDanger…ConstraintsThatKeepPodsOff…VolumeZoneKeepsPodOffOtherZones,…LegacyVolumeZoneLabel…LegacyVolumeZoneLabel…ConstraintsComeFromOwnerTemplateType: hyperdisk-balancedTestDiskTypeTestDiskType,TestPDPlacement_NotApplicableTestReadScopeResolvesPDPlacementGrants, denied-scope case in the runner testVerification
make tscpasses.make testpasses.cd pkg && go test ./audit ./gkediskpasses, including matching and mismatching beta-PV/stable-node zone and region cases.make buildpasses (frontend, embed, Go binary).