fix(csi): route RWX Block volumes to upstream hotplug detach

WrappedControllerService.ControllerUnpublishVolume decided between NFS
cleanup and upstream hotplug detach using only hasRWXAccessMode(pvc).
RWX Block volumes (KubeVirt live-migration disks) match that check and
were routed to the NFS path, which only deletes a CiliumNetworkPolicy.
For a block volume that CNP does not exist, so removeCNPOwnerReference
returns successfully without doing anything, and the function reports
success without ever calling upstream's hotplug detach. The DataVolume
entry stays in VM.spec.template.spec.volumes; when the same PVC later
attaches to a different VM, linstor-csi's anti-split-brain check sees
pods from two different VMs referencing the same RWX block PVC and
rejects the attach permanently.

Introduce isNFSVolume(pvc) requiring both ReadWriteMany access mode and
VolumeMode=Filesystem (always true for PVCs provisioned via our NFS
CreateVolume path). RWX Block now delegates to upstream, which removes
the entry from VM spec via RemoveVolumeFromVM. The same predicate
replaces the access-mode check in ControllerExpandVolume - RWX Block
volumes still need in-VM disk expansion.

Fixes #2634

Assisted-By: Claude <noreply@anthropic.com>
Signed-off-by: Myasnikov Daniil <myasnikovdaniil2001@gmail.com>
This commit is contained in:
Myasnikov Daniil committed 2026-05-21 23:28:42 +05:00
1 parent ee9d1544e1
commit 712c93da07
1 file changed
+23 -6
@@ -362,12 +362,15 @@ func (w *WrappedControllerService) ControllerPublishVolume(ctx context.Context,
}, nil
}
// ControllerUnpublishVolume for NFS volumes: deletes CiliumNetworkPolicy.
// For RWO volumes, delegates to upstream (hotplug removal).
// ControllerUnpublishVolume for NFS volumes (RWX Filesystem): deletes CiliumNetworkPolicy.
// For everything else (RWO and RWX Block), delegates to upstream so the hotplug
// DataVolume is removed from VM.spec.template.spec.volumes. RWX Block volumes used
// for live migration must NOT be treated as NFS — their cleanup goes through upstream
// hotplug removal, otherwise stale VM-spec entries permanently block re-attachment to
// a different VM via the anti-split-brain check (issue #2634).
func (w *WrappedControllerService) ControllerUnpublishVolume(ctx context.Context, req *csi.ControllerUnpublishVolumeRequest) (*csi.ControllerUnpublishVolumeResponse, error) {
dvName := req.GetVolumeId()
// Determine if NFS by checking infra PVC access modes
pvc, err := w.infraClient.CoreV1().PersistentVolumeClaims(w.infraNamespace).Get(ctx, dvName, metav1.GetOptions{})
if err != nil {
if errors.IsNotFound(err) {
@@ -376,7 +379,7 @@ func (w *WrappedControllerService) ControllerUnpublishVolume(ctx context.Context
return nil, err
}
if !hasRWXAccessMode(pvc) {
if !isNFSVolume(pvc) {
return w.ControllerService.ControllerUnpublishVolume(ctx, req)
}
@@ -404,11 +407,12 @@ func (w *WrappedControllerService) ControllerExpandVolume(ctx context.Context, r
return nil, err
}
// For NFS volumes, no node-side expansion is needed
// For NFS volumes (RWX Filesystem), no node-side expansion is needed. RWX Block
// volumes still require node-side expansion of the in-VM disk.
pvc, err := w.infraClient.CoreV1().PersistentVolumeClaims(w.infraNamespace).Get(ctx, req.GetVolumeId(), metav1.GetOptions{})
if err != nil {
klog.Warningf("Failed to check PVC access mode for %s/%s: %v", w.infraNamespace, req.GetVolumeId(), err)
} else if hasRWXAccessMode(pvc) {
} else if isNFSVolume(pvc) {
resp.NodeExpansionRequired = false
}
@@ -544,6 +548,19 @@ func hasRWXAccessMode(pvc *corev1.PersistentVolumeClaim) bool {
return false
}
// isNFSVolume reports whether the PVC was provisioned by our CreateVolume NFS path
// (RWX + Filesystem). RWX Block volumes are upstream hotplug volumes used for live
// migration and must NOT be handled as NFS.
func isNFSVolume(pvc *corev1.PersistentVolumeClaim) bool {
if !hasRWXAccessMode(pvc) {
return false
}
if pvc.Spec.VolumeMode != nil && *pvc.Spec.VolumeMode == corev1.PersistentVolumeBlock {
return false
}
return true
}
// getNFSExport extracts the NFS export URL from a PersistentVolume.
// Supports both native NFS PVs and CSI PVs with nfs-export volume attribute.
func getNFSExport(pv *corev1.PersistentVolume) (string, error) {