[kubernetes] Fix CiliumNetworkPolicy endpointSelector for multi-node RWX volumes (#2227)

## What this PR does

When an NFS-backed RWX volume is published to multiple VMs, the
`CiliumNetworkPolicy` `endpointSelector.matchLabels` only included the
first VM. Subsequent `ControllerPublishVolume` calls added
`ownerReferences` but never broadened the selector, causing Cilium to
block NFS egress — mounts hang on all nodes except the first.

This PR switches from `matchLabels` to `matchExpressions` (`operator:
In`) so the selector can list multiple VM names, and rebuilds it
whenever ownerReferences are added or removed.

### Release note

```release-note
[kubernetes] Fixed CiliumNetworkPolicy endpointSelector not being updated when NFS-backed RWX volumes are published to multiple VMs, which caused NFS mounts to hang on all nodes except the first.
```

<!-- This is an auto-generated comment: release notes by coderabbit.ai
-->

## Summary by CodeRabbit

* **New Features**
* KubeVirt CSI driver now supports selecting and targeting multiple
virtual machines for volume publishing.

* **Improvements**
* Network policy targets are rebuilt automatically when VM ownership
references change, improving correctness and lifecycle handling in
multi-VM scenarios.

<!-- end of auto-generated comment: release notes by coderabbit.ai -->
This commit is contained in:
Andrei Kvapil 2026-03-16 17:21:46 +01:00 committed by GitHub
commit 9fb9354fd2
No known key found for this signature in database
GPG key ID: B5690EEEBB952194

View file

@ -317,11 +317,7 @@ func (w *WrappedControllerService) ControllerPublishVolume(ctx context.Context,
"ownerReferences": []interface{}{vmiOwnerRef},
},
"spec": map[string]interface{}{
"endpointSelector": map[string]interface{}{
"matchLabels": map[string]interface{}{
"kubevirt.io/vm": vmName,
},
},
"endpointSelector": buildEndpointSelector([]string{vmName}),
"egress": []interface{}{
map[string]interface{}{
"toEndpoints": []interface{}{
@ -441,6 +437,13 @@ func (w *WrappedControllerService) addCNPOwnerReference(ctx context.Context, nam
if err := unstructured.SetNestedSlice(existing.Object, ownerRefs, "metadata", "ownerReferences"); err != nil {
return status.Errorf(codes.Internal, "failed to set ownerReferences: %v", err)
}
// Rebuild endpointSelector to include all VMs
selector := buildEndpointSelector(vmNamesFromOwnerRefs(ownerRefs))
if err := unstructured.SetNestedField(existing.Object, selector, "spec", "endpointSelector"); err != nil {
return status.Errorf(codes.Internal, "failed to set endpointSelector: %v", err)
}
if _, err := w.dynamicClient.Resource(ciliumNetworkPolicyGVR).Namespace(namespace).Update(ctx, existing, metav1.UpdateOptions{}); err != nil {
return err
}
@ -486,6 +489,13 @@ func (w *WrappedControllerService) removeCNPOwnerReference(ctx context.Context,
if err := unstructured.SetNestedSlice(existing.Object, remaining, "metadata", "ownerReferences"); err != nil {
return status.Errorf(codes.Internal, "failed to set ownerReferences: %v", err)
}
// Rebuild endpointSelector from remaining VMs
selector := buildEndpointSelector(vmNamesFromOwnerRefs(remaining))
if err := unstructured.SetNestedField(existing.Object, selector, "spec", "endpointSelector"); err != nil {
return status.Errorf(codes.Internal, "failed to set endpointSelector: %v", err)
}
if _, err := w.dynamicClient.Resource(ciliumNetworkPolicyGVR).Namespace(namespace).Update(ctx, existing, metav1.UpdateOptions{}); err != nil {
return err
}
@ -494,6 +504,37 @@ func (w *WrappedControllerService) removeCNPOwnerReference(ctx context.Context,
})
}
// buildEndpointSelector returns an endpointSelector using matchExpressions
// so that multiple VMs can be listed in a single selector.
func buildEndpointSelector(vmNames []string) map[string]interface{} {
values := make([]interface{}, len(vmNames))
for i, name := range vmNames {
values[i] = name
}
return map[string]interface{}{
"matchExpressions": []interface{}{
map[string]interface{}{
"key": "kubevirt.io/vm",
"operator": "In",
"values": values,
},
},
}
}
// vmNamesFromOwnerRefs extracts VM names from ownerReferences.
func vmNamesFromOwnerRefs(ownerRefs []interface{}) []string {
var names []string
for _, ref := range ownerRefs {
if refMap, ok := ref.(map[string]interface{}); ok {
if name, ok := refMap["name"].(string); ok {
names = append(names, name)
}
}
}
return names
}
func hasRWXAccessMode(pvc *corev1.PersistentVolumeClaim) bool {
for _, mode := range pvc.Spec.AccessModes {
if mode == corev1.ReadWriteMany {