Skip to content

Commit

Permalink
feat: Add support to specify file permissions for pvc hostpaths
Browse files Browse the repository at this point in the history
Signed-off-by: sushiMix <[email protected]>
  • Loading branch information
sushiMix committed Oct 20, 2023
1 parent a4a9273 commit 6e61d78
Show file tree
Hide file tree
Showing 4 changed files with 196 additions and 40 deletions.
155 changes: 125 additions & 30 deletions cmd/provisioner-localpv/app/config.go
Original file line number Diff line number Diff line change
Expand Up @@ -130,6 +130,26 @@ const (

KeyQuotaSoftLimit = "softLimitGrace"
KeyQuotaHardLimit = "hardLimitGrace"

// FilePermissions allows to define the default directory mode
// Exemple StorageClass snippet:
// - name: FilePermissions
// enabled: true
// data:
// UID: 1000
// GID: 1000
// mode: g+s
// This is the cas-template key for all file permission 'data' keys
KeyFilePermissions = "FilePermissions"

// FsUID defines the user owner of the shared directory
KeyFsUID = "UID"

// FsGID defines the group owner of the shared directory
KeyFsGID = "GID"

// FSMode defines the file permission mode of the shared directory
KeyFsMode = "mode"
)

const (
Expand All @@ -142,7 +162,7 @@ const (
k8sNodeLabelKeyHostname = "kubernetes.io/hostname"
)

//GetVolumeConfig creates a new VolumeConfig struct by
// GetVolumeConfig creates a new VolumeConfig struct by
// parsing and merging the configuration provided in the PVC
// annotation - cas.openebs.io/config with the
// default configuration of the provisioner.
Expand Down Expand Up @@ -170,15 +190,21 @@ func (p *Provisioner) GetVolumeConfig(ctx context.Context, pvName string, pvc *c
}

//TODO : extract and merge the cas volume config from pvc
//This block can be added once validation checks are added
// This block can be added once validation checks are added
// as to the type of config that can be passed via PVC
//pvcCASConfigStr := pvc.ObjectMeta.Annotations[string(mconfig.CASConfigKey)]
//if len(strings.TrimSpace(pvcCASConfigStr)) != 0 {
// pvcCASConfig, err := cast.UnMarshallToConfig(pvcCASConfigStr)
// if err == nil {
// pvConfig = cast.MergeConfig(pvcCASConfig, pvConfig)
// }
//}
pvcCASConfigStr := pvc.ObjectMeta.Annotations[string(mconfig.CASConfigKey)]
klog.V(4).Infof("PVC %v has config:%v", pvc.Name, pvcCASConfigStr)
if len(strings.TrimSpace(pvcCASConfigStr)) != 0 {
pvcCASConfig, err := cast.UnMarshallToConfig(pvcCASConfigStr)
if err == nil {
pvConfig = cast.MergeConfig(pvcCASConfig, pvConfig)
} else {
return nil, errors.Wrapf(err, "failed to get config: invalid config {%v}"+
" in pvc {%v} in namespace {%v}",
pvcCASConfigStr, pvc.Name, pvc.Namespace,
)
}
}

pvConfigMap, err := cast.ConfigToMap(pvConfig)
if err != nil {
Expand Down Expand Up @@ -206,7 +232,7 @@ func (p *Provisioner) GetVolumeConfig(ctx context.Context, pvName string, pvc *c
return c, nil
}

//GetStorageType returns the StorageType value configured
// GetStorageType returns the StorageType value configured
// in StorageClass. Default is hostpath
func (c *VolumeConfig) GetStorageType() string {
stgType := c.getValue(KeyPVStorageType)
Expand All @@ -216,15 +242,16 @@ func (c *VolumeConfig) GetStorageType() string {
return stgType
}

//GetBlockDeviceSelectors returns the BlockDeviceSelectors data configured
// GetBlockDeviceSelectors returns the BlockDeviceSelectors data configured
// in StorageClass. Default is nil
func (c *VolumeConfig) GetBlockDeviceSelectors() map[string]string {
blockDeviceSelector := c.getData(KeyBlockDeviceSelectors)
return blockDeviceSelector
}

// NOTE: This function should not be used, as KeyBDTag has been deprecated.
// GetBlockDeviceSelectors() is the right function to use.
//
// GetBlockDeviceSelectors() is the right function to use.
func (c *VolumeConfig) GetBDTagValue() string {
bdTagValue := c.getValue(KeyBDTag)
if len(strings.TrimSpace(bdTagValue)) == 0 {
Expand All @@ -233,7 +260,7 @@ func (c *VolumeConfig) GetBDTagValue() string {
return bdTagValue
}

//GetFSType returns the FSType value configured
// GetFSType returns the FSType value configured
// in StorageClass. Default is "", auto-determined
// by Local PV
func (c *VolumeConfig) GetFSType() string {
Expand All @@ -254,10 +281,10 @@ func (c *VolumeConfig) GetNodeAffinityLabelKey() string {
return nodeAffinityLabelKey
}

//GetNodeAffinityLabelKey returns the custom node affinity
//label keys as configured in StorageClass.
// GetNodeAffinityLabelKey returns the custom node affinity
// label keys as configured in StorageClass.
//
//Default is nil.
// Default is nil.
func (c *VolumeConfig) GetNodeAffinityLabelKeys() []string {
nodeAffinityLabelKeys := c.getList(KeyNodeAffinityLabels)
if nodeAffinityLabelKeys == nil {
Expand All @@ -266,14 +293,17 @@ func (c *VolumeConfig) GetNodeAffinityLabelKeys() []string {
return nodeAffinityLabelKeys
}

//GetPath returns a valid PV path based on the configuration
// GetPath returns a valid PV path based on the configuration
// or an error. The Path is constructed using the following rules:
// If AbsolutePath is specified return it. (Future)
// If PVPath is specified, suffix it with BasePath and return it. (Future)
// If neither of above are specified, suffix the PVName to BasePath
// and return it
//
// and return it
//
// Also before returning the path, validate that path is safe
// and matches the filters specified in StorageClass.
//
// and matches the filters specified in StorageClass.
func (c *VolumeConfig) GetPath() (string, error) {
//This feature need to be supported with some more
// security checks are in place, so that rouge pods
Expand Down Expand Up @@ -338,18 +368,83 @@ func (c *VolumeConfig) IsExt4QuotaEnabled() bool {
return enableExt4QuotaBool
}

//getValue is a utility function to extract the value
func (c *VolumeConfig) IsPermissionEnabled() bool {
permissionEnabled := c.getEnabled(KeyFilePermissions)
permissionEnabled = strings.TrimSpace(permissionEnabled)

permissionEnabledQuotaBool, err := strconv.ParseBool(permissionEnabled)
//Default case
// this means that we have hit either of the two cases below:
// i. The value was something other than a straightforward
// true or false
// ii. The value was empty
if err != nil {
return false
}

return permissionEnabledQuotaBool
}

// GetFsGID fetches the group owner's ID from
// PVC annotation, if specified
// NOT YET USED
func (c *VolumeConfig) GetFsGID() string {
if c.IsPermissionEnabled() {
configData := c.getData(KeyFilePermissions)
if configData != nil {
if val, p := configData[KeyFsGID]; p {
return strings.TrimSpace(val)
}
}
}
return ""
}

// GetFsGID fetches the user owner's ID from
// PVC annotation, if specified
// NOT YET USED
func (c *VolumeConfig) GetFsUID() string {
if c.IsPermissionEnabled() {
configData := c.getData(KeyFilePermissions)
if configData != nil {
if val, p := configData[KeyFsUID]; p {
return strings.TrimSpace(val)
}
}
}
return ""
}

// GetFsMode fetches the file mode from PVC
// or StorageClass annotation, if specified
func (c *VolumeConfig) GetFsMode() string {
if c.IsPermissionEnabled() {
configData := c.getData(KeyFilePermissions)
if configData != nil {
if val, p := configData[KeyFsMode]; p {
return strings.TrimSpace(val)
}
}
}
//Keep the original default mode
return "0777"
}

// getValue is a utility function to extract the value
// of the `key` from the ConfigMap object - which is
// map[string]interface{map[string][string]}
// Example:
// {
// key1: {
// value: value1
// enabled: true
// }
// }
//
// {
// key1: {
// value: value1
// enabled: true
// }
// }
//
// In the above example, if `key1` is passed as input,
// `value1` will be returned.
//
// `value1` will be returned.
func (c *VolumeConfig) getValue(key string) string {
if configObj, ok := util.GetNestedField(c.options, key).(map[string]string); ok {
if val, p := configObj[string(mconfig.ValuePTP)]; p {
Expand All @@ -359,7 +454,7 @@ func (c *VolumeConfig) getValue(key string) string {
return ""
}

//Similar to getValue() above. Returns value of the
// Similar to getValue() above. Returns value of the
// 'Enabled' parameter.
func (c *VolumeConfig) getEnabled(key string) string {
if configObj, ok := util.GetNestedField(c.options, key).(map[string]string); ok {
Expand All @@ -370,7 +465,7 @@ func (c *VolumeConfig) getEnabled(key string) string {
return ""
}

//This is similar to getValue() and getEnabled().
// This is similar to getValue() and getEnabled().
// This gets the value for a specific
// 'Data' parameter key-value pair.
func (c *VolumeConfig) getDataField(key string, dataKey string) string {
Expand All @@ -383,7 +478,7 @@ func (c *VolumeConfig) getDataField(key string, dataKey string) string {
return ""
}

//This is similar to getValue() and getEnabled().
// This is similar to getValue() and getEnabled().
// This returns the value of the `Data` parameter
func (c *VolumeConfig) getData(key string) map[string]string {
if configData, ok := util.GetNestedField(c.configData, key).(map[string]string); ok {
Expand Down
12 changes: 7 additions & 5 deletions cmd/provisioner-localpv/app/provisioner_hostpath.go
Original file line number Diff line number Diff line change
Expand Up @@ -39,7 +39,8 @@ const (
)

// ProvisionHostPath is invoked by the Provisioner which expect HostPath PV
// to be provisioned and a valid PV spec returned.
//
// to be provisioned and a valid PV spec returned.
func (p *Provisioner) ProvisionHostPath(ctx context.Context, opts pvController.ProvisionOptions, volumeConfig *VolumeConfig) (*v1.PersistentVolume, pvController.ProvisioningState, error) {
pvc := opts.PVC
taints := GetTaints(opts.SelectedNode)
Expand Down Expand Up @@ -76,7 +77,7 @@ func (p *Provisioner) ProvisionHostPath(ctx context.Context, opts pvController.P
klog.Infof("Creating volume %v at node with labels {%v}, path:%v,ImagePullSecrets:%v", name, nodeAffinityLabels, path, imagePullSecrets)

//Before using the path for local PV, make sure it is created.
initCmdsForPath := []string{"mkdir", "-m", "0777", "-p"}
initCmdsForPath := []string{"mkdir", "-m", volumeConfig.GetFsMode(), "-p"}
podOpts := &HelperPodOptions{
cmdsForPath: initCmdsForPath,
name: name,
Expand Down Expand Up @@ -245,9 +246,10 @@ func (p *Provisioner) GetNodeObjectFromLabels(nodeLabels map[string]string) (*v1
}

// DeleteHostPath is invoked by the PVC controller to perform clean-up
// activities before deleteing the PV object. If reclaim policy is
// set to not-retain, then this function will create a helper pod
// to delete the host path from the node.
//
// activities before deleteing the PV object. If reclaim policy is
// set to not-retain, then this function will create a helper pod
// to delete the host path from the node.
func (p *Provisioner) DeleteHostPath(ctx context.Context, pv *v1.PersistentVolume) (err error) {
defer func() {
err = errors.Wrapf(err, "failed to delete volume %v", pv.Name)
Expand Down
15 changes: 10 additions & 5 deletions docs/quickstart.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@

## Prerequisites

A Kubernetes cluster with Kubernetes v1.16 or above.
A Kubernetes cluster with Kubernetes v1.16 or above.

For more platform-specific installation instructions, [click here](./installation/platforms/).

Expand All @@ -13,12 +13,12 @@ Install OpenEBS Dynamic LocalPV Provisioner using the openebs helm chart. Sample
#helm repo update
helm install openebs openebs/openebs -n openebs --create-namespace
```

<details>
<summary>Click here for configuration options.</summary>

1. Install OpenEBS Dynamic LocalPV Provisioner without NDM.
1. Install OpenEBS Dynamic LocalPV Provisioner without NDM.

You may choose to exclude the NDM subchart from installation if...
- you want to only use OpenEBS LocalPV Hostpath
- you already have NDM installed. Check if NDM pods exist with the command `kubectl get pods -n openebs -l 'openebs.io/component-name in (ndm, ndm-operator)'`
Expand All @@ -35,7 +35,7 @@ helm install openebs openebs/openebs -n openebs --create-namespace \
--set ndmOperator.enabled=false \
--set localprovisioner.deviceClass.enabled=false
```
3. Install OpenEBS Dynamic LocalPV Provisioner with a custom hostpath directory.
3. Install OpenEBS Dynamic LocalPV Provisioner with a custom hostpath directory.
This will change the `BasePath` value for the 'openebs-hostpath' StorageClass.
```console
helm install openebs openebs/openebs -n openebs --create-namespace \
Expand Down Expand Up @@ -92,6 +92,11 @@ You can provision LocalPV hostpath StorageType volumes dynamically using the def
# hostpath directory
#- name: BasePath
# value: "/var/openebs/local"
#Use this to set a specific mode for directory creation
#- name: FilePermissions
# enabled: true
# data:
# mode: "0770"
provisioner: openebs.io/local
reclaimPolicy: Delete
#It is necessary to have volumeBindingMode as WaitForFirstConsumer
Expand Down
54 changes: 54 additions & 0 deletions docs/tutorials/hostpath/filepermissions.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
# File permission tuning

Hostpath LocalPV will by default create folder with the following rights: `0777`. In some usecases, these rights are too wide and should be reduced.
As an important point, when using hostpath the underlying PV will be a localpath whichs allows kubelet to chown the folder based on the [fsGroup](https://kubernetes.io/docs/tasks/configure-pod-container/security-context/#configure-volume-permission-and-ownership-change-policy-for-pods))

We allow to reduce this filepermission using :

```yaml
#This is a custom StorageClass template
apiVersion: storage.k8s.io/v1
kind: StorageClass
metadata:
name: custom-hostpath
annotations:
openebs.io/cas-type: local
cas.openebs.io/config: |
- name: StorageType
value: "hostpath"
- name: BasePath
value: "/var/openebs/local"
- name: FilePermissions
enabled: true
data:
mode: "0770"
provisioner: openebs.io/local
reclaimPolicy: Delete
#It is necessary to have volumeBindingMode as WaitForFirstConsumer
volumeBindingMode: WaitForFirstConsumer
```
With such configuration the folder will be crated with `0770` rights for all the PVC using this storage class.

The same configuration is available at PVC level to have a more fined grained configuration capability (overrding the Storage class configuration level):

```yaml
kind: PersistentVolumeClaim
apiVersion: v1
metadata:
name: localpv-vol
annotations:
cas.openebs.io/config: |
- name: FilePermissions
enabled: true
data:
mode: "0770"
spec:
#Change this name if you are using a custom StorageClass
storageClassName: openebs-hostpath
accessModes: ["ReadWriteOnce"]
resources:
requests:
#Set capacity here
storage: 5Gi
```

0 comments on commit 6e61d78

Please sign in to comment.