From 36f30bc68953a61beb18e5bdb41942e6b0993b9f Mon Sep 17 00:00:00 2001 From: Scott Creeley Date: Wed, 9 Aug 2017 13:51:46 -0400 Subject: [PATCH] Add VolumeType api to PV and PVC --- hack/import-restrictions.yaml | 2 + pkg/api/persistentvolume/util.go | 10 + pkg/api/persistentvolume/util_test.go | 61 ++ pkg/api/persistentvolumeclaim/BUILD | 37 ++ pkg/api/persistentvolumeclaim/OWNERS | 4 + pkg/api/persistentvolumeclaim/util.go | 31 + pkg/api/persistentvolumeclaim/util_test.go | 71 +++ pkg/api/pod/util.go | 16 + pkg/api/pod/util_test.go | 84 +++ pkg/apis/core/types.go | 32 + pkg/apis/core/v1/defaults.go | 10 + pkg/apis/core/v1/defaults_test.go | 54 ++ pkg/apis/core/validation/validation.go | 204 +++++- pkg/apis/core/validation/validation_test.go | 592 +++++++++++++++++- pkg/apis/settings/validation/validation.go | 4 +- pkg/features/kube_features.go | 7 + .../core/persistentvolume/strategy.go | 6 + .../core/persistentvolumeclaim/strategy.go | 10 +- staging/src/k8s.io/api/core/v1/types.go | 34 + 19 files changed, 1215 insertions(+), 54 deletions(-) create mode 100644 pkg/api/persistentvolumeclaim/BUILD create mode 100755 pkg/api/persistentvolumeclaim/OWNERS create mode 100644 pkg/api/persistentvolumeclaim/util.go create mode 100644 pkg/api/persistentvolumeclaim/util_test.go diff --git a/hack/import-restrictions.yaml b/hack/import-restrictions.yaml index 1b740a293cf..bd4479bd455 100644 --- a/hack/import-restrictions.yaml +++ b/hack/import-restrictions.yaml @@ -1,7 +1,9 @@ - baseImportPath: "./pkg/apis/core/" allowedImports: - k8s.io/apimachinery + - k8s.io/apiserver/pkg/util/feature - k8s.io/kubernetes/pkg/apis/core + - k8s.io/kubernetes/pkg/features - k8s.io/kubernetes/pkg/util - k8s.io/api/core/v1 diff --git a/pkg/api/persistentvolume/util.go b/pkg/api/persistentvolume/util.go index 6f651961338..ede89525f58 100644 --- a/pkg/api/persistentvolume/util.go +++ b/pkg/api/persistentvolume/util.go @@ -17,7 +17,9 @@ limitations under the License. package persistentvolume import ( + utilfeature "k8s.io/apiserver/pkg/util/feature" api "k8s.io/kubernetes/pkg/apis/core" + "k8s.io/kubernetes/pkg/features" ) func getClaimRefNamespace(pv *api.PersistentVolume) string { @@ -96,3 +98,11 @@ func VisitPVSecretNames(pv *api.PersistentVolume, visitor Visitor) bool { } return true } + +// DropDisabledAlphaFields removes disabled fields from the pv spec. +// This should be called from PrepareForCreate/PrepareForUpdate for all resources containing a pv spec. +func DropDisabledAlphaFields(pvSpec *api.PersistentVolumeSpec) { + if !utilfeature.DefaultFeatureGate.Enabled(features.BlockVolume) { + pvSpec.VolumeMode = nil + } +} diff --git a/pkg/api/persistentvolume/util_test.go b/pkg/api/persistentvolume/util_test.go index 974a6dafa70..bd536e3cea0 100644 --- a/pkg/api/persistentvolume/util_test.go +++ b/pkg/api/persistentvolume/util_test.go @@ -24,7 +24,9 @@ import ( "k8s.io/apimachinery/pkg/util/sets" "k8s.io/apimachinery/pkg/util/validation/field" + utilfeature "k8s.io/apiserver/pkg/util/feature" api "k8s.io/kubernetes/pkg/apis/core" + "k8s.io/kubernetes/pkg/features" ) func TestPVSecrets(t *testing.T) { @@ -204,3 +206,62 @@ func collectSecretPaths(t *testing.T, path *field.Path, name string, tp reflect. return secretPaths } + +func newHostPathType(pathType string) *api.HostPathType { + hostPathType := new(api.HostPathType) + *hostPathType = api.HostPathType(pathType) + return hostPathType +} + +func TestDropAlphaPVVolumeMode(t *testing.T) { + vmode := api.PersistentVolumeFilesystem + + // PersistentVolume with VolumeMode set + pv := api.PersistentVolume{ + Spec: api.PersistentVolumeSpec{ + AccessModes: []api.PersistentVolumeAccessMode{api.ReadWriteOnce}, + PersistentVolumeSource: api.PersistentVolumeSource{ + HostPath: &api.HostPathVolumeSource{ + Path: "/foo", + Type: newHostPathType(string(api.HostPathDirectory)), + }, + }, + StorageClassName: "test-storage-class", + VolumeMode: &vmode, + }, + } + + // Enable alpha feature BlockVolume + err1 := utilfeature.DefaultFeatureGate.Set("BlockVolume=true") + if err1 != nil { + t.Fatalf("Failed to enable feature gate for BlockVolume: %v", err1) + } + + // now test dropping the fields - should not be dropped + DropDisabledAlphaFields(&pv.Spec) + + // check to make sure VolumeDevices is still present + // if featureset is set to true + if utilfeature.DefaultFeatureGate.Enabled(features.BlockVolume) { + if pv.Spec.VolumeMode == nil { + t.Error("VolumeMode in pv.Spec should not have been dropped based on feature-gate") + } + } + + // Disable alpha feature BlockVolume + err := utilfeature.DefaultFeatureGate.Set("BlockVolume=false") + if err != nil { + t.Fatalf("Failed to disable feature gate for BlockVolume: %v", err) + } + + // now test dropping the fields + DropDisabledAlphaFields(&pv.Spec) + + // check to make sure VolumeDevices is nil + // if featureset is set to false + if !utilfeature.DefaultFeatureGate.Enabled(features.BlockVolume) { + if pv.Spec.VolumeMode != nil { + t.Error("DropDisabledAlphaFields VolumeMode for pv.Spec failed") + } + } +} diff --git a/pkg/api/persistentvolumeclaim/BUILD b/pkg/api/persistentvolumeclaim/BUILD new file mode 100644 index 00000000000..26471bdb6db --- /dev/null +++ b/pkg/api/persistentvolumeclaim/BUILD @@ -0,0 +1,37 @@ +package(default_visibility = ["//visibility:public"]) + +load( + "@io_bazel_rules_go//go:def.bzl", + "go_library", + "go_test", +) + +go_library( + name = "go_default_library", + srcs = ["util.go"], + deps = ["//pkg/api:go_default_library"], +) + +filegroup( + name = "package-srcs", + srcs = glob(["**"]), + tags = ["automanaged"], + visibility = ["//visibility:private"], +) + +filegroup( + name = "all-srcs", + srcs = [":package-srcs"], + tags = ["automanaged"], +) + +go_test( + name = "go_default_test", + srcs = ["util_test.go"], + library = ":go_default_library", + deps = [ + "//pkg/api:go_default_library", + "//vendor/k8s.io/apimachinery/pkg/util/sets:go_default_library", + "//vendor/k8s.io/apimachinery/pkg/util/validation/field:go_default_library", + ], +) diff --git a/pkg/api/persistentvolumeclaim/OWNERS b/pkg/api/persistentvolumeclaim/OWNERS new file mode 100755 index 00000000000..8575aa9767a --- /dev/null +++ b/pkg/api/persistentvolumeclaim/OWNERS @@ -0,0 +1,4 @@ +reviewers: +- smarterclayton +- jsafrane +- david-mcmahon diff --git a/pkg/api/persistentvolumeclaim/util.go b/pkg/api/persistentvolumeclaim/util.go new file mode 100644 index 00000000000..a6321d3404f --- /dev/null +++ b/pkg/api/persistentvolumeclaim/util.go @@ -0,0 +1,31 @@ +/* +Copyright 2017 The Kubernetes Authors. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package persistentvolumeclaim + +import ( + utilfeature "k8s.io/apiserver/pkg/util/feature" + "k8s.io/kubernetes/pkg/apis/core" + "k8s.io/kubernetes/pkg/features" +) + +// DropDisabledAlphaFields removes disabled fields from the pvc spec. +// This should be called from PrepareForCreate/PrepareForUpdate for all resources containing a pvc spec. +func DropDisabledAlphaFields(pvcSpec *core.PersistentVolumeClaimSpec) { + if !utilfeature.DefaultFeatureGate.Enabled(features.BlockVolume) { + pvcSpec.VolumeMode = nil + } +} diff --git a/pkg/api/persistentvolumeclaim/util_test.go b/pkg/api/persistentvolumeclaim/util_test.go new file mode 100644 index 00000000000..e12acb3a9e7 --- /dev/null +++ b/pkg/api/persistentvolumeclaim/util_test.go @@ -0,0 +1,71 @@ +/* +Copyright 2017 The Kubernetes Authors. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package persistentvolumeclaim + +import ( + "testing" + + utilfeature "k8s.io/apiserver/pkg/util/feature" + "k8s.io/kubernetes/pkg/apis/core" + "k8s.io/kubernetes/pkg/features" +) + +func TestDropAlphaPVCVolumeMode(t *testing.T) { + vmode := core.PersistentVolumeFilesystem + + // PersistentVolume with VolumeMode set + pvc := core.PersistentVolumeClaim{ + Spec: core.PersistentVolumeClaimSpec{ + AccessModes: []core.PersistentVolumeAccessMode{core.ReadWriteOnce}, + VolumeMode: &vmode, + }, + } + + // Enable alpha feature BlockVolume + err1 := utilfeature.DefaultFeatureGate.Set("BlockVolume=true") + if err1 != nil { + t.Fatalf("Failed to enable feature gate for BlockVolume: %v", err1) + } + + // now test dropping the fields - should not be dropped + DropDisabledAlphaFields(&pvc.Spec) + + // check to make sure VolumeDevices is still present + // if featureset is set to true + if utilfeature.DefaultFeatureGate.Enabled(features.BlockVolume) { + if pvc.Spec.VolumeMode == nil { + t.Error("VolumeMode in pvc.Spec should not have been dropped based on feature-gate") + } + } + + // Disable alpha feature BlockVolume + err := utilfeature.DefaultFeatureGate.Set("BlockVolume=false") + if err != nil { + t.Fatalf("Failed to disable feature gate for BlockVolume: %v", err) + } + + // now test dropping the fields + DropDisabledAlphaFields(&pvc.Spec) + + // check to make sure VolumeDevices is nil + // if featureset is set to false + if !utilfeature.DefaultFeatureGate.Enabled(features.BlockVolume) { + if pvc.Spec.VolumeMode != nil { + t.Error("DropDisabledAlphaFields VolumeMode for pvc.Spec failed") + } + } +} diff --git a/pkg/api/pod/util.go b/pkg/api/pod/util.go index 3b0f61c9038..3cb7d69f399 100644 --- a/pkg/api/pod/util.go +++ b/pkg/api/pod/util.go @@ -243,12 +243,15 @@ func DropDisabledAlphaFields(podSpec *api.PodSpec) { } } } + for i := range podSpec.Containers { DropDisabledVolumeMountsAlphaFields(podSpec.Containers[i].VolumeMounts) } for i := range podSpec.InitContainers { DropDisabledVolumeMountsAlphaFields(podSpec.InitContainers[i].VolumeMounts) } + + DropDisabledVolumeDevicesAlphaFields(podSpec) } // DropDisabledVolumeMountsAlphaFields removes disabled fields from []VolumeMount. @@ -260,3 +263,16 @@ func DropDisabledVolumeMountsAlphaFields(volumeMounts []api.VolumeMount) { } } } + +// DropDisabledVolumeDevicesAlphaFields removes disabled fields from []VolumeDevice. +// This should be called from PrepareForCreate/PrepareForUpdate for all resources containing a VolumeDevice +func DropDisabledVolumeDevicesAlphaFields(podSpec *api.PodSpec) { + if !utilfeature.DefaultFeatureGate.Enabled(features.BlockVolume) { + for i := range podSpec.Containers { + podSpec.Containers[i].VolumeDevices = nil + } + for i := range podSpec.InitContainers { + podSpec.InitContainers[i].VolumeDevices = nil + } + } +} diff --git a/pkg/api/pod/util_test.go b/pkg/api/pod/util_test.go index 4e87a1189a2..1c7eb328fca 100644 --- a/pkg/api/pod/util_test.go +++ b/pkg/api/pod/util_test.go @@ -24,7 +24,9 @@ import ( "k8s.io/apimachinery/pkg/util/sets" "k8s.io/apimachinery/pkg/util/validation/field" + utilfeature "k8s.io/apiserver/pkg/util/feature" api "k8s.io/kubernetes/pkg/apis/core" + "k8s.io/kubernetes/pkg/features" ) func TestPodSecrets(t *testing.T) { @@ -254,3 +256,85 @@ func TestPodConfigmaps(t *testing.T) { t.Error("Extra names extracted. Verify VisitPodConfigmapNames() is correctly extracting resource names") } } + +func TestDropAlphaVolumeDevices(t *testing.T) { + testPod := api.Pod{ + Spec: api.PodSpec{ + RestartPolicy: api.RestartPolicyNever, + Containers: []api.Container{ + { + Name: "container1", + Image: "testimage", + VolumeDevices: []api.VolumeDevice{ + { + Name: "myvolume", + DevicePath: "/usr/test", + }, + }, + }, + }, + InitContainers: []api.Container{ + { + Name: "container1", + Image: "testimage", + VolumeDevices: []api.VolumeDevice{ + { + Name: "myvolume", + DevicePath: "/usr/test", + }, + }, + }, + }, + Volumes: []api.Volume{ + { + Name: "myvolume", + VolumeSource: api.VolumeSource{ + HostPath: &api.HostPathVolumeSource{ + Path: "/dev/xvdc", + }, + }, + }, + }, + }, + } + + // Enable alpha feature BlockVolume + err1 := utilfeature.DefaultFeatureGate.Set("BlockVolume=true") + if err1 != nil { + t.Fatalf("Failed to enable feature gate for BlockVolume: %v", err1) + } + + // now test dropping the fields - should not be dropped + DropDisabledAlphaFields(&testPod.Spec) + + // check to make sure VolumeDevices is still present + // if featureset is set to true + if utilfeature.DefaultFeatureGate.Enabled(features.BlockVolume) { + if testPod.Spec.Containers[0].VolumeDevices == nil { + t.Error("VolumeDevices in Container should not have been dropped based on feature-gate") + } + if testPod.Spec.InitContainers[0].VolumeDevices == nil { + t.Error("VolumeDevices in Container should not have been dropped based on feature-gate") + } + } + + // Disable alpha feature BlockVolume + err := utilfeature.DefaultFeatureGate.Set("BlockVolume=false") + if err != nil { + t.Fatalf("Failed to disable feature gate for BlockVolume: %v", err) + } + + // now test dropping the fields + DropDisabledAlphaFields(&testPod.Spec) + + // check to make sure VolumeDevices is nil + // if featureset is set to false + if !utilfeature.DefaultFeatureGate.Enabled(features.BlockVolume) { + if testPod.Spec.Containers[0].VolumeDevices != nil { + t.Error("DropDisabledAlphaFields for Containers failed") + } + if testPod.Spec.InitContainers[0].VolumeDevices != nil { + t.Error("DropDisabledAlphaFields for InitContainers failed") + } + } +} diff --git a/pkg/apis/core/types.go b/pkg/apis/core/types.go index 289426cc3ae..2fe5e50a03b 100644 --- a/pkg/apis/core/types.go +++ b/pkg/apis/core/types.go @@ -462,6 +462,11 @@ type PersistentVolumeSpec struct { // simply fail if one is invalid. // +optional MountOptions []string + // volumeMode defines if a volume is intended to be used with a formatted filesystem + // or to remain in raw block state. Value of Filesystem is implied when not included in spec. + // This is an alpha feature and may change in the future. + // +optional + VolumeMode *PersistentVolumeMode } // PersistentVolumeReclaimPolicy describes a policy for end-of-life maintenance of persistent volumes @@ -479,6 +484,16 @@ const ( PersistentVolumeReclaimRetain PersistentVolumeReclaimPolicy = "Retain" ) +// PersistentVolumeMode describes how a volume is intended to be consumed, either Block or Filesystem. +type PersistentVolumeMode string + +const ( + // PersistentVolumeBlock means the volume will not be formatted with a filesystem and will remain a raw block device. + PersistentVolumeBlock PersistentVolumeMode = "Block" + // PersistentVolumeFilesystem means the volume will be or is formatted with a filesystem. + PersistentVolumeFilesystem PersistentVolumeMode = "Filesystem" +) + type PersistentVolumeStatus struct { // Phase indicates if a volume is available, bound to a claim, or released by a claim // +optional @@ -548,6 +563,11 @@ type PersistentVolumeClaimSpec struct { // More info: https://kubernetes.io/docs/concepts/storage/persistent-volumes/#class-1 // +optional StorageClassName *string + // volumeMode defines what type of volume is required by the claim. + // Value of Filesystem is implied when not included in claim spec. + // This is an alpha feature and may change in the future. + // +optional + VolumeMode *PersistentVolumeMode } type PersistentVolumeClaimConditionType string @@ -1586,6 +1606,14 @@ const ( MountPropagationBidirectional MountPropagationMode = "Bidirectional" ) +// VolumeDevice describes a mapping of a raw block device within a container. +type VolumeDevice struct { + // name must match the name of a persistentVolumeClaim in the pod + Name string + // devicePath is the path inside of the container that the device will be mapped to. + DevicePath string +} + // EnvVar represents an environment variable present in a Container. type EnvVar struct { // Required: This must be a C_IDENTIFIER. @@ -1879,6 +1907,10 @@ type Container struct { Resources ResourceRequirements // +optional VolumeMounts []VolumeMount + // volumeDevices is the list of block devices to be used by the container. + // This is an alpha feature and may change in the future. + // +optional + VolumeDevices []VolumeDevice // +optional LivenessProbe *Probe // +optional diff --git a/pkg/apis/core/v1/defaults.go b/pkg/apis/core/v1/defaults.go index d6b98368f34..236905db55b 100644 --- a/pkg/apis/core/v1/defaults.go +++ b/pkg/apis/core/v1/defaults.go @@ -20,6 +20,8 @@ import ( "k8s.io/api/core/v1" "k8s.io/apimachinery/pkg/runtime" "k8s.io/apimachinery/pkg/util/intstr" + utilfeature "k8s.io/apiserver/pkg/util/feature" + "k8s.io/kubernetes/pkg/features" "k8s.io/kubernetes/pkg/util/parsers" utilpointer "k8s.io/kubernetes/pkg/util/pointer" ) @@ -228,11 +230,19 @@ func SetDefaults_PersistentVolume(obj *v1.PersistentVolume) { if obj.Spec.PersistentVolumeReclaimPolicy == "" { obj.Spec.PersistentVolumeReclaimPolicy = v1.PersistentVolumeReclaimRetain } + if obj.Spec.VolumeMode == nil && utilfeature.DefaultFeatureGate.Enabled(features.BlockVolume) { + obj.Spec.VolumeMode = new(v1.PersistentVolumeMode) + *obj.Spec.VolumeMode = v1.PersistentVolumeFilesystem + } } func SetDefaults_PersistentVolumeClaim(obj *v1.PersistentVolumeClaim) { if obj.Status.Phase == "" { obj.Status.Phase = v1.ClaimPending } + if obj.Spec.VolumeMode == nil && utilfeature.DefaultFeatureGate.Enabled(features.BlockVolume) { + obj.Spec.VolumeMode = new(v1.PersistentVolumeMode) + *obj.Spec.VolumeMode = v1.PersistentVolumeFilesystem + } } func SetDefaults_ISCSIVolumeSource(obj *v1.ISCSIVolumeSource) { if obj.ISCSIInterface == "" { diff --git a/pkg/apis/core/v1/defaults_test.go b/pkg/apis/core/v1/defaults_test.go index 89984dc1142..9a071f38ae9 100644 --- a/pkg/apis/core/v1/defaults_test.go +++ b/pkg/apis/core/v1/defaults_test.go @@ -26,6 +26,7 @@ import ( metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/apimachinery/pkg/runtime" "k8s.io/apimachinery/pkg/util/intstr" + utilfeature "k8s.io/apiserver/pkg/util/feature" "k8s.io/kubernetes/pkg/api/legacyscheme" corev1 "k8s.io/kubernetes/pkg/apis/core/v1" @@ -817,6 +818,33 @@ func TestSetDefaultPersistentVolume(t *testing.T) { if pv2.Spec.PersistentVolumeReclaimPolicy != v1.PersistentVolumeReclaimRetain { t.Errorf("Expected pv reclaim policy %v, got %v", v1.PersistentVolumeReclaimRetain, pv2.Spec.PersistentVolumeReclaimPolicy) } + + // When feature gate is disabled, field should not be defaulted + defaultMode := v1.PersistentVolumeFilesystem + outputMode := pv2.Spec.VolumeMode + if outputMode != nil { + t.Errorf("Expected VolumeMode to not be defaulted, got: %+v", outputMode) + } + + // When feature gate is enabled, field should be defaulted + err := utilfeature.DefaultFeatureGate.Set("BlockVolume=true") + if err != nil { + t.Fatalf("Failed to enable feature gate for BlockVolume: %v", err) + } + obj3 := roundTrip(t, runtime.Object(pv)).(*v1.PersistentVolume) + outputMode3 := obj3.Spec.VolumeMode + + if outputMode3 == nil { + t.Errorf("Expected VolumeMode to be defaulted to: %+v, got: nil", defaultMode) + } else if *outputMode3 != defaultMode { + t.Errorf("Expected VolumeMode to be defaulted to: %+v, got: %+v", defaultMode, outputMode3) + } + + err = utilfeature.DefaultFeatureGate.Set("BlockVolume=false") + if err != nil { + t.Fatalf("Failed to disable feature gate for BlockVolume: %v", err) + } + } func TestSetDefaultPersistentVolumeClaim(t *testing.T) { @@ -827,6 +855,32 @@ func TestSetDefaultPersistentVolumeClaim(t *testing.T) { if pvc2.Status.Phase != v1.ClaimPending { t.Errorf("Expected claim phase %v, got %v", v1.ClaimPending, pvc2.Status.Phase) } + + // When feature gate is disabled, field should not be defaulted + defaultMode := v1.PersistentVolumeFilesystem + outputMode := pvc2.Spec.VolumeMode + if outputMode != nil { + t.Errorf("Expected VolumeMode to not be defaulted, got: %+v", outputMode) + } + + // When feature gate is enabled, field should be defaulted + err := utilfeature.DefaultFeatureGate.Set("BlockVolume=true") + if err != nil { + t.Fatalf("Failed to enable feature gate for BlockVolume: %v", err) + } + obj3 := roundTrip(t, runtime.Object(pvc)).(*v1.PersistentVolumeClaim) + outputMode3 := obj3.Spec.VolumeMode + + if outputMode3 == nil { + t.Errorf("Expected VolumeMode to be defaulted to: %+v, got: nil", defaultMode) + } else if *outputMode3 != defaultMode { + t.Errorf("Expected VolumeMode to be defaulted to: %+v, got: %+v", defaultMode, outputMode3) + } + + err = utilfeature.DefaultFeatureGate.Set("BlockVolume=false") + if err != nil { + t.Fatalf("Failed to disable feature gate for BlockVolume: %v", err) + } } func TestSetDefaulEndpointsProtocol(t *testing.T) { diff --git a/pkg/apis/core/validation/validation.go b/pkg/apis/core/validation/validation.go index 846f2d62de1..c67f2c49066 100644 --- a/pkg/apis/core/validation/validation.go +++ b/pkg/apis/core/validation/validation.go @@ -64,7 +64,7 @@ const isNotIntegerErrorMsg string = `must be an integer` const isNotPositiveErrorMsg string = `must be greater than zero` var pdPartitionErrorMsg string = validation.InclusiveRangeError(1, 255) -var volumeModeErrorMsg string = "must be a number between 0 and 0777 (octal), both inclusive" +var fileModeErrorMsg string = "must be a number between 0 and 0777 (octal), both inclusive" // BannedOwners is a black list of object that are not allowed to be owners. var BannedOwners = apimachineryvalidation.BannedOwners @@ -364,10 +364,11 @@ func ValidateNoNewFinalizers(newFinalizers []string, oldFinalizers []string, fld return apimachineryvalidation.ValidateNoNewFinalizers(newFinalizers, oldFinalizers, fldPath) } -func ValidateVolumes(volumes []core.Volume, fldPath *field.Path) (sets.String, field.ErrorList) { +func ValidateVolumes(volumes []core.Volume, fldPath *field.Path) (map[string]core.VolumeSource, field.ErrorList) { allErrs := field.ErrorList{} allNames := sets.String{} + vols := make(map[string]core.VolumeSource) for i, vol := range volumes { idxPath := fldPath.Index(i) namePath := idxPath.Child("name") @@ -382,12 +383,69 @@ func ValidateVolumes(volumes []core.Volume, fldPath *field.Path) (sets.String, f } if len(el) == 0 { allNames.Insert(vol.Name) + vols[vol.Name] = vol.VolumeSource } else { allErrs = append(allErrs, el...) } } - return allNames, allErrs + return vols, allErrs +} + +func IsMatchedVolume(name string, volumes map[string]core.VolumeSource) bool { + if _, ok := volumes[name]; ok { + return true + } else { + return false + } +} + +func isMatchedDevice(name string, volumes map[string]core.VolumeSource) (bool, bool) { + if source, ok := volumes[name]; ok { + if source.PersistentVolumeClaim != nil { + return true, true + } else { + return true, false + } + } else { + return false, false + } +} + +func mountNameAlreadyExists(name string, devices map[string]string) bool { + if _, ok := devices[name]; ok { + return true + } else { + return false + } +} + +func mountPathAlreadyExists(mountPath string, devices map[string]string) bool { + for _, devPath := range devices { + if mountPath == devPath { + return true + } + } + + return false +} + +func deviceNameAlreadyExists(name string, mounts map[string]string) bool { + if _, ok := mounts[name]; ok { + return true + } else { + return false + } +} + +func devicePathAlreadyExists(devicePath string, mounts map[string]string) bool { + for _, mountPath := range mounts { + if mountPath == devicePath { + return true + } + } + + return false } func validateVolumeSource(source *core.VolumeSource, fldPath *field.Path, volName string) field.ErrorList { @@ -745,7 +803,7 @@ func validateSecretVolumeSource(secretSource *core.SecretVolumeSource, fldPath * secretMode := secretSource.DefaultMode if secretMode != nil && (*secretMode > 0777 || *secretMode < 0) { - allErrs = append(allErrs, field.Invalid(fldPath.Child("defaultMode"), *secretMode, volumeModeErrorMsg)) + allErrs = append(allErrs, field.Invalid(fldPath.Child("defaultMode"), *secretMode, fileModeErrorMsg)) } itemsPath := fldPath.Child("items") @@ -764,7 +822,7 @@ func validateConfigMapVolumeSource(configMapSource *core.ConfigMapVolumeSource, configMapMode := configMapSource.DefaultMode if configMapMode != nil && (*configMapMode > 0777 || *configMapMode < 0) { - allErrs = append(allErrs, field.Invalid(fldPath.Child("defaultMode"), *configMapMode, volumeModeErrorMsg)) + allErrs = append(allErrs, field.Invalid(fldPath.Child("defaultMode"), *configMapMode, fileModeErrorMsg)) } itemsPath := fldPath.Child("items") @@ -785,7 +843,7 @@ func validateKeyToPath(kp *core.KeyToPath, fldPath *field.Path) field.ErrorList } allErrs = append(allErrs, validateLocalNonReservedPath(kp.Path, fldPath.Child("path"))...) if kp.Mode != nil && (*kp.Mode > 0777 || *kp.Mode < 0) { - allErrs = append(allErrs, field.Invalid(fldPath.Child("mode"), *kp.Mode, volumeModeErrorMsg)) + allErrs = append(allErrs, field.Invalid(fldPath.Child("mode"), *kp.Mode, fileModeErrorMsg)) } return allErrs @@ -882,7 +940,7 @@ func validateDownwardAPIVolumeFile(file *core.DownwardAPIVolumeFile, fldPath *fi allErrs = append(allErrs, field.Required(fldPath, "one of fieldRef and resourceFieldRef is required")) } if file.Mode != nil && (*file.Mode > 0777 || *file.Mode < 0) { - allErrs = append(allErrs, field.Invalid(fldPath.Child("mode"), *file.Mode, volumeModeErrorMsg)) + allErrs = append(allErrs, field.Invalid(fldPath.Child("mode"), *file.Mode, fileModeErrorMsg)) } return allErrs @@ -893,7 +951,7 @@ func validateDownwardAPIVolumeSource(downwardAPIVolume *core.DownwardAPIVolumeSo downwardAPIMode := downwardAPIVolume.DefaultMode if downwardAPIMode != nil && (*downwardAPIMode > 0777 || *downwardAPIMode < 0) { - allErrs = append(allErrs, field.Invalid(fldPath.Child("defaultMode"), *downwardAPIMode, volumeModeErrorMsg)) + allErrs = append(allErrs, field.Invalid(fldPath.Child("defaultMode"), *downwardAPIMode, fileModeErrorMsg)) } for _, file := range downwardAPIVolume.Items { @@ -983,7 +1041,7 @@ func validateProjectedVolumeSource(projection *core.ProjectedVolumeSource, fldPa projectionMode := projection.DefaultMode if projectionMode != nil && (*projectionMode > 0777 || *projectionMode < 0) { - allErrs = append(allErrs, field.Invalid(fldPath.Child("defaultMode"), *projectionMode, volumeModeErrorMsg)) + allErrs = append(allErrs, field.Invalid(fldPath.Child("defaultMode"), *projectionMode, fileModeErrorMsg)) } allErrs = append(allErrs, validateProjectionSources(projection, projectionMode, fldPath)...) @@ -1344,6 +1402,8 @@ var supportedAccessModes = sets.NewString(string(core.ReadWriteOnce), string(cor var supportedReclaimPolicy = sets.NewString(string(core.PersistentVolumeReclaimDelete), string(core.PersistentVolumeReclaimRecycle), string(core.PersistentVolumeReclaimRetain)) +var supportedVolumeModes = sets.NewString(string(core.PersistentVolumeBlock), string(core.PersistentVolumeFilesystem)) + func ValidatePersistentVolume(pv *core.PersistentVolume) field.ErrorList { metaPath := field.NewPath("metadata") allErrs := ValidateObjectMeta(&pv.ObjectMeta, false, ValidatePersistentVolumeName, metaPath) @@ -1582,7 +1642,11 @@ func ValidatePersistentVolume(pv *core.PersistentVolume) field.ErrorList { allErrs = append(allErrs, field.Invalid(specPath.Child("storageClassName"), pv.Spec.StorageClassName, msg)) } } - + if pv.Spec.VolumeMode != nil && !utilfeature.DefaultFeatureGate.Enabled(features.BlockVolume) { + allErrs = append(allErrs, field.Forbidden(specPath.Child("volumeMode"), "PersistentVolume volumeMode is disabled by feature-gate")) + } else if pv.Spec.VolumeMode != nil && !supportedVolumeModes.Has(string(*pv.Spec.VolumeMode)) { + allErrs = append(allErrs, field.NotSupported(specPath.Child("volumeMode"), *pv.Spec.VolumeMode, supportedVolumeModes.List())) + } return allErrs } @@ -1598,6 +1662,11 @@ func ValidatePersistentVolumeUpdate(newPv, oldPv *core.PersistentVolume) field.E } newPv.Status = oldPv.Status + + if utilfeature.DefaultFeatureGate.Enabled(features.BlockVolume) { + allErrs = append(allErrs, ValidateImmutableField(newPv.Spec.VolumeMode, oldPv.Spec.VolumeMode, field.NewPath("volumeMode"))...) + } + return allErrs } @@ -1646,6 +1715,11 @@ func ValidatePersistentVolumeClaimSpec(spec *core.PersistentVolumeClaimSpec, fld allErrs = append(allErrs, field.Invalid(fldPath.Child("storageClassName"), *spec.StorageClassName, msg)) } } + if spec.VolumeMode != nil && !utilfeature.DefaultFeatureGate.Enabled(features.BlockVolume) { + allErrs = append(allErrs, field.Forbidden(fldPath.Child("volumeMode"), "PersistentVolumeClaim volumeMode is disabled by feature-gate")) + } else if spec.VolumeMode != nil && !supportedVolumeModes.Has(string(*spec.VolumeMode)) { + allErrs = append(allErrs, field.NotSupported(fldPath.Child("volumeMode"), *spec.VolumeMode, supportedVolumeModes.List())) + } return allErrs } @@ -1692,6 +1766,10 @@ func ValidatePersistentVolumeClaimUpdate(newPvc, oldPvc *core.PersistentVolumeCl // TODO: remove Beta when no longer needed allErrs = append(allErrs, ValidateImmutableAnnotation(newPvc.ObjectMeta.Annotations[v1.BetaStorageClassAnnotation], oldPvc.ObjectMeta.Annotations[v1.BetaStorageClassAnnotation], v1.BetaStorageClassAnnotation, field.NewPath("metadata"))...) + if utilfeature.DefaultFeatureGate.Enabled(features.BlockVolume) { + allErrs = append(allErrs, ValidateImmutableField(newPvc.Spec.VolumeMode, oldPvc.Spec.VolumeMode, field.NewPath("volumeMode"))...) + } + newPvc.Status = oldPvc.Status return allErrs } @@ -1975,7 +2053,27 @@ func validateSecretKeySelector(s *core.SecretKeySelector, fldPath *field.Path) f return allErrs } -func ValidateVolumeMounts(mounts []core.VolumeMount, volumes sets.String, container *core.Container, fldPath *field.Path) field.ErrorList { +func GetVolumeMountMap(mounts []core.VolumeMount) map[string]string { + volmounts := make(map[string]string) + + for _, mnt := range mounts { + volmounts[mnt.Name] = mnt.MountPath + } + + return volmounts +} + +func GetVolumeDeviceMap(devices []core.VolumeDevice) map[string]string { + voldevices := make(map[string]string) + + for _, dev := range devices { + voldevices[dev.Name] = dev.DevicePath + } + + return voldevices +} + +func ValidateVolumeMounts(mounts []core.VolumeMount, voldevices map[string]string, volumes map[string]core.VolumeSource, container *core.Container, fldPath *field.Path) field.ErrorList { allErrs := field.ErrorList{} mountpoints := sets.NewString() @@ -1983,7 +2081,8 @@ func ValidateVolumeMounts(mounts []core.VolumeMount, volumes sets.String, contai idxPath := fldPath.Index(i) if len(mnt.Name) == 0 { allErrs = append(allErrs, field.Required(idxPath.Child("name"), "")) - } else if !volumes.Has(mnt.Name) { + } + if !IsMatchedVolume(mnt.Name, volumes) { allErrs = append(allErrs, field.NotFound(idxPath.Child("name"), mnt.Name)) } if len(mnt.MountPath) == 0 { @@ -1993,6 +2092,15 @@ func ValidateVolumeMounts(mounts []core.VolumeMount, volumes sets.String, contai allErrs = append(allErrs, field.Invalid(idxPath.Child("mountPath"), mnt.MountPath, "must be unique")) } mountpoints.Insert(mnt.MountPath) + + // check for overlap with VolumeDevice + if mountNameAlreadyExists(mnt.Name, voldevices) { + allErrs = append(allErrs, field.Invalid(idxPath.Child("name"), mnt.Name, "must not already exist in volumeDevices")) + } + if mountPathAlreadyExists(mnt.MountPath, voldevices) { + allErrs = append(allErrs, field.Invalid(idxPath.Child("mountPath"), mnt.MountPath, "must not already exist as a path in volumeDevices")) + } + if len(mnt.SubPath) > 0 { allErrs = append(allErrs, validateLocalDescendingPath(mnt.SubPath, fldPath.Child("subPath"))...) } @@ -2004,6 +2112,60 @@ func ValidateVolumeMounts(mounts []core.VolumeMount, volumes sets.String, contai return allErrs } +func ValidateVolumeDevices(devices []core.VolumeDevice, volmounts map[string]string, volumes map[string]core.VolumeSource, fldPath *field.Path) field.ErrorList { + allErrs := field.ErrorList{} + devicepath := sets.NewString() + devicename := sets.NewString() + + if devices != nil && !utilfeature.DefaultFeatureGate.Enabled(features.BlockVolume) { + allErrs = append(allErrs, field.Forbidden(fldPath.Child("volumeDevices"), "Container volumeDevices is disabled by feature-gate")) + return allErrs + } + if devices != nil { + for i, dev := range devices { + idxPath := fldPath.Index(i) + devName := dev.Name + devPath := dev.DevicePath + didMatch, isPVC := isMatchedDevice(devName, volumes) + if len(devName) == 0 { + allErrs = append(allErrs, field.Required(idxPath.Child("name"), "")) + } + if devicename.Has(devName) { + allErrs = append(allErrs, field.Invalid(idxPath.Child("name"), devName, "must be unique")) + } + // Must be PersistentVolumeClaim volume source + if didMatch && !isPVC { + allErrs = append(allErrs, field.Invalid(idxPath.Child("name"), devName, "can only use volume source type of PersistentVolumeClaim for block mode")) + } + if !didMatch { + allErrs = append(allErrs, field.NotFound(idxPath.Child("name"), devName)) + } + if len(devPath) == 0 { + allErrs = append(allErrs, field.Required(idxPath.Child("devicePath"), "")) + } + if devicepath.Has(devPath) { + allErrs = append(allErrs, field.Invalid(idxPath.Child("devicePath"), devPath, "must be unique")) + } + if len(devPath) > 0 && len(validatePathNoBacksteps(devPath, fldPath.Child("devicePath"))) > 0 { + allErrs = append(allErrs, field.Invalid(idxPath.Child("devicePath"), devPath, "can not contain backsteps ('..')")) + } else { + devicepath.Insert(devPath) + } + // check for overlap with VolumeMount + if deviceNameAlreadyExists(devName, volmounts) { + allErrs = append(allErrs, field.Invalid(idxPath.Child("name"), devName, "must not already exist in volumeMounts")) + } + if devicePathAlreadyExists(devPath, volmounts) { + allErrs = append(allErrs, field.Invalid(idxPath.Child("devicePath"), devPath, "must not already exist as a path in volumeMounts")) + } + if len(devName) > 0 { + devicename.Insert(devName) + } + } + } + return allErrs +} + func validateProbe(probe *core.Probe, fldPath *field.Path) field.ErrorList { allErrs := field.ErrorList{} @@ -2187,10 +2349,10 @@ func validatePullPolicy(policy core.PullPolicy, fldPath *field.Path) field.Error return allErrors } -func validateInitContainers(containers, otherContainers []core.Container, volumes sets.String, fldPath *field.Path) field.ErrorList { +func validateInitContainers(containers, otherContainers []core.Container, deviceVolumes map[string]core.VolumeSource, fldPath *field.Path) field.ErrorList { var allErrs field.ErrorList if len(containers) > 0 { - allErrs = append(allErrs, validateContainers(containers, volumes, fldPath)...) + allErrs = append(allErrs, validateContainers(containers, deviceVolumes, fldPath)...) } allNames := sets.String{} @@ -2218,7 +2380,7 @@ func validateInitContainers(containers, otherContainers []core.Container, volume return allErrs } -func validateContainers(containers []core.Container, volumes sets.String, fldPath *field.Path) field.ErrorList { +func validateContainers(containers []core.Container, volumes map[string]core.VolumeSource, fldPath *field.Path) field.ErrorList { allErrs := field.ErrorList{} if len(containers) == 0 { @@ -2229,6 +2391,9 @@ func validateContainers(containers []core.Container, volumes sets.String, fldPat for i, ctr := range containers { idxPath := fldPath.Index(i) namePath := idxPath.Child("name") + volMounts := GetVolumeMountMap(ctr.VolumeMounts) + volDevices := GetVolumeDeviceMap(ctr.VolumeDevices) + if len(ctr.Name) == 0 { allErrs = append(allErrs, field.Required(namePath, "")) } else { @@ -2266,7 +2431,8 @@ func validateContainers(containers []core.Container, volumes sets.String, fldPat allErrs = append(allErrs, validateContainerPorts(ctr.Ports, idxPath.Child("ports"))...) allErrs = append(allErrs, ValidateEnv(ctr.Env, idxPath.Child("env"))...) allErrs = append(allErrs, ValidateEnvFrom(ctr.EnvFrom, idxPath.Child("envFrom"))...) - allErrs = append(allErrs, ValidateVolumeMounts(ctr.VolumeMounts, volumes, &ctr, idxPath.Child("volumeMounts"))...) + allErrs = append(allErrs, ValidateVolumeMounts(ctr.VolumeMounts, volDevices, volumes, &ctr, idxPath.Child("volumeMounts"))...) + allErrs = append(allErrs, ValidateVolumeDevices(ctr.VolumeDevices, volMounts, volumes, idxPath.Child("volumeDevices"))...) allErrs = append(allErrs, validatePullPolicy(ctr.ImagePullPolicy, idxPath.Child("imagePullPolicy"))...) allErrs = append(allErrs, ValidateResourceRequirements(&ctr.Resources, idxPath.Child("resources"))...) allErrs = append(allErrs, ValidateSecurityContext(ctr.SecurityContext, idxPath.Child("securityContext"))...) @@ -2546,10 +2712,10 @@ func ValidatePod(pod *core.Pod) field.ErrorList { func ValidatePodSpec(spec *core.PodSpec, fldPath *field.Path) field.ErrorList { allErrs := field.ErrorList{} - allVolumes, vErrs := ValidateVolumes(spec.Volumes, fldPath.Child("volumes")) + vols, vErrs := ValidateVolumes(spec.Volumes, fldPath.Child("volumes")) allErrs = append(allErrs, vErrs...) - allErrs = append(allErrs, validateContainers(spec.Containers, allVolumes, fldPath.Child("containers"))...) - allErrs = append(allErrs, validateInitContainers(spec.InitContainers, spec.Containers, allVolumes, fldPath.Child("initContainers"))...) + allErrs = append(allErrs, validateContainers(spec.Containers, vols, fldPath.Child("containers"))...) + allErrs = append(allErrs, validateInitContainers(spec.InitContainers, spec.Containers, vols, fldPath.Child("initContainers"))...) allErrs = append(allErrs, validateRestartPolicy(&spec.RestartPolicy, fldPath.Child("restartPolicy"))...) allErrs = append(allErrs, validateDNSPolicy(&spec.DNSPolicy, fldPath.Child("dnsPolicy"))...) allErrs = append(allErrs, unversionedvalidation.ValidateLabels(spec.NodeSelector, fldPath.Child("nodeSelector"))...) diff --git a/pkg/apis/core/validation/validation_test.go b/pkg/apis/core/validation/validation_test.go index 4b8d1373e82..5583eb9a857 100644 --- a/pkg/apis/core/validation/validation_test.go +++ b/pkg/apis/core/validation/validation_test.go @@ -26,14 +26,12 @@ import ( "k8s.io/apimachinery/pkg/api/resource" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/apimachinery/pkg/util/intstr" - "k8s.io/apimachinery/pkg/util/sets" "k8s.io/apimachinery/pkg/util/validation" "k8s.io/apimachinery/pkg/util/validation/field" utilfeature "k8s.io/apiserver/pkg/util/feature" "k8s.io/kubernetes/pkg/api/legacyscheme" _ "k8s.io/kubernetes/pkg/api/testapi" "k8s.io/kubernetes/pkg/apis/core" - api "k8s.io/kubernetes/pkg/apis/core" "k8s.io/kubernetes/pkg/apis/core/helper" "k8s.io/kubernetes/pkg/capabilities" "k8s.io/kubernetes/pkg/security/apparmor" @@ -82,6 +80,7 @@ func testVolumeWithNodeAffinity(t *testing.T, name string, namespace string, aff } func TestValidatePersistentVolumes(t *testing.T) { + validMode := core.PersistentVolumeFilesystem scenarios := map[string]struct { isExpectedFailure bool volume *core.PersistentVolume @@ -352,6 +351,25 @@ func TestValidatePersistentVolumes(t *testing.T) { StorageClassName: "-invalid-", }), }, + // VolumeMode alpha feature disabled + // TODO: remove when no longer alpha + "alpha disabled valid volume mode": { + isExpectedFailure: true, + volume: testVolume("foo", "", core.PersistentVolumeSpec{ + Capacity: core.ResourceList{ + core.ResourceName(core.ResourceStorage): resource.MustParse("10G"), + }, + AccessModes: []core.PersistentVolumeAccessMode{core.ReadWriteOnce}, + PersistentVolumeSource: core.PersistentVolumeSource{ + HostPath: &core.HostPathVolumeSource{ + Path: "/foo", + Type: newHostPathType(string(core.HostPathDirectory)), + }, + }, + StorageClassName: "valid", + VolumeMode: &validMode, + }), + }, // LocalVolume alpha feature disabled // TODO: remove when no longer alpha "alpha disabled valid local volume": { @@ -434,38 +452,38 @@ func TestValidatePersistentVolumes(t *testing.T) { } func TestValidatePersistentVolumeSourceUpdate(t *testing.T) { - validVolume := testVolume("foo", "", api.PersistentVolumeSpec{ - Capacity: api.ResourceList{ - api.ResourceName(api.ResourceStorage): resource.MustParse("1G"), + validVolume := testVolume("foo", "", core.PersistentVolumeSpec{ + Capacity: core.ResourceList{ + core.ResourceName(core.ResourceStorage): resource.MustParse("1G"), }, - AccessModes: []api.PersistentVolumeAccessMode{api.ReadWriteOnce}, - PersistentVolumeSource: api.PersistentVolumeSource{ - HostPath: &api.HostPathVolumeSource{ + AccessModes: []core.PersistentVolumeAccessMode{core.ReadWriteOnce}, + PersistentVolumeSource: core.PersistentVolumeSource{ + HostPath: &core.HostPathVolumeSource{ Path: "/foo", - Type: newHostPathType(string(api.HostPathDirectory)), + Type: newHostPathType(string(core.HostPathDirectory)), }, }, StorageClassName: "valid", }) validPvSourceNoUpdate := validVolume.DeepCopy() invalidPvSourceUpdateType := validVolume.DeepCopy() - invalidPvSourceUpdateType.Spec.PersistentVolumeSource = api.PersistentVolumeSource{ - FlexVolume: &api.FlexVolumeSource{ + invalidPvSourceUpdateType.Spec.PersistentVolumeSource = core.PersistentVolumeSource{ + FlexVolume: &core.FlexVolumeSource{ Driver: "kubernetes.io/blue", FSType: "ext4", }, } invalidPvSourceUpdateDeep := validVolume.DeepCopy() - invalidPvSourceUpdateDeep.Spec.PersistentVolumeSource = api.PersistentVolumeSource{ - HostPath: &api.HostPathVolumeSource{ + invalidPvSourceUpdateDeep.Spec.PersistentVolumeSource = core.PersistentVolumeSource{ + HostPath: &core.HostPathVolumeSource{ Path: "/updated", - Type: newHostPathType(string(api.HostPathDirectory)), + Type: newHostPathType(string(core.HostPathDirectory)), }, } scenarios := map[string]struct { isExpectedFailure bool - oldVolume *api.PersistentVolume - newVolume *api.PersistentVolume + oldVolume *core.PersistentVolume + newVolume *core.PersistentVolume }{ "condition-no-update": { isExpectedFailure: false, @@ -720,6 +738,7 @@ func testVolumeClaimAnnotation(name string, namespace string, ann string, annval func TestValidatePersistentVolumeClaim(t *testing.T) { invalidClassName := "-invalid-" validClassName := "valid" + validMode := core.PersistentVolumeFilesystem scenarios := map[string]struct { isExpectedFailure bool claim *core.PersistentVolumeClaim @@ -888,7 +907,7 @@ func TestValidatePersistentVolumeClaim(t *testing.T) { }, Resources: core.ResourceRequirements{ Requests: core.ResourceList{ - core.ResourceName(api.ResourceStorage): resource.MustParse("0G"), + core.ResourceName(core.ResourceStorage): resource.MustParse("0G"), }, }, }), @@ -916,6 +935,32 @@ func TestValidatePersistentVolumeClaim(t *testing.T) { StorageClassName: &invalidClassName, }), }, + // VolumeMode alpha feature disabled + // TODO: remove when no longer alpha + "disabled alpha valid volume mode": { + isExpectedFailure: true, + claim: testVolumeClaim("foo", "ns", core.PersistentVolumeClaimSpec{ + Selector: &metav1.LabelSelector{ + MatchExpressions: []metav1.LabelSelectorRequirement{ + { + Key: "key2", + Operator: "Exists", + }, + }, + }, + AccessModes: []core.PersistentVolumeAccessMode{ + core.ReadWriteOnce, + core.ReadOnlyMany, + }, + Resources: core.ResourceRequirements{ + Requests: core.ResourceList{ + core.ResourceName(core.ResourceStorage): resource.MustParse("10G"), + }, + }, + StorageClassName: &validClassName, + VolumeMode: &validMode, + }), + }, } for name, scenario := range scenarios { @@ -929,7 +974,101 @@ func TestValidatePersistentVolumeClaim(t *testing.T) { } } +func TestAlphaPVVolumeModeUpdate(t *testing.T) { + block := core.PersistentVolumeBlock + file := core.PersistentVolumeFilesystem + + scenarios := map[string]struct { + isExpectedFailure bool + oldPV *core.PersistentVolume + newPV *core.PersistentVolume + enableBlock bool + }{ + "valid-update-volume-mode-block-to-block": { + isExpectedFailure: false, + oldPV: createTestVolModePV(&block), + newPV: createTestVolModePV(&block), + enableBlock: true, + }, + "valid-update-volume-mode-file-to-file": { + isExpectedFailure: false, + oldPV: createTestVolModePV(&file), + newPV: createTestVolModePV(&file), + enableBlock: true, + }, + "invalid-update-volume-mode-to-block": { + isExpectedFailure: true, + oldPV: createTestVolModePV(&file), + newPV: createTestVolModePV(&block), + enableBlock: true, + }, + "invalid-update-volume-mode-to-file": { + isExpectedFailure: true, + oldPV: createTestVolModePV(&block), + newPV: createTestVolModePV(&file), + enableBlock: true, + }, + "invalid-update-blocksupport-disabled": { + isExpectedFailure: true, + oldPV: createTestVolModePV(&block), + newPV: createTestVolModePV(&block), + enableBlock: false, + }, + "invalid-update-volume-mode-nil-to-file": { + isExpectedFailure: true, + oldPV: createTestVolModePV(nil), + newPV: createTestVolModePV(&file), + enableBlock: true, + }, + "invalid-update-volume-mode-nil-to-block": { + isExpectedFailure: true, + oldPV: createTestVolModePV(nil), + newPV: createTestVolModePV(&block), + enableBlock: true, + }, + "invalid-update-volume-mode-file-to-nil": { + isExpectedFailure: true, + oldPV: createTestVolModePV(&file), + newPV: createTestVolModePV(nil), + enableBlock: true, + }, + "invalid-update-volume-mode-block-to-nil": { + isExpectedFailure: true, + oldPV: createTestVolModePV(&block), + newPV: createTestVolModePV(nil), + enableBlock: true, + }, + "invalid-update-volume-mode-nil-to-nil": { + isExpectedFailure: false, + oldPV: createTestVolModePV(nil), + newPV: createTestVolModePV(nil), + enableBlock: true, + }, + "invalid-update-volume-mode-empty-to-mode": { + isExpectedFailure: true, + oldPV: createTestPV(), + newPV: createTestVolModePV(&block), + enableBlock: true, + }, + } + + for name, scenario := range scenarios { + // ensure we have a resource version specified for updates + toggleBlockVolumeFeature(scenario.enableBlock, t) + errs := ValidatePersistentVolumeUpdate(scenario.newPV, scenario.oldPV) + if len(errs) == 0 && scenario.isExpectedFailure { + t.Errorf("Unexpected success for scenario: %s", name) + } + if len(errs) > 0 && !scenario.isExpectedFailure { + t.Errorf("Unexpected failure for scenario: %s - %+v", name, errs) + } + } +} + func TestValidatePersistentVolumeClaimUpdate(t *testing.T) { + block := core.PersistentVolumeBlock + file := core.PersistentVolumeFilesystem + validClaim := testVolumeClaimWithStatus("foo", "ns", core.PersistentVolumeClaimSpec{ AccessModes: []core.PersistentVolumeAccessMode{ core.ReadWriteOnce, @@ -999,6 +1138,42 @@ func TestValidatePersistentVolumeClaimUpdate(t *testing.T) { }, VolumeName: "volume", }) + validClaimVolumeModeFile := testVolumeClaim("foo", "ns", core.PersistentVolumeClaimSpec{ + AccessModes: []core.PersistentVolumeAccessMode{ + core.ReadWriteOnce, + }, + VolumeMode: &file, + Resources: core.ResourceRequirements{ + Requests: core.ResourceList{ + core.ResourceName(core.ResourceStorage): resource.MustParse("10G"), + }, + }, + VolumeName: "volume", + }) + validClaimVolumeModeBlock := testVolumeClaim("foo", "ns", core.PersistentVolumeClaimSpec{ + AccessModes: []core.PersistentVolumeAccessMode{ + core.ReadWriteOnce, + }, + VolumeMode: &block, + Resources: core.ResourceRequirements{ + Requests: core.ResourceList{ + core.ResourceName(core.ResourceStorage): resource.MustParse("10G"), + }, + }, + VolumeName: "volume", + }) + invalidClaimVolumeModeNil := testVolumeClaim("foo", "ns", core.PersistentVolumeClaimSpec{ + AccessModes: []core.PersistentVolumeAccessMode{ + core.ReadWriteOnce, + }, + VolumeMode: nil, + Resources: core.ResourceRequirements{ + Requests: core.ResourceList{ + core.ResourceName(core.ResourceStorage): resource.MustParse("10G"), + }, + }, + VolumeName: "volume", + }) invalidUpdateClaimStorageClass := testVolumeClaimStorageClass("foo", "ns", "fast2", core.PersistentVolumeClaimSpec{ AccessModes: []core.PersistentVolumeAccessMode{ core.ReadOnlyMany, @@ -1080,78 +1255,168 @@ func TestValidatePersistentVolumeClaimUpdate(t *testing.T) { oldClaim *core.PersistentVolumeClaim newClaim *core.PersistentVolumeClaim enableResize bool + enableBlock bool }{ "valid-update-volumeName-only": { isExpectedFailure: false, oldClaim: validClaim, newClaim: validUpdateClaim, enableResize: false, + enableBlock: false, }, "valid-no-op-update": { isExpectedFailure: false, oldClaim: validUpdateClaim, newClaim: validUpdateClaim, enableResize: false, + enableBlock: false, }, "invalid-update-change-resources-on-bound-claim": { isExpectedFailure: true, oldClaim: validUpdateClaim, newClaim: invalidUpdateClaimResources, enableResize: false, + enableBlock: false, }, "invalid-update-change-access-modes-on-bound-claim": { isExpectedFailure: true, oldClaim: validUpdateClaim, newClaim: invalidUpdateClaimAccessModes, enableResize: false, + enableBlock: false, + }, + "valid-update-volume-mode-block-to-block": { + isExpectedFailure: false, + oldClaim: validClaimVolumeModeBlock, + newClaim: validClaimVolumeModeBlock, + enableResize: false, + enableBlock: true, + }, + "valid-update-volume-mode-file-to-file": { + isExpectedFailure: false, + oldClaim: validClaimVolumeModeFile, + newClaim: validClaimVolumeModeFile, + enableResize: false, + enableBlock: true, + }, + "invalid-update-volume-mode-to-block": { + isExpectedFailure: true, + oldClaim: validClaimVolumeModeFile, + newClaim: validClaimVolumeModeBlock, + enableResize: false, + enableBlock: true, + }, + "invalid-update-volume-mode-to-file": { + isExpectedFailure: true, + oldClaim: validClaimVolumeModeBlock, + newClaim: validClaimVolumeModeFile, + enableResize: false, + enableBlock: true, + }, + "invalid-update-volume-mode-nil-to-file": { + isExpectedFailure: true, + oldClaim: invalidClaimVolumeModeNil, + newClaim: validClaimVolumeModeFile, + enableResize: false, + enableBlock: true, + }, + "invalid-update-volume-mode-nil-to-block": { + isExpectedFailure: true, + oldClaim: invalidClaimVolumeModeNil, + newClaim: validClaimVolumeModeBlock, + enableResize: false, + enableBlock: true, + }, + "invalid-update-volume-mode-block-to-nil": { + isExpectedFailure: true, + oldClaim: validClaimVolumeModeBlock, + newClaim: invalidClaimVolumeModeNil, + enableResize: false, + enableBlock: true, + }, + "invalid-update-volume-mode-file-to-nil": { + isExpectedFailure: true, + oldClaim: validClaimVolumeModeFile, + newClaim: invalidClaimVolumeModeNil, + enableResize: false, + enableBlock: true, + }, + "invalid-update-volume-mode-empty-to-mode": { + isExpectedFailure: true, + oldClaim: validClaim, + newClaim: validClaimVolumeModeBlock, + enableResize: false, + enableBlock: true, + }, + "invalid-update-volume-mode-mode-to-empty": { + isExpectedFailure: true, + oldClaim: validClaimVolumeModeBlock, + newClaim: validClaim, + enableResize: false, + enableBlock: true, + }, + "invalid-update-blocksupport-disabled": { + isExpectedFailure: true, + oldClaim: validClaimVolumeModeFile, + newClaim: validClaimVolumeModeFile, + enableResize: false, + enableBlock: false, }, "invalid-update-change-storage-class-annotation-after-creation": { isExpectedFailure: true, oldClaim: validClaimStorageClass, newClaim: invalidUpdateClaimStorageClass, enableResize: false, + enableBlock: false, }, "valid-update-mutable-annotation": { isExpectedFailure: false, oldClaim: validClaimAnnotation, newClaim: validUpdateClaimMutableAnnotation, enableResize: false, + enableBlock: false, }, "valid-update-add-annotation": { isExpectedFailure: false, oldClaim: validClaim, newClaim: validAddClaimAnnotation, enableResize: false, + enableBlock: false, }, "valid-size-update-resize-disabled": { isExpectedFailure: true, oldClaim: validClaim, newClaim: validSizeUpdate, enableResize: false, + enableBlock: false, }, "valid-size-update-resize-enabled": { isExpectedFailure: false, oldClaim: validClaim, newClaim: validSizeUpdate, enableResize: true, + enableBlock: false, }, "invalid-size-update-resize-enabled": { isExpectedFailure: true, oldClaim: validClaim, newClaim: invalidSizeUpdate, enableResize: true, + enableBlock: false, }, "unbound-size-update-resize-enabled": { isExpectedFailure: true, oldClaim: validClaim, newClaim: unboundSizeUpdate, enableResize: true, + enableBlock: false, }, } for name, scenario := range scenarios { // ensure we have a resource version specified for updates togglePVExpandFeature(scenario.enableResize, t) + toggleBlockVolumeFeature(scenario.enableBlock, t) scenario.oldClaim.ResourceVersion = "1" scenario.newClaim.ResourceVersion = "1" errs := ValidatePersistentVolumeClaimUpdate(scenario.newClaim, scenario.oldClaim) @@ -1164,6 +1429,23 @@ func TestValidatePersistentVolumeClaimUpdate(t *testing.T) { } } +func toggleBlockVolumeFeature(toggleFlag bool, t *testing.T) { + if toggleFlag { + // Enable alpha feature BlockVolume + err := utilfeature.DefaultFeatureGate.Set("BlockVolume=true") + if err != nil { + t.Errorf("Failed to enable feature gate for BlockVolume: %v", err) + return + } + } else { + err := utilfeature.DefaultFeatureGate.Set("BlockVolume=false") + if err != nil { + t.Errorf("Failed to disable feature gate for BlockVolume: %v", err) + return + } + } +} + func togglePVExpandFeature(toggleFlag bool, t *testing.T) { if toggleFlag { // Enable alpha feature LocalStorageCapacityIsolation @@ -1356,27 +1638,27 @@ func TestValidateGlusterfs(t *testing.T) { func TestValidateCSIVolumeSource(t *testing.T) { testCases := []struct { name string - csi *api.CSIPersistentVolumeSource + csi *core.CSIPersistentVolumeSource errtype field.ErrorType errfield string }{ { name: "all required fields ok", - csi: &api.CSIPersistentVolumeSource{Driver: "test-driver", VolumeHandle: "test-123", ReadOnly: true}, + csi: &core.CSIPersistentVolumeSource{Driver: "test-driver", VolumeHandle: "test-123", ReadOnly: true}, }, { name: "with default values ok", - csi: &api.CSIPersistentVolumeSource{Driver: "test-driver", VolumeHandle: "test-123"}, + csi: &core.CSIPersistentVolumeSource{Driver: "test-driver", VolumeHandle: "test-123"}, }, { name: "missing driver name", - csi: &api.CSIPersistentVolumeSource{VolumeHandle: "test-123"}, + csi: &core.CSIPersistentVolumeSource{VolumeHandle: "test-123"}, errtype: field.ErrorTypeRequired, errfield: "driver", }, { name: "missing volume handle", - csi: &api.CSIPersistentVolumeSource{Driver: "my-driver"}, + csi: &core.CSIPersistentVolumeSource{Driver: "my-driver"}, errtype: field.ErrorTypeRequired, errfield: "volumeHandle", }, @@ -2988,7 +3270,7 @@ func TestValidateVolumes(t *testing.T) { t.Errorf("[%d: %q] expected error detail %q, got %q", i, tc.name, tc.errdetail, errs[0].Detail) } } else { - if len(names) != 1 || !names.Has(tc.vol.Name) { + if len(names) != 1 || !IsMatchedVolume(tc.vol.Name, names) { t.Errorf("[%d: %q] wrong names result: %v", i, tc.name, names) } } @@ -3130,6 +3412,153 @@ func TestAlphaHugePagesIsolation(t *testing.T) { } } +func TestAlphaPVCVolumeMode(t *testing.T) { + // Enable alpha feature BlockVolume for PVC + err := utilfeature.DefaultFeatureGate.Set("BlockVolume=true") + if err != nil { + t.Errorf("Failed to enable feature gate for BlockVolume: %v", err) + return + } + + block := core.PersistentVolumeBlock + file := core.PersistentVolumeFilesystem + fake := core.PersistentVolumeMode("fake") + empty := core.PersistentVolumeMode("") + + // Success Cases + successCasesPVC := map[string]*core.PersistentVolumeClaim{ + "valid block value": createTestVolModePVC(&block), + "valid filesystem value": createTestVolModePVC(&file), + "valid nil value": createTestVolModePVC(nil), + } + for k, v := range successCasesPVC { + if errs := ValidatePersistentVolumeClaim(v); len(errs) != 0 { + t.Errorf("expected success for %s", k) + } + } + + // Error Cases + errorCasesPVC := map[string]*core.PersistentVolumeClaim{ + "invalid value": createTestVolModePVC(&fake), + "empty value": createTestVolModePVC(&empty), + } + for k, v := range errorCasesPVC { + if errs := ValidatePersistentVolumeClaim(v); len(errs) == 0 { + t.Errorf("expected failure for %s", k) + } + } +} + +func TestAlphaPVVolumeMode(t *testing.T) { + // Enable alpha feature BlockVolume for PV + err := utilfeature.DefaultFeatureGate.Set("BlockVolume=true") + if err != nil { + t.Errorf("Failed to enable feature gate for BlockVolume: %v", err) + return + } + + block := core.PersistentVolumeBlock + file := core.PersistentVolumeFilesystem + fake := core.PersistentVolumeMode("fake") + empty := core.PersistentVolumeMode("") + + // Success Cases + successCasesPV := map[string]*core.PersistentVolume{ + "valid block value": createTestVolModePV(&block), + "valid filesystem value": createTestVolModePV(&file), + "valid nil value": createTestVolModePV(nil), + } + for k, v := range successCasesPV { + if errs := ValidatePersistentVolume(v); len(errs) != 0 { + t.Errorf("expected success for %s", k) + } + } + + // Error Cases + errorCasesPV := map[string]*core.PersistentVolume{ + "invalid value": createTestVolModePV(&fake), + "empty value": createTestVolModePV(&empty), + } + for k, v := range errorCasesPV { + if errs := ValidatePersistentVolume(v); len(errs) == 0 { + t.Errorf("expected failure for %s", k) + } + } +} + +func createTestVolModePVC(vmode *core.PersistentVolumeMode) *core.PersistentVolumeClaim { + validName := "valid-storage-class" + + pvc := core.PersistentVolumeClaim{ + ObjectMeta: metav1.ObjectMeta{ + Name: "foo", + Namespace: "default", + }, + Spec: core.PersistentVolumeClaimSpec{ + Resources: core.ResourceRequirements{ + Requests: core.ResourceList{ + core.ResourceName(core.ResourceStorage): resource.MustParse("10G"), + }, + }, + AccessModes: []core.PersistentVolumeAccessMode{core.ReadWriteOnce}, + StorageClassName: &validName, + VolumeMode: vmode, + }, + } + return &pvc +} + +func createTestVolModePV(vmode *core.PersistentVolumeMode) *core.PersistentVolume { + + // PersistentVolume with VolumeMode set (valid and invalid) + pv := core.PersistentVolume{ + ObjectMeta: metav1.ObjectMeta{ + Name: "foo", + Namespace: "", + }, + Spec: core.PersistentVolumeSpec{ + Capacity: core.ResourceList{ + core.ResourceName(core.ResourceStorage): resource.MustParse("10G"), + }, + AccessModes: []core.PersistentVolumeAccessMode{core.ReadWriteOnce}, + PersistentVolumeSource: core.PersistentVolumeSource{ + HostPath: &core.HostPathVolumeSource{ + Path: "/foo", + Type: newHostPathType(string(core.HostPathDirectory)), + }, + }, + StorageClassName: "test-storage-class", + VolumeMode: vmode, + }, + } + return &pv +} + +func createTestPV() *core.PersistentVolume { + + // PersistentVolume with VolumeMode set (valid and invalid) + pv := core.PersistentVolume{ + ObjectMeta: metav1.ObjectMeta{ + Name: "foo", + Namespace: "", + }, + Spec: core.PersistentVolumeSpec{ + Capacity: core.ResourceList{ + core.ResourceName(core.ResourceStorage): resource.MustParse("10G"), + }, + AccessModes: []core.PersistentVolumeAccessMode{core.ReadWriteOnce}, + PersistentVolumeSource: core.PersistentVolumeSource{ + HostPath: &core.HostPathVolumeSource{ + Path: "/foo", + Type: newHostPathType(string(core.HostPathDirectory)), + }, + }, + StorageClassName: "test-storage-class", + }, + } + return &pv +} + func TestAlphaLocalStorageCapacityIsolation(t *testing.T) { testCases := []core.VolumeSource{ @@ -3881,7 +4310,16 @@ func TestValidateEnvFrom(t *testing.T) { } func TestValidateVolumeMounts(t *testing.T) { - volumes := sets.NewString("abc", "123", "abc-123") + volumes := []core.Volume{ + {Name: "abc", VolumeSource: core.VolumeSource{PersistentVolumeClaim: &core.PersistentVolumeClaimVolumeSource{ClaimName: "testclaim1"}}}, + {Name: "abc-123", VolumeSource: core.VolumeSource{PersistentVolumeClaim: &core.PersistentVolumeClaimVolumeSource{ClaimName: "testclaim2"}}}, + {Name: "123", VolumeSource: core.VolumeSource{HostPath: &core.HostPathVolumeSource{Path: "/foo/baz", Type: newHostPathType(string(core.HostPathUnset))}}}, + } + vols, v1err := ValidateVolumes(volumes, field.NewPath("field")) + if len(v1err) > 0 { + t.Errorf("Invalid test volume - expected success %v", v1err) + return + } container := core.Container{ SecurityContext: nil, } @@ -3899,7 +4337,11 @@ func TestValidateVolumeMounts(t *testing.T) { {Name: "abc-123", MountPath: "/bac", SubPath: ".baz"}, {Name: "abc-123", MountPath: "/bad", SubPath: "..baz"}, } - if errs := ValidateVolumeMounts(successCase, volumes, &container, field.NewPath("field")); len(errs) != 0 { + goodVolumeDevices := []core.VolumeDevice{ + {Name: "xyz", DevicePath: "/foofoo"}, + {Name: "uvw", DevicePath: "/foofoo/share/test"}, + } + if errs := ValidateVolumeMounts(successCase, GetVolumeDeviceMap(goodVolumeDevices), vols, &container, field.NewPath("field")); len(errs) != 0 { t.Errorf("expected success: %v", errs) } @@ -3913,9 +4355,16 @@ func TestValidateVolumeMounts(t *testing.T) { "subpath contains ..": {{Name: "abc", MountPath: "/bar", SubPath: "baz/../bat"}}, "subpath ends in ..": {{Name: "abc", MountPath: "/bar", SubPath: "./.."}}, "disabled MountPropagation feature gate": {{Name: "abc", MountPath: "/bar", MountPropagation: &propagation}}, + "name exists in volumeDevice": {{Name: "xyz", MountPath: "/bar"}}, + "mountpath exists in volumeDevice": {{Name: "uvw", MountPath: "/mnt/exists"}}, + "both exist in volumeDevice": {{Name: "xyz", MountPath: "/mnt/exists"}}, } + badVolumeDevice := []core.VolumeDevice{ + {Name: "xyz", DevicePath: "/mnt/exists"}, + } + for k, v := range errorCases { - if errs := ValidateVolumeMounts(v, volumes, &container, field.NewPath("field")); len(errs) == 0 { + if errs := ValidateVolumeMounts(v, GetVolumeDeviceMap(badVolumeDevice), vols, &container, field.NewPath("field")); len(errs) == 0 { t.Errorf("expected failure for %s", k) } } @@ -4033,9 +4482,16 @@ func TestValidateMountPropagation(t *testing.T) { return } + volumes := []core.Volume{ + {Name: "foo", VolumeSource: core.VolumeSource{HostPath: &core.HostPathVolumeSource{Path: "/foo/baz", Type: newHostPathType(string(core.HostPathUnset))}}}, + } + vols2, v2err := ValidateVolumes(volumes, field.NewPath("field")) + if len(v2err) > 0 { + t.Errorf("Invalid test volume - expected success %v", v2err) + return + } for i, test := range tests { - volumes := sets.NewString("foo") - errs := ValidateVolumeMounts([]core.VolumeMount{test.mount}, volumes, test.container, field.NewPath("field")) + errs := ValidateVolumeMounts([]core.VolumeMount{test.mount}, nil, vols2, test.container, field.NewPath("field")) if test.expectError && len(errs) == 0 { t.Errorf("test %d expected error, got none", i) } @@ -4043,7 +4499,81 @@ func TestValidateMountPropagation(t *testing.T) { t.Errorf("test %d expected success, got error: %v", i, errs) } } +} +func TestAlphaValidateVolumeDevices(t *testing.T) { + volumes := []core.Volume{ + {Name: "abc", VolumeSource: core.VolumeSource{PersistentVolumeClaim: &core.PersistentVolumeClaimVolumeSource{ClaimName: "testclaim1"}}}, + {Name: "abc-123", VolumeSource: core.VolumeSource{PersistentVolumeClaim: &core.PersistentVolumeClaimVolumeSource{ClaimName: "testclaim2"}}}, + {Name: "def", VolumeSource: core.VolumeSource{HostPath: &core.HostPathVolumeSource{Path: "/foo/baz", Type: newHostPathType(string(core.HostPathUnset))}}}, + } + + vols, v1err := ValidateVolumes(volumes, field.NewPath("field")) + if len(v1err) > 0 { + t.Errorf("Invalid test volumes - expected success %v", v1err) + return + } + + disabledAlphaVolDevice := []core.VolumeDevice{ + {Name: "abc", DevicePath: "/foo"}, + } + + successCase := []core.VolumeDevice{ + {Name: "abc", DevicePath: "/foo"}, + {Name: "abc-123", DevicePath: "/usr/share/test"}, + } + goodVolumeMounts := []core.VolumeMount{ + {Name: "xyz", MountPath: "/foofoo"}, + {Name: "ghi", MountPath: "/foo/usr/share/test"}, + } + + errorCases := map[string][]core.VolumeDevice{ + "empty name": {{Name: "", DevicePath: "/foo"}}, + "duplicate name": {{Name: "abc", DevicePath: "/foo"}, {Name: "abc", DevicePath: "/foo/bar"}}, + "name not found": {{Name: "not-found", DevicePath: "/usr/share/test"}}, + "name found but invalid source": {{Name: "def", DevicePath: "/usr/share/test"}}, + "empty devicepath": {{Name: "abc", DevicePath: ""}}, + "relative devicepath": {{Name: "abc-123", DevicePath: "baz"}}, + "duplicate devicepath": {{Name: "abc", DevicePath: "/foo"}, {Name: "abc-123", DevicePath: "/foo"}}, + "no backsteps": {{Name: "def", DevicePath: "/baz/../"}}, + "name exists in volumemounts": {{Name: "abc", DevicePath: "/baz/../"}}, + "path exists in volumemounts": {{Name: "xyz", DevicePath: "/this/path/exists"}}, + "both exist in volumemounts": {{Name: "abc", DevicePath: "/this/path/exists"}}, + } + badVolumeMounts := []core.VolumeMount{ + {Name: "abc", MountPath: "/foo"}, + {Name: "abc-123", MountPath: "/this/path/exists"}, + } + + // enable Alpha BlockVolume + err1 := utilfeature.DefaultFeatureGate.Set("BlockVolume=true") + if err1 != nil { + t.Errorf("Failed to enable feature gate for BlockVolume: %v", err1) + return + } + // Success Cases: + // Validate normal success cases - only PVC volumeSource + if errs := ValidateVolumeDevices(successCase, GetVolumeMountMap(goodVolumeMounts), vols, field.NewPath("field")); len(errs) != 0 { + t.Errorf("expected success: %v", errs) + } + + // Error Cases: + // Validate normal error cases - only PVC volumeSource + for k, v := range errorCases { + if errs := ValidateVolumeDevices(v, GetVolumeMountMap(badVolumeMounts), vols, field.NewPath("field")); len(errs) == 0 { + t.Errorf("expected failure for %s", k) + } + } + + // disable Alpha BlockVolume + err2 := utilfeature.DefaultFeatureGate.Set("BlockVolume=false") + if err2 != nil { + t.Errorf("Failed to disable feature gate for BlockVolume: %v", err2) + return + } + if errs := ValidateVolumeDevices(disabledAlphaVolDevice, GetVolumeMountMap(goodVolumeMounts), vols, field.NewPath("field")); len(errs) == 0 { + t.Errorf("expected failure: %v", errs) + } } func TestValidateProbe(t *testing.T) { @@ -4158,7 +4688,7 @@ func getResourceLimits(cpu, memory string) core.ResourceList { } func TestValidateContainers(t *testing.T) { - volumes := sets.String{} + volumeDevices := make(map[string]core.VolumeSource) capabilities.SetForTests(capabilities.Capabilities{ AllowPrivileged: true, }) @@ -4328,7 +4858,7 @@ func TestValidateContainers(t *testing.T) { }, {Name: "abc-1234", Image: "image", ImagePullPolicy: "IfNotPresent", TerminationMessagePolicy: "File", SecurityContext: fakeValidSecurityContext(true)}, } - if errs := validateContainers(successCase, volumes, field.NewPath("field")); len(errs) != 0 { + if errs := validateContainers(successCase, volumeDevices, field.NewPath("field")); len(errs) != 0 { t.Errorf("expected success: %v", errs) } @@ -4590,7 +5120,7 @@ func TestValidateContainers(t *testing.T) { }, } for k, v := range errorCases { - if errs := validateContainers(v, volumes, field.NewPath("field")); len(errs) == 0 { + if errs := validateContainers(v, volumeDevices, field.NewPath("field")); len(errs) == 0 { t.Errorf("expected failure for %s", k) } } diff --git a/pkg/apis/settings/validation/validation.go b/pkg/apis/settings/validation/validation.go index 1acfef0d76e..50ac75aab44 100644 --- a/pkg/apis/settings/validation/validation.go +++ b/pkg/apis/settings/validation/validation.go @@ -43,11 +43,11 @@ func ValidatePodPresetSpec(spec *settings.PodPresetSpec, fldPath *field.Path) fi allErrs = append(allErrs, field.Required(fldPath.Child("volumes", "env", "envFrom", "volumeMounts"), "must specify at least one")) } - volumes, vErrs := apivalidation.ValidateVolumes(spec.Volumes, fldPath.Child("volumes")) + vols, vErrs := apivalidation.ValidateVolumes(spec.Volumes, fldPath.Child("volumes")) allErrs = append(allErrs, vErrs...) allErrs = append(allErrs, apivalidation.ValidateEnv(spec.Env, fldPath.Child("env"))...) allErrs = append(allErrs, apivalidation.ValidateEnvFrom(spec.EnvFrom, fldPath.Child("envFrom"))...) - allErrs = append(allErrs, apivalidation.ValidateVolumeMounts(spec.VolumeMounts, volumes, nil, fldPath.Child("volumeMounts"))...) + allErrs = append(allErrs, apivalidation.ValidateVolumeMounts(spec.VolumeMounts, nil, vols, nil, fldPath.Child("volumeMounts"))...) return allErrs } diff --git a/pkg/features/kube_features.go b/pkg/features/kube_features.go index 14890722178..cc70071263d 100644 --- a/pkg/features/kube_features.go +++ b/pkg/features/kube_features.go @@ -187,6 +187,12 @@ const ( // // Enable mount/attachment of Container Storage Interface (CSI) backed PVs CSIPersistentVolume utilfeature.Feature = "CSIPersistentVolume" + + // owner: @screeley44 + // alpha: v1.9 + // + // Enable Block volume support in containers. + BlockVolume utilfeature.Feature = "BlockVolume" ) func init() { @@ -222,6 +228,7 @@ var defaultKubernetesFeatureGates = map[utilfeature.Feature]utilfeature.FeatureS MountContainers: {Default: false, PreRelease: utilfeature.Alpha}, VolumeScheduling: {Default: false, PreRelease: utilfeature.Alpha}, CSIPersistentVolume: {Default: false, PreRelease: utilfeature.Alpha}, + BlockVolume: {Default: false, PreRelease: utilfeature.Alpha}, // inherited features from generic apiserver, relisted here to get a conflict if it is changed // unintentionally on either side: diff --git a/pkg/registry/core/persistentvolume/strategy.go b/pkg/registry/core/persistentvolume/strategy.go index 924baabe109..477bca2bad5 100644 --- a/pkg/registry/core/persistentvolume/strategy.go +++ b/pkg/registry/core/persistentvolume/strategy.go @@ -28,6 +28,7 @@ import ( "k8s.io/apiserver/pkg/storage" "k8s.io/apiserver/pkg/storage/names" "k8s.io/kubernetes/pkg/api/legacyscheme" + pvutil "k8s.io/kubernetes/pkg/api/persistentvolume" api "k8s.io/kubernetes/pkg/apis/core" "k8s.io/kubernetes/pkg/apis/core/validation" volumevalidation "k8s.io/kubernetes/pkg/volume/validation" @@ -51,6 +52,8 @@ func (persistentvolumeStrategy) NamespaceScoped() bool { func (persistentvolumeStrategy) PrepareForCreate(ctx genericapirequest.Context, obj runtime.Object) { pv := obj.(*api.PersistentVolume) pv.Status = api.PersistentVolumeStatus{} + + pvutil.DropDisabledAlphaFields(&pv.Spec) } func (persistentvolumeStrategy) Validate(ctx genericapirequest.Context, obj runtime.Object) field.ErrorList { @@ -72,6 +75,9 @@ func (persistentvolumeStrategy) PrepareForUpdate(ctx genericapirequest.Context, newPv := obj.(*api.PersistentVolume) oldPv := old.(*api.PersistentVolume) newPv.Status = oldPv.Status + + pvutil.DropDisabledAlphaFields(&newPv.Spec) + pvutil.DropDisabledAlphaFields(&oldPv.Spec) } func (persistentvolumeStrategy) ValidateUpdate(ctx genericapirequest.Context, obj, old runtime.Object) field.ErrorList { diff --git a/pkg/registry/core/persistentvolumeclaim/strategy.go b/pkg/registry/core/persistentvolumeclaim/strategy.go index 1d9f3be68c4..1a63e1faea8 100644 --- a/pkg/registry/core/persistentvolumeclaim/strategy.go +++ b/pkg/registry/core/persistentvolumeclaim/strategy.go @@ -28,6 +28,7 @@ import ( "k8s.io/apiserver/pkg/storage" "k8s.io/apiserver/pkg/storage/names" "k8s.io/kubernetes/pkg/api/legacyscheme" + pvcutil "k8s.io/kubernetes/pkg/api/persistentvolumeclaim" api "k8s.io/kubernetes/pkg/apis/core" "k8s.io/kubernetes/pkg/apis/core/validation" ) @@ -48,8 +49,10 @@ func (persistentvolumeclaimStrategy) NamespaceScoped() bool { // PrepareForCreate clears the Status field which is not allowed to be set by end users on creation. func (persistentvolumeclaimStrategy) PrepareForCreate(ctx genericapirequest.Context, obj runtime.Object) { - pv := obj.(*api.PersistentVolumeClaim) - pv.Status = api.PersistentVolumeClaimStatus{} + pvc := obj.(*api.PersistentVolumeClaim) + pvc.Status = api.PersistentVolumeClaimStatus{} + + pvcutil.DropDisabledAlphaFields(&pvc.Spec) } func (persistentvolumeclaimStrategy) Validate(ctx genericapirequest.Context, obj runtime.Object) field.ErrorList { @@ -70,6 +73,9 @@ func (persistentvolumeclaimStrategy) PrepareForUpdate(ctx genericapirequest.Cont newPvc := obj.(*api.PersistentVolumeClaim) oldPvc := old.(*api.PersistentVolumeClaim) newPvc.Status = oldPvc.Status + + pvcutil.DropDisabledAlphaFields(&newPvc.Spec) + pvcutil.DropDisabledAlphaFields(&oldPvc.Spec) } func (persistentvolumeclaimStrategy) ValidateUpdate(ctx genericapirequest.Context, obj, old runtime.Object) field.ErrorList { diff --git a/staging/src/k8s.io/api/core/v1/types.go b/staging/src/k8s.io/api/core/v1/types.go index 5865795b1b1..900454c6963 100644 --- a/staging/src/k8s.io/api/core/v1/types.go +++ b/staging/src/k8s.io/api/core/v1/types.go @@ -527,6 +527,11 @@ type PersistentVolumeSpec struct { // More info: https://kubernetes.io/docs/concepts/storage/persistent-volumes/#mount-options // +optional MountOptions []string `json:"mountOptions,omitempty" protobuf:"bytes,7,opt,name=mountOptions"` + // volumeMode defines if a volume is intended to be used with a formatted filesystem + // or to remain in raw block state. Value of Filesystem is implied when not included in spec. + // This is an alpha feature and may change in the future. + // +optional + VolumeMode *PersistentVolumeMode `json:"volumeMode,omitempty" protobuf:"bytes,8,opt,name=volumeMode,casttype=PersistentVolumeMode"` } // PersistentVolumeReclaimPolicy describes a policy for end-of-life maintenance of persistent volumes. @@ -544,6 +549,16 @@ const ( PersistentVolumeReclaimRetain PersistentVolumeReclaimPolicy = "Retain" ) +// PersistentVolumeMode describes how a volume is intended to be consumed, either Block or Filesystem. +type PersistentVolumeMode string + +const ( + // PersistentVolumeBlock means the volume will not be formatted with a filesystem and will remain a raw block device. + PersistentVolumeBlock PersistentVolumeMode = "Block" + // PersistentVolumeFilesystem means the volume will be or is formatted with a filesystem. + PersistentVolumeFilesystem PersistentVolumeMode = "Filesystem" +) + // PersistentVolumeStatus is the current status of a persistent volume. type PersistentVolumeStatus struct { // Phase indicates if a volume is available, bound to a claim, or released by a claim. @@ -631,6 +646,11 @@ type PersistentVolumeClaimSpec struct { // More info: https://kubernetes.io/docs/concepts/storage/persistent-volumes#class-1 // +optional StorageClassName *string `json:"storageClassName,omitempty" protobuf:"bytes,5,opt,name=storageClassName"` + // volumeMode defines what type of volume is required by the claim. + // Value of Filesystem is implied when not included in claim spec. + // This is an alpha feature and may change in the future. + // +optional + VolumeMode *PersistentVolumeMode `json:"volumeMode,omitempty" protobuf:"bytes,6,opt,name=volumeMode,casttype=PersistentVolumeMode"` } // PersistentVolumeClaimConditionType is a valid value of PersistentVolumeClaimCondition.Type @@ -1709,6 +1729,14 @@ const ( MountPropagationBidirectional MountPropagationMode = "Bidirectional" ) +// volumeDevice describes a mapping of a raw block device within a container. +type VolumeDevice struct { + // name must match the name of a persistentVolumeClaim in the pod + Name string `json:"name" protobuf:"bytes,1,opt,name=name"` + // devicePath is the path inside of the container that the device will be mapped to. + DevicePath string `json:"devicePath" protobuf:"bytes,2,opt,name=devicePath"` +} + // EnvVar represents an environment variable present in a Container. type EnvVar struct { // Name of the environment variable. Must be a C_IDENTIFIER. @@ -2052,6 +2080,12 @@ type Container struct { // +patchMergeKey=mountPath // +patchStrategy=merge VolumeMounts []VolumeMount `json:"volumeMounts,omitempty" patchStrategy:"merge" patchMergeKey:"mountPath" protobuf:"bytes,9,rep,name=volumeMounts"` + // volumeDevices is the list of block devices to be used by the container. + // This is an alpha feature and may change in the future. + // +patchMergeKey=devicePath + // +patchStrategy=merge + // +optional + VolumeDevices []VolumeDevice `json:"volumeDevices,omitempty" patchStrategy:"merge" patchMergeKey:"devicePath" protobuf:"bytes,21,rep,name=volumeDevices"` // Periodic probe of container liveness. // Container will be restarted if the probe fails. // Cannot be updated.