diff --git a/e2e/nfs.go b/e2e/nfs.go index 2f7697d9d0f..c9951cd896b 100644 --- a/e2e/nfs.go +++ b/e2e/nfs.go @@ -38,6 +38,9 @@ import ( ) var ( + // nfsWithSlowTests enables negative testing (pod creation expected to fail) + nfsWithSlowTests = false + nfsProvisioner = "csi-nfsplugin-provisioner.yaml" nfsProvisionerRBAC = "csi-provisioner-rbac.yaml" nfsNodePlugin = "csi-nfsplugin.yaml" @@ -218,7 +221,12 @@ func createNFSStorageClass( if err != nil { framework.Logf("error creating StorageClass %q: %v", sc.Name, err) if apierrs.IsAlreadyExists(err) { - return true, nil + err = deleteResource(nfsExamplePath + "storageclass.yaml") + if err != nil { + logAndFail("failed to delete NFS storageclass: %v", err) + } + + return false, nil } if isRetryableAPIError(err) { return false, nil @@ -227,6 +235,8 @@ func createNFSStorageClass( return false, fmt.Errorf("failed to create StorageClass %q: %w", sc.Name, err) } + framework.Logf("created StorageClass %q", sc.Name) + return true, nil }) } @@ -504,11 +514,6 @@ var _ = Describe("nfs", func() { if err != nil { logAndFail("failed to verify mount options: %v", err) } - - err = deleteResource(nfsExamplePath + "storageclass.yaml") - if err != nil { - logAndFail("failed to delete NFS storageclass: %v", err) - } }) It("verify RWOP volume support", func() { @@ -550,10 +555,6 @@ var _ = Describe("nfs", func() { logAndFail("failed to validate RWOP pod creation: %v", err) } validateSubvolumeCount(f, 0, fileSystemName, defaultSubvolumegroup) - err = deleteResource(nfsExamplePath + "storageclass.yaml") - if err != nil { - logAndFail("failed to delete NFS storageclass: %v", err) - } }) It("create a storageclass with pool and a PVC then bind it to an app", func() { @@ -565,10 +566,6 @@ var _ = Describe("nfs", func() { if err != nil { logAndFail("failed to validate NFS pvc and application binding: %v", err) } - err = deleteResource(nfsExamplePath + "storageclass.yaml") - if err != nil { - logAndFail("failed to delete NFS storageclass: %v", err) - } }) It("create a storageclass with relocated server and a PVC then bind it to an app", func() { @@ -615,10 +612,6 @@ var _ = Describe("nfs", func() { 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 voluemattributesclass: %v", err) @@ -636,10 +629,6 @@ var _ = Describe("nfs", func() { if err != nil { logAndFail("failed to validate NFS pvc and application binding: %v", err) } - err = deleteResource(nfsExamplePath + "storageclass.yaml") - if err != nil { - logAndFail("failed to delete NFS storageclass: %v", err) - } }) It("create a storageclass with a restricted set of clients allowed to mount it", func() { @@ -668,10 +657,6 @@ var _ = Describe("nfs", func() { if err != nil { logAndFail("failed to delete PVC: %v", err) } - err = deleteResource(nfsExamplePath + "storageclass.yaml") - if err != nil { - logAndFail("failed to delete NFS storageclass: %v", err) - } }) It("create a PVC and bind it to an app", func() { @@ -1249,6 +1234,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{ + "csi.storage.k8s.io/controller-modify-secret-namespace": cephCSINamespace, + "csi.storage.k8s.io/controller-modify-secret-name": cephFSProvisionerSecretName, + "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 + + By("creating PVC first without VolumeAttributesClass") + err = createPVCAndvalidatePV(f.ClientSet, pvc, deployTimeout) + if err != nil { + logAndFail("failed to create PVC: %v", err) + } + + By("verifying 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 + + if nfsWithSlowTests { + By("trying 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) + + By("deleting the failed app") + err = deletePod(app.Name, app.Namespace, f.ClientSet, deployTimeout) + if err != nil { + logAndFail("failed to delete app: %v", err) + } + } + + By("applying VolumeAttributesClass to PVC to update clients") + vacName := "updated-parameters" + patchData := []byte(fmt.Sprintf(`{"spec":{"volumeAttributesClassName":"%s"}}`, vacName)) + _, err = f.ClientSet.CoreV1().PersistentVolumeClaims(pvc.Namespace).Patch( + context.TODO(), pvc.Name, "application/strategic-merge-patch+json", patchData, metav1.PatchOptions{}) + if err != nil { + logAndFail("failed to patch PVC with VolumeAttributesClass: %v", err) + } + + By("verifying updated clients parameter") + if !checkExports(f, "my-nfs", updatedClients) { + logAndFail("failed to verify initial clients in exports") + } + + By("creating app with PVC that has updated clients") + err = createApp(f.ClientSet, app, deployTimeout) + if err != nil { + logAndFail("failed to create application with updated clients: %v", err) + } + + By("deleting PVC and app") + err = deletePVCAndApp("", f, pvc, app) + if err != nil { + logAndFail("failed to delete PVC or application: %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/e2e/utils.go b/e2e/utils.go index 0915fbd49bd..2e1ee8c9841 100644 --- a/e2e/utils.go +++ b/e2e/utils.go @@ -2111,7 +2111,7 @@ func checkExports(f *framework.Framework, clusterID, clientString string) bool { } if !found { - framework.Logf("Could not find the configured clients in the list of exports") + framework.Logf("Could not find the configured clients (%s) in the list of exports (%+v)", clientString, exportList) return false } 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/go.mod b/go.mod index 4ef59d1318b..49f862a539b 100644 --- a/go.mod +++ b/go.mod @@ -44,6 +44,9 @@ require ( k8s.io/utils v0.0.0-20251002143259-bc988d571ff4 ) +// for nfsa.UpdateCephFSExport, github.com/nixpanic/go-ceph:nfs/export-update +replace github.com/ceph/go-ceph => github.com/nixpanic/go-ceph v0.0.0-20260417074741-878fc984e61b + require ( // sigs.k8s.io/controller-runtime wants this version, it gets replaced below k8s.io/client-go v12.0.0+incompatible diff --git a/go.sum b/go.sum index e0a8db6b954..8309af86617 100644 --- a/go.sum +++ b/go.sum @@ -487,8 +487,6 @@ github.com/census-instrumentation/opencensus-proto v0.3.0/go.mod h1:f6KPmirojxKA github.com/census-instrumentation/opencensus-proto v0.4.1/go.mod h1:4T9NM4+4Vw91VeyqjLS6ao50K5bOcLKN6Q42XnYaRYw= github.com/ceph/ceph-nvmeof/lib/go/nvmeof v0.0.0-20260120065657-2425981cdad5 h1:giK4rX93KFW4C361EHurz46iM5nqFOKOT4EMuuroi+E= github.com/ceph/ceph-nvmeof/lib/go/nvmeof v0.0.0-20260120065657-2425981cdad5/go.mod h1:P+rCBJEUy2Yt6vOcTh7pyh9/g+o9pOZc7kyY6k/PS5U= -github.com/ceph/go-ceph v0.38.0 h1:Ux0sIpl6VJNgY21hxuBZI9Z2Z8tQsBMJhjLjYBoa7s0= -github.com/ceph/go-ceph v0.38.0/go.mod h1:GQVPe5YWoCMOrGnpDDieQoQZRLkB0tJmIokbqxbwPBQ= github.com/cespare/xxhash v1.1.0/go.mod h1:XrSqR1VqqWfGrhpAt58auRo0WTKS1nRRg3ghfAqPWnc= github.com/cespare/xxhash/v2 v2.1.1/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= github.com/cespare/xxhash/v2 v2.2.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= @@ -919,6 +917,8 @@ github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822/go.mod h1:+n7T8m github.com/mxk/go-flowrate v0.0.0-20140419014527-cca7078d478f/go.mod h1:ZdcZmHo+o7JKHSa8/e818NopupXU1YMK5fe1lsApnBw= github.com/nbio/st v0.0.0-20140626010706-e9e8d9816f32/go.mod h1:9wM+0iRr9ahx58uYLpLIr5fm8diHn0JbqRycJi6w0Ms= github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e/go.mod h1:zD1mROLANZcx1PVRCS0qkT7pwLkGfwJo4zjcN/Tysno= +github.com/nixpanic/go-ceph v0.0.0-20260417074741-878fc984e61b h1:4yk3TAir4QxFb/V4ctiGvs3yxf62qNBnOATMFyqd6xw= +github.com/nixpanic/go-ceph v0.0.0-20260417074741-878fc984e61b/go.mod h1:UId58dqtDKTwnv3OY8rdpC+Ulz/AVpcvZqjXDICcd5c= github.com/nxadm/tail v1.4.4/go.mod h1:kenIhsEOeOJmVchQTgglprH7qJGnHDVpk1VPCcaMI8A= github.com/nxadm/tail v1.4.8/go.mod h1:+ncqLTQzXmGhMZNUePPaPqPvBxHAIsmXswZKocGu+AU= github.com/onsi/ginkgo v1.6.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE= 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 935028ea672..ad2b900c95e 100644 --- a/internal/nfs/types/volume.go +++ b/internal/nfs/types/volume.go @@ -40,6 +40,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, @@ -251,6 +256,48 @@ func (nv *NFSVolume) GetServer() (string, error) { return nv.getAttribute(ParameterServer) } +// SetClients updates the NFS-clients list in the NFS export. +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 + if clients != "" { + clientAddrs := strings.Split(clients, ",") + exportInfo.Clients = []nfs.ClientInfo{ + { + Addresses: clientAddrs, + AccessType: "rw", + Squash: nfs.NoneSquash, + }, + } + } + + err = nfsa.ApplyExportInfo(nfsCluster, exportInfo) + if err != nil { + return fmt.Errorf("failed to update export %q with new clients: %w", nv.GetExportPath(), err) + } + + return nil +} + // 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 diff --git a/vendor/github.com/ceph/go-ceph/cephfs/snap_diff.go b/vendor/github.com/ceph/go-ceph/cephfs/snap_diff.go index 2d5553a567f..545dabb41a6 100644 --- a/vendor/github.com/ceph/go-ceph/cephfs/snap_diff.go +++ b/vendor/github.com/ceph/go-ceph/cephfs/snap_diff.go @@ -1,5 +1,3 @@ -//go:build ceph_preview - package cephfs /* diff --git a/vendor/github.com/ceph/go-ceph/common/admin/nfs/admin.go b/vendor/github.com/ceph/go-ceph/common/admin/nfs/admin.go index 43a73021caa..642a4c5d757 100644 --- a/vendor/github.com/ceph/go-ceph/common/admin/nfs/admin.go +++ b/vendor/github.com/ceph/go-ceph/common/admin/nfs/admin.go @@ -7,14 +7,19 @@ import ( ccom "github.com/ceph/go-ceph/common/commands" ) +// Commander interface supports sending commands to Ceph. +type Commander interface { + ccom.RadosBufferCommander +} + // Admin is used to administer ceph nfs features. type Admin struct { - conn ccom.RadosCommander + conn Commander } // NewFromConn creates an new management object from a preexisting // rados connection. The existing connection can be rados.Conn or any // type implementing the RadosCommander interface. -func NewFromConn(conn ccom.RadosCommander) *Admin { +func NewFromConn(conn Commander) *Admin { return &Admin{conn} } diff --git a/vendor/github.com/ceph/go-ceph/common/admin/nfs/export.go b/vendor/github.com/ceph/go-ceph/common/admin/nfs/export.go index 0a96a8ddacb..9a1d3ab6f25 100644 --- a/vendor/github.com/ceph/go-ceph/common/admin/nfs/export.go +++ b/vendor/github.com/ceph/go-ceph/common/admin/nfs/export.go @@ -208,10 +208,6 @@ func (nfsa *Admin) ExportInfo(clusterID, pseudoPath string) (ExportInfo, error) /* TODO? -'nfs export apply': cluster_id: str, inbuf: str -"""Create or update an export by `-i `""" - - 'nfs export create rgw': bucket: str, cluster_id: str, diff --git a/vendor/github.com/ceph/go-ceph/common/admin/nfs/export_apply.go b/vendor/github.com/ceph/go-ceph/common/admin/nfs/export_apply.go new file mode 100644 index 00000000000..54019d28475 --- /dev/null +++ b/vendor/github.com/ceph/go-ceph/common/admin/nfs/export_apply.go @@ -0,0 +1,57 @@ +//go:build ceph_preview && !(nautilus || octopus) +// +build ceph_preview,!nautilus,!octopus + +package nfs + +import ( + "encoding/json" + "errors" + "fmt" + + "github.com/ceph/go-ceph/internal/commands" +) + +var errUnknownApplyState = errors.New("apply returned unknown state") + +// applyRes is used to parse the result from "nfs export apply" which can +// modify multiple exports with a single call. Each export that was (attempted +// to be) modified has JSON resonse with "pseudo" and "state" in it. +// ApplyExportInfo() modifies only a single export, but the returned response +// is still formatted in a JSON list. +type applyRes []*struct { + Pseudo string `json:"pseudo"` + State string `json:"state"` +} + +func parseApplyResults(res commands.Response) error { + results := applyRes{} + if err := res.NoStatus().Unmarshal(&results).End(); err != nil { + return err + } + for _, ar := range results { + if ar.State == "added" || ar.State == "updated" { + // succes, nothing to do for this ar + continue + } + return fmt.Errorf("%w %s for pseudo %s", errUnknownApplyState, ar.State, ar.Pseudo) + } + return nil +} + +// ApplyExportInfo will create or update an existing NFS export. +// +// Similar To: +// +// ceph nfs export apply +func (nfsa *Admin) ApplyExportInfo(clusterID string, info ExportInfo) error { + buf, err := json.Marshal(info) + if err != nil { + return err + } + m := map[string]string{ + "prefix": "nfs export apply", + "format": "json", + "cluster_id": clusterID, + } + return parseApplyResults(commands.MarshalMgrCommandWithBuffer(nfsa.conn, m, buf)) +} diff --git a/vendor/github.com/ceph/go-ceph/common/admin/osd/errors.go b/vendor/github.com/ceph/go-ceph/common/admin/osd/errors.go index 3bef7c154ab..3de0bc70a9c 100644 --- a/vendor/github.com/ceph/go-ceph/common/admin/osd/errors.go +++ b/vendor/github.com/ceph/go-ceph/common/admin/osd/errors.go @@ -1,5 +1,3 @@ -//go:build ceph_preview - package osd /* diff --git a/vendor/github.com/ceph/go-ceph/common/admin/osd/osd_blocklist.go b/vendor/github.com/ceph/go-ceph/common/admin/osd/osd_blocklist.go index 4cbaa0b18be..9e8ed957219 100644 --- a/vendor/github.com/ceph/go-ceph/common/admin/osd/osd_blocklist.go +++ b/vendor/github.com/ceph/go-ceph/common/admin/osd/osd_blocklist.go @@ -1,4 +1,4 @@ -//go:build !octopus && ceph_preview +//go:build !octopus package osd diff --git a/vendor/github.com/ceph/go-ceph/rbd/snapshot_remove_by_id.go b/vendor/github.com/ceph/go-ceph/rbd/snapshot_remove_by_id.go index 95fc404ac53..945edd6ebe0 100644 --- a/vendor/github.com/ceph/go-ceph/rbd/snapshot_remove_by_id.go +++ b/vendor/github.com/ceph/go-ceph/rbd/snapshot_remove_by_id.go @@ -1,6 +1,3 @@ -//go:build ceph_preview -// +build ceph_preview - package rbd /* diff --git a/vendor/modules.txt b/vendor/modules.txt index 82339c1538f..b3c23e4570d 100644 --- a/vendor/modules.txt +++ b/vendor/modules.txt @@ -208,8 +208,8 @@ github.com/ceph/ceph-csi/api/deploy/ocp # github.com/ceph/ceph-nvmeof/lib/go/nvmeof v0.0.0-20260120065657-2425981cdad5 ## explicit; go 1.24.4 github.com/ceph/ceph-nvmeof/lib/go/nvmeof -# github.com/ceph/go-ceph v0.38.0 -## explicit; go 1.24.0 +# github.com/ceph/go-ceph v0.38.0 => github.com/nixpanic/go-ceph v0.0.0-20260417074741-878fc984e61b +## explicit; go 1.25.0 github.com/ceph/go-ceph/cephfs github.com/ceph/go-ceph/cephfs/admin github.com/ceph/go-ceph/common/admin/manager @@ -1282,4 +1282,5 @@ sigs.k8s.io/structured-merge-diff/v6/value ## explicit; go 1.22 sigs.k8s.io/yaml # github.com/ceph/ceph-csi/api => ./api +# github.com/ceph/go-ceph => github.com/nixpanic/go-ceph v0.0.0-20260417074741-878fc984e61b # k8s.io/client-go => k8s.io/client-go v0.35.3