diff --git a/README.md b/README.md index 68a9835b..fa0588e3 100644 --- a/README.md +++ b/README.md @@ -293,6 +293,18 @@ spec: storage: 1Mi ``` +**Step 8: Controlling the permissions and ownership of subdirs** + +By default new directories will be created with `root:root` ownership, and `0777` permissions in most environments. If you have a need to control this, you can do so by providing the `NFS_DEFAULT_MODE`, `NFS_DEFAULT_UID` and `NFS_DEFAULT_GID` environment variables (or the appropriate configuration in the Helm chart values). The mode must be an octal representation of a file mode, for example `777`, `0755` etc. The uid and gid must be the numeric ids of your desired user and group, so `1000` not `my_user`. + +If your usecase requires per-PVC ownership and/or mode, this can be done via annotations on your PVC: + +- `k8s-sigs.io/nfs-directory-mode` +- `k8s-sigs.io/nfs-directory-uid` +- `k8s-sigs.io/nfs-directory-gid` + +The order of precedence is PVC annotations, ENV vars, then root:root 0777 if nothing else has been specified. + # Build and publish your own container image To build your own custom container image from this repository, you will have to build and push the nfs-subdir-external-provisioner image using the following instructions. diff --git a/charts/nfs-subdir-external-provisioner/templates/deployment.yaml b/charts/nfs-subdir-external-provisioner/templates/deployment.yaml index 15a574b2..f84ff538 100644 --- a/charts/nfs-subdir-external-provisioner/templates/deployment.yaml +++ b/charts/nfs-subdir-external-provisioner/templates/deployment.yaml @@ -57,6 +57,12 @@ spec: value: {{ .Values.nfs.server }} - name: NFS_PATH value: {{ .Values.nfs.path }} + - name: NFS_DEFAULT_MODE + value: {{ .Values.nfs.defaultMode}} + - name: NFS_DEFAULT_UID + value: {{ .Values.nfs.defaultUid }} + - name: NFS_DEFAULT_GID + value: {{ .Values.nfs.defaultGid }} {{- if eq .Values.leaderElection.enabled false }} - name: ENABLE_LEADER_ELECTION value: "false" diff --git a/charts/nfs-subdir-external-provisioner/values.yaml b/charts/nfs-subdir-external-provisioner/values.yaml index b7d11878..7d37ba87 100644 --- a/charts/nfs-subdir-external-provisioner/values.yaml +++ b/charts/nfs-subdir-external-provisioner/values.yaml @@ -12,6 +12,9 @@ nfs: path: /nfs-storage mountOptions: volumeName: nfs-subdir-external-provisioner-root + defaultMode: "777" + defaultUid: "0" + defaultGid: "0" # Reclaim policy for the main nfs volume reclaimPolicy: Retain diff --git a/cmd/nfs-subdir-external-provisioner/provisioner.go b/cmd/nfs-subdir-external-provisioner/provisioner.go index e124757d..02ec6ef9 100644 --- a/cmd/nfs-subdir-external-provisioner/provisioner.go +++ b/cmd/nfs-subdir-external-provisioner/provisioner.go @@ -44,9 +44,12 @@ const ( ) type nfsProvisioner struct { - client kubernetes.Interface - server string - path string + client kubernetes.Interface + server string + path string + defaultMode os.FileMode + defaultUid int + defaultGid int } type pvcMetadata struct { @@ -74,7 +77,8 @@ func (meta *pvcMetadata) stringParser(str string) string { } const ( - mountPath = "/persistentvolumes" + mountPath = "/persistentvolumes" + annotationPrefix = "k8s-sigs.io" ) var _ controller.Provisioner = &nfsProvisioner{} @@ -111,15 +115,54 @@ func (p *nfsProvisioner) Provision(ctx context.Context, options controller.Provi } } + // Check if the PVC has an annotation requesting a specific mode. Fallback to defaults if not. + mode := p.defaultMode + pvcMode := metadata.annotations[annotationPrefix+"/nfs-directory-mode"] + if pvcMode != "" { + var err error + mode, err = getModeFromString(pvcMode) + if err != nil { + return nil, controller.ProvisioningFinished, fmt.Errorf("invalid directoryMode %s: %v", pvcMode, err) + } + } glog.V(4).Infof("creating path %s", fullPath) - if err := os.MkdirAll(fullPath, 0o777); err != nil { + if err := os.MkdirAll(fullPath, mode); err != nil { return nil, controller.ProvisioningFinished, errors.New("unable to create directory to provision new pv: " + err.Error()) } - err := os.Chmod(fullPath, 0o777) + err := os.Chmod(fullPath, mode) if err != nil { return nil, "", err } + // Check if the PVC has an annotation requesting a specific UID and GID. Again, fallback to defaults if not. + uid := p.defaultUid + pvcUid := metadata.annotations[annotationPrefix+"/nfs-directory-uid"] + if pvcUid != "" { + var err error + uid, err = getIdFromString(pvcUid) + if err != nil { + // No real point in returning an error here as the dir will have already been created as root:root + // log the error and continue with the default uid + glog.Errorf("invalid directoryUid %s: %v", pvcUid, err) + uid = p.defaultUid + } + } + gid := p.defaultGid + pvcGid := metadata.annotations[annotationPrefix+"/nfs-directory-gid"] + if pvcGid != "" { + var err error + gid, err = getIdFromString(pvcGid) + if err != nil { + // No real point in returning an error here as the dir will have already been created as root:root + // log the error and continue with the default gid + glog.Errorf("invalid directoryGid %s: %v", pvcGid, err) + gid = p.defaultGid + } + } + err = os.Chown(fullPath, uid, gid) + if err != nil { + return nil, "", err + } pv := &v1.PersistentVolume{ ObjectMeta: metav1.ObjectMeta{ Name: options.PVName, @@ -205,6 +248,36 @@ func (p *nfsProvisioner) getClassForVolume(ctx context.Context, pv *v1.Persisten return class, nil } +func getModeFromString(mode string) (os.FileMode, error) { + if mode == "" { + return os.FileMode(0o777), nil // Default to 0777, per current behavior + } + var modeInt int64 + var err error + modeInt, err = strconv.ParseInt(mode, 8, 64) + if err != nil { + return 0, fmt.Errorf("invalid mode %s: %v", mode, err) + } + if modeInt < 0 || modeInt > 0o777 { + return 0, fmt.Errorf("mode must be between 0 and 0777, got %s", mode) + } + return os.FileMode(modeInt), nil +} + +func getIdFromString(id string) (int, error) { + if id == "" { + return 0, nil // Default to 0 aka root, per current behavior + } + idInt, err := strconv.Atoi(id) + if err != nil { + return 0, fmt.Errorf("invalid id %s: %v", id, err) + } + if idInt < 0 || idInt > 65535 { + return 0, fmt.Errorf("id must be between 0 and 65535, got %s", id) + } + return idInt, nil +} + func main() { flag.Parse() flag.Set("logtostderr", "true") @@ -221,6 +294,19 @@ func main() { if provisionerName == "" { glog.Fatalf("environment variable %s is not set! Please set it.", provisionerNameKey) } + // Get the default mode, uid, and gid from environment variables + mode, err := getModeFromString(os.Getenv("NFS_DEFAULT_MODE")) + if err != nil { + glog.Fatalf("Failed to parse NFS_DEFAULT_MODE: %v", err) + } + uid, err := getIdFromString(os.Getenv("NFS_DEFAULT_UID")) + if err != nil { + glog.Fatalf("Failed to parse NFS_DEFAULT_UID: %v", err) + } + gid, err := getIdFromString(os.Getenv("NFS_DEFAULT_GID")) + if err != nil { + glog.Fatalf("Failed to parse NFS_DEFAULT_GID: %v", err) + } kubeconfig := os.Getenv("KUBECONFIG") var config *rest.Config if kubeconfig != "" { @@ -262,9 +348,12 @@ func main() { } clientNFSProvisioner := &nfsProvisioner{ - client: clientset, - server: server, - path: path, + client: clientset, + server: server, + path: path, + defaultMode: mode, + defaultUid: uid, + defaultGid: gid, } // Start the provision controller which will dynamically provision efs NFS // PVs