volumeattributesclass and core api changes
This commit is contained in:
		| @@ -39,6 +39,11 @@ func DropDisabledSpecFields(pvSpec *api.PersistentVolumeSpec, oldPVSpec *api.Per | ||||
| 			pvSpec.CSI.NodeExpandSecretRef = nil | ||||
| 		} | ||||
| 	} | ||||
| 	if !utilfeature.DefaultFeatureGate.Enabled(features.VolumeAttributesClass) { | ||||
| 		if oldPVSpec == nil || oldPVSpec.VolumeAttributesClassName == nil { | ||||
| 			pvSpec.VolumeAttributesClassName = nil | ||||
| 		} | ||||
| 	} | ||||
| } | ||||
|  | ||||
| // DropDisabledStatusFields removes disabled fields from the pv status. | ||||
|   | ||||
| @@ -28,6 +28,7 @@ import ( | ||||
| 	featuregatetesting "k8s.io/component-base/featuregate/testing" | ||||
| 	api "k8s.io/kubernetes/pkg/apis/core" | ||||
| 	"k8s.io/kubernetes/pkg/features" | ||||
| 	"k8s.io/utils/ptr" | ||||
| ) | ||||
|  | ||||
| func TestDropDisabledFields(t *testing.T) { | ||||
| @@ -35,6 +36,7 @@ func TestDropDisabledFields(t *testing.T) { | ||||
| 		Name:      "expansion-secret", | ||||
| 		Namespace: "default", | ||||
| 	} | ||||
| 	vacName := ptr.To("vac") | ||||
|  | ||||
| 	tests := map[string]struct { | ||||
| 		oldSpec             *api.PersistentVolumeSpec | ||||
| @@ -42,6 +44,7 @@ func TestDropDisabledFields(t *testing.T) { | ||||
| 		expectOldSpec       *api.PersistentVolumeSpec | ||||
| 		expectNewSpec       *api.PersistentVolumeSpec | ||||
| 		csiExpansionEnabled bool | ||||
| 		vacEnabled          bool | ||||
| 	}{ | ||||
| 		"disabled csi expansion clears secrets": { | ||||
| 			csiExpansionEnabled: false, | ||||
| @@ -85,11 +88,54 @@ func TestDropDisabledFields(t *testing.T) { | ||||
| 			oldSpec:             specWithCSISecrets(nil), | ||||
| 			expectOldSpec:       specWithCSISecrets(nil), | ||||
| 		}, | ||||
| 		"disabled vac clears volume attributes class name": { | ||||
| 			vacEnabled:    false, | ||||
| 			newSpec:       specWithVACName(vacName), | ||||
| 			expectNewSpec: specWithVACName(nil), | ||||
| 			oldSpec:       nil, | ||||
| 			expectOldSpec: nil, | ||||
| 		}, | ||||
| 		"enabled vac preserve volume attributes class name": { | ||||
| 			vacEnabled:    true, | ||||
| 			newSpec:       specWithVACName(vacName), | ||||
| 			expectNewSpec: specWithVACName(vacName), | ||||
| 			oldSpec:       nil, | ||||
| 			expectOldSpec: nil, | ||||
| 		}, | ||||
| 		"enabled vac preserve volume attributes class name when both old and new have it": { | ||||
| 			vacEnabled:    true, | ||||
| 			newSpec:       specWithVACName(vacName), | ||||
| 			expectNewSpec: specWithVACName(vacName), | ||||
| 			oldSpec:       specWithVACName(vacName), | ||||
| 			expectOldSpec: specWithVACName(vacName), | ||||
| 		}, | ||||
| 		"disabled vac old pv had volume attributes class name": { | ||||
| 			vacEnabled:    false, | ||||
| 			newSpec:       specWithVACName(vacName), | ||||
| 			expectNewSpec: specWithVACName(vacName), | ||||
| 			oldSpec:       specWithVACName(vacName), | ||||
| 			expectOldSpec: specWithVACName(vacName), | ||||
| 		}, | ||||
| 		"enabled vac preserves volume attributes class name when old pv did not had it": { | ||||
| 			vacEnabled:    true, | ||||
| 			newSpec:       specWithVACName(vacName), | ||||
| 			expectNewSpec: specWithVACName(vacName), | ||||
| 			oldSpec:       specWithVACName(nil), | ||||
| 			expectOldSpec: specWithVACName(nil), | ||||
| 		}, | ||||
| 		"disabled vac neither new pv nor old pv had volume attributes class name": { | ||||
| 			vacEnabled:    false, | ||||
| 			newSpec:       specWithVACName(nil), | ||||
| 			expectNewSpec: specWithVACName(nil), | ||||
| 			oldSpec:       specWithVACName(nil), | ||||
| 			expectOldSpec: specWithVACName(nil), | ||||
| 		}, | ||||
| 	} | ||||
|  | ||||
| 	for name, tc := range tests { | ||||
| 		t.Run(name, func(t *testing.T) { | ||||
| 			defer featuregatetesting.SetFeatureGateDuringTest(t, utilfeature.DefaultFeatureGate, features.CSINodeExpandSecret, tc.csiExpansionEnabled)() | ||||
| 			defer featuregatetesting.SetFeatureGateDuringTest(t, utilfeature.DefaultFeatureGate, features.VolumeAttributesClass, tc.vacEnabled)() | ||||
|  | ||||
| 			DropDisabledSpecFields(tc.newSpec, tc.oldSpec) | ||||
| 			if !reflect.DeepEqual(tc.newSpec, tc.expectNewSpec) { | ||||
| @@ -118,6 +164,22 @@ func specWithCSISecrets(secret *api.SecretReference) *api.PersistentVolumeSpec { | ||||
| 	return pvSpec | ||||
| } | ||||
|  | ||||
| func specWithVACName(vacName *string) *api.PersistentVolumeSpec { | ||||
| 	pvSpec := &api.PersistentVolumeSpec{ | ||||
| 		PersistentVolumeSource: api.PersistentVolumeSource{ | ||||
| 			CSI: &api.CSIPersistentVolumeSource{ | ||||
| 				Driver:       "com.google.gcepd", | ||||
| 				VolumeHandle: "foobar", | ||||
| 			}, | ||||
| 		}, | ||||
| 	} | ||||
|  | ||||
| 	if vacName != nil { | ||||
| 		pvSpec.VolumeAttributesClassName = vacName | ||||
| 	} | ||||
| 	return pvSpec | ||||
| } | ||||
|  | ||||
| func TestWarnings(t *testing.T) { | ||||
| 	testcases := []struct { | ||||
| 		name     string | ||||
|   | ||||
| @@ -35,6 +35,14 @@ const ( | ||||
| // DropDisabledFields removes disabled fields from the pvc spec. | ||||
| // This should be called from PrepareForCreate/PrepareForUpdate for all resources containing a pvc spec. | ||||
| func DropDisabledFields(pvcSpec, oldPVCSpec *core.PersistentVolumeClaimSpec) { | ||||
| 	// Drop the contents of the volumeAttributesClassName if the VolumeAttributesClass | ||||
| 	// feature gate is disabled. | ||||
| 	if !utilfeature.DefaultFeatureGate.Enabled(features.VolumeAttributesClass) { | ||||
| 		if oldPVCSpec == nil || oldPVCSpec.VolumeAttributesClassName == nil { | ||||
| 			pvcSpec.VolumeAttributesClassName = nil | ||||
| 		} | ||||
| 	} | ||||
|  | ||||
| 	// Drop the contents of the dataSourceRef field if the AnyVolumeDataSource | ||||
| 	// feature gate is disabled. | ||||
| 	if !utilfeature.DefaultFeatureGate.Enabled(features.AnyVolumeDataSource) { | ||||
| @@ -91,6 +99,15 @@ func EnforceDataSourceBackwardsCompatibility(pvcSpec, oldPVCSpec *core.Persisten | ||||
| } | ||||
|  | ||||
| func DropDisabledFieldsFromStatus(pvc, oldPVC *core.PersistentVolumeClaim) { | ||||
| 	if !utilfeature.DefaultFeatureGate.Enabled(features.VolumeAttributesClass) { | ||||
| 		if oldPVC == nil || oldPVC.Status.CurrentVolumeAttributesClassName == nil { | ||||
| 			pvc.Status.CurrentVolumeAttributesClassName = nil | ||||
| 		} | ||||
| 		if oldPVC == nil || oldPVC.Status.ModifyVolumeStatus == nil { | ||||
| 			pvc.Status.ModifyVolumeStatus = nil | ||||
| 		} | ||||
| 	} | ||||
|  | ||||
| 	if !utilfeature.DefaultFeatureGate.Enabled(features.RecoverVolumeExpansionFailure) { | ||||
| 		if !helper.ClaimContainsAllocatedResources(oldPVC) { | ||||
| 			pvc.Status.AllocatedResources = nil | ||||
|   | ||||
| @@ -23,11 +23,12 @@ import ( | ||||
|  | ||||
| 	"github.com/google/go-cmp/cmp" | ||||
| 	"k8s.io/apimachinery/pkg/api/resource" | ||||
| 	"k8s.io/apimachinery/pkg/util/sets" | ||||
|  | ||||
| 	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" | ||||
| 	"k8s.io/apimachinery/pkg/util/sets" | ||||
| 	utilfeature "k8s.io/apiserver/pkg/util/feature" | ||||
| 	featuregatetesting "k8s.io/component-base/featuregate/testing" | ||||
| 	"k8s.io/utils/ptr" | ||||
|  | ||||
| 	"k8s.io/kubernetes/pkg/apis/core" | ||||
| 	"k8s.io/kubernetes/pkg/features" | ||||
| ) | ||||
| @@ -384,82 +385,217 @@ func TestDataSourceRef(t *testing.T) { | ||||
| 	} | ||||
| } | ||||
|  | ||||
| func TestDropDisabledVolumeAttributesClass(t *testing.T) { | ||||
| 	vacName := ptr.To("foo") | ||||
|  | ||||
| 	var tests = map[string]struct { | ||||
| 		spec       core.PersistentVolumeClaimSpec | ||||
| 		oldSpec    core.PersistentVolumeClaimSpec | ||||
| 		vacEnabled bool | ||||
| 		wantVAC    *string | ||||
| 	}{ | ||||
| 		"vac disabled with empty vac": { | ||||
| 			spec: core.PersistentVolumeClaimSpec{}, | ||||
| 		}, | ||||
| 		"vac disabled with vac": { | ||||
| 			spec: core.PersistentVolumeClaimSpec{VolumeAttributesClassName: vacName}, | ||||
| 		}, | ||||
| 		"vac enabled with empty vac": { | ||||
| 			spec:       core.PersistentVolumeClaimSpec{}, | ||||
| 			vacEnabled: true, | ||||
| 		}, | ||||
| 		"vac enabled with vac": { | ||||
| 			spec:       core.PersistentVolumeClaimSpec{VolumeAttributesClassName: vacName}, | ||||
| 			vacEnabled: true, | ||||
| 			wantVAC:    vacName, | ||||
| 		}, | ||||
| 		"vac disabled with vac when vac doesn't exists in oldSpec": { | ||||
| 			spec:    core.PersistentVolumeClaimSpec{VolumeAttributesClassName: vacName}, | ||||
| 			oldSpec: core.PersistentVolumeClaimSpec{}, | ||||
| 		}, | ||||
| 		"vac disabled with vac when vac exists in oldSpec": { | ||||
| 			spec:       core.PersistentVolumeClaimSpec{VolumeAttributesClassName: vacName}, | ||||
| 			oldSpec:    core.PersistentVolumeClaimSpec{VolumeAttributesClassName: vacName}, | ||||
| 			vacEnabled: false, | ||||
| 			wantVAC:    vacName, | ||||
| 		}, | ||||
| 		"vac enabled with vac when vac doesn't exists in oldSpec": { | ||||
| 			spec:       core.PersistentVolumeClaimSpec{VolumeAttributesClassName: vacName}, | ||||
| 			oldSpec:    core.PersistentVolumeClaimSpec{}, | ||||
| 			vacEnabled: true, | ||||
| 			wantVAC:    vacName, | ||||
| 		}, | ||||
| 		"vac enable with vac when vac exists in oldSpec": { | ||||
| 			spec:       core.PersistentVolumeClaimSpec{VolumeAttributesClassName: vacName}, | ||||
| 			oldSpec:    core.PersistentVolumeClaimSpec{VolumeAttributesClassName: vacName}, | ||||
| 			vacEnabled: true, | ||||
| 			wantVAC:    vacName, | ||||
| 		}, | ||||
| 	} | ||||
|  | ||||
| 	for testName, test := range tests { | ||||
| 		t.Run(testName, func(t *testing.T) { | ||||
| 			defer featuregatetesting.SetFeatureGateDuringTest(t, utilfeature.DefaultFeatureGate, features.VolumeAttributesClass, test.vacEnabled)() | ||||
| 			DropDisabledFields(&test.spec, &test.oldSpec) | ||||
| 			if test.spec.VolumeAttributesClassName != test.wantVAC { | ||||
| 				t.Errorf("expected vac was not met, test: %s, vacEnabled: %v, spec: %+v, expected VAC: %+v", | ||||
| 					testName, test.vacEnabled, test.spec, test.wantVAC) | ||||
| 			} | ||||
| 		}) | ||||
| 	} | ||||
| } | ||||
|  | ||||
| func TestDropDisabledFieldsFromStatus(t *testing.T) { | ||||
| 	tests := []struct { | ||||
| 		name     string | ||||
| 		feature  bool | ||||
| 		pvc      *core.PersistentVolumeClaim | ||||
| 		oldPVC   *core.PersistentVolumeClaim | ||||
| 		expected *core.PersistentVolumeClaim | ||||
| 		name                                string | ||||
| 		enableRecoverVolumeExpansionFailure bool | ||||
| 		enableVolumeAttributesClass         bool | ||||
| 		pvc                                 *core.PersistentVolumeClaim | ||||
| 		oldPVC                              *core.PersistentVolumeClaim | ||||
| 		expected                            *core.PersistentVolumeClaim | ||||
| 	}{ | ||||
| 		{ | ||||
| 			name:     "for:newPVC=hasAllocatedResource,oldPVC=doesnot,featuregate=false; should drop field", | ||||
| 			feature:  false, | ||||
| 			pvc:      withAllocatedResource("5G"), | ||||
| 			oldPVC:   getPVC(), | ||||
| 			expected: getPVC(), | ||||
| 			name:                                "for:newPVC=hasAllocatedResource,oldPVC=doesnot,featuregate=false; should drop field", | ||||
| 			enableRecoverVolumeExpansionFailure: false, | ||||
| 			enableVolumeAttributesClass:         false, | ||||
| 			pvc:                                 withAllocatedResource("5G"), | ||||
| 			oldPVC:                              getPVC(), | ||||
| 			expected:                            getPVC(), | ||||
| 		}, | ||||
| 		{ | ||||
| 			name:     "for:newPVC=hasAllocatedResource,oldPVC=doesnot,featuregate=true; should keep field", | ||||
| 			feature:  true, | ||||
| 			pvc:      withAllocatedResource("5G"), | ||||
| 			oldPVC:   getPVC(), | ||||
| 			expected: withAllocatedResource("5G"), | ||||
| 			name:                                "for:newPVC=hasAllocatedResource,oldPVC=doesnot,featuregate=RecoverVolumeExpansionFailure=true; should keep field", | ||||
| 			enableRecoverVolumeExpansionFailure: true, | ||||
| 			enableVolumeAttributesClass:         false, | ||||
| 			pvc:                                 withAllocatedResource("5G"), | ||||
| 			oldPVC:                              getPVC(), | ||||
| 			expected:                            withAllocatedResource("5G"), | ||||
| 		}, | ||||
| 		{ | ||||
| 			name:     "for:newPVC=hasAllocatedResource,oldPVC=hasAllocatedResource,featuregate=true; should keep field", | ||||
| 			feature:  true, | ||||
| 			pvc:      withAllocatedResource("5G"), | ||||
| 			oldPVC:   withAllocatedResource("5G"), | ||||
| 			expected: withAllocatedResource("5G"), | ||||
| 			name:                                "for:newPVC=hasAllocatedResource,oldPVC=hasAllocatedResource,featuregate=RecoverVolumeExpansionFailure=true; should keep field", | ||||
| 			enableRecoverVolumeExpansionFailure: true, | ||||
| 			enableVolumeAttributesClass:         false, | ||||
| 			pvc:                                 withAllocatedResource("5G"), | ||||
| 			oldPVC:                              withAllocatedResource("5G"), | ||||
| 			expected:                            withAllocatedResource("5G"), | ||||
| 		}, | ||||
| 		{ | ||||
| 			name:     "for:newPVC=hasAllocatedResource,oldPVC=hasAllocatedResource,featuregate=false; should keep field", | ||||
| 			feature:  false, | ||||
| 			pvc:      withAllocatedResource("10G"), | ||||
| 			oldPVC:   withAllocatedResource("5G"), | ||||
| 			expected: withAllocatedResource("10G"), | ||||
| 			name:                                "for:newPVC=hasAllocatedResource,oldPVC=hasAllocatedResource,featuregate=false; should keep field", | ||||
| 			enableRecoverVolumeExpansionFailure: false, | ||||
| 			enableVolumeAttributesClass:         false, | ||||
| 			pvc:                                 withAllocatedResource("10G"), | ||||
| 			oldPVC:                              withAllocatedResource("5G"), | ||||
| 			expected:                            withAllocatedResource("10G"), | ||||
| 		}, | ||||
| 		{ | ||||
| 			name:     "for:newPVC=hasAllocatedResource,oldPVC=nil,featuregate=false; should drop field", | ||||
| 			feature:  false, | ||||
| 			pvc:      withAllocatedResource("5G"), | ||||
| 			oldPVC:   nil, | ||||
| 			expected: getPVC(), | ||||
| 			name:                                "for:newPVC=hasAllocatedResource,oldPVC=nil,featuregate=false; should drop field", | ||||
| 			enableRecoverVolumeExpansionFailure: false, | ||||
| 			enableVolumeAttributesClass:         false, | ||||
| 			pvc:                                 withAllocatedResource("5G"), | ||||
| 			oldPVC:                              nil, | ||||
| 			expected:                            getPVC(), | ||||
| 		}, | ||||
| 		{ | ||||
| 			name:     "for:newPVC=hasResizeStatus,oldPVC=nil, featuregate=false should drop field", | ||||
| 			feature:  false, | ||||
| 			pvc:      withResizeStatus(core.PersistentVolumeClaimNodeResizeFailed), | ||||
| 			oldPVC:   nil, | ||||
| 			expected: getPVC(), | ||||
| 			name:                                "for:newPVC=hasResizeStatus,oldPVC=nil, featuregate=false should drop field", | ||||
| 			enableRecoverVolumeExpansionFailure: false, | ||||
| 			enableVolumeAttributesClass:         false, | ||||
| 			pvc:                                 withResizeStatus(core.PersistentVolumeClaimNodeResizeFailed), | ||||
| 			oldPVC:                              nil, | ||||
| 			expected:                            getPVC(), | ||||
| 		}, | ||||
| 		{ | ||||
| 			name:     "for:newPVC=hasResizeStatus,oldPVC=doesnot,featuregate=true; should keep field", | ||||
| 			feature:  true, | ||||
| 			pvc:      withResizeStatus(core.PersistentVolumeClaimNodeResizeFailed), | ||||
| 			oldPVC:   getPVC(), | ||||
| 			expected: withResizeStatus(core.PersistentVolumeClaimNodeResizeFailed), | ||||
| 			name:                                "for:newPVC=hasResizeStatus,oldPVC=doesnot,featuregate=RecoverVolumeExpansionFailure=true; should keep field", | ||||
| 			enableRecoverVolumeExpansionFailure: true, | ||||
| 			enableVolumeAttributesClass:         false, | ||||
| 			pvc:                                 withResizeStatus(core.PersistentVolumeClaimNodeResizeFailed), | ||||
| 			oldPVC:                              getPVC(), | ||||
| 			expected:                            withResizeStatus(core.PersistentVolumeClaimNodeResizeFailed), | ||||
| 		}, | ||||
| 		{ | ||||
| 			name:     "for:newPVC=hasResizeStatus,oldPVC=hasResizeStatus,featuregate=true; should keep field", | ||||
| 			feature:  true, | ||||
| 			pvc:      withResizeStatus(core.PersistentVolumeClaimNodeResizeFailed), | ||||
| 			oldPVC:   withResizeStatus(core.PersistentVolumeClaimNodeResizeFailed), | ||||
| 			expected: withResizeStatus(core.PersistentVolumeClaimNodeResizeFailed), | ||||
| 			name:                                "for:newPVC=hasResizeStatus,oldPVC=hasResizeStatus,featuregate=RecoverVolumeExpansionFailure=true; should keep field", | ||||
| 			enableRecoverVolumeExpansionFailure: true, | ||||
| 			enableVolumeAttributesClass:         false, | ||||
| 			pvc:                                 withResizeStatus(core.PersistentVolumeClaimNodeResizeFailed), | ||||
| 			oldPVC:                              withResizeStatus(core.PersistentVolumeClaimNodeResizeFailed), | ||||
| 			expected:                            withResizeStatus(core.PersistentVolumeClaimNodeResizeFailed), | ||||
| 		}, | ||||
| 		{ | ||||
| 			name:     "for:newPVC=hasResizeStatus,oldPVC=hasResizeStatus,featuregate=false; should keep field", | ||||
| 			feature:  false, | ||||
| 			pvc:      withResizeStatus(core.PersistentVolumeClaimNodeResizeFailed), | ||||
| 			oldPVC:   withResizeStatus(core.PersistentVolumeClaimNodeResizeFailed), | ||||
| 			expected: withResizeStatus(core.PersistentVolumeClaimNodeResizeFailed), | ||||
| 			name:                                "for:newPVC=hasResizeStatus,oldPVC=hasResizeStatus,featuregate=false; should keep field", | ||||
| 			enableRecoverVolumeExpansionFailure: false, | ||||
| 			enableVolumeAttributesClass:         false, | ||||
| 			pvc:                                 withResizeStatus(core.PersistentVolumeClaimNodeResizeFailed), | ||||
| 			oldPVC:                              withResizeStatus(core.PersistentVolumeClaimNodeResizeFailed), | ||||
| 			expected:                            withResizeStatus(core.PersistentVolumeClaimNodeResizeFailed), | ||||
| 		}, | ||||
| 		{ | ||||
| 			name:                                "for:newPVC=hasVolumeAttributeClass,oldPVC=nil, featuregate=false should drop field", | ||||
| 			enableRecoverVolumeExpansionFailure: false, | ||||
| 			enableVolumeAttributesClass:         false, | ||||
| 			pvc:                                 withVolumeAttributesClassName("foo"), | ||||
| 			oldPVC:                              nil, | ||||
| 			expected:                            getPVC(), | ||||
| 		}, | ||||
| 		{ | ||||
| 			name:                                "for:newPVC=hasVolumeAttributeClass,oldPVC=doesnot,featuregate=VolumeAttributesClass=true; should keep field", | ||||
| 			enableRecoverVolumeExpansionFailure: false, | ||||
| 			enableVolumeAttributesClass:         true, | ||||
| 			pvc:                                 withVolumeAttributesClassName("foo"), | ||||
| 			oldPVC:                              getPVC(), | ||||
| 			expected:                            withVolumeAttributesClassName("foo"), | ||||
| 		}, | ||||
| 		{ | ||||
| 			name:                                "for:newPVC=hasVolumeAttributeClass,oldPVC=hasVolumeAttributeClass,featuregate=VolumeAttributesClass=true; should keep field", | ||||
| 			enableRecoverVolumeExpansionFailure: false, | ||||
| 			enableVolumeAttributesClass:         true, | ||||
| 			pvc:                                 withVolumeAttributesClassName("foo"), | ||||
| 			oldPVC:                              withVolumeAttributesClassName("foo"), | ||||
| 			expected:                            withVolumeAttributesClassName("foo"), | ||||
| 		}, | ||||
| 		{ | ||||
| 			name:                                "for:newPVC=hasVolumeAttributeClass,oldPVC=hasVolumeAttributeClass,featuregate=false; should keep field", | ||||
| 			enableRecoverVolumeExpansionFailure: false, | ||||
| 			enableVolumeAttributesClass:         false, | ||||
| 			pvc:                                 withVolumeAttributesClassName("foo"), | ||||
| 			oldPVC:                              withVolumeAttributesClassName("foo"), | ||||
| 			expected:                            withVolumeAttributesClassName("foo"), | ||||
| 		}, | ||||
| 		{ | ||||
| 			name:                                "for:newPVC=hasVolumeAttributesModifyStatus,oldPVC=nil, featuregate=false should drop field", | ||||
| 			enableRecoverVolumeExpansionFailure: false, | ||||
| 			enableVolumeAttributesClass:         false, | ||||
| 			pvc:                                 withVolumeAttributesModifyStatus("bar", core.PersistentVolumeClaimModifyVolumePending), | ||||
| 			oldPVC:                              nil, | ||||
| 			expected:                            getPVC(), | ||||
| 		}, | ||||
| 		{ | ||||
| 			name:                                "for:newPVC=hasVolumeAttributesModifyStatus,oldPVC=doesnot,featuregate=VolumeAttributesClass=true; should keep field", | ||||
| 			enableRecoverVolumeExpansionFailure: false, | ||||
| 			enableVolumeAttributesClass:         true, | ||||
| 			pvc:                                 withVolumeAttributesModifyStatus("bar", core.PersistentVolumeClaimModifyVolumePending), | ||||
| 			oldPVC:                              getPVC(), | ||||
| 			expected:                            withVolumeAttributesModifyStatus("bar", core.PersistentVolumeClaimModifyVolumePending), | ||||
| 		}, | ||||
| 		{ | ||||
| 			name:                                "for:newPVC=hasVolumeAttributesModifyStatus,oldPVC=hasVolumeAttributesModifyStatus,featuregate=VolumeAttributesClass=true; should keep field", | ||||
| 			enableRecoverVolumeExpansionFailure: false, | ||||
| 			enableVolumeAttributesClass:         true, | ||||
| 			pvc:                                 withVolumeAttributesModifyStatus("bar", core.PersistentVolumeClaimModifyVolumePending), | ||||
| 			oldPVC:                              withVolumeAttributesModifyStatus("bar", core.PersistentVolumeClaimModifyVolumePending), | ||||
| 			expected:                            withVolumeAttributesModifyStatus("bar", core.PersistentVolumeClaimModifyVolumePending), | ||||
| 		}, | ||||
| 		{ | ||||
| 			name:                                "for:newPVC=hasVolumeAttributesModifyStatus,oldPVC=hasVolumeAttributesModifyStatus,featuregate=false; should keep field", | ||||
| 			enableRecoverVolumeExpansionFailure: false, | ||||
| 			enableVolumeAttributesClass:         false, | ||||
| 			pvc:                                 withVolumeAttributesModifyStatus("bar", core.PersistentVolumeClaimModifyVolumePending), | ||||
| 			oldPVC:                              withVolumeAttributesModifyStatus("bar", core.PersistentVolumeClaimModifyVolumePending), | ||||
| 			expected:                            withVolumeAttributesModifyStatus("bar", core.PersistentVolumeClaimModifyVolumePending), | ||||
| 		}, | ||||
| 	} | ||||
|  | ||||
| 	for _, test := range tests { | ||||
| 		t.Run(test.name, func(t *testing.T) { | ||||
| 			defer featuregatetesting.SetFeatureGateDuringTest(t, utilfeature.DefaultFeatureGate, features.RecoverVolumeExpansionFailure, test.feature)() | ||||
| 			defer featuregatetesting.SetFeatureGateDuringTest(t, utilfeature.DefaultFeatureGate, features.RecoverVolumeExpansionFailure, test.enableRecoverVolumeExpansionFailure)() | ||||
| 			defer featuregatetesting.SetFeatureGateDuringTest(t, utilfeature.DefaultFeatureGate, features.VolumeAttributesClass, test.enableVolumeAttributesClass)() | ||||
|  | ||||
| 			DropDisabledFieldsFromStatus(test.pvc, test.oldPVC) | ||||
|  | ||||
| @@ -494,6 +630,25 @@ func withResizeStatus(status core.ClaimResourceStatus) *core.PersistentVolumeCla | ||||
| 	} | ||||
| } | ||||
|  | ||||
| func withVolumeAttributesClassName(vacName string) *core.PersistentVolumeClaim { | ||||
| 	return &core.PersistentVolumeClaim{ | ||||
| 		Status: core.PersistentVolumeClaimStatus{ | ||||
| 			CurrentVolumeAttributesClassName: &vacName, | ||||
| 		}, | ||||
| 	} | ||||
| } | ||||
|  | ||||
| func withVolumeAttributesModifyStatus(target string, status core.PersistentVolumeClaimModifyVolumeStatus) *core.PersistentVolumeClaim { | ||||
| 	return &core.PersistentVolumeClaim{ | ||||
| 		Status: core.PersistentVolumeClaimStatus{ | ||||
| 			ModifyVolumeStatus: &core.ModifyVolumeStatus{ | ||||
| 				TargetVolumeAttributesClassName: target, | ||||
| 				Status:                          status, | ||||
| 			}, | ||||
| 		}, | ||||
| 	} | ||||
| } | ||||
|  | ||||
| func TestWarnings(t *testing.T) { | ||||
| 	testcases := []struct { | ||||
| 		name     string | ||||
|   | ||||
| @@ -335,6 +335,16 @@ type PersistentVolumeSpec struct { | ||||
| 	// This field influences the scheduling of pods that use this volume. | ||||
| 	// +optional | ||||
| 	NodeAffinity *VolumeNodeAffinity | ||||
| 	// Name of VolumeAttributesClass to which this persistent volume belongs. Empty value | ||||
| 	// is not allowed. When this field is not set, it indicates that this volume does not belong to any | ||||
| 	// VolumeAttributesClass. This field is mutable and can be changed by the CSI driver | ||||
| 	// after a volume has been updated successfully to a new class. | ||||
| 	// For an unbound PersistentVolume, the volumeAttributesClassName will be matched with unbound | ||||
| 	// PersistentVolumeClaims during the binding process. | ||||
| 	// This is an alpha field and requires enabling VolumeAttributesClass feature. | ||||
| 	// +featureGate=VolumeAttributesClass | ||||
| 	// +optional | ||||
| 	VolumeAttributesClassName *string | ||||
| } | ||||
|  | ||||
| // VolumeNodeAffinity defines constraints that limit what nodes this volume can be accessed from. | ||||
| @@ -488,6 +498,21 @@ type PersistentVolumeClaimSpec struct { | ||||
| 	// (Alpha) Using the namespace field of dataSourceRef requires the CrossNamespaceVolumeDataSource feature gate to be enabled. | ||||
| 	// +optional | ||||
| 	DataSourceRef *TypedObjectReference | ||||
| 	// volumeAttributesClassName may be used to set the VolumeAttributesClass used by this claim. | ||||
| 	// If specified, the CSI driver will create or update the volume with the attributes defined | ||||
| 	// in the corresponding VolumeAttributesClass. This has a different purpose than storageClassName, | ||||
| 	// it can be changed after the claim is created. An empty string value means that no VolumeAttributesClass | ||||
| 	// will be applied to the claim but it's not allowed to reset this field to empty string once it is set. | ||||
| 	// If unspecified and the PersistentVolumeClaim is unbound, the default VolumeAttributesClass | ||||
| 	// will be set by the persistentvolume controller if it exists. | ||||
| 	// If the resource referred to by volumeAttributesClass does not exist, this PersistentVolumeClaim will be | ||||
| 	// set to a Pending state, as reflected by the modifyVolumeStatus field, until such as a resource | ||||
| 	// exists. | ||||
| 	// More info: https://kubernetes.io/docs/concepts/storage/persistent-volumes#volumeattributesclass | ||||
| 	// (Alpha) Using this field requires the VolumeAttributesClass feature gate to be enabled. | ||||
| 	// +featureGate=VolumeAttributesClass | ||||
| 	// +optional | ||||
| 	VolumeAttributesClassName *string | ||||
| } | ||||
|  | ||||
| type TypedObjectReference struct { | ||||
| @@ -518,6 +543,11 @@ const ( | ||||
| 	PersistentVolumeClaimResizing PersistentVolumeClaimConditionType = "Resizing" | ||||
| 	// PersistentVolumeClaimFileSystemResizePending - controller resize is finished and a file system resize is pending on node | ||||
| 	PersistentVolumeClaimFileSystemResizePending PersistentVolumeClaimConditionType = "FileSystemResizePending" | ||||
|  | ||||
| 	// Applying the target VolumeAttributesClass encountered an error | ||||
| 	PersistentVolumeClaimVolumeModifyVolumeError PersistentVolumeClaimConditionType = "ModifyVolumeError" | ||||
| 	// Volume is being modified | ||||
| 	PersistentVolumeClaimVolumeModifyingVolume PersistentVolumeClaimConditionType = "ModifyingVolume" | ||||
| ) | ||||
|  | ||||
| // +enum | ||||
| @@ -544,6 +574,38 @@ const ( | ||||
| 	PersistentVolumeClaimNodeResizeFailed ClaimResourceStatus = "NodeResizeFailed" | ||||
| ) | ||||
|  | ||||
| // +enum | ||||
| // New statuses can be added in the future. Consumers should check for unknown statuses and fail appropriately | ||||
| type PersistentVolumeClaimModifyVolumeStatus string | ||||
|  | ||||
| const ( | ||||
| 	// Pending indicates that the PersistentVolumeClaim cannot be modified due to unmet requirements, such as | ||||
| 	// the specified VolumeAttributesClass not existing | ||||
| 	PersistentVolumeClaimModifyVolumePending PersistentVolumeClaimModifyVolumeStatus = "Pending" | ||||
| 	// InProgress indicates that the volume is being modified | ||||
| 	PersistentVolumeClaimModifyVolumeInProgress PersistentVolumeClaimModifyVolumeStatus = "InProgress" | ||||
| 	// Infeasible indicates that the request has been rejected as invalid by the CSI driver. To | ||||
| 	// resolve the error, a valid VolumeAttributesClass needs to be specified | ||||
| 	PersistentVolumeClaimModifyVolumeInfeasible PersistentVolumeClaimModifyVolumeStatus = "Infeasible" | ||||
| ) | ||||
|  | ||||
| // ModifyVolumeStatus represents the status object of ControllerModifyVolume operation | ||||
| type ModifyVolumeStatus struct { | ||||
| 	// targetVolumeAttributesClassName is the name of the VolumeAttributesClass the PVC currently being reconciled | ||||
| 	TargetVolumeAttributesClassName string | ||||
| 	// status is the status of the ControllerModifyVolume operation. It can be in any of following states: | ||||
| 	//  - Pending | ||||
| 	//    Pending indicates that the PersistentVolumeClaim cannot be modified due to unmet requirements, such as | ||||
| 	//    the specified VolumeAttributesClass not existing. | ||||
| 	//  - InProgress | ||||
| 	//    InProgress indicates that the volume is being modified. | ||||
| 	//  - Infeasible | ||||
| 	//   Infeasible indicates that the request has been rejected as invalid by the CSI driver. To | ||||
| 	// 	  resolve the error, a valid VolumeAttributesClass needs to be specified. | ||||
| 	// Note: New statuses can be added in the future. Consumers should check for unknown statuses and fail appropriately. | ||||
| 	Status PersistentVolumeClaimModifyVolumeStatus | ||||
| } | ||||
|  | ||||
| // PersistentVolumeClaimCondition represents the current condition of PV claim | ||||
| type PersistentVolumeClaimCondition struct { | ||||
| 	Type   PersistentVolumeClaimConditionType | ||||
| @@ -635,6 +697,18 @@ type PersistentVolumeClaimStatus struct { | ||||
| 	// +mapType=granular | ||||
| 	// +optional | ||||
| 	AllocatedResourceStatuses map[ResourceName]ClaimResourceStatus | ||||
| 	// currentVolumeAttributesClassName is the current name of the VolumeAttributesClass the PVC is using. | ||||
| 	// When unset, there is no VolumeAttributeClass applied to this PersistentVolumeClaim | ||||
| 	// This is an alpha field and requires enabling VolumeAttributesClass feature. | ||||
| 	// +featureGate=VolumeAttributesClass | ||||
| 	// +optional | ||||
| 	CurrentVolumeAttributesClassName *string | ||||
| 	// ModifyVolumeStatus represents the status object of ControllerModifyVolume operation. | ||||
| 	// When this is unset, there is no ModifyVolume operation being attempted. | ||||
| 	// This is an alpha field and requires enabling VolumeAttributesClass feature. | ||||
| 	// +featureGate=VolumeAttributesClass | ||||
| 	// +optional | ||||
| 	ModifyVolumeStatus *ModifyVolumeStatus | ||||
| } | ||||
|  | ||||
| // PersistentVolumeAccessMode defines various access modes for PV. | ||||
|   | ||||
| @@ -1654,6 +1654,8 @@ var allowedTemplateObjectMetaFields = map[string]bool{ | ||||
|  | ||||
| // PersistentVolumeSpecValidationOptions contains the different settings for PeristentVolume validation | ||||
| type PersistentVolumeSpecValidationOptions struct { | ||||
| 	// Allow users to modify the class of volume attributes | ||||
| 	EnableVolumeAttributesClass bool | ||||
| } | ||||
|  | ||||
| // ValidatePersistentVolumeName checks that a name is appropriate for a | ||||
| @@ -1667,7 +1669,13 @@ var supportedReclaimPolicy = sets.NewString(string(core.PersistentVolumeReclaimD | ||||
| var supportedVolumeModes = sets.NewString(string(core.PersistentVolumeBlock), string(core.PersistentVolumeFilesystem)) | ||||
|  | ||||
| func ValidationOptionsForPersistentVolume(pv, oldPv *core.PersistentVolume) PersistentVolumeSpecValidationOptions { | ||||
| 	return PersistentVolumeSpecValidationOptions{} | ||||
| 	opts := PersistentVolumeSpecValidationOptions{ | ||||
| 		EnableVolumeAttributesClass: utilfeature.DefaultMutableFeatureGate.Enabled(features.VolumeAttributesClass), | ||||
| 	} | ||||
| 	if oldPv != nil && oldPv.Spec.VolumeAttributesClassName != nil { | ||||
| 		opts.EnableVolumeAttributesClass = true | ||||
| 	} | ||||
| 	return opts | ||||
| } | ||||
|  | ||||
| func ValidatePersistentVolumeSpec(pvSpec *core.PersistentVolumeSpec, pvName string, validateInlinePersistentVolumeSpec bool, fldPath *field.Path, opts PersistentVolumeSpecValidationOptions) field.ErrorList { | ||||
| @@ -1952,6 +1960,18 @@ func ValidatePersistentVolumeSpec(pvSpec *core.PersistentVolumeSpec, pvName stri | ||||
| 			} | ||||
| 		} | ||||
| 	} | ||||
| 	if pvSpec.VolumeAttributesClassName != nil && opts.EnableVolumeAttributesClass { | ||||
| 		if len(*pvSpec.VolumeAttributesClassName) == 0 { | ||||
| 			allErrs = append(allErrs, field.Required(fldPath.Child("volumeAttributesClassName"), "an empty string is disallowed")) | ||||
| 		} else { | ||||
| 			for _, msg := range ValidateClassName(*pvSpec.VolumeAttributesClassName, false) { | ||||
| 				allErrs = append(allErrs, field.Invalid(fldPath.Child("volumeAttributesClassName"), *pvSpec.VolumeAttributesClassName, msg)) | ||||
| 			} | ||||
| 		} | ||||
| 		if pvSpec.CSI == nil { | ||||
| 			allErrs = append(allErrs, field.Required(fldPath.Child("csi"), "has to be specified when using volumeAttributesClassName")) | ||||
| 		} | ||||
| 	} | ||||
| 	return allErrs | ||||
| } | ||||
|  | ||||
| @@ -1986,6 +2006,17 @@ func ValidatePersistentVolumeUpdate(newPv, oldPv *core.PersistentVolume, opts Pe | ||||
| 		allErrs = append(allErrs, validatePvNodeAffinity(newPv.Spec.NodeAffinity, oldPv.Spec.NodeAffinity, field.NewPath("nodeAffinity"))...) | ||||
| 	} | ||||
|  | ||||
| 	if !apiequality.Semantic.DeepEqual(oldPv.Spec.VolumeAttributesClassName, newPv.Spec.VolumeAttributesClassName) { | ||||
| 		if !utilfeature.DefaultFeatureGate.Enabled(features.VolumeAttributesClass) { | ||||
| 			allErrs = append(allErrs, field.Forbidden(field.NewPath("spec", "volumeAttributesClassName"), "update is forbidden when the VolumeAttributesClass feature gate is disabled")) | ||||
| 		} | ||||
| 		if opts.EnableVolumeAttributesClass { | ||||
| 			if oldPv.Spec.VolumeAttributesClassName != nil && newPv.Spec.VolumeAttributesClassName == nil { | ||||
| 				allErrs = append(allErrs, field.Forbidden(field.NewPath("spec", "volumeAttributesClassName"), "update from non-nil value to nil is forbidden")) | ||||
| 			} | ||||
| 		} | ||||
| 	} | ||||
|  | ||||
| 	return allErrs | ||||
| } | ||||
|  | ||||
| @@ -2005,12 +2036,15 @@ type PersistentVolumeClaimSpecValidationOptions struct { | ||||
| 	AllowInvalidLabelValueInSelector bool | ||||
| 	// Allow to validate the API group of the data source and data source reference | ||||
| 	AllowInvalidAPIGroupInDataSourceOrRef bool | ||||
| 	// Allow users to modify the class of volume attributes | ||||
| 	EnableVolumeAttributesClass bool | ||||
| } | ||||
|  | ||||
| func ValidationOptionsForPersistentVolumeClaim(pvc, oldPvc *core.PersistentVolumeClaim) PersistentVolumeClaimSpecValidationOptions { | ||||
| 	opts := PersistentVolumeClaimSpecValidationOptions{ | ||||
| 		EnableRecoverFromExpansionFailure: utilfeature.DefaultFeatureGate.Enabled(features.RecoverVolumeExpansionFailure), | ||||
| 		AllowInvalidLabelValueInSelector:  false, | ||||
| 		EnableVolumeAttributesClass:       utilfeature.DefaultFeatureGate.Enabled(features.VolumeAttributesClass), | ||||
| 	} | ||||
| 	if oldPvc == nil { | ||||
| 		// If there's no old PVC, use the options based solely on feature enablement | ||||
| @@ -2020,6 +2054,11 @@ func ValidationOptionsForPersistentVolumeClaim(pvc, oldPvc *core.PersistentVolum | ||||
| 	// If the old object had an invalid API group in the data source or data source reference, continue to allow it in the new object | ||||
| 	opts.AllowInvalidAPIGroupInDataSourceOrRef = allowInvalidAPIGroupInDataSourceOrRef(&oldPvc.Spec) | ||||
|  | ||||
| 	if oldPvc.Spec.VolumeAttributesClassName != nil { | ||||
| 		// If the old object had a volume attributes class, continue to validate it in the new object. | ||||
| 		opts.EnableVolumeAttributesClass = true | ||||
| 	} | ||||
|  | ||||
| 	labelSelectorValidationOpts := unversionedvalidation.LabelSelectorValidationOptions{ | ||||
| 		AllowInvalidLabelValueInSelector: opts.AllowInvalidLabelValueInSelector, | ||||
| 	} | ||||
| @@ -2038,6 +2077,7 @@ func ValidationOptionsForPersistentVolumeClaim(pvc, oldPvc *core.PersistentVolum | ||||
| func ValidationOptionsForPersistentVolumeClaimTemplate(claimTemplate, oldClaimTemplate *core.PersistentVolumeClaimTemplate) PersistentVolumeClaimSpecValidationOptions { | ||||
| 	opts := PersistentVolumeClaimSpecValidationOptions{ | ||||
| 		AllowInvalidLabelValueInSelector: false, | ||||
| 		EnableVolumeAttributesClass:      utilfeature.DefaultFeatureGate.Enabled(features.VolumeAttributesClass), | ||||
| 	} | ||||
| 	if oldClaimTemplate == nil { | ||||
| 		// If there's no old PVC template, use the options based solely on feature enablement | ||||
| @@ -2193,6 +2233,11 @@ func ValidatePersistentVolumeClaimSpec(spec *core.PersistentVolumeClaimSpec, fld | ||||
| 				"must match dataSourceRef")) | ||||
| 		} | ||||
| 	} | ||||
| 	if spec.VolumeAttributesClassName != nil && len(*spec.VolumeAttributesClassName) > 0 && opts.EnableVolumeAttributesClass { | ||||
| 		for _, msg := range ValidateClassName(*spec.VolumeAttributesClassName, false) { | ||||
| 			allErrs = append(allErrs, field.Invalid(fldPath.Child("volumeAttributesClassName"), *spec.VolumeAttributesClassName, msg)) | ||||
| 		} | ||||
| 	} | ||||
|  | ||||
| 	return allErrs | ||||
| } | ||||
| @@ -2236,6 +2281,8 @@ func ValidatePersistentVolumeClaimUpdate(newPvc, oldPvc *core.PersistentVolumeCl | ||||
| 	if newPvc.Status.Phase == core.ClaimBound && newPvcClone.Spec.Resources.Requests != nil { | ||||
| 		newPvcClone.Spec.Resources.Requests["storage"] = oldPvc.Spec.Resources.Requests["storage"] // +k8s:verify-mutation:reason=clone | ||||
| 	} | ||||
| 	// lets make sure volume attributes class name is same. | ||||
| 	newPvcClone.Spec.VolumeAttributesClassName = oldPvcClone.Spec.VolumeAttributesClassName // +k8s:verify-mutation:reason=clone | ||||
|  | ||||
| 	oldSize := oldPvc.Spec.Resources.Requests["storage"] | ||||
| 	newSize := newPvc.Spec.Resources.Requests["storage"] | ||||
| @@ -2243,7 +2290,7 @@ func ValidatePersistentVolumeClaimUpdate(newPvc, oldPvc *core.PersistentVolumeCl | ||||
|  | ||||
| 	if !apiequality.Semantic.DeepEqual(newPvcClone.Spec, oldPvcClone.Spec) { | ||||
| 		specDiff := cmp.Diff(oldPvcClone.Spec, newPvcClone.Spec) | ||||
| 		allErrs = append(allErrs, field.Forbidden(field.NewPath("spec"), fmt.Sprintf("spec is immutable after creation except resources.requests for bound claims\n%v", specDiff))) | ||||
| 		allErrs = append(allErrs, field.Forbidden(field.NewPath("spec"), fmt.Sprintf("spec is immutable after creation except resources.requests and volumeAttributesClassName for bound claims\n%v", specDiff))) | ||||
| 	} | ||||
| 	if newSize.Cmp(oldSize) < 0 { | ||||
| 		if !opts.EnableRecoverFromExpansionFailure { | ||||
| @@ -2260,6 +2307,21 @@ func ValidatePersistentVolumeClaimUpdate(newPvc, oldPvc *core.PersistentVolumeCl | ||||
|  | ||||
| 	allErrs = append(allErrs, ValidateImmutableField(newPvc.Spec.VolumeMode, oldPvc.Spec.VolumeMode, field.NewPath("volumeMode"))...) | ||||
|  | ||||
| 	if !apiequality.Semantic.DeepEqual(oldPvc.Spec.VolumeAttributesClassName, newPvc.Spec.VolumeAttributesClassName) { | ||||
| 		if !utilfeature.DefaultFeatureGate.Enabled(features.VolumeAttributesClass) { | ||||
| 			allErrs = append(allErrs, field.Forbidden(field.NewPath("spec", "volumeAttributesClassName"), "update is forbidden when the VolumeAttributesClass feature gate is disabled")) | ||||
| 		} | ||||
| 		if opts.EnableVolumeAttributesClass { | ||||
| 			if oldPvc.Spec.VolumeAttributesClassName != nil { | ||||
| 				if newPvc.Spec.VolumeAttributesClassName == nil { | ||||
| 					allErrs = append(allErrs, field.Forbidden(field.NewPath("spec", "volumeAttributesClassName"), "update from non-nil value to nil is forbidden")) | ||||
| 				} else if len(*newPvc.Spec.VolumeAttributesClassName) == 0 { | ||||
| 					allErrs = append(allErrs, field.Forbidden(field.NewPath("spec", "volumeAttributesClassName"), "update from non-nil value to an empty string is forbidden")) | ||||
| 				} | ||||
| 			} | ||||
| 		} | ||||
| 	} | ||||
|  | ||||
| 	return allErrs | ||||
| } | ||||
|  | ||||
|   | ||||
| @@ -46,6 +46,7 @@ import ( | ||||
| 	"k8s.io/kubernetes/pkg/capabilities" | ||||
| 	"k8s.io/kubernetes/pkg/features" | ||||
| 	utilpointer "k8s.io/utils/pointer" | ||||
| 	"k8s.io/utils/ptr" | ||||
| ) | ||||
|  | ||||
| const ( | ||||
| @@ -109,8 +110,9 @@ func TestValidatePersistentVolumes(t *testing.T) { | ||||
| 	validMode := core.PersistentVolumeFilesystem | ||||
| 	invalidMode := core.PersistentVolumeMode("fakeVolumeMode") | ||||
| 	scenarios := map[string]struct { | ||||
| 		isExpectedFailure bool | ||||
| 		volume            *core.PersistentVolume | ||||
| 		isExpectedFailure           bool | ||||
| 		enableVolumeAttributesClass bool | ||||
| 		volume                      *core.PersistentVolume | ||||
| 	}{ | ||||
| 		"good-volume": { | ||||
| 			isExpectedFailure: false, | ||||
| @@ -478,10 +480,84 @@ func TestValidatePersistentVolumes(t *testing.T) { | ||||
| 					}, | ||||
| 				}), | ||||
| 		}, | ||||
| 		"invalid-volume-attributes-class-name": { | ||||
| 			isExpectedFailure:           true, | ||||
| 			enableVolumeAttributesClass: true, | ||||
| 			volume: testVolume("invalid-volume-attributes-class-name", "", 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:          "invalid", | ||||
| 				VolumeAttributesClassName: ptr.To("-invalid-"), | ||||
| 			}), | ||||
| 		}, | ||||
| 		"invalid-empty-volume-attributes-class-name": { | ||||
| 			isExpectedFailure:           true, | ||||
| 			enableVolumeAttributesClass: true, | ||||
| 			volume: testVolume("invalid-empty-volume-attributes-class-name", "", 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:          "invalid", | ||||
| 				VolumeAttributesClassName: ptr.To(""), | ||||
| 			}), | ||||
| 		}, | ||||
| 		"volume-with-good-volume-attributes-class-and-matched-volume-resource-when-feature-gate-is-on": { | ||||
| 			isExpectedFailure:           false, | ||||
| 			enableVolumeAttributesClass: true, | ||||
| 			volume: testVolume("foo", "", core.PersistentVolumeSpec{ | ||||
| 				Capacity: core.ResourceList{ | ||||
| 					core.ResourceName(core.ResourceStorage): resource.MustParse("10G"), | ||||
| 				}, | ||||
| 				AccessModes: []core.PersistentVolumeAccessMode{core.ReadWriteOnce}, | ||||
| 				PersistentVolumeSource: core.PersistentVolumeSource{ | ||||
| 					CSI: &core.CSIPersistentVolumeSource{ | ||||
| 						Driver:       "test-driver", | ||||
| 						VolumeHandle: "test-123", | ||||
| 					}, | ||||
| 				}, | ||||
| 				StorageClassName:          "valid", | ||||
| 				VolumeAttributesClassName: ptr.To("valid"), | ||||
| 			}), | ||||
| 		}, | ||||
| 		"volume-with-good-volume-attributes-class-and-mismatched-volume-resource-when-feature-gate-is-on": { | ||||
| 			isExpectedFailure:           true, | ||||
| 			enableVolumeAttributesClass: 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", | ||||
| 				VolumeAttributesClassName: ptr.To("valid"), | ||||
| 			}), | ||||
| 		}, | ||||
| 	} | ||||
|  | ||||
| 	for name, scenario := range scenarios { | ||||
| 		t.Run(name, func(t *testing.T) { | ||||
| 			defer featuregatetesting.SetFeatureGateDuringTest(t, utilfeature.DefaultFeatureGate, features.VolumeAttributesClass, scenario.enableVolumeAttributesClass)() | ||||
|  | ||||
| 			opts := ValidationOptionsForPersistentVolume(scenario.volume, nil) | ||||
| 			errs := ValidatePersistentVolume(scenario.volume, opts) | ||||
| 			if len(errs) == 0 && scenario.isExpectedFailure { | ||||
| @@ -882,17 +958,48 @@ func TestValidatePersistentVolumeSourceUpdate(t *testing.T) { | ||||
|  | ||||
| func TestValidationOptionsForPersistentVolume(t *testing.T) { | ||||
| 	tests := map[string]struct { | ||||
| 		oldPv                *core.PersistentVolume | ||||
| 		expectValidationOpts PersistentVolumeSpecValidationOptions | ||||
| 		oldPv                       *core.PersistentVolume | ||||
| 		enableVolumeAttributesClass bool | ||||
| 		expectValidationOpts        PersistentVolumeSpecValidationOptions | ||||
| 	}{ | ||||
| 		"nil old pv": { | ||||
| 			oldPv:                nil, | ||||
| 			expectValidationOpts: PersistentVolumeSpecValidationOptions{}, | ||||
| 		}, | ||||
| 		"nil old pv and feature-gate VolumeAttrributesClass is on": { | ||||
| 			oldPv:                       nil, | ||||
| 			enableVolumeAttributesClass: true, | ||||
| 			expectValidationOpts:        PersistentVolumeSpecValidationOptions{EnableVolumeAttributesClass: true}, | ||||
| 		}, | ||||
| 		"nil old pv and feature-gate VolumeAttrributesClass is off": { | ||||
| 			oldPv:                       nil, | ||||
| 			enableVolumeAttributesClass: false, | ||||
| 			expectValidationOpts:        PersistentVolumeSpecValidationOptions{EnableVolumeAttributesClass: false}, | ||||
| 		}, | ||||
| 		"old pv has volumeAttributesClass and feature-gate VolumeAttrributesClass is on": { | ||||
| 			oldPv: &core.PersistentVolume{ | ||||
| 				Spec: core.PersistentVolumeSpec{ | ||||
| 					VolumeAttributesClassName: ptr.To("foo"), | ||||
| 				}, | ||||
| 			}, | ||||
| 			enableVolumeAttributesClass: true, | ||||
| 			expectValidationOpts:        PersistentVolumeSpecValidationOptions{EnableVolumeAttributesClass: true}, | ||||
| 		}, | ||||
| 		"old pv has volumeAttributesClass and feature-gate VolumeAttrributesClass is off": { | ||||
| 			oldPv: &core.PersistentVolume{ | ||||
| 				Spec: core.PersistentVolumeSpec{ | ||||
| 					VolumeAttributesClassName: ptr.To("foo"), | ||||
| 				}, | ||||
| 			}, | ||||
| 			enableVolumeAttributesClass: false, | ||||
| 			expectValidationOpts:        PersistentVolumeSpecValidationOptions{EnableVolumeAttributesClass: true}, | ||||
| 		}, | ||||
| 	} | ||||
|  | ||||
| 	for name, tc := range tests { | ||||
| 		t.Run(name, func(t *testing.T) { | ||||
| 			defer featuregatetesting.SetFeatureGateDuringTest(t, utilfeature.DefaultFeatureGate, features.VolumeAttributesClass, tc.enableVolumeAttributesClass)() | ||||
|  | ||||
| 			opts := ValidationOptionsForPersistentVolume(nil, tc.oldPv) | ||||
| 			if opts != tc.expectValidationOpts { | ||||
| 				t.Errorf("Expected opts: %+v, received: %+v", opts, tc.expectValidationOpts) | ||||
| @@ -919,6 +1026,14 @@ func getCSIVolumeWithSecret(pv *core.PersistentVolume, secret *core.SecretRefere | ||||
| 	return pvCopy | ||||
| } | ||||
|  | ||||
| func pvcWithVolumeAttributesClassName(vacName *string) *core.PersistentVolumeClaim { | ||||
| 	return &core.PersistentVolumeClaim{ | ||||
| 		Spec: core.PersistentVolumeClaimSpec{ | ||||
| 			VolumeAttributesClassName: vacName, | ||||
| 		}, | ||||
| 	} | ||||
| } | ||||
|  | ||||
| func pvcWithDataSource(dataSource *core.TypedLocalObjectReference) *core.PersistentVolumeClaim { | ||||
| 	return &core.PersistentVolumeClaim{ | ||||
| 		Spec: core.PersistentVolumeClaimSpec{ | ||||
| @@ -934,6 +1049,14 @@ func pvcWithDataSourceRef(ref *core.TypedObjectReference) *core.PersistentVolume | ||||
| 	} | ||||
| } | ||||
|  | ||||
| func pvcTemplateWithVolumeAttributesClassName(vacName *string) *core.PersistentVolumeClaimTemplate { | ||||
| 	return &core.PersistentVolumeClaimTemplate{ | ||||
| 		Spec: core.PersistentVolumeClaimSpec{ | ||||
| 			VolumeAttributesClassName: vacName, | ||||
| 		}, | ||||
| 	} | ||||
| } | ||||
|  | ||||
| func testLocalVolume(path string, affinity *core.VolumeNodeAffinity) core.PersistentVolumeSpec { | ||||
| 	return core.PersistentVolumeSpec{ | ||||
| 		Capacity: core.ResourceList{ | ||||
| @@ -1001,6 +1124,24 @@ func TestValidateLocalVolumes(t *testing.T) { | ||||
| 	} | ||||
| } | ||||
|  | ||||
| func testVolumeWithVolumeAttributesClass(vacName *string) *core.PersistentVolume { | ||||
| 	return testVolume("test-volume-with-volume-attributes-class", "", | ||||
| 		core.PersistentVolumeSpec{ | ||||
| 			Capacity: core.ResourceList{ | ||||
| 				core.ResourceName(core.ResourceStorage): resource.MustParse("10G"), | ||||
| 			}, | ||||
| 			AccessModes: []core.PersistentVolumeAccessMode{core.ReadWriteOnce}, | ||||
| 			PersistentVolumeSource: core.PersistentVolumeSource{ | ||||
| 				CSI: &core.CSIPersistentVolumeSource{ | ||||
| 					Driver:       "test-driver", | ||||
| 					VolumeHandle: "test-123", | ||||
| 				}, | ||||
| 			}, | ||||
| 			StorageClassName:          "test-storage-class", | ||||
| 			VolumeAttributesClassName: vacName, | ||||
| 		}) | ||||
| } | ||||
|  | ||||
| func testVolumeWithNodeAffinity(affinity *core.VolumeNodeAffinity) *core.PersistentVolume { | ||||
| 	return testVolume("test-affinity-volume", "", | ||||
| 		core.PersistentVolumeSpec{ | ||||
| @@ -1341,6 +1482,115 @@ func TestValidateVolumeNodeAffinityUpdate(t *testing.T) { | ||||
| 	} | ||||
| } | ||||
|  | ||||
| func TestValidatePeristentVolumeAttributesClassUpdate(t *testing.T) { | ||||
| 	scenarios := map[string]struct { | ||||
| 		isExpectedFailure           bool | ||||
| 		enableVolumeAttributesClass bool | ||||
| 		oldPV                       *core.PersistentVolume | ||||
| 		newPV                       *core.PersistentVolume | ||||
| 	}{ | ||||
| 		"nil-nothing-changed": { | ||||
| 			isExpectedFailure:           false, | ||||
| 			enableVolumeAttributesClass: true, | ||||
| 			oldPV:                       testVolumeWithVolumeAttributesClass(nil), | ||||
| 			newPV:                       testVolumeWithVolumeAttributesClass(nil), | ||||
| 		}, | ||||
| 		"vac-nothing-changed": { | ||||
| 			isExpectedFailure:           false, | ||||
| 			enableVolumeAttributesClass: true, | ||||
| 			oldPV:                       testVolumeWithVolumeAttributesClass(ptr.To("foo")), | ||||
| 			newPV:                       testVolumeWithVolumeAttributesClass(ptr.To("foo")), | ||||
| 		}, | ||||
| 		"vac-changed": { | ||||
| 			isExpectedFailure:           false, | ||||
| 			enableVolumeAttributesClass: true, | ||||
| 			oldPV:                       testVolumeWithVolumeAttributesClass(ptr.To("foo")), | ||||
| 			newPV:                       testVolumeWithVolumeAttributesClass(ptr.To("bar")), | ||||
| 		}, | ||||
| 		"nil-to-string": { | ||||
| 			isExpectedFailure:           false, | ||||
| 			enableVolumeAttributesClass: true, | ||||
| 			oldPV:                       testVolumeWithVolumeAttributesClass(nil), | ||||
| 			newPV:                       testVolumeWithVolumeAttributesClass(ptr.To("foo")), | ||||
| 		}, | ||||
| 		"nil-to-empty-string": { | ||||
| 			isExpectedFailure:           true, | ||||
| 			enableVolumeAttributesClass: true, | ||||
| 			oldPV:                       testVolumeWithVolumeAttributesClass(nil), | ||||
| 			newPV:                       testVolumeWithVolumeAttributesClass(ptr.To("")), | ||||
| 		}, | ||||
| 		"string-to-nil": { | ||||
| 			isExpectedFailure:           true, | ||||
| 			enableVolumeAttributesClass: true, | ||||
| 			oldPV:                       testVolumeWithVolumeAttributesClass(ptr.To("foo")), | ||||
| 			newPV:                       testVolumeWithVolumeAttributesClass(nil), | ||||
| 		}, | ||||
| 		"string-to-empty-string": { | ||||
| 			isExpectedFailure:           true, | ||||
| 			enableVolumeAttributesClass: true, | ||||
| 			oldPV:                       testVolumeWithVolumeAttributesClass(ptr.To("foo")), | ||||
| 			newPV:                       testVolumeWithVolumeAttributesClass(ptr.To("")), | ||||
| 		}, | ||||
| 		"vac-nothing-changed-when-feature-gate-is-off": { | ||||
| 			isExpectedFailure:           false, | ||||
| 			enableVolumeAttributesClass: false, | ||||
| 			oldPV:                       testVolumeWithVolumeAttributesClass(ptr.To("foo")), | ||||
| 			newPV:                       testVolumeWithVolumeAttributesClass(ptr.To("foo")), | ||||
| 		}, | ||||
| 		"vac-changed-when-feature-gate-is-off": { | ||||
| 			isExpectedFailure:           true, | ||||
| 			enableVolumeAttributesClass: false, | ||||
| 			oldPV:                       testVolumeWithVolumeAttributesClass(ptr.To("foo")), | ||||
| 			newPV:                       testVolumeWithVolumeAttributesClass(ptr.To("bar")), | ||||
| 		}, | ||||
| 		"nil-to-string-when-feature-gate-is-off": { | ||||
| 			isExpectedFailure:           true, | ||||
| 			enableVolumeAttributesClass: false, | ||||
| 			oldPV:                       testVolumeWithVolumeAttributesClass(nil), | ||||
| 			newPV:                       testVolumeWithVolumeAttributesClass(ptr.To("foo")), | ||||
| 		}, | ||||
| 		"nil-to-empty-string-when-feature-gate-is-off": { | ||||
| 			isExpectedFailure:           true, | ||||
| 			enableVolumeAttributesClass: false, | ||||
| 			oldPV:                       testVolumeWithVolumeAttributesClass(nil), | ||||
| 			newPV:                       testVolumeWithVolumeAttributesClass(ptr.To("")), | ||||
| 		}, | ||||
| 		"string-to-nil-when-feature-gate-is-off": { | ||||
| 			isExpectedFailure:           true, | ||||
| 			enableVolumeAttributesClass: false, | ||||
| 			oldPV:                       testVolumeWithVolumeAttributesClass(ptr.To("foo")), | ||||
| 			newPV:                       testVolumeWithVolumeAttributesClass(nil), | ||||
| 		}, | ||||
| 		"string-to-empty-string-when-feature-gate-is-off": { | ||||
| 			isExpectedFailure:           true, | ||||
| 			enableVolumeAttributesClass: false, | ||||
| 			oldPV:                       testVolumeWithVolumeAttributesClass(ptr.To("foo")), | ||||
| 			newPV:                       testVolumeWithVolumeAttributesClass(ptr.To("")), | ||||
| 		}, | ||||
| 	} | ||||
|  | ||||
| 	for name, scenario := range scenarios { | ||||
| 		defer featuregatetesting.SetFeatureGateDuringTest(t, utilfeature.DefaultFeatureGate, features.VolumeAttributesClass, scenario.enableVolumeAttributesClass)() | ||||
|  | ||||
| 		originalNewPV := scenario.newPV.DeepCopy() | ||||
| 		originalOldPV := scenario.oldPV.DeepCopy() | ||||
| 		opts := ValidationOptionsForPersistentVolume(scenario.newPV, scenario.oldPV) | ||||
| 		errs := ValidatePersistentVolumeUpdate(scenario.newPV, scenario.oldPV, opts) | ||||
| 		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) | ||||
| 		} | ||||
| 		if diff := cmp.Diff(originalNewPV, scenario.newPV); len(diff) > 0 { | ||||
| 			t.Errorf("newPV was modified: %s", diff) | ||||
| 		} | ||||
| 		if diff := cmp.Diff(originalOldPV, scenario.oldPV); len(diff) > 0 { | ||||
| 			t.Errorf("oldPV was modified: %s", diff) | ||||
| 		} | ||||
| 	} | ||||
| } | ||||
|  | ||||
| func testVolumeClaim(name string, namespace string, spec core.PersistentVolumeClaimSpec) *core.PersistentVolumeClaim { | ||||
| 	return &core.PersistentVolumeClaim{ | ||||
| 		ObjectMeta: metav1.ObjectMeta{Name: name, Namespace: namespace}, | ||||
| @@ -1516,8 +1766,9 @@ func testValidatePVC(t *testing.T, ephemeral bool) { | ||||
| 	ten := int64(10) | ||||
|  | ||||
| 	scenarios := map[string]struct { | ||||
| 		isExpectedFailure bool | ||||
| 		claim             *core.PersistentVolumeClaim | ||||
| 		isExpectedFailure           bool | ||||
| 		enableVolumeAttributesClass bool | ||||
| 		claim                       *core.PersistentVolumeClaim | ||||
| 	}{ | ||||
| 		"good-claim": { | ||||
| 			isExpectedFailure: false, | ||||
| @@ -1894,10 +2145,34 @@ func testValidatePVC(t *testing.T, ephemeral bool) { | ||||
| 				}, | ||||
| 			}), | ||||
| 		}, | ||||
| 		"invalid-volume-attributes-class-name": { | ||||
| 			isExpectedFailure:           true, | ||||
| 			enableVolumeAttributesClass: true, | ||||
| 			claim: testVolumeClaim(goodName, goodNS, core.PersistentVolumeClaimSpec{ | ||||
| 				Selector: &metav1.LabelSelector{ | ||||
| 					MatchExpressions: []metav1.LabelSelectorRequirement{{ | ||||
| 						Key:      "key2", | ||||
| 						Operator: "Exists", | ||||
| 					}}, | ||||
| 				}, | ||||
| 				AccessModes: []core.PersistentVolumeAccessMode{ | ||||
| 					core.ReadWriteOnce, | ||||
| 					core.ReadOnlyMany, | ||||
| 				}, | ||||
| 				Resources: core.VolumeResourceRequirements{ | ||||
| 					Requests: core.ResourceList{ | ||||
| 						core.ResourceName(core.ResourceStorage): resource.MustParse("10G"), | ||||
| 					}, | ||||
| 				}, | ||||
| 				VolumeAttributesClassName: &invalidClassName, | ||||
| 			}), | ||||
| 		}, | ||||
| 	} | ||||
|  | ||||
| 	for name, scenario := range scenarios { | ||||
| 		t.Run(name, func(t *testing.T) { | ||||
| 			defer featuregatetesting.SetFeatureGateDuringTest(t, utilfeature.DefaultFeatureGate, features.VolumeAttributesClass, scenario.enableVolumeAttributesClass)() | ||||
|  | ||||
| 			var errs field.ErrorList | ||||
| 			if ephemeral { | ||||
| 				volumes := []core.Volume{{ | ||||
| @@ -2422,11 +2697,68 @@ func TestValidatePersistentVolumeClaimUpdate(t *testing.T) { | ||||
| 		}, | ||||
| 	}) | ||||
|  | ||||
| 	validClaimNilVolumeAttributesClass := testVolumeClaimWithStatus("foo", "ns", core.PersistentVolumeClaimSpec{ | ||||
| 		AccessModes: []core.PersistentVolumeAccessMode{ | ||||
| 			core.ReadWriteOnce, | ||||
| 			core.ReadOnlyMany, | ||||
| 		}, | ||||
| 		Resources: core.VolumeResourceRequirements{ | ||||
| 			Requests: core.ResourceList{ | ||||
| 				core.ResourceName(core.ResourceStorage): resource.MustParse("10G"), | ||||
| 			}, | ||||
| 		}, | ||||
| 	}, core.PersistentVolumeClaimStatus{ | ||||
| 		Phase: core.ClaimBound, | ||||
| 	}) | ||||
| 	validClaimEmptyVolumeAttributesClass := testVolumeClaimWithStatus("foo", "ns", core.PersistentVolumeClaimSpec{ | ||||
| 		VolumeAttributesClassName: utilpointer.String(""), | ||||
| 		AccessModes: []core.PersistentVolumeAccessMode{ | ||||
| 			core.ReadWriteOnce, | ||||
| 			core.ReadOnlyMany, | ||||
| 		}, | ||||
| 		Resources: core.VolumeResourceRequirements{ | ||||
| 			Requests: core.ResourceList{ | ||||
| 				core.ResourceName(core.ResourceStorage): resource.MustParse("10G"), | ||||
| 			}, | ||||
| 		}, | ||||
| 	}, core.PersistentVolumeClaimStatus{ | ||||
| 		Phase: core.ClaimBound, | ||||
| 	}) | ||||
| 	validClaimVolumeAttributesClass1 := testVolumeClaimWithStatus("foo", "ns", core.PersistentVolumeClaimSpec{ | ||||
| 		VolumeAttributesClassName: utilpointer.String("vac1"), | ||||
| 		AccessModes: []core.PersistentVolumeAccessMode{ | ||||
| 			core.ReadWriteOnce, | ||||
| 			core.ReadOnlyMany, | ||||
| 		}, | ||||
| 		Resources: core.VolumeResourceRequirements{ | ||||
| 			Requests: core.ResourceList{ | ||||
| 				core.ResourceName(core.ResourceStorage): resource.MustParse("10G"), | ||||
| 			}, | ||||
| 		}, | ||||
| 	}, core.PersistentVolumeClaimStatus{ | ||||
| 		Phase: core.ClaimBound, | ||||
| 	}) | ||||
| 	validClaimVolumeAttributesClass2 := testVolumeClaimWithStatus("foo", "ns", core.PersistentVolumeClaimSpec{ | ||||
| 		VolumeAttributesClassName: utilpointer.String("vac2"), | ||||
| 		AccessModes: []core.PersistentVolumeAccessMode{ | ||||
| 			core.ReadWriteOnce, | ||||
| 			core.ReadOnlyMany, | ||||
| 		}, | ||||
| 		Resources: core.VolumeResourceRequirements{ | ||||
| 			Requests: core.ResourceList{ | ||||
| 				core.ResourceName(core.ResourceStorage): resource.MustParse("10G"), | ||||
| 			}, | ||||
| 		}, | ||||
| 	}, core.PersistentVolumeClaimStatus{ | ||||
| 		Phase: core.ClaimBound, | ||||
| 	}) | ||||
|  | ||||
| 	scenarios := map[string]struct { | ||||
| 		isExpectedFailure          bool | ||||
| 		oldClaim                   *core.PersistentVolumeClaim | ||||
| 		newClaim                   *core.PersistentVolumeClaim | ||||
| 		enableRecoverFromExpansion bool | ||||
| 		isExpectedFailure           bool | ||||
| 		oldClaim                    *core.PersistentVolumeClaim | ||||
| 		newClaim                    *core.PersistentVolumeClaim | ||||
| 		enableRecoverFromExpansion  bool | ||||
| 		enableVolumeAttributesClass bool | ||||
| 	}{ | ||||
| 		"valid-update-volumeName-only": { | ||||
| 			isExpectedFailure: false, | ||||
| @@ -2636,11 +2968,61 @@ func TestValidatePersistentVolumeClaimUpdate(t *testing.T) { | ||||
| 			newClaim:          invalidClaimDataSourceRefAPIGroup, | ||||
| 			isExpectedFailure: false, | ||||
| 		}, | ||||
| 		"valid-update-volume-attributes-class-from-nil": { | ||||
| 			oldClaim:                    validClaimNilVolumeAttributesClass, | ||||
| 			newClaim:                    validClaimVolumeAttributesClass1, | ||||
| 			enableVolumeAttributesClass: true, | ||||
| 			isExpectedFailure:           false, | ||||
| 		}, | ||||
| 		"valid-update-volume-attributes-class-from-empty": { | ||||
| 			oldClaim:                    validClaimEmptyVolumeAttributesClass, | ||||
| 			newClaim:                    validClaimVolumeAttributesClass1, | ||||
| 			enableVolumeAttributesClass: true, | ||||
| 			isExpectedFailure:           false, | ||||
| 		}, | ||||
| 		"valid-update-volume-attributes-class": { | ||||
| 			oldClaim:                    validClaimVolumeAttributesClass1, | ||||
| 			newClaim:                    validClaimVolumeAttributesClass2, | ||||
| 			enableVolumeAttributesClass: true, | ||||
| 			isExpectedFailure:           false, | ||||
| 		}, | ||||
| 		"invalid-update-volume-attributes-class": { | ||||
| 			oldClaim:                    validClaimVolumeAttributesClass1, | ||||
| 			newClaim:                    validClaimNilVolumeAttributesClass, | ||||
| 			enableVolumeAttributesClass: true, | ||||
| 			isExpectedFailure:           true, | ||||
| 		}, | ||||
| 		"invalid-update-volume-attributes-class-to-nil": { | ||||
| 			oldClaim:                    validClaimVolumeAttributesClass1, | ||||
| 			newClaim:                    validClaimNilVolumeAttributesClass, | ||||
| 			enableVolumeAttributesClass: true, | ||||
| 			isExpectedFailure:           true, | ||||
| 		}, | ||||
| 		"invalid-update-volume-attributes-class-to-empty": { | ||||
| 			oldClaim:                    validClaimVolumeAttributesClass1, | ||||
| 			newClaim:                    validClaimEmptyVolumeAttributesClass, | ||||
| 			enableVolumeAttributesClass: true, | ||||
| 			isExpectedFailure:           true, | ||||
| 		}, | ||||
| 		"invalid-update-volume-attributes-class-to-nil-without-featuregate-enabled": { | ||||
| 			oldClaim:                    validClaimVolumeAttributesClass1, | ||||
| 			newClaim:                    validClaimNilVolumeAttributesClass, | ||||
| 			enableVolumeAttributesClass: false, | ||||
| 			isExpectedFailure:           true, | ||||
| 		}, | ||||
| 		"invalid-update-volume-attributes-class-without-featuregate-enabled": { | ||||
| 			oldClaim:                    validClaimVolumeAttributesClass1, | ||||
| 			newClaim:                    validClaimVolumeAttributesClass2, | ||||
| 			enableVolumeAttributesClass: false, | ||||
| 			isExpectedFailure:           true, | ||||
| 		}, | ||||
| 	} | ||||
|  | ||||
| 	for name, scenario := range scenarios { | ||||
| 		t.Run(name, func(t *testing.T) { | ||||
| 			defer featuregatetesting.SetFeatureGateDuringTest(t, utilfeature.DefaultFeatureGate, features.RecoverVolumeExpansionFailure, scenario.enableRecoverFromExpansion)() | ||||
| 			defer featuregatetesting.SetFeatureGateDuringTest(t, utilfeature.DefaultFeatureGate, features.VolumeAttributesClass, scenario.enableVolumeAttributesClass)() | ||||
|  | ||||
| 			scenario.oldClaim.ResourceVersion = "1" | ||||
| 			scenario.newClaim.ResourceVersion = "1" | ||||
| 			opts := ValidationOptionsForPersistentVolumeClaim(scenario.newClaim, scenario.oldClaim) | ||||
| @@ -2659,13 +3041,15 @@ func TestValidationOptionsForPersistentVolumeClaim(t *testing.T) { | ||||
| 	invaildAPIGroup := "^invalid" | ||||
|  | ||||
| 	tests := map[string]struct { | ||||
| 		oldPvc               *core.PersistentVolumeClaim | ||||
| 		expectValidationOpts PersistentVolumeClaimSpecValidationOptions | ||||
| 		oldPvc                      *core.PersistentVolumeClaim | ||||
| 		enableVolumeAttributesClass bool | ||||
| 		expectValidationOpts        PersistentVolumeClaimSpecValidationOptions | ||||
| 	}{ | ||||
| 		"nil pv": { | ||||
| 			oldPvc: nil, | ||||
| 			expectValidationOpts: PersistentVolumeClaimSpecValidationOptions{ | ||||
| 				EnableRecoverFromExpansionFailure: false, | ||||
| 				EnableVolumeAttributesClass:       false, | ||||
| 			}, | ||||
| 		}, | ||||
| 		"invaild apiGroup in dataSource allowed because the old pvc is used": { | ||||
| @@ -2680,10 +3064,28 @@ func TestValidationOptionsForPersistentVolumeClaim(t *testing.T) { | ||||
| 				AllowInvalidAPIGroupInDataSourceOrRef: true, | ||||
| 			}, | ||||
| 		}, | ||||
| 		"volume attributes class allowed because feature enable": { | ||||
| 			oldPvc:                      pvcWithVolumeAttributesClassName(utilpointer.String("foo")), | ||||
| 			enableVolumeAttributesClass: true, | ||||
| 			expectValidationOpts: PersistentVolumeClaimSpecValidationOptions{ | ||||
| 				EnableRecoverFromExpansionFailure: false, | ||||
| 				EnableVolumeAttributesClass:       true, | ||||
| 			}, | ||||
| 		}, | ||||
| 		"volume attributes class validated because used and feature disabled": { | ||||
| 			oldPvc:                      pvcWithVolumeAttributesClassName(utilpointer.String("foo")), | ||||
| 			enableVolumeAttributesClass: false, | ||||
| 			expectValidationOpts: PersistentVolumeClaimSpecValidationOptions{ | ||||
| 				EnableRecoverFromExpansionFailure: false, | ||||
| 				EnableVolumeAttributesClass:       true, | ||||
| 			}, | ||||
| 		}, | ||||
| 	} | ||||
|  | ||||
| 	for name, tc := range tests { | ||||
| 		t.Run(name, func(t *testing.T) { | ||||
| 			defer featuregatetesting.SetFeatureGateDuringTest(t, utilfeature.DefaultFeatureGate, features.VolumeAttributesClass, tc.enableVolumeAttributesClass)() | ||||
|  | ||||
| 			opts := ValidationOptionsForPersistentVolumeClaim(nil, tc.oldPvc) | ||||
| 			if opts != tc.expectValidationOpts { | ||||
| 				t.Errorf("Expected opts: %+v, received: %+v", tc.expectValidationOpts, opts) | ||||
| @@ -2694,17 +3096,27 @@ func TestValidationOptionsForPersistentVolumeClaim(t *testing.T) { | ||||
|  | ||||
| func TestValidationOptionsForPersistentVolumeClaimTemplate(t *testing.T) { | ||||
| 	tests := map[string]struct { | ||||
| 		oldPvcTemplate       *core.PersistentVolumeClaimTemplate | ||||
| 		expectValidationOpts PersistentVolumeClaimSpecValidationOptions | ||||
| 		oldPvcTemplate              *core.PersistentVolumeClaimTemplate | ||||
| 		enableVolumeAttributesClass bool | ||||
| 		expectValidationOpts        PersistentVolumeClaimSpecValidationOptions | ||||
| 	}{ | ||||
| 		"nil pv": { | ||||
| 			oldPvcTemplate:       nil, | ||||
| 			expectValidationOpts: PersistentVolumeClaimSpecValidationOptions{}, | ||||
| 		}, | ||||
| 		"volume attributes class allowed because feature enable": { | ||||
| 			oldPvcTemplate:              pvcTemplateWithVolumeAttributesClassName(utilpointer.String("foo")), | ||||
| 			enableVolumeAttributesClass: true, | ||||
| 			expectValidationOpts: PersistentVolumeClaimSpecValidationOptions{ | ||||
| 				EnableVolumeAttributesClass: true, | ||||
| 			}, | ||||
| 		}, | ||||
| 	} | ||||
|  | ||||
| 	for name, tc := range tests { | ||||
| 		t.Run(name, func(t *testing.T) { | ||||
| 			defer featuregatetesting.SetFeatureGateDuringTest(t, utilfeature.DefaultFeatureGate, features.VolumeAttributesClass, tc.enableVolumeAttributesClass)() | ||||
|  | ||||
| 			opts := ValidationOptionsForPersistentVolumeClaimTemplate(nil, tc.oldPvcTemplate) | ||||
| 			if opts != tc.expectValidationOpts { | ||||
| 				t.Errorf("Expected opts: %+v, received: %+v", opts, tc.expectValidationOpts) | ||||
| @@ -22180,6 +22592,71 @@ func TestCrossNamespaceSource(t *testing.T) { | ||||
| 	} | ||||
| } | ||||
|  | ||||
| func pvcSpecWithVolumeAttributesClassName(vacName *string) *core.PersistentVolumeClaimSpec { | ||||
| 	scName := "csi-plugin" | ||||
| 	spec := core.PersistentVolumeClaimSpec{ | ||||
| 		AccessModes: []core.PersistentVolumeAccessMode{ | ||||
| 			core.ReadOnlyMany, | ||||
| 		}, | ||||
| 		Resources: core.VolumeResourceRequirements{ | ||||
| 			Requests: core.ResourceList{ | ||||
| 				core.ResourceName(core.ResourceStorage): resource.MustParse("10G"), | ||||
| 			}, | ||||
| 		}, | ||||
| 		StorageClassName:          &scName, | ||||
| 		VolumeAttributesClassName: vacName, | ||||
| 	} | ||||
| 	return &spec | ||||
| } | ||||
|  | ||||
| func TestVolumeAttributesClass(t *testing.T) { | ||||
| 	testCases := []struct { | ||||
| 		testName                    string | ||||
| 		expectedFail                bool | ||||
| 		enableVolumeAttributesClass bool | ||||
| 		claimSpec                   *core.PersistentVolumeClaimSpec | ||||
| 	}{ | ||||
| 		{ | ||||
| 			testName:                    "Feature gate enabled and valid no volumeAttributesClassName specified", | ||||
| 			expectedFail:                false, | ||||
| 			enableVolumeAttributesClass: true, | ||||
| 			claimSpec:                   pvcSpecWithVolumeAttributesClassName(nil), | ||||
| 		}, | ||||
| 		{ | ||||
| 			testName:                    "Feature gate enabled and an empty volumeAttributesClassName specified", | ||||
| 			expectedFail:                false, | ||||
| 			enableVolumeAttributesClass: true, | ||||
| 			claimSpec:                   pvcSpecWithVolumeAttributesClassName(utilpointer.String("")), | ||||
| 		}, | ||||
| 		{ | ||||
| 			testName:                    "Feature gate enabled and valid volumeAttributesClassName specified", | ||||
| 			expectedFail:                false, | ||||
| 			enableVolumeAttributesClass: true, | ||||
| 			claimSpec:                   pvcSpecWithVolumeAttributesClassName(utilpointer.String("foo")), | ||||
| 		}, | ||||
| 		{ | ||||
| 			testName:                    "Feature gate enabled and invalid volumeAttributesClassName specified", | ||||
| 			expectedFail:                true, | ||||
| 			enableVolumeAttributesClass: true, | ||||
| 			claimSpec:                   pvcSpecWithVolumeAttributesClassName(utilpointer.String("-invalid-")), | ||||
| 		}, | ||||
| 	} | ||||
| 	for _, tc := range testCases { | ||||
| 		opts := PersistentVolumeClaimSpecValidationOptions{ | ||||
| 			EnableVolumeAttributesClass: tc.enableVolumeAttributesClass, | ||||
| 		} | ||||
| 		if tc.expectedFail { | ||||
| 			if errs := ValidatePersistentVolumeClaimSpec(tc.claimSpec, field.NewPath("spec"), opts); len(errs) == 0 { | ||||
| 				t.Errorf("%s: expected failure: %v", tc.testName, errs) | ||||
| 			} | ||||
| 		} else { | ||||
| 			if errs := ValidatePersistentVolumeClaimSpec(tc.claimSpec, field.NewPath("spec"), opts); len(errs) != 0 { | ||||
| 				t.Errorf("%s: expected success: %v", tc.testName, errs) | ||||
| 			} | ||||
| 		} | ||||
| 	} | ||||
| } | ||||
|  | ||||
| func TestValidateTopologySpreadConstraints(t *testing.T) { | ||||
| 	fieldPath := field.NewPath("field") | ||||
| 	subFldPath0 := fieldPath.Index(0) | ||||
|   | ||||
| @@ -54,6 +54,8 @@ func addKnownTypes(scheme *runtime.Scheme) error { | ||||
| 		&CSIDriverList{}, | ||||
| 		&CSIStorageCapacity{}, | ||||
| 		&CSIStorageCapacityList{}, | ||||
| 		&VolumeAttributesClass{}, | ||||
| 		&VolumeAttributesClassList{}, | ||||
| 	) | ||||
| 	return nil | ||||
| } | ||||
|   | ||||
| @@ -672,3 +672,53 @@ type CSIStorageCapacityList struct { | ||||
| 	// Items is the list of CSIStorageCapacity objects. | ||||
| 	Items []CSIStorageCapacity | ||||
| } | ||||
|  | ||||
| // +k8s:deepcopy-gen:interfaces=k8s.io/apimachinery/pkg/runtime.Object | ||||
|  | ||||
| // VolumeAttributesClass represents a specification of mutable volume attributes | ||||
| // defined by the CSI driver. The class can be specified during dynamic provisioning | ||||
| // of PersistentVolumeClaims, and changed in the PersistentVolumeClaim spec after provisioning. | ||||
| type VolumeAttributesClass struct { | ||||
| 	metav1.TypeMeta | ||||
|  | ||||
| 	// Standard object's metadata. | ||||
| 	// More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#metadata | ||||
| 	// +optional | ||||
| 	metav1.ObjectMeta | ||||
|  | ||||
| 	// Name of the CSI driver | ||||
| 	// This field is immutable. | ||||
| 	DriverName string | ||||
|  | ||||
| 	// parameters hold volume attributes defined by the CSI driver. These values | ||||
| 	// are opaque to the Kubernetes and are passed directly to the CSI driver. | ||||
| 	// The underlying storage provider supports changing these attributes on an | ||||
| 	// existing volume, however the parameters field itself is immutable. To | ||||
| 	// invoke a volume update, a new VolumeAttributesClass should be created with | ||||
| 	// new parameters, and the PersistentVolumeClaim should be updated to reference | ||||
| 	// the new VolumeAttributesClass. | ||||
| 	// | ||||
| 	// This field is required and must contain at least one key/value pair. | ||||
| 	// The keys cannot be empty, and the maximum number of parameters is 512, with | ||||
| 	// a cumulative max size of 256K. If the CSI driver rejects invalid parameters, | ||||
| 	// the target PersistentVolumeClaim will be set to an "Infeasible" state in the | ||||
| 	// modifyVolumeStatus field. | ||||
| 	Parameters map[string]string | ||||
| } | ||||
|  | ||||
| // +k8s:deepcopy-gen:interfaces=k8s.io/apimachinery/pkg/runtime.Object | ||||
|  | ||||
| // VolumeAttributesClassList is a collection of VolumeAttributesClass objects. | ||||
| type VolumeAttributesClassList struct { | ||||
| 	metav1.TypeMeta | ||||
|  | ||||
| 	// Standard list metadata | ||||
| 	// More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#metadata | ||||
| 	// +optional | ||||
| 	metav1.ListMeta | ||||
|  | ||||
| 	// items is the list of VolumeAttributesClass objects. | ||||
| 	// +listType=map | ||||
| 	// +listMapKey=name | ||||
| 	Items []VolumeAttributesClass | ||||
| } | ||||
|   | ||||
| @@ -26,6 +26,9 @@ const IsDefaultStorageClassAnnotation = "storageclass.kubernetes.io/is-default-c | ||||
| // TODO: remove Beta when no longer used | ||||
| const BetaIsDefaultStorageClassAnnotation = "storageclass.beta.kubernetes.io/is-default-class" | ||||
|  | ||||
| // AlphaIsDefaultVolumeAttributesClassAnnotation is the alpha version of IsDefaultVolumeAttributesClassAnnotation. | ||||
| const AlphaIsDefaultVolumeAttributesClassAnnotation = "volumeattributesclass.alpha.kubernetes.io/is-default-class" | ||||
|  | ||||
| // IsDefaultAnnotation returns a boolean if | ||||
| // the annotation is set | ||||
| // TODO: remove Beta when no longer needed | ||||
| @@ -39,3 +42,9 @@ func IsDefaultAnnotation(obj metav1.ObjectMeta) bool { | ||||
|  | ||||
| 	return false | ||||
| } | ||||
|  | ||||
| // IsDefaultAnnotationForVolumeAttributesClass returns a boolean if | ||||
| // the annotation is set | ||||
| func IsDefaultAnnotationForVolumeAttributesClass(obj metav1.ObjectMeta) bool { | ||||
| 	return obj.Annotations[AlphaIsDefaultVolumeAttributesClassAnnotation] == "true" | ||||
| } | ||||
|   | ||||
| @@ -55,7 +55,7 @@ type CSINodeValidationOptions struct { | ||||
| func ValidateStorageClass(storageClass *storage.StorageClass) field.ErrorList { | ||||
| 	allErrs := apivalidation.ValidateObjectMeta(&storageClass.ObjectMeta, false, apivalidation.ValidateClassName, field.NewPath("metadata")) | ||||
| 	allErrs = append(allErrs, validateProvisioner(storageClass.Provisioner, field.NewPath("provisioner"))...) | ||||
| 	allErrs = append(allErrs, validateParameters(storageClass.Parameters, field.NewPath("parameters"))...) | ||||
| 	allErrs = append(allErrs, validateParameters(storageClass.Parameters, true, field.NewPath("parameters"))...) | ||||
| 	allErrs = append(allErrs, validateReclaimPolicy(storageClass.ReclaimPolicy, field.NewPath("reclaimPolicy"))...) | ||||
| 	allErrs = append(allErrs, validateVolumeBindingMode(storageClass.VolumeBindingMode, field.NewPath("volumeBindingMode"))...) | ||||
| 	allErrs = append(allErrs, validateAllowedTopologies(storageClass.AllowedTopologies, field.NewPath("allowedTopologies"))...) | ||||
| @@ -95,7 +95,7 @@ func validateProvisioner(provisioner string, fldPath *field.Path) field.ErrorLis | ||||
| } | ||||
|  | ||||
| // validateParameters tests that keys are qualified names and that provisionerParameter are < 256kB. | ||||
| func validateParameters(params map[string]string, fldPath *field.Path) field.ErrorList { | ||||
| func validateParameters(params map[string]string, allowEmpty bool, fldPath *field.Path) field.ErrorList { | ||||
| 	var totalSize int64 | ||||
| 	allErrs := field.ErrorList{} | ||||
|  | ||||
| @@ -114,6 +114,10 @@ func validateParameters(params map[string]string, fldPath *field.Path) field.Err | ||||
| 	if totalSize > maxProvisionerParameterSize { | ||||
| 		allErrs = append(allErrs, field.TooLong(fldPath, "", maxProvisionerParameterSize)) | ||||
| 	} | ||||
|  | ||||
| 	if !allowEmpty && len(params) == 0 { | ||||
| 		allErrs = append(allErrs, field.Required(fldPath, "must contain at least one key/value pair")) | ||||
| 	} | ||||
| 	return allErrs | ||||
| } | ||||
|  | ||||
| @@ -578,3 +582,23 @@ func ValidateCSIStorageCapacityUpdate(capacity, oldCapacity *storage.CSIStorageC | ||||
|  | ||||
| 	return allErrs | ||||
| } | ||||
|  | ||||
| // ValidateVolumeAttributesClass validates a VolumeAttributesClass. | ||||
| func ValidateVolumeAttributesClass(volumeAttributesClass *storage.VolumeAttributesClass) field.ErrorList { | ||||
| 	allErrs := apivalidation.ValidateObjectMeta(&volumeAttributesClass.ObjectMeta, false, apivalidation.ValidateClassName, field.NewPath("metadata")) | ||||
| 	allErrs = append(allErrs, validateProvisioner(volumeAttributesClass.DriverName, field.NewPath("driverName"))...) | ||||
| 	allErrs = append(allErrs, validateParameters(volumeAttributesClass.Parameters, false, field.NewPath("parameters"))...) | ||||
| 	return allErrs | ||||
| } | ||||
|  | ||||
| // ValidateVolumeAttributesClassUpdate tests if an update to VolumeAttributesClass is valid. | ||||
| func ValidateVolumeAttributesClassUpdate(volumeAttributesClass, oldVolumeAttributesClass *storage.VolumeAttributesClass) field.ErrorList { | ||||
| 	allErrs := apivalidation.ValidateObjectMetaUpdate(&volumeAttributesClass.ObjectMeta, &oldVolumeAttributesClass.ObjectMeta, field.NewPath("metadata")) | ||||
| 	if volumeAttributesClass.DriverName != oldVolumeAttributesClass.DriverName { | ||||
| 		allErrs = append(allErrs, field.Forbidden(field.NewPath("driverName"), "updates to driverName are forbidden.")) | ||||
| 	} | ||||
| 	if !reflect.DeepEqual(oldVolumeAttributesClass.Parameters, volumeAttributesClass.Parameters) { | ||||
| 		allErrs = append(allErrs, field.Forbidden(field.NewPath("parameters"), "updates to parameters are forbidden.")) | ||||
| 	} | ||||
| 	return allErrs | ||||
| } | ||||
|   | ||||
| @@ -2163,3 +2163,178 @@ func TestCSIDriverValidationSELinuxMountEnabledDisabled(t *testing.T) { | ||||
| 		}) | ||||
| 	} | ||||
| } | ||||
|  | ||||
| func TestValidateVolumeAttributesClass(t *testing.T) { | ||||
| 	successCases := []storage.VolumeAttributesClass{ | ||||
| 		{ | ||||
| 			// driverName without a slash | ||||
| 			ObjectMeta: metav1.ObjectMeta{Name: "foo"}, | ||||
| 			DriverName: "foo", | ||||
| 			Parameters: map[string]string{ | ||||
| 				"foo-parameter": "free-form-string", | ||||
| 			}, | ||||
| 		}, | ||||
| 		{ | ||||
| 			// some parameters | ||||
| 			ObjectMeta: metav1.ObjectMeta{Name: "foo"}, | ||||
| 			DriverName: "kubernetes.io/foo", | ||||
| 			Parameters: map[string]string{ | ||||
| 				"kubernetes.io/foo-parameter": "free/form/string", | ||||
| 				"foo-parameter":               "free-form-string", | ||||
| 				"foo-parameter2":              "{\"embedded\": \"json\", \"with\": {\"structures\":\"inside\"}}", | ||||
| 				"foo-parameter3":              "", | ||||
| 			}, | ||||
| 		}} | ||||
|  | ||||
| 	// Success cases are expected to pass validation. | ||||
| 	for testName, v := range successCases { | ||||
| 		if errs := ValidateVolumeAttributesClass(&v); len(errs) != 0 { | ||||
| 			t.Errorf("Expected success for %d, got %v", testName, errs) | ||||
| 		} | ||||
| 	} | ||||
|  | ||||
| 	// generate a map longer than maxParameterSize | ||||
| 	longParameters := make(map[string]string) | ||||
| 	totalSize := 0 | ||||
| 	for totalSize < maxProvisionerParameterSize { | ||||
| 		k := fmt.Sprintf("param/%d", totalSize) | ||||
| 		v := fmt.Sprintf("value-%d", totalSize) | ||||
| 		longParameters[k] = v | ||||
| 		totalSize = totalSize + len(k) + len(v) | ||||
| 	} | ||||
|  | ||||
| 	errorCases := map[string]storage.VolumeAttributesClass{ | ||||
| 		"namespace is present": { | ||||
| 			ObjectMeta: metav1.ObjectMeta{Name: "foo", Namespace: "bar"}, | ||||
| 			DriverName: "kubernetes.io/foo", | ||||
| 			Parameters: map[string]string{ | ||||
| 				"foo-parameter": "free-form-string", | ||||
| 			}, | ||||
| 		}, | ||||
| 		"invalid driverName": { | ||||
| 			ObjectMeta: metav1.ObjectMeta{Name: "foo"}, | ||||
| 			DriverName: "kubernetes.io/invalid/foo", | ||||
| 			Parameters: map[string]string{ | ||||
| 				"foo-parameter": "free-form-string", | ||||
| 			}, | ||||
| 		}, | ||||
| 		"invalid driverName with invalid chars": { | ||||
| 			ObjectMeta: metav1.ObjectMeta{Name: "foo"}, | ||||
| 			DriverName: "^/ ", | ||||
| 			Parameters: map[string]string{ | ||||
| 				"foo-parameter": "free-form-string", | ||||
| 			}, | ||||
| 		}, | ||||
| 		"empty parameters": { | ||||
| 			ObjectMeta: metav1.ObjectMeta{Name: "foo"}, | ||||
| 			DriverName: "kubernetes.io/foo", | ||||
| 			Parameters: map[string]string{}, | ||||
| 		}, | ||||
| 		"nil parameters": { | ||||
| 			ObjectMeta: metav1.ObjectMeta{Name: "foo"}, | ||||
| 			DriverName: "kubernetes.io/foo", | ||||
| 		}, | ||||
| 		"invalid empty parameter name": { | ||||
| 			ObjectMeta: metav1.ObjectMeta{Name: "foo"}, | ||||
| 			DriverName: "kubernetes.io/foo", | ||||
| 			Parameters: map[string]string{ | ||||
| 				"": "value", | ||||
| 			}, | ||||
| 		}, | ||||
| 		"driverName: Required value": { | ||||
| 			ObjectMeta: metav1.ObjectMeta{Name: "foo"}, | ||||
| 			DriverName: "", | ||||
| 			Parameters: map[string]string{ | ||||
| 				"foo-parameter": "free-form-string", | ||||
| 			}, | ||||
| 		}, | ||||
| 		"driverName: whitespace": { | ||||
| 			ObjectMeta: metav1.ObjectMeta{Name: "foo"}, | ||||
| 			DriverName: " ", | ||||
| 			Parameters: map[string]string{ | ||||
| 				"foo-parameter": "free-form-string", | ||||
| 			}, | ||||
| 		}, | ||||
| 		"too long parameters": { | ||||
| 			ObjectMeta: metav1.ObjectMeta{Name: "foo"}, | ||||
| 			DriverName: "kubernetes.io/foo", | ||||
| 			Parameters: longParameters, | ||||
| 		}, | ||||
| 	} | ||||
|  | ||||
| 	// Error cases are not expected to pass validation. | ||||
| 	for testName, v := range errorCases { | ||||
| 		if errs := ValidateVolumeAttributesClass(&v); len(errs) == 0 { | ||||
| 			t.Errorf("Expected failure for test: %s", testName) | ||||
| 		} | ||||
| 	} | ||||
| } | ||||
|  | ||||
| func TestValidateVolumeAttributesClassUpdate(t *testing.T) { | ||||
| 	cases := map[string]struct { | ||||
| 		oldClass      *storage.VolumeAttributesClass | ||||
| 		newClass      *storage.VolumeAttributesClass | ||||
| 		shouldSucceed bool | ||||
| 	}{ | ||||
| 		"invalid driverName update": { | ||||
| 			oldClass: &storage.VolumeAttributesClass{ | ||||
| 				DriverName: "kubernetes.io/foo", | ||||
| 			}, | ||||
| 			newClass: &storage.VolumeAttributesClass{ | ||||
| 				DriverName: "kubernetes.io/bar", | ||||
| 			}, | ||||
| 			shouldSucceed: false, | ||||
| 		}, | ||||
| 		"invalid parameter update which changes values": { | ||||
| 			oldClass: &storage.VolumeAttributesClass{ | ||||
| 				DriverName: "kubernetes.io/foo", | ||||
| 				Parameters: map[string]string{ | ||||
| 					"foo": "bar1", | ||||
| 				}, | ||||
| 			}, | ||||
| 			newClass: &storage.VolumeAttributesClass{ | ||||
| 				DriverName: "kubernetes.io/foo", | ||||
| 				Parameters: map[string]string{ | ||||
| 					"foo": "bar2", | ||||
| 				}, | ||||
| 			}, | ||||
| 			shouldSucceed: false, | ||||
| 		}, | ||||
| 		"invalid parameter update which add new item": { | ||||
| 			oldClass: &storage.VolumeAttributesClass{ | ||||
| 				DriverName: "kubernetes.io/foo", | ||||
| 				Parameters: map[string]string{}, | ||||
| 			}, | ||||
| 			newClass: &storage.VolumeAttributesClass{ | ||||
| 				DriverName: "kubernetes.io/foo", | ||||
| 				Parameters: map[string]string{ | ||||
| 					"foo": "bar", | ||||
| 				}, | ||||
| 			}, | ||||
| 			shouldSucceed: false, | ||||
| 		}, | ||||
| 		"invalid parameter update which remove a item": { | ||||
| 			oldClass: &storage.VolumeAttributesClass{ | ||||
| 				DriverName: "kubernetes.io/foo", | ||||
| 				Parameters: map[string]string{ | ||||
| 					"foo": "bar", | ||||
| 				}, | ||||
| 			}, | ||||
| 			newClass: &storage.VolumeAttributesClass{ | ||||
| 				DriverName: "kubernetes.io/foo", | ||||
| 				Parameters: map[string]string{}, | ||||
| 			}, | ||||
| 			shouldSucceed: false, | ||||
| 		}, | ||||
| 	} | ||||
|  | ||||
| 	for testName, testCase := range cases { | ||||
| 		errs := ValidateVolumeAttributesClassUpdate(testCase.newClass, testCase.oldClass) | ||||
| 		if testCase.shouldSucceed && len(errs) != 0 { | ||||
| 			t.Errorf("Expected success for %v, got %v", testName, errs) | ||||
| 		} | ||||
| 		if !testCase.shouldSucceed && len(errs) == 0 { | ||||
| 			t.Errorf("Expected failure for %v, got success", testName) | ||||
| 		} | ||||
| 	} | ||||
| } | ||||
|   | ||||
| @@ -862,6 +862,13 @@ const ( | ||||
| 	// Enables user namespace support for stateless pods. | ||||
| 	UserNamespacesSupport featuregate.Feature = "UserNamespacesSupport" | ||||
|  | ||||
| 	// owner: @mattcarry, @sunnylovestiramisu | ||||
| 	// kep: https://kep.k8s.io/3751 | ||||
| 	// alpha: v1.29 | ||||
| 	// | ||||
| 	// Enables user specified volume attributes for persistent volumes, like iops and throughput. | ||||
| 	VolumeAttributesClass featuregate.Feature = "VolumeAttributesClass" | ||||
|  | ||||
| 	// owner: @cofyc | ||||
| 	// alpha: v1.21 | ||||
| 	VolumeCapacityPriority featuregate.Feature = "VolumeCapacityPriority" | ||||
| @@ -1162,6 +1169,8 @@ var defaultKubernetesFeatureGates = map[featuregate.Feature]featuregate.FeatureS | ||||
|  | ||||
| 	UnknownVersionInteroperabilityProxy: {Default: false, PreRelease: featuregate.Alpha}, | ||||
|  | ||||
| 	VolumeAttributesClass: {Default: false, PreRelease: featuregate.Alpha}, | ||||
|  | ||||
| 	VolumeCapacityPriority: {Default: false, PreRelease: featuregate.Alpha}, | ||||
|  | ||||
| 	UserNamespacesSupport: {Default: false, PreRelease: featuregate.Alpha}, | ||||
|   | ||||
| @@ -34,6 +34,7 @@ import ( | ||||
| 	"k8s.io/kubernetes/pkg/apis/extensions" | ||||
| 	"k8s.io/kubernetes/pkg/apis/networking" | ||||
| 	"k8s.io/kubernetes/pkg/apis/policy" | ||||
| 	"k8s.io/kubernetes/pkg/apis/storage" | ||||
| ) | ||||
|  | ||||
| // SpecialDefaultResourcePrefixes are prefixes compiled into Kubernetes. | ||||
| @@ -73,6 +74,7 @@ func NewStorageFactoryConfig() *StorageFactoryConfig { | ||||
| 		admissionregistration.Resource("validatingadmissionpolicybindings").WithVersion("v1beta1"), | ||||
| 		networking.Resource("ipaddresses").WithVersion("v1alpha1"), | ||||
| 		certificates.Resource("clustertrustbundles").WithVersion("v1alpha1"), | ||||
| 		storage.Resource("volumeattributesclasses").WithVersion("v1alpha1"), | ||||
| 	} | ||||
|  | ||||
| 	return &StorageFactoryConfig{ | ||||
|   | ||||
| @@ -43,6 +43,7 @@ import ( | ||||
| 	resourcev1alpha2 "k8s.io/api/resource/v1alpha2" | ||||
| 	schedulingv1 "k8s.io/api/scheduling/v1" | ||||
| 	storagev1 "k8s.io/api/storage/v1" | ||||
| 	storagev1alpha1 "k8s.io/api/storage/v1alpha1" | ||||
| 	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" | ||||
| 	"k8s.io/apimachinery/pkg/labels" | ||||
| 	"k8s.io/apimachinery/pkg/runtime" | ||||
| @@ -305,6 +306,7 @@ func AddHandlers(h printers.PrintHandler) { | ||||
| 		{Name: "Status", Type: "string", Description: apiv1.PersistentVolumeStatus{}.SwaggerDoc()["phase"]}, | ||||
| 		{Name: "Claim", Type: "string", Description: apiv1.PersistentVolumeSpec{}.SwaggerDoc()["claimRef"]}, | ||||
| 		{Name: "StorageClass", Type: "string", Description: "StorageClass of the pv"}, | ||||
| 		{Name: "VolumeAttributesClass", Type: "string", Description: "VolumeAttributesClass of the pv"}, | ||||
| 		{Name: "Reason", Type: "string", Description: apiv1.PersistentVolumeStatus{}.SwaggerDoc()["reason"]}, | ||||
| 		{Name: "Age", Type: "string", Description: metav1.ObjectMeta{}.SwaggerDoc()["creationTimestamp"]}, | ||||
| 		{Name: "VolumeMode", Type: "string", Priority: 1, Description: apiv1.PersistentVolumeSpec{}.SwaggerDoc()["volumeMode"]}, | ||||
| @@ -319,6 +321,7 @@ func AddHandlers(h printers.PrintHandler) { | ||||
| 		{Name: "Capacity", Type: "string", Description: apiv1.PersistentVolumeClaimStatus{}.SwaggerDoc()["capacity"]}, | ||||
| 		{Name: "Access Modes", Type: "string", Description: apiv1.PersistentVolumeClaimStatus{}.SwaggerDoc()["accessModes"]}, | ||||
| 		{Name: "StorageClass", Type: "string", Description: "StorageClass of the pvc"}, | ||||
| 		{Name: "VolumeAttributesClass", Type: "string", Description: "VolumeAttributesClass of the pvc"}, | ||||
| 		{Name: "Age", Type: "string", Description: metav1.ObjectMeta{}.SwaggerDoc()["creationTimestamp"]}, | ||||
| 		{Name: "VolumeMode", Type: "string", Priority: 1, Description: apiv1.PersistentVolumeClaimSpec{}.SwaggerDoc()["volumeMode"]}, | ||||
| 	} | ||||
| @@ -435,6 +438,15 @@ func AddHandlers(h printers.PrintHandler) { | ||||
| 	_ = h.TableHandler(storageClassColumnDefinitions, printStorageClass) | ||||
| 	_ = h.TableHandler(storageClassColumnDefinitions, printStorageClassList) | ||||
|  | ||||
| 	volumeAttributesClassColumnDefinitions := []metav1.TableColumnDefinition{ | ||||
| 		{Name: "Name", Type: "string", Format: "name", Description: metav1.ObjectMeta{}.SwaggerDoc()["name"]}, | ||||
| 		{Name: "DriverName", Type: "string", Description: storagev1alpha1.VolumeAttributesClass{}.SwaggerDoc()["driverName"]}, | ||||
| 		{Name: "Age", Type: "string", Description: metav1.ObjectMeta{}.SwaggerDoc()["creationTimestamp"]}, | ||||
| 	} | ||||
|  | ||||
| 	_ = h.TableHandler(volumeAttributesClassColumnDefinitions, printVolumeAttributesClass) | ||||
| 	_ = h.TableHandler(volumeAttributesClassColumnDefinitions, printVolumeAttributesClassList) | ||||
|  | ||||
| 	statusColumnDefinitions := []metav1.TableColumnDefinition{ | ||||
| 		{Name: "Status", Type: "string", Description: metav1.Status{}.SwaggerDoc()["status"]}, | ||||
| 		{Name: "Reason", Type: "string", Description: metav1.Status{}.SwaggerDoc()["reason"]}, | ||||
| @@ -1882,8 +1894,13 @@ func printPersistentVolume(obj *api.PersistentVolume, options printers.GenerateO | ||||
| 		volumeMode = string(*obj.Spec.VolumeMode) | ||||
| 	} | ||||
|  | ||||
| 	volumeAttributeClass := "<unset>" | ||||
| 	if obj.Spec.VolumeAttributesClassName != nil { | ||||
| 		volumeAttributeClass = *obj.Spec.VolumeAttributesClassName | ||||
| 	} | ||||
|  | ||||
| 	row.Cells = append(row.Cells, obj.Name, aSize, modesStr, reclaimPolicyStr, | ||||
| 		string(phase), claimRefUID, helper.GetPersistentVolumeClass(obj), | ||||
| 		string(phase), claimRefUID, helper.GetPersistentVolumeClass(obj), volumeAttributeClass, | ||||
| 		obj.Status.Reason, translateTimestampSince(obj.CreationTimestamp), volumeMode) | ||||
| 	return []metav1.TableRow{row}, nil | ||||
| } | ||||
| @@ -1910,10 +1927,16 @@ func printPersistentVolumeClaim(obj *api.PersistentVolumeClaim, options printers | ||||
| 		phase = "Terminating" | ||||
| 	} | ||||
|  | ||||
| 	volumeAttributeClass := "<unset>" | ||||
| 	storage := obj.Spec.Resources.Requests[api.ResourceStorage] | ||||
| 	capacity := "" | ||||
| 	accessModes := "" | ||||
| 	volumeMode := "<unset>" | ||||
|  | ||||
| 	if obj.Spec.VolumeAttributesClassName != nil { | ||||
| 		volumeAttributeClass = *obj.Spec.VolumeAttributesClassName | ||||
| 	} | ||||
|  | ||||
| 	if obj.Spec.VolumeName != "" { | ||||
| 		accessModes = helper.GetAccessModesAsString(obj.Status.AccessModes) | ||||
| 		storage = obj.Status.Capacity[api.ResourceStorage] | ||||
| @@ -1925,7 +1948,7 @@ func printPersistentVolumeClaim(obj *api.PersistentVolumeClaim, options printers | ||||
| 	} | ||||
|  | ||||
| 	row.Cells = append(row.Cells, obj.Name, string(phase), obj.Spec.VolumeName, capacity, accessModes, | ||||
| 		helper.GetPersistentVolumeClaimClass(obj), translateTimestampSince(obj.CreationTimestamp), volumeMode) | ||||
| 		helper.GetPersistentVolumeClaimClass(obj), volumeAttributeClass, translateTimestampSince(obj.CreationTimestamp), volumeMode) | ||||
| 	return []metav1.TableRow{row}, nil | ||||
| } | ||||
|  | ||||
| @@ -2434,6 +2457,33 @@ func printStorageClassList(list *storage.StorageClassList, options printers.Gene | ||||
| 	return rows, nil | ||||
| } | ||||
|  | ||||
| func printVolumeAttributesClass(obj *storage.VolumeAttributesClass, options printers.GenerateOptions) ([]metav1.TableRow, error) { | ||||
| 	row := metav1.TableRow{ | ||||
| 		Object: runtime.RawExtension{Object: obj}, | ||||
| 	} | ||||
|  | ||||
| 	name := obj.Name | ||||
| 	if storageutil.IsDefaultAnnotationForVolumeAttributesClass(obj.ObjectMeta) { | ||||
| 		name += " (default)" | ||||
| 	} | ||||
|  | ||||
| 	row.Cells = append(row.Cells, name, obj.DriverName, translateTimestampSince(obj.CreationTimestamp)) | ||||
|  | ||||
| 	return []metav1.TableRow{row}, nil | ||||
| } | ||||
|  | ||||
| func printVolumeAttributesClassList(list *storage.VolumeAttributesClassList, options printers.GenerateOptions) ([]metav1.TableRow, error) { | ||||
| 	rows := make([]metav1.TableRow, 0, len(list.Items)) | ||||
| 	for i := range list.Items { | ||||
| 		r, err := printVolumeAttributesClass(&list.Items[i], options) | ||||
| 		if err != nil { | ||||
| 			return nil, err | ||||
| 		} | ||||
| 		rows = append(rows, r...) | ||||
| 	} | ||||
| 	return rows, nil | ||||
| } | ||||
|  | ||||
| func printLease(obj *coordination.Lease, options printers.GenerateOptions) ([]metav1.TableRow, error) { | ||||
| 	row := metav1.TableRow{ | ||||
| 		Object: runtime.RawExtension{Object: obj}, | ||||
|   | ||||
| @@ -4711,6 +4711,7 @@ func TestPrintStatefulSet(t *testing.T) { | ||||
|  | ||||
| func TestPrintPersistentVolume(t *testing.T) { | ||||
| 	myScn := "my-scn" | ||||
| 	myVacn := "my-vacn" | ||||
|  | ||||
| 	claimRef := api.ObjectReference{ | ||||
| 		Name:      "test", | ||||
| @@ -4737,7 +4738,7 @@ func TestPrintPersistentVolume(t *testing.T) { | ||||
| 					Phase: api.VolumeBound, | ||||
| 				}, | ||||
| 			}, | ||||
| 			expected: []metav1.TableRow{{Cells: []interface{}{"test1", "4Gi", "ROX", "", "Bound", "default/test", "", "", "<unknown>", "<unset>"}}}, | ||||
| 			expected: []metav1.TableRow{{Cells: []interface{}{"test1", "4Gi", "ROX", "", "Bound", "default/test", "", "<unset>", "", "<unknown>", "<unset>"}}}, | ||||
| 		}, | ||||
| 		{ | ||||
| 			// Test failed | ||||
| @@ -4756,7 +4757,7 @@ func TestPrintPersistentVolume(t *testing.T) { | ||||
| 					Phase: api.VolumeFailed, | ||||
| 				}, | ||||
| 			}, | ||||
| 			expected: []metav1.TableRow{{Cells: []interface{}{"test2", "4Gi", "ROX", "", "Failed", "default/test", "", "", "<unknown>", "<unset>"}}}, | ||||
| 			expected: []metav1.TableRow{{Cells: []interface{}{"test2", "4Gi", "ROX", "", "Failed", "default/test", "", "<unset>", "", "<unknown>", "<unset>"}}}, | ||||
| 		}, | ||||
| 		{ | ||||
| 			// Test pending | ||||
| @@ -4775,7 +4776,7 @@ func TestPrintPersistentVolume(t *testing.T) { | ||||
| 					Phase: api.VolumePending, | ||||
| 				}, | ||||
| 			}, | ||||
| 			expected: []metav1.TableRow{{Cells: []interface{}{"test3", "10Gi", "RWX", "", "Pending", "default/test", "", "", "<unknown>", "<unset>"}}}, | ||||
| 			expected: []metav1.TableRow{{Cells: []interface{}{"test3", "10Gi", "RWX", "", "Pending", "default/test", "", "<unset>", "", "<unknown>", "<unset>"}}}, | ||||
| 		}, | ||||
| 		{ | ||||
| 			// Test pending, storageClass | ||||
| @@ -4795,7 +4796,28 @@ func TestPrintPersistentVolume(t *testing.T) { | ||||
| 					Phase: api.VolumePending, | ||||
| 				}, | ||||
| 			}, | ||||
| 			expected: []metav1.TableRow{{Cells: []interface{}{"test4", "10Gi", "RWO", "", "Pending", "default/test", "my-scn", "", "<unknown>", "<unset>"}}}, | ||||
| 			expected: []metav1.TableRow{{Cells: []interface{}{"test4", "10Gi", "RWO", "", "Pending", "default/test", "my-scn", "<unset>", "", "<unknown>", "<unset>"}}}, | ||||
| 		}, | ||||
| 		{ | ||||
| 			// Test pending, storageClass, volumeAttributesClass | ||||
| 			pv: api.PersistentVolume{ | ||||
| 				ObjectMeta: metav1.ObjectMeta{ | ||||
| 					Name: "test4", | ||||
| 				}, | ||||
| 				Spec: api.PersistentVolumeSpec{ | ||||
| 					ClaimRef:                  &claimRef, | ||||
| 					StorageClassName:          myScn, | ||||
| 					VolumeAttributesClassName: &myVacn, | ||||
| 					AccessModes:               []api.PersistentVolumeAccessMode{api.ReadWriteOnce}, | ||||
| 					Capacity: map[api.ResourceName]resource.Quantity{ | ||||
| 						api.ResourceStorage: resource.MustParse("10Gi"), | ||||
| 					}, | ||||
| 				}, | ||||
| 				Status: api.PersistentVolumeStatus{ | ||||
| 					Phase: api.VolumePending, | ||||
| 				}, | ||||
| 			}, | ||||
| 			expected: []metav1.TableRow{{Cells: []interface{}{"test4", "10Gi", "RWO", "", "Pending", "default/test", "my-scn", "my-vacn", "", "<unknown>", "<unset>"}}}, | ||||
| 		}, | ||||
| 		{ | ||||
| 			// Test available | ||||
| @@ -4815,7 +4837,7 @@ func TestPrintPersistentVolume(t *testing.T) { | ||||
| 					Phase: api.VolumeAvailable, | ||||
| 				}, | ||||
| 			}, | ||||
| 			expected: []metav1.TableRow{{Cells: []interface{}{"test5", "10Gi", "RWO", "", "Available", "default/test", "my-scn", "", "<unknown>", "<unset>"}}}, | ||||
| 			expected: []metav1.TableRow{{Cells: []interface{}{"test5", "10Gi", "RWO", "", "Available", "default/test", "my-scn", "<unset>", "", "<unknown>", "<unset>"}}}, | ||||
| 		}, | ||||
| 		{ | ||||
| 			// Test released | ||||
| @@ -4835,7 +4857,7 @@ func TestPrintPersistentVolume(t *testing.T) { | ||||
| 					Phase: api.VolumeReleased, | ||||
| 				}, | ||||
| 			}, | ||||
| 			expected: []metav1.TableRow{{Cells: []interface{}{"test6", "10Gi", "RWO", "", "Released", "default/test", "my-scn", "", "<unknown>", "<unset>"}}}, | ||||
| 			expected: []metav1.TableRow{{Cells: []interface{}{"test6", "10Gi", "RWO", "", "Released", "default/test", "my-scn", "<unset>", "", "<unknown>", "<unset>"}}}, | ||||
| 		}, | ||||
| 	} | ||||
|  | ||||
| @@ -4855,6 +4877,7 @@ func TestPrintPersistentVolume(t *testing.T) { | ||||
|  | ||||
| func TestPrintPersistentVolumeClaim(t *testing.T) { | ||||
| 	volumeMode := api.PersistentVolumeFilesystem | ||||
| 	myVacn := "my-vacn" | ||||
| 	myScn := "my-scn" | ||||
| 	tests := []struct { | ||||
| 		pvc      api.PersistentVolumeClaim | ||||
| @@ -4878,7 +4901,7 @@ func TestPrintPersistentVolumeClaim(t *testing.T) { | ||||
| 					}, | ||||
| 				}, | ||||
| 			}, | ||||
| 			expected: []metav1.TableRow{{Cells: []interface{}{"test1", "Bound", "my-volume", "4Gi", "ROX", "", "<unknown>", "Filesystem"}}}, | ||||
| 			expected: []metav1.TableRow{{Cells: []interface{}{"test1", "Bound", "my-volume", "4Gi", "ROX", "", "<unset>", "<unknown>", "Filesystem"}}}, | ||||
| 		}, | ||||
| 		{ | ||||
| 			// Test name, num of containers, restarts, container ready status | ||||
| @@ -4897,7 +4920,7 @@ func TestPrintPersistentVolumeClaim(t *testing.T) { | ||||
| 					}, | ||||
| 				}, | ||||
| 			}, | ||||
| 			expected: []metav1.TableRow{{Cells: []interface{}{"test2", "Lost", "", "", "", "", "<unknown>", "Filesystem"}}}, | ||||
| 			expected: []metav1.TableRow{{Cells: []interface{}{"test2", "Lost", "", "", "", "", "<unset>", "<unknown>", "Filesystem"}}}, | ||||
| 		}, | ||||
| 		{ | ||||
| 			// Test name, num of containers, restarts, container ready status | ||||
| @@ -4917,7 +4940,7 @@ func TestPrintPersistentVolumeClaim(t *testing.T) { | ||||
| 					}, | ||||
| 				}, | ||||
| 			}, | ||||
| 			expected: []metav1.TableRow{{Cells: []interface{}{"test3", "Pending", "my-volume", "10Gi", "RWX", "", "<unknown>", "Filesystem"}}}, | ||||
| 			expected: []metav1.TableRow{{Cells: []interface{}{"test3", "Pending", "my-volume", "10Gi", "RWX", "", "<unset>", "<unknown>", "Filesystem"}}}, | ||||
| 		}, | ||||
| 		{ | ||||
| 			// Test name, num of containers, restarts, container ready status | ||||
| @@ -4938,7 +4961,7 @@ func TestPrintPersistentVolumeClaim(t *testing.T) { | ||||
| 					}, | ||||
| 				}, | ||||
| 			}, | ||||
| 			expected: []metav1.TableRow{{Cells: []interface{}{"test4", "Pending", "my-volume", "10Gi", "RWO", "my-scn", "<unknown>", "Filesystem"}}}, | ||||
| 			expected: []metav1.TableRow{{Cells: []interface{}{"test4", "Pending", "my-volume", "10Gi", "RWO", "my-scn", "<unset>", "<unknown>", "Filesystem"}}}, | ||||
| 		}, | ||||
| 		{ | ||||
| 			// Test name, num of containers, restarts, container ready status | ||||
| @@ -4958,7 +4981,28 @@ func TestPrintPersistentVolumeClaim(t *testing.T) { | ||||
| 					}, | ||||
| 				}, | ||||
| 			}, | ||||
| 			expected: []metav1.TableRow{{Cells: []interface{}{"test5", "Pending", "my-volume", "10Gi", "RWO", "my-scn", "<unknown>", "<unset>"}}}, | ||||
| 			expected: []metav1.TableRow{{Cells: []interface{}{"test5", "Pending", "my-volume", "10Gi", "RWO", "my-scn", "<unset>", "<unknown>", "<unset>"}}}, | ||||
| 		}, | ||||
| 		{ | ||||
| 			// Test name, num of containers, restarts, container ready status | ||||
| 			pvc: api.PersistentVolumeClaim{ | ||||
| 				ObjectMeta: metav1.ObjectMeta{ | ||||
| 					Name: "test5", | ||||
| 				}, | ||||
| 				Spec: api.PersistentVolumeClaimSpec{ | ||||
| 					VolumeName:                "my-volume", | ||||
| 					StorageClassName:          &myScn, | ||||
| 					VolumeAttributesClassName: &myVacn, | ||||
| 				}, | ||||
| 				Status: api.PersistentVolumeClaimStatus{ | ||||
| 					Phase:       api.ClaimPending, | ||||
| 					AccessModes: []api.PersistentVolumeAccessMode{api.ReadWriteOnce}, | ||||
| 					Capacity: map[api.ResourceName]resource.Quantity{ | ||||
| 						api.ResourceStorage: resource.MustParse("10Gi"), | ||||
| 					}, | ||||
| 				}, | ||||
| 			}, | ||||
| 			expected: []metav1.TableRow{{Cells: []interface{}{"test5", "Pending", "my-volume", "10Gi", "RWO", "my-scn", "my-vacn", "<unknown>", "<unset>"}}}, | ||||
| 		}, | ||||
| 	} | ||||
|  | ||||
| @@ -5346,6 +5390,51 @@ func TestPrintStorageClass(t *testing.T) { | ||||
| 	} | ||||
| } | ||||
|  | ||||
| func TestPrintVolumeAttributesClass(t *testing.T) { | ||||
| 	tests := []struct { | ||||
| 		vac      storage.VolumeAttributesClass | ||||
| 		expected []metav1.TableRow | ||||
| 	}{ | ||||
| 		{ | ||||
| 			vac: storage.VolumeAttributesClass{ | ||||
| 				ObjectMeta: metav1.ObjectMeta{ | ||||
| 					Name:              "vac1", | ||||
| 					CreationTimestamp: metav1.Time{Time: time.Now().Add(1.9e9)}, | ||||
| 				}, | ||||
| 				DriverName: "fake", | ||||
| 			}, | ||||
| 			expected: []metav1.TableRow{{Cells: []interface{}{"vac1", "fake", "0s"}}}, | ||||
| 		}, | ||||
| 		{ | ||||
| 			vac: storage.VolumeAttributesClass{ | ||||
| 				ObjectMeta: metav1.ObjectMeta{ | ||||
| 					Name:              "vac2", | ||||
| 					CreationTimestamp: metav1.Time{Time: time.Now().Add(-3e11)}, | ||||
| 				}, | ||||
| 				DriverName: "fake", | ||||
| 				Parameters: map[string]string{ | ||||
| 					"iops":       "500", | ||||
| 					"throughput": "50MiB/s", | ||||
| 				}, | ||||
| 			}, | ||||
| 			expected: []metav1.TableRow{{Cells: []interface{}{"vac2", "fake", "5m"}}}, | ||||
| 		}, | ||||
| 	} | ||||
|  | ||||
| 	for i, test := range tests { | ||||
| 		rows, err := printVolumeAttributesClass(&test.vac, printers.GenerateOptions{}) | ||||
| 		if err != nil { | ||||
| 			t.Fatal(err) | ||||
| 		} | ||||
| 		for i := range rows { | ||||
| 			rows[i].Object.Object = nil | ||||
| 		} | ||||
| 		if !reflect.DeepEqual(test.expected, rows) { | ||||
| 			t.Errorf("%d mismatch: %s", i, cmp.Diff(test.expected, rows)) | ||||
| 		} | ||||
| 	} | ||||
| } | ||||
|  | ||||
| func TestPrintLease(t *testing.T) { | ||||
| 	holder1 := "holder1" | ||||
| 	holder2 := "holder2" | ||||
|   | ||||
| @@ -31,6 +31,7 @@ import ( | ||||
| 	csistoragecapacitystore "k8s.io/kubernetes/pkg/registry/storage/csistoragecapacity/storage" | ||||
| 	storageclassstore "k8s.io/kubernetes/pkg/registry/storage/storageclass/storage" | ||||
| 	volumeattachmentstore "k8s.io/kubernetes/pkg/registry/storage/volumeattachment/storage" | ||||
| 	volumeattributesclassstore "k8s.io/kubernetes/pkg/registry/storage/volumeattributesclass/storage" | ||||
| ) | ||||
|  | ||||
| type RESTStorageProvider struct { | ||||
| @@ -72,6 +73,15 @@ func (p RESTStorageProvider) v1alpha1Storage(apiResourceConfigSource serverstora | ||||
| 		storage[resource] = csiStorageStorage.CSIStorageCapacity | ||||
| 	} | ||||
|  | ||||
| 	// register volumeattributesclasses | ||||
| 	if resource := "volumeattributesclasses"; apiResourceConfigSource.ResourceEnabled(storageapiv1alpha1.SchemeGroupVersion.WithResource(resource)) { | ||||
| 		volumeAttributesClassStorage, err := volumeattributesclassstore.NewREST(restOptionsGetter) | ||||
| 		if err != nil { | ||||
| 			return storage, err | ||||
| 		} | ||||
| 		storage[resource] = volumeAttributesClassStorage | ||||
| 	} | ||||
|  | ||||
| 	return storage, nil | ||||
| } | ||||
|  | ||||
|   | ||||
							
								
								
									
										19
									
								
								pkg/registry/storage/volumeattributesclass/doc.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										19
									
								
								pkg/registry/storage/volumeattributesclass/doc.go
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,19 @@ | ||||
| /* | ||||
| Copyright 2023 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 volumeattributesclass provides Registry interface and its REST | ||||
| // implementation for storing volumeattributesclass api objects. | ||||
| package volumeattributesclass | ||||
| @@ -0,0 +1,65 @@ | ||||
| /* | ||||
| Copyright 2023 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 storage | ||||
|  | ||||
| import ( | ||||
| 	"k8s.io/apimachinery/pkg/runtime" | ||||
| 	"k8s.io/apiserver/pkg/registry/generic" | ||||
| 	genericregistry "k8s.io/apiserver/pkg/registry/generic/registry" | ||||
| 	"k8s.io/apiserver/pkg/registry/rest" | ||||
| 	storageapi "k8s.io/kubernetes/pkg/apis/storage" | ||||
| 	"k8s.io/kubernetes/pkg/printers" | ||||
| 	printersinternal "k8s.io/kubernetes/pkg/printers/internalversion" | ||||
| 	printerstorage "k8s.io/kubernetes/pkg/printers/storage" | ||||
| 	"k8s.io/kubernetes/pkg/registry/storage/volumeattributesclass" | ||||
| ) | ||||
|  | ||||
| // REST implements a RESTStorage for volume attributes classes. | ||||
| type REST struct { | ||||
| 	*genericregistry.Store | ||||
| } | ||||
|  | ||||
| // NewREST returns a RESTStorage object that will work against storage classes. | ||||
| func NewREST(optsGetter generic.RESTOptionsGetter) (*REST, error) { | ||||
| 	store := &genericregistry.Store{ | ||||
| 		NewFunc:                   func() runtime.Object { return &storageapi.VolumeAttributesClass{} }, | ||||
| 		NewListFunc:               func() runtime.Object { return &storageapi.VolumeAttributesClassList{} }, | ||||
| 		DefaultQualifiedResource:  storageapi.Resource("volumeattributesclasses"), | ||||
| 		SingularQualifiedResource: storageapi.Resource("volumeattributesclass"), | ||||
|  | ||||
| 		CreateStrategy:      volumeattributesclass.Strategy, | ||||
| 		UpdateStrategy:      volumeattributesclass.Strategy, | ||||
| 		DeleteStrategy:      volumeattributesclass.Strategy, | ||||
| 		ReturnDeletedObject: true, | ||||
|  | ||||
| 		TableConvertor: printerstorage.TableConvertor{TableGenerator: printers.NewTableGenerator().With(printersinternal.AddHandlers)}, | ||||
| 	} | ||||
| 	options := &generic.StoreOptions{RESTOptions: optsGetter} | ||||
| 	if err := store.CompleteWithOptions(options); err != nil { | ||||
| 		return nil, err | ||||
| 	} | ||||
|  | ||||
| 	return &REST{store}, nil | ||||
| } | ||||
|  | ||||
| // Implement ShortNamesProvider | ||||
| var _ rest.ShortNamesProvider = &REST{} | ||||
|  | ||||
| // ShortNames implements the ShortNamesProvider interface. Returns a list of short names for a resource. | ||||
| func (r *REST) ShortNames() []string { | ||||
| 	return []string{"vac"} | ||||
| } | ||||
| @@ -0,0 +1,148 @@ | ||||
| /* | ||||
| Copyright 2023 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 storage | ||||
|  | ||||
| import ( | ||||
| 	"testing" | ||||
|  | ||||
| 	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" | ||||
| 	"k8s.io/apimachinery/pkg/fields" | ||||
| 	"k8s.io/apimachinery/pkg/labels" | ||||
| 	"k8s.io/apimachinery/pkg/runtime" | ||||
| 	"k8s.io/apiserver/pkg/registry/generic" | ||||
| 	genericregistrytest "k8s.io/apiserver/pkg/registry/generic/testing" | ||||
| 	etcd3testing "k8s.io/apiserver/pkg/storage/etcd3/testing" | ||||
| 	storageapi "k8s.io/kubernetes/pkg/apis/storage" | ||||
| 	"k8s.io/kubernetes/pkg/registry/registrytest" | ||||
| ) | ||||
|  | ||||
| func newStorage(t *testing.T) (*REST, *etcd3testing.EtcdTestServer) { | ||||
| 	etcdStorage, server := registrytest.NewEtcdStorageForResource(t, storageapi.SchemeGroupVersion.WithResource("volumeattributesclasses").GroupResource()) | ||||
| 	restOptions := generic.RESTOptions{ | ||||
| 		StorageConfig:           etcdStorage, | ||||
| 		Decorator:               generic.UndecoratedStorage, | ||||
| 		DeleteCollectionWorkers: 1, | ||||
| 		ResourcePrefix:          "volumeattributesclasses", | ||||
| 	} | ||||
| 	volumeAttributesClassStorage, err := NewREST(restOptions) | ||||
| 	if err != nil { | ||||
| 		t.Fatalf("unexpected error from REST storage: %v", err) | ||||
| 	} | ||||
| 	return volumeAttributesClassStorage, server | ||||
| } | ||||
|  | ||||
| func validNewVolumeAttributesClass(name string) *storageapi.VolumeAttributesClass { | ||||
| 	return &storageapi.VolumeAttributesClass{ | ||||
| 		ObjectMeta: metav1.ObjectMeta{ | ||||
| 			Name: name, | ||||
| 		}, | ||||
| 		DriverName: "fake", | ||||
| 		Parameters: map[string]string{ | ||||
| 			"foo": "bar", | ||||
| 		}, | ||||
| 	} | ||||
| } | ||||
|  | ||||
| func TestCreate(t *testing.T) { | ||||
| 	storage, server := newStorage(t) | ||||
| 	defer server.Terminate(t) | ||||
| 	defer storage.Store.DestroyFunc() | ||||
| 	test := genericregistrytest.New(t, storage.Store).ClusterScope() | ||||
| 	volumeAttributesClass := validNewVolumeAttributesClass("foo") | ||||
| 	volumeAttributesClass.ObjectMeta = metav1.ObjectMeta{GenerateName: "foo"} | ||||
| 	test.TestCreate( | ||||
| 		// valid | ||||
| 		volumeAttributesClass, | ||||
| 		// invalid | ||||
| 		&storageapi.VolumeAttributesClass{ | ||||
| 			ObjectMeta: metav1.ObjectMeta{Name: "*BadName!"}, | ||||
| 			Parameters: map[string]string{"foo": "bar"}, | ||||
| 		}, | ||||
| 	) | ||||
| } | ||||
|  | ||||
| func TestUpdate(t *testing.T) { | ||||
| 	storage, server := newStorage(t) | ||||
| 	defer server.Terminate(t) | ||||
| 	defer storage.Store.DestroyFunc() | ||||
| 	test := genericregistrytest.New(t, storage.Store).ClusterScope() | ||||
| 	test.TestUpdate( | ||||
| 		// valid | ||||
| 		validNewVolumeAttributesClass("foo"), | ||||
| 		// updateFunc | ||||
| 		func(obj runtime.Object) runtime.Object { | ||||
| 			object := obj.(*storageapi.VolumeAttributesClass) | ||||
| 			object.Parameters = map[string]string{"foo": "bar"} | ||||
| 			return object | ||||
| 		}, | ||||
| 		// invalid update | ||||
| 		func(obj runtime.Object) runtime.Object { | ||||
| 			object := obj.(*storageapi.VolumeAttributesClass) | ||||
| 			object.Parameters = map[string]string{"faz": "bar"} | ||||
| 			return object | ||||
| 		}, | ||||
| 	) | ||||
|  | ||||
| } | ||||
|  | ||||
| func TestDelete(t *testing.T) { | ||||
| 	storage, server := newStorage(t) | ||||
| 	defer server.Terminate(t) | ||||
| 	defer storage.Store.DestroyFunc() | ||||
| 	test := genericregistrytest.New(t, storage.Store).ClusterScope().ReturnDeletedObject() | ||||
| 	test.TestDelete(validNewVolumeAttributesClass("foo")) | ||||
| } | ||||
|  | ||||
| func TestGet(t *testing.T) { | ||||
| 	storage, server := newStorage(t) | ||||
| 	defer server.Terminate(t) | ||||
| 	defer storage.Store.DestroyFunc() | ||||
| 	test := genericregistrytest.New(t, storage.Store).ClusterScope() | ||||
| 	test.TestGet(validNewVolumeAttributesClass("foo")) | ||||
| } | ||||
|  | ||||
| func TestList(t *testing.T) { | ||||
| 	storage, server := newStorage(t) | ||||
| 	defer server.Terminate(t) | ||||
| 	defer storage.Store.DestroyFunc() | ||||
| 	test := genericregistrytest.New(t, storage.Store).ClusterScope() | ||||
| 	test.TestList(validNewVolumeAttributesClass("foo")) | ||||
| } | ||||
|  | ||||
| func TestWatch(t *testing.T) { | ||||
| 	storage, server := newStorage(t) | ||||
| 	defer server.Terminate(t) | ||||
| 	defer storage.Store.DestroyFunc() | ||||
| 	test := genericregistrytest.New(t, storage.Store).ClusterScope() | ||||
| 	test.TestWatch( | ||||
| 		validNewVolumeAttributesClass("foo"), | ||||
| 		// matching labels | ||||
| 		[]labels.Set{}, | ||||
| 		// not matching labels | ||||
| 		[]labels.Set{ | ||||
| 			{"foo": "bar"}, | ||||
| 		}, | ||||
| 		// matching fields | ||||
| 		[]fields.Set{ | ||||
| 			{"metadata.name": "foo"}, | ||||
| 		}, | ||||
| 		// not matching fields | ||||
| 		[]fields.Set{ | ||||
| 			{"metadata.name": "bar"}, | ||||
| 		}, | ||||
| 	) | ||||
| } | ||||
							
								
								
									
										82
									
								
								pkg/registry/storage/volumeattributesclass/strategy.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										82
									
								
								pkg/registry/storage/volumeattributesclass/strategy.go
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,82 @@ | ||||
| /* | ||||
| Copyright 2023 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 volumeattributesclass | ||||
|  | ||||
| import ( | ||||
| 	"context" | ||||
|  | ||||
| 	"k8s.io/apimachinery/pkg/runtime" | ||||
| 	"k8s.io/apimachinery/pkg/util/validation/field" | ||||
| 	"k8s.io/apiserver/pkg/storage/names" | ||||
| 	"k8s.io/kubernetes/pkg/api/legacyscheme" | ||||
| 	"k8s.io/kubernetes/pkg/apis/storage" | ||||
| 	"k8s.io/kubernetes/pkg/apis/storage/validation" | ||||
| ) | ||||
|  | ||||
| // volumeAttributesClassStrategy implements behavior for VolumeAttributesClassStrategy objects | ||||
| type volumeAttributesClassStrategy struct { | ||||
| 	runtime.ObjectTyper | ||||
| 	names.NameGenerator | ||||
| } | ||||
|  | ||||
| // Strategy is the default logic that applies when creating and updating | ||||
| // VolumeAttributesClass objects via the REST API. | ||||
| var Strategy = volumeAttributesClassStrategy{legacyscheme.Scheme, names.SimpleNameGenerator} | ||||
|  | ||||
| func (volumeAttributesClassStrategy) NamespaceScoped() bool { | ||||
| 	return false | ||||
| } | ||||
|  | ||||
| // ResetBeforeCreate clears the Status field which is not allowed to be set by end users on creation. | ||||
| func (volumeAttributesClassStrategy) PrepareForCreate(ctx context.Context, obj runtime.Object) { | ||||
| } | ||||
|  | ||||
| func (volumeAttributesClassStrategy) Validate(ctx context.Context, obj runtime.Object) field.ErrorList { | ||||
| 	volumeAttributesClass := obj.(*storage.VolumeAttributesClass) | ||||
| 	return validation.ValidateVolumeAttributesClass(volumeAttributesClass) | ||||
| } | ||||
|  | ||||
| // WarningsOnCreate returns warnings for the creation of the given object. | ||||
| func (volumeAttributesClassStrategy) WarningsOnCreate(ctx context.Context, obj runtime.Object) []string { | ||||
| 	return nil | ||||
| } | ||||
|  | ||||
| // Canonicalize normalizes the object after validation. | ||||
| func (volumeAttributesClassStrategy) Canonicalize(obj runtime.Object) { | ||||
| } | ||||
|  | ||||
| func (volumeAttributesClassStrategy) AllowCreateOnUpdate() bool { | ||||
| 	return false | ||||
| } | ||||
|  | ||||
| // PrepareForUpdate sets the Status fields which is not allowed to be set by an end user updating a PV | ||||
| func (volumeAttributesClassStrategy) PrepareForUpdate(ctx context.Context, obj, old runtime.Object) { | ||||
| } | ||||
|  | ||||
| func (volumeAttributesClassStrategy) ValidateUpdate(ctx context.Context, obj, old runtime.Object) field.ErrorList { | ||||
| 	errorList := validation.ValidateVolumeAttributesClass(obj.(*storage.VolumeAttributesClass)) | ||||
| 	return append(errorList, validation.ValidateVolumeAttributesClassUpdate(obj.(*storage.VolumeAttributesClass), old.(*storage.VolumeAttributesClass))...) | ||||
| } | ||||
|  | ||||
| // WarningsOnUpdate returns warnings for the given update. | ||||
| func (volumeAttributesClassStrategy) WarningsOnUpdate(ctx context.Context, obj, old runtime.Object) []string { | ||||
| 	return nil | ||||
| } | ||||
|  | ||||
| func (volumeAttributesClassStrategy) AllowUnconditionalUpdate() bool { | ||||
| 	return true | ||||
| } | ||||
							
								
								
									
										70
									
								
								pkg/registry/storage/volumeattributesclass/strategy_test.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										70
									
								
								pkg/registry/storage/volumeattributesclass/strategy_test.go
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,70 @@ | ||||
| /* | ||||
| Copyright 2023 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 volumeattributesclass | ||||
|  | ||||
| import ( | ||||
| 	"testing" | ||||
|  | ||||
| 	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" | ||||
| 	genericapirequest "k8s.io/apiserver/pkg/endpoints/request" | ||||
| 	"k8s.io/kubernetes/pkg/apis/storage" | ||||
| ) | ||||
|  | ||||
| func TestVolumeAttributesClassStrategy(t *testing.T) { | ||||
| 	ctx := genericapirequest.NewDefaultContext() | ||||
| 	if Strategy.NamespaceScoped() { | ||||
| 		t.Errorf("VolumeAttributesClassStrategy must not be namespace scoped") | ||||
| 	} | ||||
| 	if Strategy.AllowCreateOnUpdate() { | ||||
| 		t.Errorf("VolumeAttributesClassStrategy should not allow create on update") | ||||
| 	} | ||||
|  | ||||
| 	class := &storage.VolumeAttributesClass{ | ||||
| 		ObjectMeta: metav1.ObjectMeta{ | ||||
| 			Name: "valid-class", | ||||
| 		}, | ||||
| 		DriverName: "fake", | ||||
| 		Parameters: map[string]string{ | ||||
| 			"foo": "bar", | ||||
| 		}, | ||||
| 	} | ||||
|  | ||||
| 	Strategy.PrepareForCreate(ctx, class) | ||||
|  | ||||
| 	errs := Strategy.Validate(ctx, class) | ||||
| 	if len(errs) != 0 { | ||||
| 		t.Errorf("unexpected error validating %v", errs) | ||||
| 	} | ||||
|  | ||||
| 	newClass := &storage.VolumeAttributesClass{ | ||||
| 		ObjectMeta: metav1.ObjectMeta{ | ||||
| 			Name:            "valid-class-2", | ||||
| 			ResourceVersion: "4", | ||||
| 		}, | ||||
| 		DriverName: "fake", | ||||
| 		Parameters: map[string]string{ | ||||
| 			"foo": "bar", | ||||
| 		}, | ||||
| 	} | ||||
|  | ||||
| 	Strategy.PrepareForUpdate(ctx, newClass, class) | ||||
|  | ||||
| 	errs = Strategy.ValidateUpdate(ctx, newClass, class) | ||||
| 	if len(errs) == 0 { | ||||
| 		t.Errorf("Expected a validation error") | ||||
| 	} | ||||
| } | ||||
							
								
								
									
										72
									
								
								pkg/volume/util/volumeattributesclass.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										72
									
								
								pkg/volume/util/volumeattributesclass.go
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,72 @@ | ||||
| /* | ||||
| Copyright 2023 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 util | ||||
|  | ||||
| import ( | ||||
| 	"sort" | ||||
|  | ||||
| 	storagev1alpha1 "k8s.io/api/storage/v1alpha1" | ||||
| 	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" | ||||
| 	"k8s.io/apimachinery/pkg/labels" | ||||
| 	storagev1alpha1listers "k8s.io/client-go/listers/storage/v1alpha1" | ||||
| 	"k8s.io/klog/v2" | ||||
| ) | ||||
|  | ||||
| const ( | ||||
| 	// AlphaIsDefaultVolumeAttributesClassAnnotation is the alpha version of IsDefaultVolumeAttributesClassAnnotation. | ||||
| 	AlphaIsDefaultVolumeAttributesClassAnnotation = "volumeattributesclass.alpha.kubernetes.io/is-default-class" | ||||
| ) | ||||
|  | ||||
| // GetDefaultVolumeAttributesClass returns the default VolumeAttributesClass from the store, or nil. | ||||
| func GetDefaultVolumeAttributesClass(lister storagev1alpha1listers.VolumeAttributesClassLister, driverName string) (*storagev1alpha1.VolumeAttributesClass, error) { | ||||
| 	list, err := lister.List(labels.Everything()) | ||||
| 	if err != nil { | ||||
| 		return nil, err | ||||
| 	} | ||||
|  | ||||
| 	defaultClasses := []*storagev1alpha1.VolumeAttributesClass{} | ||||
| 	for _, class := range list { | ||||
| 		if IsDefaultVolumeAttributesClassAnnotation(class.ObjectMeta) && class.DriverName == driverName { | ||||
| 			defaultClasses = append(defaultClasses, class) | ||||
| 			klog.V(4).Infof("GetDefaultVolumeAttributesClass added: %s", class.Name) | ||||
| 		} | ||||
| 	} | ||||
|  | ||||
| 	if len(defaultClasses) == 0 { | ||||
| 		return nil, nil | ||||
| 	} | ||||
|  | ||||
| 	// Primary sort by creation timestamp, newest first | ||||
| 	// Secondary sort by class name, ascending order | ||||
| 	sort.Slice(defaultClasses, func(i, j int) bool { | ||||
| 		if defaultClasses[i].CreationTimestamp.UnixNano() == defaultClasses[j].CreationTimestamp.UnixNano() { | ||||
| 			return defaultClasses[i].Name < defaultClasses[j].Name | ||||
| 		} | ||||
| 		return defaultClasses[i].CreationTimestamp.UnixNano() > defaultClasses[j].CreationTimestamp.UnixNano() | ||||
| 	}) | ||||
| 	if len(defaultClasses) > 1 { | ||||
| 		klog.V(4).Infof("%d default VolumeAttributesClass were found, choosing: %s", len(defaultClasses), defaultClasses[0].Name) | ||||
| 	} | ||||
|  | ||||
| 	return defaultClasses[0], nil | ||||
| } | ||||
|  | ||||
| // IsDefaultVolumeAttributesClassAnnotation returns a boolean if the default | ||||
| // volume attributes class annotation is set | ||||
| func IsDefaultVolumeAttributesClassAnnotation(obj metav1.ObjectMeta) bool { | ||||
| 	return obj.Annotations[AlphaIsDefaultVolumeAttributesClassAnnotation] == "true" | ||||
| } | ||||
							
								
								
									
										224
									
								
								pkg/volume/util/volumeattributesclass_test.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										224
									
								
								pkg/volume/util/volumeattributesclass_test.go
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,224 @@ | ||||
| /* | ||||
| Copyright 2023 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 util | ||||
|  | ||||
| import ( | ||||
| 	"testing" | ||||
| 	"time" | ||||
|  | ||||
| 	storagev1alpha1 "k8s.io/api/storage/v1alpha1" | ||||
| 	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" | ||||
| 	"k8s.io/client-go/informers" | ||||
| 	"k8s.io/kubernetes/pkg/controller" | ||||
| ) | ||||
|  | ||||
| func TestGetDefaultVolumeAttributesClass(t *testing.T) { | ||||
| 	var ( | ||||
| 		t1 = time.Now() | ||||
| 		t2 = time.Now().Add(1 * time.Hour) | ||||
| 	) | ||||
|  | ||||
| 	dirverName1 := "my-driver1" | ||||
| 	vac1 := &storagev1alpha1.VolumeAttributesClass{ | ||||
| 		ObjectMeta: metav1.ObjectMeta{ | ||||
| 			Name: "my-vac1", | ||||
| 			Annotations: map[string]string{ | ||||
| 				"a": "b", | ||||
| 			}, | ||||
| 		}, | ||||
| 		DriverName: dirverName1, | ||||
| 	} | ||||
| 	vac2 := &storagev1alpha1.VolumeAttributesClass{ | ||||
| 		ObjectMeta: metav1.ObjectMeta{ | ||||
| 			Name: "my-vac2", | ||||
| 			Annotations: map[string]string{ | ||||
| 				"a": "b", | ||||
| 			}, | ||||
| 		}, | ||||
| 		DriverName: dirverName1, | ||||
| 	} | ||||
| 	vac3 := &storagev1alpha1.VolumeAttributesClass{ | ||||
| 		ObjectMeta: metav1.ObjectMeta{ | ||||
| 			Name: "my-vac3", | ||||
| 			Annotations: map[string]string{ | ||||
| 				AlphaIsDefaultVolumeAttributesClassAnnotation: "true", | ||||
| 			}, | ||||
| 			CreationTimestamp: metav1.Time{Time: t1}, | ||||
| 		}, | ||||
| 		DriverName: dirverName1, | ||||
| 	} | ||||
| 	vac4 := &storagev1alpha1.VolumeAttributesClass{ | ||||
| 		ObjectMeta: metav1.ObjectMeta{ | ||||
| 			Name: "my-vac4", | ||||
| 			Annotations: map[string]string{ | ||||
| 				AlphaIsDefaultVolumeAttributesClassAnnotation: "true", | ||||
| 			}, | ||||
| 			CreationTimestamp: metav1.Time{Time: t2}, | ||||
| 		}, | ||||
| 		DriverName: dirverName1, | ||||
| 	} | ||||
| 	vac5 := &storagev1alpha1.VolumeAttributesClass{ | ||||
| 		ObjectMeta: metav1.ObjectMeta{ | ||||
| 			Name: "my-vac5", | ||||
| 			Annotations: map[string]string{ | ||||
| 				AlphaIsDefaultVolumeAttributesClassAnnotation: "true", | ||||
| 			}, | ||||
| 			CreationTimestamp: metav1.Time{Time: t2}, | ||||
| 		}, | ||||
| 		DriverName: dirverName1, | ||||
| 	} | ||||
|  | ||||
| 	dirverName2 := "my-driver2" | ||||
| 	vac6 := &storagev1alpha1.VolumeAttributesClass{ | ||||
| 		ObjectMeta: metav1.ObjectMeta{ | ||||
| 			Name: "my-vac6", | ||||
| 			Annotations: map[string]string{ | ||||
| 				"a": "b", | ||||
| 			}, | ||||
| 		}, | ||||
| 		DriverName: dirverName2, | ||||
| 	} | ||||
| 	vac7 := &storagev1alpha1.VolumeAttributesClass{ | ||||
| 		ObjectMeta: metav1.ObjectMeta{ | ||||
| 			Name: "my-vac7", | ||||
| 			Annotations: map[string]string{ | ||||
| 				AlphaIsDefaultVolumeAttributesClassAnnotation: "true", | ||||
| 			}, | ||||
| 		}, | ||||
| 		DriverName: dirverName2, | ||||
| 	} | ||||
|  | ||||
| 	testCases := []struct { | ||||
| 		name       string | ||||
| 		driverName string | ||||
| 		classes    []*storagev1alpha1.VolumeAttributesClass | ||||
| 		expect     *storagev1alpha1.VolumeAttributesClass | ||||
| 	}{ | ||||
| 		{ | ||||
| 			name:       "no volume attributes class", | ||||
| 			driverName: dirverName1, | ||||
| 		}, | ||||
| 		{ | ||||
| 			name:       "no default volume attributes class", | ||||
| 			driverName: dirverName1, | ||||
| 			classes:    []*storagev1alpha1.VolumeAttributesClass{vac1, vac2, vac6}, | ||||
| 			expect:     nil, | ||||
| 		}, | ||||
| 		{ | ||||
| 			name:       "no default volume attributes class for the driverName1", | ||||
| 			driverName: dirverName1, | ||||
| 			classes:    []*storagev1alpha1.VolumeAttributesClass{vac1, vac2, vac6, vac7}, | ||||
| 			expect:     nil, | ||||
| 		}, | ||||
| 		{ | ||||
| 			name:       "one default volume attributes class for the driverName1", | ||||
| 			driverName: dirverName1, | ||||
| 			classes:    []*storagev1alpha1.VolumeAttributesClass{vac1, vac2, vac3, vac6, vac7}, | ||||
| 			expect:     vac3, | ||||
| 		}, | ||||
| 		{ | ||||
| 			name:       "two default volume attributes class with different creation timestamp for the driverName1", | ||||
| 			driverName: dirverName1, | ||||
| 			classes:    []*storagev1alpha1.VolumeAttributesClass{vac3, vac4, vac6, vac7}, | ||||
| 			expect:     vac4, | ||||
| 		}, | ||||
| 		{ | ||||
| 			name:       "two default volume attributes class with same creation timestamp for the driverName1", | ||||
| 			driverName: dirverName1, | ||||
| 			classes:    []*storagev1alpha1.VolumeAttributesClass{vac4, vac5, vac6, vac7}, | ||||
| 			expect:     vac4, | ||||
| 		}, | ||||
| 	} | ||||
|  | ||||
| 	for _, tc := range testCases { | ||||
| 		t.Run(tc.name, func(t *testing.T) { | ||||
| 			informerFactory := informers.NewSharedInformerFactory(nil, controller.NoResyncPeriodFunc()) | ||||
| 			for _, c := range tc.classes { | ||||
| 				err := informerFactory.Storage().V1alpha1().VolumeAttributesClasses().Informer().GetStore().Add(c) | ||||
| 				if err != nil { | ||||
| 					t.Errorf("Expected no error, got %v", err) | ||||
| 					return | ||||
| 				} | ||||
| 			} | ||||
| 			lister := informerFactory.Storage().V1alpha1().VolumeAttributesClasses().Lister() | ||||
| 			actual, err := GetDefaultVolumeAttributesClass(lister, tc.driverName) | ||||
| 			if err != nil { | ||||
| 				t.Errorf("Expected no error, got %v", err) | ||||
| 				return | ||||
| 			} | ||||
| 			if tc.expect != actual { | ||||
| 				t.Errorf("Expected %v, got %v", tc.expect, actual) | ||||
| 			} | ||||
| 		}) | ||||
| 	} | ||||
| } | ||||
|  | ||||
| func TestIsDefaultVolumeAttributesClassAnnotation(t *testing.T) { | ||||
| 	testCases := []struct { | ||||
| 		name   string | ||||
| 		class  *storagev1alpha1.VolumeAttributesClass | ||||
| 		expect bool | ||||
| 	}{ | ||||
| 		{ | ||||
| 			name:   "no annotation", | ||||
| 			class:  &storagev1alpha1.VolumeAttributesClass{}, | ||||
| 			expect: false, | ||||
| 		}, | ||||
| 		{ | ||||
| 			name: "annotation is not boolean", | ||||
| 			class: &storagev1alpha1.VolumeAttributesClass{ | ||||
| 				ObjectMeta: metav1.ObjectMeta{ | ||||
| 					Annotations: map[string]string{ | ||||
| 						AlphaIsDefaultVolumeAttributesClassAnnotation: "not-boolean", | ||||
| 					}, | ||||
| 				}, | ||||
| 			}, | ||||
| 			expect: false, | ||||
| 		}, | ||||
| 		{ | ||||
| 			name: "annotation is false", | ||||
| 			class: &storagev1alpha1.VolumeAttributesClass{ | ||||
| 				ObjectMeta: metav1.ObjectMeta{ | ||||
| 					Annotations: map[string]string{ | ||||
| 						AlphaIsDefaultVolumeAttributesClassAnnotation: "false", | ||||
| 					}, | ||||
| 				}, | ||||
| 			}, | ||||
| 			expect: false, | ||||
| 		}, | ||||
| 		{ | ||||
| 			name: "annotation is true", | ||||
| 			class: &storagev1alpha1.VolumeAttributesClass{ | ||||
| 				ObjectMeta: metav1.ObjectMeta{ | ||||
| 					Annotations: map[string]string{ | ||||
| 						AlphaIsDefaultVolumeAttributesClassAnnotation: "true", | ||||
| 					}, | ||||
| 				}, | ||||
| 			}, | ||||
| 			expect: true, | ||||
| 		}, | ||||
| 	} | ||||
|  | ||||
| 	for _, tc := range testCases { | ||||
| 		t.Run(tc.name, func(t *testing.T) { | ||||
| 			actual := IsDefaultVolumeAttributesClassAnnotation(tc.class.ObjectMeta) | ||||
| 			if tc.expect != actual { | ||||
| 				t.Errorf("Expected %v, got %v", tc.expect, actual) | ||||
| 			} | ||||
| 		}) | ||||
| 	} | ||||
| } | ||||
| @@ -363,6 +363,16 @@ type PersistentVolumeSpec struct { | ||||
| 	// This field influences the scheduling of pods that use this volume. | ||||
| 	// +optional | ||||
| 	NodeAffinity *VolumeNodeAffinity `json:"nodeAffinity,omitempty" protobuf:"bytes,9,opt,name=nodeAffinity"` | ||||
| 	// Name of VolumeAttributesClass to which this persistent volume belongs. Empty value | ||||
| 	// is not allowed. When this field is not set, it indicates that this volume does not belong to any | ||||
| 	// VolumeAttributesClass. This field is mutable and can be changed by the CSI driver | ||||
| 	// after a volume has been updated successfully to a new class. | ||||
| 	// For an unbound PersistentVolume, the volumeAttributesClassName will be matched with unbound | ||||
| 	// PersistentVolumeClaims during the binding process. | ||||
| 	// This is an alpha field and requires enabling VolumeAttributesClass feature. | ||||
| 	// +featureGate=VolumeAttributesClass | ||||
| 	// +optional | ||||
| 	VolumeAttributesClassName *string `json:"volumeAttributesClassName,omitempty" protobuf:"bytes,10,opt,name=volumeAttributesClassName"` | ||||
| } | ||||
|  | ||||
| // VolumeNodeAffinity defines constraints that limit what nodes this volume can be accessed from. | ||||
| @@ -533,6 +543,21 @@ type PersistentVolumeClaimSpec struct { | ||||
| 	// (Alpha) Using the namespace field of dataSourceRef requires the CrossNamespaceVolumeDataSource feature gate to be enabled. | ||||
| 	// +optional | ||||
| 	DataSourceRef *TypedObjectReference `json:"dataSourceRef,omitempty" protobuf:"bytes,8,opt,name=dataSourceRef"` | ||||
| 	// volumeAttributesClassName may be used to set the VolumeAttributesClass used by this claim. | ||||
| 	// If specified, the CSI driver will create or update the volume with the attributes defined | ||||
| 	// in the corresponding VolumeAttributesClass. This has a different purpose than storageClassName, | ||||
| 	// it can be changed after the claim is created. An empty string value means that no VolumeAttributesClass | ||||
| 	// will be applied to the claim but it's not allowed to reset this field to empty string once it is set. | ||||
| 	// If unspecified and the PersistentVolumeClaim is unbound, the default VolumeAttributesClass | ||||
| 	// will be set by the persistentvolume controller if it exists. | ||||
| 	// If the resource referred to by volumeAttributesClass does not exist, this PersistentVolumeClaim will be | ||||
| 	// set to a Pending state, as reflected by the modifyVolumeStatus field, until such as a resource | ||||
| 	// exists. | ||||
| 	// More info: https://kubernetes.io/docs/concepts/storage/persistent-volumes#volumeattributesclass | ||||
| 	// (Alpha) Using this field requires the VolumeAttributesClass feature gate to be enabled. | ||||
| 	// +featureGate=VolumeAttributesClass | ||||
| 	// +optional | ||||
| 	VolumeAttributesClassName *string `json:"volumeAttributesClassName,omitempty" protobuf:"bytes,9,opt,name=volumeAttributesClassName"` | ||||
| } | ||||
|  | ||||
| type TypedObjectReference struct { | ||||
| @@ -561,6 +586,11 @@ const ( | ||||
| 	PersistentVolumeClaimResizing PersistentVolumeClaimConditionType = "Resizing" | ||||
| 	// PersistentVolumeClaimFileSystemResizePending - controller resize is finished and a file system resize is pending on node | ||||
| 	PersistentVolumeClaimFileSystemResizePending PersistentVolumeClaimConditionType = "FileSystemResizePending" | ||||
|  | ||||
| 	// Applying the target VolumeAttributesClass encountered an error | ||||
| 	PersistentVolumeClaimVolumeModifyVolumeError PersistentVolumeClaimConditionType = "ModifyVolumeError" | ||||
| 	// Volume is being modified | ||||
| 	PersistentVolumeClaimVolumeModifyingVolume PersistentVolumeClaimConditionType = "ModifyingVolume" | ||||
| ) | ||||
|  | ||||
| // +enum | ||||
| @@ -587,6 +617,38 @@ const ( | ||||
| 	PersistentVolumeClaimNodeResizeFailed ClaimResourceStatus = "NodeResizeFailed" | ||||
| ) | ||||
|  | ||||
| // +enum | ||||
| // New statuses can be added in the future. Consumers should check for unknown statuses and fail appropriately | ||||
| type PersistentVolumeClaimModifyVolumeStatus string | ||||
|  | ||||
| const ( | ||||
| 	// Pending indicates that the PersistentVolumeClaim cannot be modified due to unmet requirements, such as | ||||
| 	// the specified VolumeAttributesClass not existing | ||||
| 	PersistentVolumeClaimModifyVolumePending PersistentVolumeClaimModifyVolumeStatus = "Pending" | ||||
| 	// InProgress indicates that the volume is being modified | ||||
| 	PersistentVolumeClaimModifyVolumeInProgress PersistentVolumeClaimModifyVolumeStatus = "InProgress" | ||||
| 	// Infeasible indicates that the request has been rejected as invalid by the CSI driver. To | ||||
| 	// resolve the error, a valid VolumeAttributesClass needs to be specified | ||||
| 	PersistentVolumeClaimModifyVolumeInfeasible PersistentVolumeClaimModifyVolumeStatus = "Infeasible" | ||||
| ) | ||||
|  | ||||
| // ModifyVolumeStatus represents the status object of ControllerModifyVolume operation | ||||
| type ModifyVolumeStatus struct { | ||||
| 	// targetVolumeAttributesClassName is the name of the VolumeAttributesClass the PVC currently being reconciled | ||||
| 	TargetVolumeAttributesClassName string `json:"targetVolumeAttributesClassName,omitempty" protobuf:"bytes,1,opt,name=targetVolumeAttributesClassName"` | ||||
| 	// status is the status of the ControllerModifyVolume operation. It can be in any of following states: | ||||
| 	//  - Pending | ||||
| 	//    Pending indicates that the PersistentVolumeClaim cannot be modified due to unmet requirements, such as | ||||
| 	//    the specified VolumeAttributesClass not existing. | ||||
| 	//  - InProgress | ||||
| 	//    InProgress indicates that the volume is being modified. | ||||
| 	//  - Infeasible | ||||
| 	//   Infeasible indicates that the request has been rejected as invalid by the CSI driver. To | ||||
| 	// 	  resolve the error, a valid VolumeAttributesClass needs to be specified. | ||||
| 	// Note: New statuses can be added in the future. Consumers should check for unknown statuses and fail appropriately. | ||||
| 	Status PersistentVolumeClaimModifyVolumeStatus `json:"status" protobuf:"bytes,2,opt,name=status,casttype=PersistentVolumeClaimModifyVolumeStatus"` | ||||
| } | ||||
|  | ||||
| // PersistentVolumeClaimCondition contains details about state of pvc | ||||
| type PersistentVolumeClaimCondition struct { | ||||
| 	Type   PersistentVolumeClaimConditionType `json:"type" protobuf:"bytes,1,opt,name=type,casttype=PersistentVolumeClaimConditionType"` | ||||
| @@ -693,6 +755,18 @@ type PersistentVolumeClaimStatus struct { | ||||
| 	// +mapType=granular | ||||
| 	// +optional | ||||
| 	AllocatedResourceStatuses map[ResourceName]ClaimResourceStatus `json:"allocatedResourceStatuses,omitempty" protobuf:"bytes,7,rep,name=allocatedResourceStatuses"` | ||||
| 	// currentVolumeAttributesClassName is the current name of the VolumeAttributesClass the PVC is using. | ||||
| 	// When unset, there is no VolumeAttributeClass applied to this PersistentVolumeClaim | ||||
| 	// This is an alpha field and requires enabling VolumeAttributesClass feature. | ||||
| 	// +featureGate=VolumeAttributesClass | ||||
| 	// +optional | ||||
| 	CurrentVolumeAttributesClassName *string `json:"currentVolumeAttributesClassName,omitempty" protobuf:"bytes,8,opt,name=currentVolumeAttributesClassName"` | ||||
| 	// ModifyVolumeStatus represents the status object of ControllerModifyVolume operation. | ||||
| 	// When this is unset, there is no ModifyVolume operation being attempted. | ||||
| 	// This is an alpha field and requires enabling VolumeAttributesClass feature. | ||||
| 	// +featureGate=VolumeAttributesClass | ||||
| 	// +optional | ||||
| 	ModifyVolumeStatus *ModifyVolumeStatus `json:"modifyVolumeStatus,omitempty" protobuf:"bytes,9,opt,name=modifyVolumeStatus"` | ||||
| } | ||||
|  | ||||
| // +enum | ||||
|   | ||||
| @@ -45,6 +45,8 @@ func addKnownTypes(scheme *runtime.Scheme) error { | ||||
| 		&VolumeAttachmentList{}, | ||||
| 		&CSIStorageCapacity{}, | ||||
| 		&CSIStorageCapacityList{}, | ||||
| 		&VolumeAttributesClass{}, | ||||
| 		&VolumeAttributesClassList{}, | ||||
| 	) | ||||
|  | ||||
| 	metav1.AddToGroupVersion(scheme, SchemeGroupVersion) | ||||
|   | ||||
| @@ -251,3 +251,55 @@ type CSIStorageCapacityList struct { | ||||
| 	// +listMapKey=name | ||||
| 	Items []CSIStorageCapacity `json:"items" protobuf:"bytes,2,rep,name=items"` | ||||
| } | ||||
|  | ||||
| // +genclient | ||||
| // +genclient:nonNamespaced | ||||
| // +k8s:prerelease-lifecycle-gen:introduced=1.29 | ||||
| // +k8s:deepcopy-gen:interfaces=k8s.io/apimachinery/pkg/runtime.Object | ||||
|  | ||||
| // VolumeAttributesClass represents a specification of mutable volume attributes | ||||
| // defined by the CSI driver. The class can be specified during dynamic provisioning | ||||
| // of PersistentVolumeClaims, and changed in the PersistentVolumeClaim spec after provisioning. | ||||
| type VolumeAttributesClass struct { | ||||
| 	metav1.TypeMeta `json:",inline"` | ||||
|  | ||||
| 	// Standard object's metadata. | ||||
| 	// More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#metadata | ||||
| 	// +optional | ||||
| 	metav1.ObjectMeta `json:"metadata,omitempty" protobuf:"bytes,1,opt,name=metadata"` | ||||
|  | ||||
| 	// Name of the CSI driver | ||||
| 	// This field is immutable. | ||||
| 	DriverName string `json:"driverName" protobuf:"bytes,2,opt,name=driverName"` | ||||
|  | ||||
| 	// parameters hold volume attributes defined by the CSI driver. These values | ||||
| 	// are opaque to the Kubernetes and are passed directly to the CSI driver. | ||||
| 	// The underlying storage provider supports changing these attributes on an | ||||
| 	// existing volume, however the parameters field itself is immutable. To | ||||
| 	// invoke a volume update, a new VolumeAttributesClass should be created with | ||||
| 	// new parameters, and the PersistentVolumeClaim should be updated to reference | ||||
| 	// the new VolumeAttributesClass. | ||||
| 	// | ||||
| 	// This field is required and must contain at least one key/value pair. | ||||
| 	// The keys cannot be empty, and the maximum number of parameters is 512, with | ||||
| 	// a cumulative max size of 256K. If the CSI driver rejects invalid parameters, | ||||
| 	// the target PersistentVolumeClaim will be set to an "Infeasible" state in the | ||||
| 	// modifyVolumeStatus field. | ||||
| 	Parameters map[string]string `json:"parameters,omitempty" protobuf:"bytes,3,rep,name=parameters"` | ||||
| } | ||||
|  | ||||
| // +k8s:prerelease-lifecycle-gen:introduced=1.29 | ||||
| // +k8s:deepcopy-gen:interfaces=k8s.io/apimachinery/pkg/runtime.Object | ||||
|  | ||||
| // VolumeAttributesClassList is a collection of VolumeAttributesClass objects. | ||||
| type VolumeAttributesClassList struct { | ||||
| 	metav1.TypeMeta `json:",inline"` | ||||
|  | ||||
| 	// Standard list metadata | ||||
| 	// More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#metadata | ||||
| 	// +optional | ||||
| 	metav1.ListMeta `json:"metadata,omitempty" protobuf:"bytes,1,opt,name=metadata"` | ||||
|  | ||||
| 	// items is the list of VolumeAttributesClass objects. | ||||
| 	Items []VolumeAttributesClass `json:"items" protobuf:"bytes,2,rep,name=items"` | ||||
| } | ||||
|   | ||||
| @@ -294,6 +294,13 @@ func GetEtcdStorageDataForNamespace(namespace string) map[schema.GroupVersionRes | ||||
| 		}, | ||||
| 		// -- | ||||
|  | ||||
| 		// k8s.io/kubernetes/pkg/apis/storage/v1alpha1 | ||||
| 		gvr("storage.k8s.io", "v1alpha1", "volumeattributesclasses"): { | ||||
| 			Stub:             `{"metadata": {"name": "vac1"}, "driverName": "example.com/driver", "parameters": {"foo": "bar"}}`, | ||||
| 			ExpectedEtcdPath: "/registry/volumeattributesclasses/vac1", | ||||
| 		}, | ||||
| 		// -- | ||||
|  | ||||
| 		// k8s.io/kubernetes/pkg/apis/storage/v1beta1 | ||||
| 		gvr("storage.k8s.io", "v1beta1", "csistoragecapacities"): { | ||||
| 			Stub:             `{"metadata": {"name": "csc-12345-2"}, "storageClassName": "sc1"}`, | ||||
|   | ||||
		Reference in New Issue
	
	Block a user
	 carlory
					carlory