From cc5ec0b7a338f0a25973a53d043efb48ff27b695 Mon Sep 17 00:00:00 2001 From: mattia-eleuteri Date: Mon, 16 Mar 2026 14:38:26 +0100 Subject: [PATCH] [kubernetes] Fix CiliumNetworkPolicy endpointSelector for multi-node RWX volumes When an NFS-backed RWX volume is published to multiple VMs, the CiliumNetworkPolicy egress rule only allowed traffic from the first VM. The endpointSelector.matchLabels was set once on creation and never broadened, causing NFS mounts to hang on all nodes except the first. Switch from matchLabels to matchExpressions (operator: In) so the selector can list multiple VM names. Rebuild the selector whenever ownerReferences are added or removed. Signed-off-by: mattia-eleuteri --- .../images/kubevirt-csi-driver/controller.go | 51 +++++++++++++++++-- 1 file changed, 46 insertions(+), 5 deletions(-) diff --git a/packages/apps/kubernetes/images/kubevirt-csi-driver/controller.go b/packages/apps/kubernetes/images/kubevirt-csi-driver/controller.go index 7192b95b..edddec09 100644 --- a/packages/apps/kubernetes/images/kubevirt-csi-driver/controller.go +++ b/packages/apps/kubernetes/images/kubevirt-csi-driver/controller.go @@ -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 {