fix: set PersistentVolume.spec.nodeAffinity for topology-gated volumes - #410
Closed
boddumanohar wants to merge 8 commits into
Closed
boddumanohar wants to merge 8 commits into
boddumanohar wants to merge 8 commits into
Conversation
boddumanohar
marked this pull request as draft
August 11, 2026 14:46
#403) CreateVolume never populated AccessibleTopology for StorageClasses that select their cluster directly via cluster_id (the common case), so external-provisioner never set PV.spec.nodeAffinity. A bound PVC could then be rescheduled onto any node, even when the StorageClass gates provisioning to specific nodes via AllowedTopologies (DHCHAP's allowed-node label today, and VDO's node-local state in the upcoming #402). Extract only the segments that represent a genuine per-node constraint from the CSI AccessibilityRequirements and echo them back, leaving plain NVMe-oF-backed volumes (which behave identically from any node) unpinned. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
The prior version matched any "simplyblock.io/pool." prefixed segment in AccessibilityRequirements, but external-provisioner copies a node's *entire* registered topology (buildAccessibleTopology dumps every pool-allowed label the node carries) regardless of which pool the current StorageClass gates on. A node can belong to more than one DHCHAP pool (AllowedNodes is set independently per Pool CR), so prefix-matching could AND unrelated pools' labels into one PV's nodeAffinity, or pin an otherwise-ungated volume that happened to land on a node hosting some unrelated DHCHAP pool. Thread the exact expected label key through instead: upsertStorageClass now also writes it into StorageClass Parameters (dhchap_node_label) alongside the AllowedTopologies term it already builds, and CreateVolume matches only that literal key rather than a shared prefix. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
…e ritual The prior comment read as if every future topology-gated feature would need its own new StorageClass parameter like paramDHCHAPNodeLabel. That's only true when the gate key is itself scoped per resource (DHCHAP's, keyed by pool). A feature gated on a single fixed, global key — e.g. VDO's upcoming simplyblock.io/vdo-capable — needs no new parameter at all: the literal key can be hardcoded and its relevance decided from params already present. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
…lap as a known gap #403 doesn't actually block #402 (it says so explicitly, and #402 already works around it with a nodeSelector pin for its own verification), so there's no need to carry the operator-side plumbing this fix grew to close a real but narrow edge case: a single Kubernetes node listed in more than one DHCHAP-gated pool's AllowedNodes. Revert the dhchap_node_label StorageClass parameter and the operator change that set it. hardPinTopologySegments goes back to matching the "simplyblock.io/pool." prefix, with the multi-pool-overlap behavior now documented as a deliberately accepted limitation (in both a code comment and a test that documents current behavior rather than asserting correctness) instead of solved. operator/ is now byte-for-byte identical to origin/main; this PR is CSI-driver-only again. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Same content, easier to read: split the two dense wall-of-text comments on hardPinTopologyKeyPrefixes and the CreateVolume merge into short paragraphs with topic sentences and bullets, and trimmed the now-redundant duplicate rationale in the test comment down to a pointer at the source of truth. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
…a prefix CreateVolume never populated AccessibleTopology for StorageClasses that select their cluster directly via cluster_id, so external-provisioner never set PersistentVolume.spec.nodeAffinity for DHCHAP-gated pools. A bound PVC's pod could then be rescheduled onto any node, even one outside the pool's AllowedNodes. CreateVolume now merges DHCHAP's allowed-node segment into AccessibleTopology alongside the zone/region segments already used for multi-cluster routing. Matching is by the exact per-pool label key, threaded through as a new StorageClass parameter (dhchap_node_label) written right next to the AllowedTopologies term the operator already builds — not a shared "simplyblock.io/pool." prefix, which would AND together any other DHCHAP pool's label a node happens to also carry (Pool.Spec.AllowedNodes can overlap across pools) and wrongly restrict, or over-restrict, the resulting nodeAffinity. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Same fix, smallest possible footprint: trimmed multi-paragraph doc comments down to one-line pointers and dropped the new test functions on both the csi-driver and operator sides. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
…pAllowedNodeSegment hardPinTopologyKeys returned a []string that only ever held zero or one element, and hardPinTopologySegments's generality (matching a list of keys) was never exercised beyond that. Collapse both into one function that returns the single key/value pair it actually deals with — clearer name, less indirection. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
boddumanohar
force-pushed
the
fix/pv-node-affinity-403
branch
from
August 12, 2026 08:33
79dfafd to
fab4d9e
Compare
5 tasks
Member
Author
|
Merged into #417, which now includes this PR's fix (PV nodeAffinity for AllowedNodes/pod-restart) alongside the DHCHAP hostNQN/connect fix, since both address the same DHCHAP feature. Closing this one as superseded. |
boddumanohar
added a commit
that referenced
this pull request
Aug 13, 2026
#403) Adds a second SPDKCSI-DHCHAP spec covering the bug #410 fixed: CreateVolume never populated AccessibleTopology for StorageClasses that select their cluster directly via cluster_id (a DHCHAP pool's StorageClass does exactly that), so external-provisioner never set PersistentVolume.spec.nodeAffinity. A pod consuming an already-bound PVC could then be deleted and recreated onto any node, even one outside the pool's allowed nodes — no drain or failure needed, a plain restart was enough. The spec labels a node the way the operator labels a DHCHAP pool's allowed nodes, sets the matching dhchap_node_label StorageClass parameter, pins the first pod there so CreateVolume sees that topology, asserts the resulting PV actually got the expected nodeAffinity, then strips the test's own placement pin and recreates the pod to confirm the PV's nodeAffinity alone — not anything this test set up — is what keeps it on the same node. Also fixes two more unparam findings the new call sites tripped: waitForControllerReady's timeout is nolint'd like its sibling waitForNodeServerReady already was, and sbctlE drops its unused stdout return. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
The bug
Closes #403.
StorageClass.AllowedTopologiesis only consulted once, at the initialWaitForFirstConsumerscheduling decision. What actually persists a node constraint for the life of a volume isPersistentVolume.spec.nodeAffinity, and that's only set ifCreateVolumetellsexternal-provisionerto viaAccessibleTopology.CreateVolumenever did that for StorageClasses that select their cluster directly viacluster_id— the path DHCHAP's allowed-node gate uses. So a DHCHAP-gated PVC's pod could be deleted and recreated onto a node outside the pool'sAllowedNodes(no drain or failure needed — a plain restart is enough). The backend still rejects the connection at the auth layer, but nothing stops the pod from landing there in the first place.Zone/region multi-cluster routing was never affected — that path already echoed its topology segment correctly.
The fix
CreateVolumenow merges DHCHAP's allowed-node segment intoAccessibleTopology.Matching has to be by the exact per-pool label key, not a shared prefix:
buildAccessibleTopologyreports every DHCHAP allowed-node label a node carries, and a node can belong to more than one pool'sAllowedNodes. A prefix match would AND unrelated pools' labels into onenodeAffinity. SocreateStorageClassIfNotExistswrites the exact key into a new StorageClass parameter (dhchap_node_label) next to theAllowedTopologiesterm it already builds, andCreateVolumepins only that literal key —req.Parameterscan't reconstruct it alone (onlypool_nameand the cluster UUID are available).Known limitation
createStorageClassIfNotExistsis create-once (StorageClass parameters are immutable in the Kubernetes API — see #401/#409), so this doesn't backfill existing pools. A DHCHAP-gated pool whose StorageClass was already created before this fix ships won't getdhchap_node_labelon the next reconcile; its PVCs stay exposed to the original #403 bug until that StorageClass is deleted (e.g. by deleting and recreating the Pool CR, or manually deleting the StorageClass so the next reconcile regenerates it with the new parameter).Test plan
go build ./.../go vet ./.../gofmt -lclean in bothcsi-driverandoperatorgo test ./...(excluding envtest/e2e, which need infra not available here) passes with no regressions — no new tests added for this change, keeping the diff minimal per requestmain(picks up bug: upsertStorageClass is create-only — Pool StorageClassParameters edits on an existing Pool silently no-op #401/fix: make StorageClassParameters immutable instead of silently ignoring edits #409'supsertStorageClass→createStorageClassIfNotExistsrename); no conflicts, build/vet/test re-verified post-rebase🤖 Generated with Claude Code