diff --git a/e2e/nfs.go b/e2e/nfs.go index 2f7697d9d0f..406d129facc 100644 --- a/e2e/nfs.go +++ b/e2e/nfs.go @@ -1249,6 +1249,103 @@ var _ = Describe("nfs", func() { validateOmapCount(f, 0, cephfsType, metadataPool, volumesType) }) + It("create a storageclass with clients restriction and modify it with VolumeAttributesClass", func() { + if !k8sVersionGreaterEquals(c, 1, 34) { + framework.Logf("skipping VolumeAttributesClass test, needs Kubernetes >= 1.34") + + return + } + + // Initial clients list - restrictive (Cloudflare DNS, should fail to mount) + initialClients := "1.1.1.1" + // Updated clients list - permissive (allow all clients) + updatedClients := "0.0.0.0/0" + + err := createNFSStorageClass(f.ClientSet, f, false, map[string]string{ + "clients": initialClients, + }) + if err != nil { + logAndFail("failed to create NFS storageclass: %v", err) + } + err = createNFSVolumeAttributesClass(f.ClientSet, f, map[string]string{ + "clients": updatedClients, + }) + if err != nil { + logAndFail("failed to create NFS volumeattributesclass: %v", err) + } + + pvc, err := loadPVC(pvcPath) + if err != nil { + logAndFail("Could not load PVC: %v", err) + } + pvc.Namespace = f.UniqueName + + // Create PVC first without VolumeAttributesClass + err = createPVCAndvalidatePV(f.ClientSet, pvc, deployTimeout) + if err != nil { + logAndFail("failed to create PVC: %v", err) + } + + // Verify initial restrictive clients parameter + if !checkExports(f, "my-nfs", initialClients) { + logAndFail("failed to verify initial clients in exports") + } + + app, err := loadApp(appPath) + if err != nil { + logAndFail("failed to load application: %v", err) + } + app.Namespace = f.UniqueName + app.Spec.Volumes[0].PersistentVolumeClaim.ClaimName = pvc.Name + + // Try to create app with restrictive clients - should fail to reach running state + err = createApp(f.ClientSet, app, deployTimeout) + if err == nil { + logAndFail("app should have failed to start with restrictive clients, but succeeded") + } + framework.Logf("app correctly failed to start with restrictive clients: %v", err) + + // Delete the failed app + err = deletePod(app.Name, app.Namespace, f.ClientSet, deployTimeout) + if err != nil { + logAndFail("failed to delete app: %v", err) + } + + // Apply VolumeAttributesClass to PVC to update clients + vacName := "updated-parameters" + pvc.Spec.VolumeAttributesClassName = &vacName + _, err = f.ClientSet.CoreV1().PersistentVolumeClaims(pvc.Namespace).Update( + context.TODO(), pvc, metav1.UpdateOptions{}) + if err != nil { + logAndFail("failed to update PVC with VolumeAttributesClass: %v", err) + } + + // Now create app again - VolumeAttributesClass should update clients to 0.0.0.0/0 + err = createApp(f.ClientSet, app, deployTimeout) + if err != nil { + logAndFail("failed to create application with updated clients: %v", err) + } + + // Verify the updated clients parameter is applied + if !checkExports(f, "my-nfs", updatedClients) { + logAndFail("failed to verify updated clients in exports") + } + + // delete PVC and app + err = deletePVCAndApp("", f, pvc, app) + if err != nil { + logAndFail("failed to delete PVC or application: %v", err) + } + err = deleteResource(nfsExamplePath + "storageclass.yaml") + if err != nil { + logAndFail("failed to delete NFS storageclass: %v", err) + } + err = deleteNFSVolumeAttributesClass(f.ClientSet, f) + if err != nil { + logAndFail("failed to delete NFS volumeattributesclass: %v", err) + } + }) + It("delete NFS provisioner and plugin secret", func() { // delete nfs provisioner secret err := deleteCephUser(f, keyringCephFSProvisionerUsername) diff --git a/examples/nfs/storageclass.yaml b/examples/nfs/storageclass.yaml index 011f21809ad..8f6cd228b6b 100644 --- a/examples/nfs/storageclass.yaml +++ b/examples/nfs/storageclass.yaml @@ -60,6 +60,8 @@ parameters: # access to the export to the set of hostnames, networks or ip addresses # specified. The is a comma delimited string, # for example: "192.168.0.10,192.168.1.0/8" + # This parameter can be updated after volume creation using a + # VolumeAttributesClass (see volumeattributesclass.yaml example). # clients: reclaimPolicy: Delete diff --git a/examples/nfs/volumeattributesclass.yaml b/examples/nfs/volumeattributesclass.yaml index 9acd95abf1d..c4ae0bbdb41 100644 --- a/examples/nfs/volumeattributesclass.yaml +++ b/examples/nfs/volumeattributesclass.yaml @@ -9,3 +9,8 @@ parameters: # "server" can be an alternative NFS-server that should be used when the # volume is attached the next time to a node. server: to-be-deployed.example.net + + # "clients" can be used to update the list of hostnames, networks or IP + # addresses that are allowed to access the NFS export. The + # is a comma delimited string, for example: "192.168.0.10,192.168.1.0/8" + # clients: diff --git a/internal/nfs/controller/controllerserver.go b/internal/nfs/controller/controllerserver.go index be8d55a8732..01f7d35c45a 100644 --- a/internal/nfs/controller/controllerserver.go +++ b/internal/nfs/controller/controllerserver.go @@ -259,5 +259,12 @@ func (cs *Server) ControllerModifyVolume( } } + if clients, ok := req.GetMutableParameters()[nfs.ParameterClients]; ok { + err := nfsVolume.SetClients(clients) + if err != nil { + return nil, status.Error(codes.Internal, err.Error()) + } + } + return &csi.ControllerModifyVolumeResponse{}, nil } diff --git a/internal/nfs/types/volume.go b/internal/nfs/types/volume.go index 1d87d99447d..d4372b212e9 100644 --- a/internal/nfs/types/volume.go +++ b/internal/nfs/types/volume.go @@ -39,6 +39,11 @@ const ( // ParameterServer is set in the parameters on volume creation and in // the VolumeContext. ParameterServer = "server" + + // ParameterClients is set in the parameters on volume creation and + // configured for the export in the NFS-server. It is not stored in + // the VolumeContext. + ParameterClients = "clients" ) // NFSVolume presents the API for consumption by the CSI-controller to create, @@ -246,6 +251,50 @@ func (nv *NFSVolume) GetServer() (string, error) { return nv.getAttribute(ParameterServer) } +// SetClients updates the NFS-clients list in the NFS export and stores it in the CephFS journal. +func (nv *NFSVolume) SetClients(clients string) error { + if !nv.connected { + return fmt.Errorf("can not set clients for %q: %w", nv, ErrNotConnected) + } + + nfsCluster, err := nv.getNFSCluster() + if err != nil { + return fmt.Errorf("failed to identify NFS cluster: %w", err) + } + + nfsa, err := nv.conn.GetNFSAdmin() + if err != nil { + return fmt.Errorf("failed to get NFSAdmin: %w", err) + } + + // Fetch current export info + exportInfo, err := nfsa.ExportInfo(nfsCluster, nv.GetExportPath()) + if err != nil { + return fmt.Errorf("failed to get export info for %q: %w", nv.GetExportPath(), err) + } + + // Update the export with new clients list + export := nfs.CephFSExportSpec{ + FileSystemName: exportInfo.FSAL.FileSystemName, + ClusterID: nfsCluster, + PseudoPath: nv.GetExportPath(), + Path: exportInfo.Path, + SecType: exportInfo.SecType, + } + + if clients != "" { + export.ClientAddr = strings.Split(clients, ",") + } + + _, err = nfsa.CreateCephFSExport(export) + if err != nil { + return fmt.Errorf("failed to update export %q with new clients: %w", nv.GetExportPath(), err) + } + + // Store the new clients value in the journal for persistence + return nv.setAttribute(ParameterClients, clients) +} + // createExportCommand returns the "ceph nfs export create ..." command // arguments (without "ceph"). The order of the parameters matches old Ceph // releases, new Ceph releases added --option formats, which can be added when