diff --git a/pkg/apis/storage/types.go b/pkg/apis/storage/types.go index 26955afde3..2a45e3557d 100644 --- a/pkg/apis/storage/types.go +++ b/pkg/apis/storage/types.go @@ -54,6 +54,11 @@ type StorageClass struct { // PersistentVolumes of this storage class are created with // +optional ReclaimPolicy *api.PersistentVolumeReclaimPolicy + + // mountOptions are the mount options that dynamically provisioned + // PersistentVolumes of this storage class are created with + // +optional + MountOptions []string } // +k8s:deepcopy-gen:interfaces=k8s.io/apimachinery/pkg/runtime.Object diff --git a/pkg/controller/volume/persistentvolume/framework_test.go b/pkg/controller/volume/persistentvolume/framework_test.go index 9c53ef3891..75cbbd7bd4 100644 --- a/pkg/controller/volume/persistentvolume/framework_test.go +++ b/pkg/controller/volume/persistentvolume/framework_test.go @@ -807,12 +807,13 @@ const operationDelete = "Delete" const operationRecycle = "Recycle" var ( - classGold string = "gold" - classSilver string = "silver" - classEmpty string = "" - classNonExisting string = "non-existing" - classExternal string = "external" - classUnknownInternal string = "unknown-internal" + classGold string = "gold" + classSilver string = "silver" + classEmpty string = "" + classNonExisting string = "non-existing" + classExternal string = "external" + classUnknownInternal string = "unknown-internal" + classUnsupportedMountOptions string = "unsupported-mountoptions" ) // wrapTestWithPluginCalls returns a testCall that: diff --git a/pkg/controller/volume/persistentvolume/provision_test.go b/pkg/controller/volume/persistentvolume/provision_test.go index ef7fa7e082..db247b080c 100644 --- a/pkg/controller/volume/persistentvolume/provision_test.go +++ b/pkg/controller/volume/persistentvolume/provision_test.go @@ -81,6 +81,18 @@ var storageClasses = []*storage.StorageClass{ Parameters: class1Parameters, ReclaimPolicy: &deleteReclaimPolicy, }, + { + TypeMeta: metav1.TypeMeta{ + Kind: "StorageClass", + }, + ObjectMeta: metav1.ObjectMeta{ + Name: "unsupported-mountoptions", + }, + Provisioner: mockPluginName, + Parameters: class1Parameters, + ReclaimPolicy: &deleteReclaimPolicy, + MountOptions: []string{"foo"}, + }, } // call to storageClass 1, returning an error @@ -392,6 +404,17 @@ func TestProvisionSync(t *testing.T) { testSyncClaim, ), }, + { + // No provisioning + warning event with unsupported storageClass.mountOptions + "11-20 - unsupported storageClass.mountOptions", + novolumes, + novolumes, + newClaimArray("claim11-20", "uid11-20", "1Gi", "", v1.ClaimPending, &classUnsupportedMountOptions), + newClaimArray("claim11-20", "uid11-20", "1Gi", "", v1.ClaimPending, &classUnsupportedMountOptions, annStorageProvisioner), + // Expect event to be prefixed with "Mount options" because saving PV will fail anyway + []string{"Warning ProvisioningFailed Mount options"}, + noerrors, wrapTestWithProvisionCalls([]provisionCall{}, testSyncClaim), + }, } runSyncTests(t, tests, storageClasses) } diff --git a/pkg/controller/volume/persistentvolume/pv_controller.go b/pkg/controller/volume/persistentvolume/pv_controller.go index 69b8b62787..0471098ce2 100644 --- a/pkg/controller/volume/persistentvolume/pv_controller.go +++ b/pkg/controller/volume/persistentvolume/pv_controller.go @@ -1320,6 +1320,7 @@ func (ctrl *PersistentVolumeController) provisionClaimOperation(claimObj interfa options := vol.VolumeOptions{ PersistentVolumeReclaimPolicy: *storageClass.ReclaimPolicy, + MountOptions: storageClass.MountOptions, CloudTags: &tags, ClusterName: ctrl.clusterName, PVName: pvName, @@ -1327,6 +1328,15 @@ func (ctrl *PersistentVolumeController) provisionClaimOperation(claimObj interfa Parameters: storageClass.Parameters, } + // Refuse to provision if the plugin doesn't support mount options, creation + // of PV would be rejected by validation anyway + if !plugin.SupportsMountOption() && len(options.MountOptions) > 0 { + strerr := fmt.Sprintf("Mount options are not supported by the provisioner but StorageClass %q has mount options %v", storageClass.Name, options.MountOptions) + glog.V(2).Infof("Mount options are not supported by the provisioner but claim %q's StorageClass %q has mount options %v", claimToClaimKey(claim), storageClass.Name, options.MountOptions) + ctrl.eventRecorder.Event(claim, v1.EventTypeWarning, events.ProvisioningFailed, strerr) + return + } + // Provision the volume provisioner, err := plugin.NewProvisioner(options) if err != nil { diff --git a/pkg/volume/aws_ebs/aws_ebs.go b/pkg/volume/aws_ebs/aws_ebs.go index 7a81f7605e..5c9f2702c0 100644 --- a/pkg/volume/aws_ebs/aws_ebs.go +++ b/pkg/volume/aws_ebs/aws_ebs.go @@ -465,6 +465,7 @@ func (c *awsElasticBlockStoreProvisioner) Provision() (*v1.PersistentVolume, err ReadOnly: false, }, }, + MountOptions: c.options.MountOptions, }, } diff --git a/pkg/volume/azure_dd/azure_provision.go b/pkg/volume/azure_dd/azure_provision.go index df27da4c58..d037f636d8 100644 --- a/pkg/volume/azure_dd/azure_provision.go +++ b/pkg/volume/azure_dd/azure_provision.go @@ -198,6 +198,7 @@ func (p *azureDiskProvisioner) Provision() (*v1.PersistentVolume, error) { FSType: &fsType, }, }, + MountOptions: p.options.MountOptions, }, } return pv, nil diff --git a/pkg/volume/azure_file/azure_provision.go b/pkg/volume/azure_file/azure_provision.go index bebec4599f..882f496d12 100644 --- a/pkg/volume/azure_file/azure_provision.go +++ b/pkg/volume/azure_file/azure_provision.go @@ -196,6 +196,7 @@ func (a *azureFileProvisioner) Provision() (*v1.PersistentVolume, error) { SecretNamespace: &secretNamespace, }, }, + MountOptions: a.options.MountOptions, }, } return pv, nil diff --git a/pkg/volume/cinder/cinder.go b/pkg/volume/cinder/cinder.go index 58bb713322..45b8d32825 100644 --- a/pkg/volume/cinder/cinder.go +++ b/pkg/volume/cinder/cinder.go @@ -499,6 +499,7 @@ func (c *cinderVolumeProvisioner) Provision() (*v1.PersistentVolume, error) { ReadOnly: false, }, }, + MountOptions: c.options.MountOptions, }, } if len(c.options.PVC.Spec.AccessModes) == 0 { diff --git a/pkg/volume/gce_pd/gce_pd.go b/pkg/volume/gce_pd/gce_pd.go index 25f7127fe8..9ddeb95787 100644 --- a/pkg/volume/gce_pd/gce_pd.go +++ b/pkg/volume/gce_pd/gce_pd.go @@ -410,6 +410,7 @@ func (c *gcePersistentDiskProvisioner) Provision() (*v1.PersistentVolume, error) FSType: fstype, }, }, + MountOptions: c.options.MountOptions, }, } if len(c.options.PVC.Spec.AccessModes) == 0 { diff --git a/pkg/volume/glusterfs/glusterfs.go b/pkg/volume/glusterfs/glusterfs.go index cb62d07470..2145fa6333 100644 --- a/pkg/volume/glusterfs/glusterfs.go +++ b/pkg/volume/glusterfs/glusterfs.go @@ -711,6 +711,7 @@ func (p *glusterfsVolumeProvisioner) Provision() (*v1.PersistentVolume, error) { if len(pv.Spec.AccessModes) == 0 { pv.Spec.AccessModes = p.plugin.GetAccessModes() } + pv.Spec.MountOptions = p.options.MountOptions gidStr := strconv.FormatInt(int64(gid), 10) diff --git a/pkg/volume/photon_pd/photon_pd.go b/pkg/volume/photon_pd/photon_pd.go index daa0470893..99965a775b 100644 --- a/pkg/volume/photon_pd/photon_pd.go +++ b/pkg/volume/photon_pd/photon_pd.go @@ -375,6 +375,7 @@ func (p *photonPersistentDiskProvisioner) Provision() (*v1.PersistentVolume, err FSType: fstype, }, }, + MountOptions: p.options.MountOptions, }, } if len(p.options.PVC.Spec.AccessModes) == 0 { diff --git a/pkg/volume/plugins.go b/pkg/volume/plugins.go index dccb99fce7..b97f54af33 100644 --- a/pkg/volume/plugins.go +++ b/pkg/volume/plugins.go @@ -50,6 +50,8 @@ type VolumeOptions struct { // Reclamation policy for a persistent volume PersistentVolumeReclaimPolicy v1.PersistentVolumeReclaimPolicy + // Mount options for a persistent volume + MountOptions []string // Suggested PV.Name of the PersistentVolume to provision. // This is a generated name guaranteed to be unique in Kubernetes cluster. // If you choose not to use it as volume name, ensure uniqueness by either diff --git a/pkg/volume/quobyte/quobyte.go b/pkg/volume/quobyte/quobyte.go index c2cff8b7fd..8e6ec1f7d1 100644 --- a/pkg/volume/quobyte/quobyte.go +++ b/pkg/volume/quobyte/quobyte.go @@ -420,6 +420,7 @@ func (provisioner *quobyteVolumeProvisioner) Provision() (*v1.PersistentVolume, pv.Spec.Capacity = v1.ResourceList{ v1.ResourceName(v1.ResourceStorage): resource.MustParse(fmt.Sprintf("%dGi", sizeGB)), } + pv.Spec.MountOptions = provisioner.options.MountOptions return pv, nil } diff --git a/pkg/volume/rbd/rbd.go b/pkg/volume/rbd/rbd.go index 4aa87d9db8..cc41c6fe0b 100644 --- a/pkg/volume/rbd/rbd.go +++ b/pkg/volume/rbd/rbd.go @@ -383,6 +383,7 @@ func (r *rbdVolumeProvisioner) Provision() (*v1.PersistentVolume, error) { pv.Spec.Capacity = v1.ResourceList{ v1.ResourceName(v1.ResourceStorage): resource.MustParse(fmt.Sprintf("%dMi", sizeMB)), } + pv.Spec.MountOptions = r.options.MountOptions return pv, nil } diff --git a/pkg/volume/vsphere_volume/vsphere_volume.go b/pkg/volume/vsphere_volume/vsphere_volume.go index 92322fa2ac..4963f179fe 100644 --- a/pkg/volume/vsphere_volume/vsphere_volume.go +++ b/pkg/volume/vsphere_volume/vsphere_volume.go @@ -379,6 +379,7 @@ func (v *vsphereVolumeProvisioner) Provision() (*v1.PersistentVolume, error) { StoragePolicyID: volSpec.StoragePolicyID, }, }, + MountOptions: v.options.MountOptions, }, } if len(v.options.PVC.Spec.AccessModes) == 0 { diff --git a/staging/src/k8s.io/api/storage/v1/types.go b/staging/src/k8s.io/api/storage/v1/types.go index 02b17795bb..e843c085a8 100644 --- a/staging/src/k8s.io/api/storage/v1/types.go +++ b/staging/src/k8s.io/api/storage/v1/types.go @@ -49,6 +49,12 @@ type StorageClass struct { // created with this reclaimPolicy. Defaults to Delete. // +optional ReclaimPolicy *v1.PersistentVolumeReclaimPolicy `json:"reclaimPolicy,omitempty" protobuf:"bytes,4,opt,name=reclaimPolicy,casttype=k8s.io/api/core/v1.PersistentVolumeReclaimPolicy"` + + // Dynamically provisioned PersistentVolumes of this storage class are + // created with these mountOptions, e.g. ["ro", "soft"]. Not validated - + // mount of the PVs will simply fail if one is invalid. + // +optional + MountOptions []string `json:"mountOptions,omitempty" protobuf:"bytes,5,opt,name=mountOptions"` } // +k8s:deepcopy-gen:interfaces=k8s.io/apimachinery/pkg/runtime.Object diff --git a/staging/src/k8s.io/api/storage/v1beta1/types.go b/staging/src/k8s.io/api/storage/v1beta1/types.go index aa828172a4..d5e2fe585e 100644 --- a/staging/src/k8s.io/api/storage/v1beta1/types.go +++ b/staging/src/k8s.io/api/storage/v1beta1/types.go @@ -49,6 +49,12 @@ type StorageClass struct { // created with this reclaimPolicy. Defaults to Delete. // +optional ReclaimPolicy *v1.PersistentVolumeReclaimPolicy `json:"reclaimPolicy,omitempty" protobuf:"bytes,4,opt,name=reclaimPolicy,casttype=k8s.io/api/core/v1.PersistentVolumeReclaimPolicy"` + + // Dynamically provisioned PersistentVolumes of this storage class are + // created with these mountOptions, e.g. ["ro", "soft"]. Not validated - + // mount of the PVs will simply fail if one is invalid. + // +optional + MountOptions []string `json:"mountOptions,omitempty" protobuf:"bytes,5,opt,name=mountOptions"` } // +k8s:deepcopy-gen:interfaces=k8s.io/apimachinery/pkg/runtime.Object