diff --git a/hack/test-update-storage-objects.sh b/hack/test-update-storage-objects.sh index f2e53edc61..9f5a533165 100755 --- a/hack/test-update-storage-objects.sh +++ b/hack/test-update-storage-objects.sh @@ -107,9 +107,9 @@ test/e2e/testing-manifests/rbd-storage-class.yaml,storageclasses,,slow,v1beta1,v ) KUBE_OLD_API_VERSION="networking.k8s.io/v1,storage.k8s.io/v1beta1,extensions/v1beta1" -KUBE_NEW_API_VERSION="networking.k8s.io/v1,storage.k8s.io/v1,extensions/v1beta1,policy/v1beta1" +KUBE_NEW_API_VERSION="networking.k8s.io/v1,storage.k8s.io/v1beta1,storage.k8s.io/v1,extensions/v1beta1,policy/v1beta1" KUBE_OLD_STORAGE_VERSIONS="storage.k8s.io/v1beta1" -KUBE_NEW_STORAGE_VERSIONS="storage.k8s.io/v1" +KUBE_NEW_STORAGE_VERSIONS="storage.k8s.io/v1beta1,storage.k8s.io/v1" ### END TEST DEFINITION CUSTOMIZATION ### diff --git a/pkg/api/testing/defaulting_test.go b/pkg/api/testing/defaulting_test.go index 5045357727..32995e4bab 100644 --- a/pkg/api/testing/defaulting_test.go +++ b/pkg/api/testing/defaulting_test.go @@ -142,6 +142,8 @@ func TestDefaulting(t *testing.T) { {Group: "networking.k8s.io", Version: "v1", Kind: "NetworkPolicyList"}: {}, {Group: "storage.k8s.io", Version: "v1beta1", Kind: "StorageClass"}: {}, {Group: "storage.k8s.io", Version: "v1beta1", Kind: "StorageClassList"}: {}, + {Group: "storage.k8s.io", Version: "v1beta1", Kind: "CSIDriver"}: {}, + {Group: "storage.k8s.io", Version: "v1beta1", Kind: "CSIDriverList"}: {}, {Group: "storage.k8s.io", Version: "v1", Kind: "StorageClass"}: {}, {Group: "storage.k8s.io", Version: "v1", Kind: "StorageClassList"}: {}, {Group: "authentication.k8s.io", Version: "v1", Kind: "TokenRequest"}: {}, diff --git a/pkg/apis/core/validation/validation.go b/pkg/apis/core/validation/validation.go index f7ba935ff8..76fc962097 100644 --- a/pkg/apis/core/validation/validation.go +++ b/pkg/apis/core/validation/validation.go @@ -61,8 +61,6 @@ const isInvalidQuotaResource string = `must be a standard resource for quota` const fieldImmutableErrorMsg string = apimachineryvalidation.FieldImmutableErrorMsg const isNotIntegerErrorMsg string = `must be an integer` const isNotPositiveErrorMsg string = `must be greater than zero` -const csiDriverNameRexpErrMsg string = "must consist of alphanumeric characters, '-', '_' or '.', and must start and end with an alphanumeric character" -const csiDriverNameRexpFmt string = `^[a-zA-Z0-9][-a-zA-Z0-9_.]{0,61}[a-zA-Z-0-9]$` var pdPartitionErrorMsg string = validation.InclusiveRangeError(1, 255) var fileModeErrorMsg string = "must be a number between 0 and 0777 (octal), both inclusive" @@ -74,8 +72,6 @@ var iscsiInitiatorIqnRegex = regexp.MustCompile(`iqn\.\d{4}-\d{2}\.([[:alnum:]-. var iscsiInitiatorEuiRegex = regexp.MustCompile(`^eui.[[:alnum:]]{16}$`) var iscsiInitiatorNaaRegex = regexp.MustCompile(`^naa.[[:alnum:]]{32}$`) -var csiDriverNameRexp = regexp.MustCompile(csiDriverNameRexpFmt) - // ValidateHasLabel requires that metav1.ObjectMeta has a Label with key and expectedValue func ValidateHasLabel(meta metav1.ObjectMeta, fldPath *field.Path, key, expectedValue string) field.ErrorList { allErrs := field.ErrorList{} @@ -1446,9 +1442,10 @@ func ValidateCSIDriverName(driverName string, fldPath *field.Path) field.ErrorLi allErrs = append(allErrs, field.TooLong(fldPath, driverName, 63)) } - if !csiDriverNameRexp.MatchString(driverName) { - allErrs = append(allErrs, field.Invalid(fldPath, driverName, validation.RegexError(csiDriverNameRexpErrMsg, csiDriverNameRexpFmt, "csi-hostpath"))) + for _, msg := range validation.IsDNS1123Subdomain(strings.ToLower(driverName)) { + allErrs = append(allErrs, field.Invalid(fldPath, driverName, msg)) } + return allErrs } diff --git a/pkg/apis/core/validation/validation_test.go b/pkg/apis/core/validation/validation_test.go index 45ec2c359c..ddb21670b9 100644 --- a/pkg/apis/core/validation/validation_test.go +++ b/pkg/apis/core/validation/validation_test.go @@ -1775,24 +1775,30 @@ func TestValidateCSIVolumeSource(t *testing.T) { csi: &core.CSIPersistentVolumeSource{Driver: "io-kubernetes-storage-csi-flex", VolumeHandle: "test-123"}, }, { - name: "driver name: ok underscore only", - csi: &core.CSIPersistentVolumeSource{Driver: "io_kubernetes_storage_csi_flex", VolumeHandle: "test-123"}, + name: "driver name: invalid underscore", + csi: &core.CSIPersistentVolumeSource{Driver: "io_kubernetes_storage_csi_flex", VolumeHandle: "test-123"}, + errtype: field.ErrorTypeInvalid, + errfield: "driver", }, { - name: "driver name: ok dot underscores", - csi: &core.CSIPersistentVolumeSource{Driver: "io.kubernetes.storage_csi.flex", VolumeHandle: "test-123"}, + name: "driver name: invalid dot underscores", + csi: &core.CSIPersistentVolumeSource{Driver: "io.kubernetes.storage_csi.flex", VolumeHandle: "test-123"}, + errtype: field.ErrorTypeInvalid, + errfield: "driver", }, { name: "driver name: ok beginnin with number", - csi: &core.CSIPersistentVolumeSource{Driver: "2io.kubernetes.storage_csi.flex", VolumeHandle: "test-123"}, + csi: &core.CSIPersistentVolumeSource{Driver: "2io.kubernetes.storage-csi.flex", VolumeHandle: "test-123"}, }, { name: "driver name: ok ending with number", - csi: &core.CSIPersistentVolumeSource{Driver: "io.kubernetes.storage_csi.flex2", VolumeHandle: "test-123"}, + csi: &core.CSIPersistentVolumeSource{Driver: "io.kubernetes.storage-csi.flex2", VolumeHandle: "test-123"}, }, { - name: "driver name: ok dot dash underscores", - csi: &core.CSIPersistentVolumeSource{Driver: "io.kubernetes-storage.csi_flex", VolumeHandle: "test-123"}, + name: "driver name: invalid dot dash underscores", + csi: &core.CSIPersistentVolumeSource{Driver: "io.kubernetes-storage.csi_flex", VolumeHandle: "test-123"}, + errtype: field.ErrorTypeInvalid, + errfield: "driver", }, { name: "driver name: invalid length 0", @@ -1801,10 +1807,8 @@ func TestValidateCSIVolumeSource(t *testing.T) { errfield: "driver", }, { - name: "driver name: invalid length 1", - csi: &core.CSIPersistentVolumeSource{Driver: "a", VolumeHandle: "test-123"}, - errtype: field.ErrorTypeInvalid, - errfield: "driver", + name: "driver name: ok length 1", + csi: &core.CSIPersistentVolumeSource{Driver: "a", VolumeHandle: "test-123"}, }, { name: "driver name: invalid length > 63", diff --git a/pkg/apis/storage/fuzzer/fuzzer.go b/pkg/apis/storage/fuzzer/fuzzer.go index e39f4e91e1..124e800335 100644 --- a/pkg/apis/storage/fuzzer/fuzzer.go +++ b/pkg/apis/storage/fuzzer/fuzzer.go @@ -34,5 +34,14 @@ var Funcs = func(codecs runtimeserializer.CodecFactory) []interface{} { bindingModes := []storage.VolumeBindingMode{storage.VolumeBindingImmediate, storage.VolumeBindingWaitForFirstConsumer} obj.VolumeBindingMode = &bindingModes[c.Rand.Intn(len(bindingModes))] }, + func(obj *storage.CSIDriver, c fuzz.Continue) { + c.FuzzNoCustom(obj) // fuzz self without calling this function again + + // match defaulting + if obj.Spec.AttachRequired == nil { + obj.Spec.AttachRequired = new(bool) + *(obj.Spec.AttachRequired) = true + } + }, } } diff --git a/pkg/apis/storage/register.go b/pkg/apis/storage/register.go index 7ae2f3efe1..fffba5fc5a 100644 --- a/pkg/apis/storage/register.go +++ b/pkg/apis/storage/register.go @@ -48,6 +48,10 @@ func addKnownTypes(scheme *runtime.Scheme) error { &StorageClassList{}, &VolumeAttachment{}, &VolumeAttachmentList{}, + &CSINode{}, + &CSINodeList{}, + &CSIDriver{}, + &CSIDriverList{}, ) return nil } diff --git a/pkg/apis/storage/types.go b/pkg/apis/storage/types.go index f8c16a7450..c7c8b638e0 100644 --- a/pkg/apis/storage/types.go +++ b/pkg/apis/storage/types.go @@ -215,3 +215,158 @@ const ( // binding will occur during Pod scheduing. VolumeBindingWaitForFirstConsumer VolumeBindingMode = "WaitForFirstConsumer" ) + +// +genclient +// +genclient:nonNamespaced +// +k8s:deepcopy-gen:interfaces=k8s.io/apimachinery/pkg/runtime.Object + +// CSIDriver captures information about a Container Storage Interface (CSI) +// volume driver deployed on the cluster. +// CSI drivers do not need to create the CSIDriver object directly. Instead they may use the +// cluster-driver-registrar sidecar container. When deployed with a CSI driver it automatically +// creates a CSIDriver object representing the driver. +// Kubernetes attach detach controller uses this object to determine whether attach is required. +// Kubelet uses this object to determine whether pod information needs to be passed on mount. +// CSIDriver objects are non-namespaced. +type CSIDriver struct { + metav1.TypeMeta + + // Standard object metadata. + // metadata.Name indicates the name of the CSI driver that this object + // refers to; it MUST be the same name returned by the CSI GetPluginName() + // call for that driver. + // The driver name must be 63 characters or less, beginning and ending with + // an alphanumeric character ([a-z0-9A-Z]) with dashes (-), dots (.), and + // alphanumerics between. + // More info: https://git.k8s.io/community/contributors/devel/api-conventions.md#metadata + metav1.ObjectMeta + + // Specification of the CSI Driver. + Spec CSIDriverSpec +} + +// +k8s:deepcopy-gen:interfaces=k8s.io/apimachinery/pkg/runtime.Object + +// CSIDriverList is a collection of CSIDriver objects. +type CSIDriverList struct { + metav1.TypeMeta + + // Standard list metadata + // More info: https://git.k8s.io/community/contributors/devel/api-conventions.md#metadata + // +optional + metav1.ListMeta + + // items is the list of CSIDriver + Items []CSIDriver +} + +// CSIDriverSpec is the specification of a CSIDriver. +type CSIDriverSpec struct { + // attachRequired indicates this CSI volume driver requires an attach + // operation (because it implements the CSI ControllerPublishVolume() + // method), and that the Kubernetes attach detach controller should call + // the attach volume interface which checks the volumeattachment status + // and waits until the volume is attached before proceeding to mounting. + // The CSI external-attacher coordinates with CSI volume driver and updates + // the volumeattachment status when the attach operation is complete. + // If the CSIDriverRegistry feature gate is enabled and the value is + // specified to false, the attach operation will be skipped. + // Otherwise the attach operation will be called. + // +optional + AttachRequired *bool + + // If set to true, podInfoOnMount indicates this CSI volume driver + // requires additional pod information (like podName, podUID, etc.) during + // mount operations. + // If not set or set to false, pod information will not be passed on mount. + // The CSI driver specifies podInfoOnMount as part of driver deployment. + // If true, Kubelet will pass pod information as VolumeContext in the CSI + // NodePublishVolume() calls. + // The CSI driver is responsible for parsing and validating the information + // passed in as VolumeContext. + // The following VolumeConext will be passed if podInfoOnMount is set to true: + // "csi.storage.k8s.io/pod.name": pod.Name + // "csi.storage.k8s.io/pod.namespace": pod.Namespace + // "csi.storage.k8s.io/pod.uid": string(pod.UID) + // +optional + PodInfoOnMount *bool +} + +// +genclient +// +genclient:nonNamespaced +// +k8s:deepcopy-gen:interfaces=k8s.io/apimachinery/pkg/runtime.Object + +// CSINode holds information about all CSI drivers installed on a node. +// CSI drivers do not need to create the CSINode object directly. As long as +// they use the node-driver-registrar sidecar container, the kubelet will +// automatically populate the CSINode object for the CSI driver as part of +// kubelet plugin registration. +// CSINode has the same name as a node. If the object is missing, it means either +// there are no CSI Drivers available on the node, or the Kubelet version is low +// enough that it doesn't create this object. +// CSINode has an OwnerReference that points to the corresponding node object. +type CSINode struct { + metav1.TypeMeta + + // metadata.name must be the Kubernetes node name. + metav1.ObjectMeta + + // spec is the specification of CSINode + Spec CSINodeSpec +} + +// CSINodeSpec holds information about the specification of all CSI drivers installed on a node +type CSINodeSpec struct { + // drivers is a list of information of all CSI Drivers existing on a node. + // It can be empty on initialization. + // +patchMergeKey=name + // +patchStrategy=merge + Drivers []CSINodeDriver +} + +// CSINodeDriver holds information about the specification of one CSI driver installed on a node +type CSINodeDriver struct { + // This is the name of the CSI driver that this object refers to. + // This MUST be the same name returned by the CSI GetPluginName() call for + // that driver. + Name string + + // nodeID of the node from the driver point of view. + // This field enables Kubernetes to communicate with storage systems that do + // not share the same nomenclature for nodes. For example, Kubernetes may + // refer to a given node as "node1", but the storage system may refer to + // the same node as "nodeA". When Kubernetes issues a command to the storage + // system to attach a volume to a specific node, it can use this field to + // refer to the node name using the ID that the storage system will + // understand, e.g. "nodeA" instead of "node1". This field is required. + NodeID string + + // topologyKeys is the list of keys supported by the driver. + // When a driver is initialized on a cluster, it provides a set of topology + // keys that it understands (e.g. "company.com/zone", "company.com/region"). + // When a driver is initialized on a node, it provides the same topology keys + // along with values. Kubelet will expose these topology keys as labels + // on its own node object. + // When Kubernetes does topology aware provisioning, it can use this list to + // determine which labels it should retrieve from the node object and pass + // back to the driver. + // It is possible for different nodes to use different topology keys. + // This can be empty if driver does not support topology. + // +optional + TopologyKeys []string +} + +// +k8s:deepcopy-gen:interfaces=k8s.io/apimachinery/pkg/runtime.Object + +// CSINodeList is a collection of CSINode objects. +type CSINodeList struct { + metav1.TypeMeta + + // Standard list metadata + // More info: https://git.k8s.io/community/contributors/devel/api-conventions.md#metadata + // +optional + metav1.ListMeta + + // items is the list of CSINode + Items []CSINode +} diff --git a/pkg/apis/storage/v1beta1/defaults.go b/pkg/apis/storage/v1beta1/defaults.go index f2db7d84bf..69a4368970 100644 --- a/pkg/apis/storage/v1beta1/defaults.go +++ b/pkg/apis/storage/v1beta1/defaults.go @@ -37,3 +37,10 @@ func SetDefaults_StorageClass(obj *storagev1beta1.StorageClass) { *obj.VolumeBindingMode = storagev1beta1.VolumeBindingImmediate } } + +func SetDefaults_CSIDriver(obj *storagev1beta1.CSIDriver) { + if obj.Spec.AttachRequired == nil { + obj.Spec.AttachRequired = new(bool) + *(obj.Spec.AttachRequired) = true + } +} diff --git a/pkg/apis/storage/v1beta1/defaults_test.go b/pkg/apis/storage/v1beta1/defaults_test.go index d920c5c7f9..f4092891b3 100644 --- a/pkg/apis/storage/v1beta1/defaults_test.go +++ b/pkg/apis/storage/v1beta1/defaults_test.go @@ -60,3 +60,17 @@ func TestSetDefaultVolumeBindingMode(t *testing.T) { t.Errorf("Expected VolumeBindingMode to be defaulted to: %+v, got: %+v", defaultMode, outMode) } } + +func TestSetDefaultAttachRequired(t *testing.T) { + driver := &storagev1beta1.CSIDriver{} + + // field should be defaulted + defaultMode := true + output := roundTrip(t, runtime.Object(driver)).(*storagev1beta1.CSIDriver) + outMode := output.Spec.AttachRequired + if outMode == nil { + t.Errorf("Expected AttachRequired to be defaulted to: %+v, got: nil", defaultMode) + } else if *outMode != defaultMode { + t.Errorf("Expected AttachRequired to be defaulted to: %+v, got: %+v", defaultMode, outMode) + } +} diff --git a/pkg/apis/storage/validation/validation.go b/pkg/apis/storage/validation/validation.go index db97493954..44bc8d2881 100644 --- a/pkg/apis/storage/validation/validation.go +++ b/pkg/apis/storage/validation/validation.go @@ -17,6 +17,7 @@ limitations under the License. package validation import ( + "fmt" "reflect" "strings" @@ -36,6 +37,8 @@ const ( maxAttachedVolumeMetadataSize = 256 * (1 << 10) // 256 kB maxVolumeErrorMessageSize = 1024 + + csiNodeIDMaxLength = 128 ) // ValidateStorageClass validates a StorageClass. @@ -266,3 +269,133 @@ func validateAllowedTopologies(topologies []api.TopologySelectorTerm, fldPath *f return allErrs } + +// ValidateCSINode validates a CSINode. +func ValidateCSINode(csiNode *storage.CSINode) field.ErrorList { + allErrs := apivalidation.ValidateObjectMeta(&csiNode.ObjectMeta, false, apivalidation.ValidateNodeName, field.NewPath("metadata")) + allErrs = append(allErrs, validateCSINodeSpec(&csiNode.Spec, field.NewPath("spec"))...) + return allErrs +} + +// ValidateCSINodeUpdate validates a CSINode. +func ValidateCSINodeUpdate(new, old *storage.CSINode) field.ErrorList { + allErrs := ValidateCSINode(new) + + // Validate modifying fields inside an existing CSINodeDriver entry is not allowed + for _, oldDriver := range old.Spec.Drivers { + for _, newDriver := range new.Spec.Drivers { + if oldDriver.Name == newDriver.Name { + if !apiequality.Semantic.DeepEqual(oldDriver, newDriver) { + allErrs = append(allErrs, field.Invalid(field.NewPath("CSINodeDriver"), newDriver, "field is immutable")) + } + } + } + } + + return allErrs +} + +// ValidateCSINodeSpec tests that the specified CSINodeSpec has valid data. +func validateCSINodeSpec( + spec *storage.CSINodeSpec, fldPath *field.Path) field.ErrorList { + allErrs := field.ErrorList{} + allErrs = append(allErrs, validateCSINodeDrivers(spec.Drivers, fldPath.Child("drivers"))...) + return allErrs +} + +// ValidateCSINodeDrivers tests that the specified CSINodeDrivers have valid data. +func validateCSINodeDrivers(drivers []storage.CSINodeDriver, fldPath *field.Path) field.ErrorList { + allErrs := field.ErrorList{} + driverNamesInSpecs := make(sets.String) + for i, driver := range drivers { + idxPath := fldPath.Index(i) + allErrs = append(allErrs, validateCSINodeDriver(driver, driverNamesInSpecs, idxPath)...) + } + + return allErrs +} + +// validateCSINodeDriverNodeID tests if Name in CSINodeDriver is a valid node id. +func validateCSINodeDriverNodeID(nodeID string, fldPath *field.Path) field.ErrorList { + allErrs := field.ErrorList{} + + // nodeID is always required + if len(nodeID) == 0 { + allErrs = append(allErrs, field.Required(fldPath, nodeID)) + } + if len(nodeID) > csiNodeIDMaxLength { + allErrs = append(allErrs, field.Invalid(fldPath, nodeID, fmt.Sprintf("nodeID must be %d characters or less", csiNodeIDMaxLength))) + } + return allErrs +} + +// validateCSINodeDriver tests if CSINodeDriver has valid entries +func validateCSINodeDriver(driver storage.CSINodeDriver, driverNamesInSpecs sets.String, fldPath *field.Path) field.ErrorList { + allErrs := field.ErrorList{} + + allErrs = append(allErrs, apivalidation.ValidateCSIDriverName(driver.Name, fldPath.Child("name"))...) + allErrs = append(allErrs, validateCSINodeDriverNodeID(driver.NodeID, fldPath.Child("nodeID"))...) + + // check for duplicate entries for the same driver in specs + if driverNamesInSpecs.Has(driver.Name) { + allErrs = append(allErrs, field.Duplicate(fldPath.Child("name"), driver.Name)) + } + driverNamesInSpecs.Insert(driver.Name) + topoKeys := make(sets.String) + for _, key := range driver.TopologyKeys { + if len(key) == 0 { + allErrs = append(allErrs, field.Required(fldPath, key)) + } + + if topoKeys.Has(key) { + allErrs = append(allErrs, field.Duplicate(fldPath, key)) + } + topoKeys.Insert(key) + + for _, msg := range validation.IsQualifiedName(key) { + allErrs = append(allErrs, field.Invalid(fldPath, driver.TopologyKeys, msg)) + } + } + + return allErrs +} + +// ValidateCSIDriver validates a CSIDriver. +func ValidateCSIDriver(csiDriver *storage.CSIDriver) field.ErrorList { + allErrs := field.ErrorList{} + allErrs = append(allErrs, apivalidation.ValidateCSIDriverName(csiDriver.Name, field.NewPath("name"))...) + + allErrs = append(allErrs, validateCSIDriverSpec(&csiDriver.Spec, field.NewPath("spec"))...) + return allErrs +} + +// ValidateCSIDriverUpdate validates a CSIDriver. +func ValidateCSIDriverUpdate(new, old *storage.CSIDriver) field.ErrorList { + allErrs := ValidateCSIDriver(new) + + // Spec is read-only + // If this ever relaxes in the future, make sure to increment the Generation number in PrepareForUpdate + if !apiequality.Semantic.DeepEqual(old.Spec, new.Spec) { + allErrs = append(allErrs, field.Invalid(field.NewPath("spec"), new.Spec, "field is immutable")) + } + return allErrs +} + +// ValidateCSIDriverSpec tests that the specified CSIDriverSpec +// has valid data. +func validateCSIDriverSpec( + spec *storage.CSIDriverSpec, fldPath *field.Path) field.ErrorList { + allErrs := field.ErrorList{} + allErrs = append(allErrs, validateAttachRequired(spec.AttachRequired, fldPath.Child("attachedRequired"))...) + return allErrs +} + +// validateAttachRequired tests if attachRequired is set for CSIDriver. +func validateAttachRequired(attachRequired *bool, fldPath *field.Path) field.ErrorList { + allErrs := field.ErrorList{} + if attachRequired == nil { + allErrs = append(allErrs, field.Required(fldPath, "")) + } + + return allErrs +} diff --git a/pkg/apis/storage/validation/validation_test.go b/pkg/apis/storage/validation/validation_test.go index f5b2004131..79fd77c0d1 100644 --- a/pkg/apis/storage/validation/validation_test.go +++ b/pkg/apis/storage/validation/validation_test.go @@ -870,3 +870,612 @@ func TestValidateAllowedTopologies(t *testing.T) { } } } + +func TestCSINodeValidation(t *testing.T) { + driverName := "driver-name" + driverName2 := "1io.kubernetes-storage-2-csi-driver3" + longName := "my-a-b-c-d-c-f-g-h-i-j-k-l-m-n-o-p-q-r-s-t-u-v-w-x-y-z-ABCDEFGHIJKLMNOPQRSTUVWXYZ-driver" + nodeID := "nodeA" + successCases := []storage.CSINode{ + { + // driver name: dot only + ObjectMeta: metav1.ObjectMeta{Name: "foo1"}, + Spec: storage.CSINodeSpec{ + Drivers: []storage.CSINodeDriver{ + { + Name: "io.kubernetes.storage.csi.driver", + NodeID: nodeID, + TopologyKeys: []string{"company.com/zone1", "company.com/zone2"}, + }, + }, + }, + }, + { + // driver name: dash only + ObjectMeta: metav1.ObjectMeta{Name: "foo2"}, + Spec: storage.CSINodeSpec{ + Drivers: []storage.CSINodeDriver{ + { + Name: "io-kubernetes-storage-csi-driver", + NodeID: nodeID, + TopologyKeys: []string{"company.com/zone1", "company.com/zone2"}, + }, + }, + }, + }, + { + // driver name: numbers + ObjectMeta: metav1.ObjectMeta{Name: "foo3"}, + Spec: storage.CSINodeSpec{ + Drivers: []storage.CSINodeDriver{ + { + Name: "1io-kubernetes-storage-2-csi-driver3", + NodeID: nodeID, + TopologyKeys: []string{"company.com/zone1", "company.com/zone2"}, + }, + }, + }, + }, + { + // driver name: dot, dash + ObjectMeta: metav1.ObjectMeta{Name: "foo4"}, + Spec: storage.CSINodeSpec{ + Drivers: []storage.CSINodeDriver{ + { + Name: "io.kubernetes.storage-csi-driver", + NodeID: nodeID, + TopologyKeys: []string{"company.com/zone1", "company.com/zone2"}, + }, + }, + }, + }, + { + // driver name: dot, dash, and numbers + ObjectMeta: metav1.ObjectMeta{Name: "foo5"}, + Spec: storage.CSINodeSpec{ + Drivers: []storage.CSINodeDriver{ + { + Name: driverName2, + NodeID: nodeID, + TopologyKeys: []string{"company.com/zone1", "company.com/zone2"}, + }, + }, + }, + }, + { + // Driver name length 1 + ObjectMeta: metav1.ObjectMeta{Name: "foo2"}, + Spec: storage.CSINodeSpec{ + Drivers: []storage.CSINodeDriver{ + { + Name: "a", + NodeID: nodeID, + TopologyKeys: []string{"company.com/zone1", "company.com/zone2"}, + }, + }, + }, + }, + { + // multiple drivers with different node IDs, topology keys + ObjectMeta: metav1.ObjectMeta{Name: "foo6"}, + Spec: storage.CSINodeSpec{ + Drivers: []storage.CSINodeDriver{ + { + Name: "driver1", + NodeID: "node1", + TopologyKeys: []string{"key1", "key2"}, + }, + { + Name: "driverB", + NodeID: "nodeA", + TopologyKeys: []string{"keyA", "keyB"}, + }, + }, + }, + }, + { + // multiple drivers with same node IDs, topology keys + ObjectMeta: metav1.ObjectMeta{Name: "foo7"}, + Spec: storage.CSINodeSpec{ + Drivers: []storage.CSINodeDriver{ + { + Name: "driver1", + NodeID: "node1", + TopologyKeys: []string{"key1"}, + }, + { + Name: "driver2", + NodeID: "node1", + TopologyKeys: []string{"key1"}, + }, + }, + }, + }, + { + // topology key names with -, _, and dot . + ObjectMeta: metav1.ObjectMeta{Name: "foo8"}, + Spec: storage.CSINodeSpec{ + Drivers: []storage.CSINodeDriver{ + { + Name: "driver1", + NodeID: "node1", + TopologyKeys: []string{"zone_1", "zone.2"}, + }, + { + Name: "driver2", + NodeID: "node1", + TopologyKeys: []string{"zone-3", "zone.4"}, + }, + }, + }, + }, + { + // topology prefix with - and dot. + ObjectMeta: metav1.ObjectMeta{Name: "foo9"}, + Spec: storage.CSINodeSpec{ + Drivers: []storage.CSINodeDriver{ + { + Name: "driver1", + NodeID: "node1", + TopologyKeys: []string{"company-com/zone1", "company.com/zone2"}, + }, + }, + }, + }, + { + // No topology keys + ObjectMeta: metav1.ObjectMeta{Name: "foo10"}, + Spec: storage.CSINodeSpec{ + Drivers: []storage.CSINodeDriver{ + { + Name: driverName, + NodeID: nodeID, + }, + }, + }, + }, + } + + for _, csiNode := range successCases { + if errs := ValidateCSINode(&csiNode); len(errs) != 0 { + t.Errorf("expected success: %v", errs) + } + } + errorCases := []storage.CSINode{ + { + // Empty driver name + ObjectMeta: metav1.ObjectMeta{Name: "foo1"}, + Spec: storage.CSINodeSpec{ + Drivers: []storage.CSINodeDriver{ + { + Name: "", + NodeID: nodeID, + TopologyKeys: []string{"company.com/zone1", "company.com/zone2"}, + }, + }, + }, + }, + { + // Invalid start char in driver name + ObjectMeta: metav1.ObjectMeta{Name: "foo3"}, + Spec: storage.CSINodeSpec{ + Drivers: []storage.CSINodeDriver{ + { + Name: "_io.kubernetes.storage.csi.driver", + NodeID: nodeID, + TopologyKeys: []string{"company.com/zone1", "company.com/zone2"}, + }, + }, + }, + }, + { + // Invalid end char in driver name + ObjectMeta: metav1.ObjectMeta{Name: "foo4"}, + Spec: storage.CSINodeSpec{ + Drivers: []storage.CSINodeDriver{ + { + Name: "io.kubernetes.storage.csi.driver/", + NodeID: nodeID, + TopologyKeys: []string{"company.com/zone1", "company.com/zone2"}, + }, + }, + }, + }, + { + // Invalid separators in driver name + ObjectMeta: metav1.ObjectMeta{Name: "foo5"}, + Spec: storage.CSINodeSpec{ + Drivers: []storage.CSINodeDriver{ + { + Name: "io/kubernetes/storage/csi~driver", + NodeID: nodeID, + TopologyKeys: []string{"company.com/zone1", "company.com/zone2"}, + }, + }, + }, + }, + { + // driver name: underscore only + ObjectMeta: metav1.ObjectMeta{Name: "foo6"}, + Spec: storage.CSINodeSpec{ + Drivers: []storage.CSINodeDriver{ + { + Name: "io_kubernetes_storage_csi_driver", + NodeID: nodeID, + TopologyKeys: []string{"company.com/zone1", "company.com/zone2"}, + }, + }, + }, + }, + { + // Driver name length > 63 + ObjectMeta: metav1.ObjectMeta{Name: "foo7"}, + Spec: storage.CSINodeSpec{ + Drivers: []storage.CSINodeDriver{ + { + Name: longName, + NodeID: nodeID, + TopologyKeys: []string{"company.com/zone1", "company.com/zone2"}, + }, + }, + }, + }, + { + // No driver name + ObjectMeta: metav1.ObjectMeta{Name: "foo8"}, + Spec: storage.CSINodeSpec{ + Drivers: []storage.CSINodeDriver{ + { + NodeID: nodeID, + TopologyKeys: []string{"company.com/zone1", "company.com/zone2"}, + }, + }, + }, + }, + { + // Empty individual topology key + ObjectMeta: metav1.ObjectMeta{Name: "foo9"}, + Spec: storage.CSINodeSpec{ + Drivers: []storage.CSINodeDriver{ + { + Name: driverName, + NodeID: nodeID, + TopologyKeys: []string{"company.com/zone1", ""}, + }, + }, + }, + }, + { + // duplicate drivers in driver specs + ObjectMeta: metav1.ObjectMeta{Name: "foo10"}, + Spec: storage.CSINodeSpec{ + Drivers: []storage.CSINodeDriver{ + { + Name: "driver1", + NodeID: "node1", + TopologyKeys: []string{"key1", "key2"}, + }, + { + Name: "driver1", + NodeID: "nodeX", + TopologyKeys: []string{"keyA", "keyB"}, + }, + }, + }, + }, + { + // single driver with duplicate topology keys in driver specs + ObjectMeta: metav1.ObjectMeta{Name: "foo11"}, + Spec: storage.CSINodeSpec{ + Drivers: []storage.CSINodeDriver{ + { + Name: "driver1", + NodeID: "node1", + TopologyKeys: []string{"key1", "key1"}, + }, + }, + }, + }, + { + // multiple drivers with one set of duplicate topology keys in driver specs + ObjectMeta: metav1.ObjectMeta{Name: "foo12"}, + Spec: storage.CSINodeSpec{ + Drivers: []storage.CSINodeDriver{ + { + Name: "driver1", + NodeID: "node1", + TopologyKeys: []string{"key1"}, + }, + { + Name: "driver2", + NodeID: "nodeX", + TopologyKeys: []string{"keyA", "keyA"}, + }, + }, + }, + }, + { + // Empty NodeID + ObjectMeta: metav1.ObjectMeta{Name: "foo13"}, + Spec: storage.CSINodeSpec{ + Drivers: []storage.CSINodeDriver{ + { + Name: driverName, + NodeID: "", + TopologyKeys: []string{"company.com/zone1", "company.com/zone2"}, + }, + }, + }, + }, + { + // topology prefix should be lower case + ObjectMeta: metav1.ObjectMeta{Name: "foo14"}, + Spec: storage.CSINodeSpec{ + Drivers: []storage.CSINodeDriver{ + { + Name: driverName, + NodeID: "node1", + TopologyKeys: []string{"Company.Com/zone1", "company.com/zone2"}, + }, + }, + }, + }, + } + + for _, csiNode := range errorCases { + if errs := ValidateCSINode(&csiNode); len(errs) == 0 { + t.Errorf("Expected failure for test: %v", csiNode) + } + } +} + +func TestCSINodeUpdateValidation(t *testing.T) { + //driverName := "driver-name" + //driverName2 := "1io.kubernetes-storage-2-csi-driver3" + //longName := "my-a-b-c-d-c-f-g-h-i-j-k-l-m-n-o-p-q-r-s-t-u-v-w-x-y-z-ABCDEFGHIJKLMNOPQRSTUVWXYZ-driver" + nodeID := "nodeA" + + old := storage.CSINode{ + ObjectMeta: metav1.ObjectMeta{Name: "foo1"}, + Spec: storage.CSINodeSpec{ + Drivers: []storage.CSINodeDriver{ + { + Name: "io.kubernetes.storage.csi.driver-1", + NodeID: nodeID, + TopologyKeys: []string{"company.com/zone1", "company.com/zone2"}, + }, + { + Name: "io.kubernetes.storage.csi.driver-2", + NodeID: nodeID, + TopologyKeys: []string{"company.com/zone1", "company.com/zone2"}, + }, + }, + }, + } + + successCases := []storage.CSINode{ + { + // no change + ObjectMeta: metav1.ObjectMeta{Name: "foo1"}, + Spec: storage.CSINodeSpec{ + Drivers: []storage.CSINodeDriver{ + { + Name: "io.kubernetes.storage.csi.driver-1", + NodeID: nodeID, + TopologyKeys: []string{"company.com/zone1", "company.com/zone2"}, + }, + { + Name: "io.kubernetes.storage.csi.driver-2", + NodeID: nodeID, + TopologyKeys: []string{"company.com/zone1", "company.com/zone2"}, + }, + }, + }, + }, + { + // remove a driver + ObjectMeta: metav1.ObjectMeta{Name: "foo1"}, + Spec: storage.CSINodeSpec{ + Drivers: []storage.CSINodeDriver{ + { + Name: "io.kubernetes.storage.csi.driver-1", + NodeID: nodeID, + TopologyKeys: []string{"company.com/zone1", "company.com/zone2"}, + }, + }, + }, + }, + { + // add a driver + ObjectMeta: metav1.ObjectMeta{Name: "foo1"}, + Spec: storage.CSINodeSpec{ + Drivers: []storage.CSINodeDriver{ + { + Name: "io.kubernetes.storage.csi.driver-1", + NodeID: nodeID, + TopologyKeys: []string{"company.com/zone1", "company.com/zone2"}, + }, + { + Name: "io.kubernetes.storage.csi.driver-2", + NodeID: nodeID, + TopologyKeys: []string{"company.com/zone1", "company.com/zone2"}, + }, + { + Name: "io.kubernetes.storage.csi.driver-3", + NodeID: nodeID, + TopologyKeys: []string{"company.com/zone1", "company.com/zone2"}, + }, + }, + }, + }, + { + // remove a driver and add a driver + ObjectMeta: metav1.ObjectMeta{Name: "foo1"}, + Spec: storage.CSINodeSpec{ + Drivers: []storage.CSINodeDriver{ + { + Name: "io.kubernetes.storage.csi.driver-1", + NodeID: nodeID, + TopologyKeys: []string{"company.com/zone1", "company.com/zone2"}, + }, + { + Name: "io.kubernetes.storage.csi.new-driver", + NodeID: nodeID, + TopologyKeys: []string{"company.com/zone1", "company.com/zone2"}, + }, + }, + }, + }, + } + + for _, csiNode := range successCases { + if errs := ValidateCSINodeUpdate(&csiNode, &old); len(errs) != 0 { + t.Errorf("expected success: %+v", errs) + } + } + + errorCases := []storage.CSINode{ + { + // invalid change node id + ObjectMeta: metav1.ObjectMeta{Name: "foo1"}, + Spec: storage.CSINodeSpec{ + Drivers: []storage.CSINodeDriver{ + { + Name: "io.kubernetes.storage.csi.driver-1", + NodeID: "nodeB", + TopologyKeys: []string{"company.com/zone1", "company.com/zone2"}, + }, + { + Name: "io.kubernetes.storage.csi.driver-2", + NodeID: nodeID, + TopologyKeys: []string{"company.com/zone1", "company.com/zone2"}, + }, + }, + }, + }, + { + // invalid change topology keys + ObjectMeta: metav1.ObjectMeta{Name: "foo1"}, + Spec: storage.CSINodeSpec{ + Drivers: []storage.CSINodeDriver{ + { + Name: "io.kubernetes.storage.csi.driver-1", + NodeID: "nodeB", + TopologyKeys: []string{"company.com/zone1", "company.com/zone2"}, + }, + { + Name: "io.kubernetes.storage.csi.driver-2", + NodeID: nodeID, + TopologyKeys: []string{"company.com/zone2"}, + }, + }, + }, + }, + } + + for _, csiNode := range errorCases { + if errs := ValidateCSINodeUpdate(&csiNode, &old); len(errs) == 0 { + t.Errorf("Expected failure for test: %+v", csiNode) + } + } +} + +func TestCSIDriverValidation(t *testing.T) { + driverName := "test-driver" + longName := "my-a-b-c-d-c-f-g-h-i-j-k-l-m-n-o-p-q-r-s-t-u-v-w-x-y-z-ABCDEFGHIJKLMNOPQRSTUVWXYZ-driver" + invalidName := "-invalid-@#$%^&*()-" + attachRequired := true + attachNotRequired := false + podInfoOnMount := true + notPodInfoOnMount := false + successCases := []storage.CSIDriver{ + { + ObjectMeta: metav1.ObjectMeta{Name: driverName}, + Spec: storage.CSIDriverSpec{ + AttachRequired: &attachRequired, + PodInfoOnMount: &podInfoOnMount, + }, + }, + { + // driver name: dot only + ObjectMeta: metav1.ObjectMeta{Name: "io.kubernetes.storage.csi.driver"}, + Spec: storage.CSIDriverSpec{ + AttachRequired: &attachRequired, + PodInfoOnMount: &podInfoOnMount, + }, + }, + { + // driver name: dash only + ObjectMeta: metav1.ObjectMeta{Name: "io-kubernetes-storage-csi-driver"}, + Spec: storage.CSIDriverSpec{ + AttachRequired: &attachRequired, + PodInfoOnMount: ¬PodInfoOnMount, + }, + }, + { + // driver name: numbers + ObjectMeta: metav1.ObjectMeta{Name: "1csi2driver3"}, + Spec: storage.CSIDriverSpec{ + AttachRequired: &attachRequired, + PodInfoOnMount: &podInfoOnMount, + }, + }, + { + // driver name: dot and dash + ObjectMeta: metav1.ObjectMeta{Name: "io.kubernetes.storage.csi-driver"}, + Spec: storage.CSIDriverSpec{ + AttachRequired: &attachRequired, + PodInfoOnMount: &podInfoOnMount, + }, + }, + { + ObjectMeta: metav1.ObjectMeta{Name: driverName}, + Spec: storage.CSIDriverSpec{ + AttachRequired: &attachRequired, + PodInfoOnMount: ¬PodInfoOnMount, + }, + }, + { + ObjectMeta: metav1.ObjectMeta{Name: driverName}, + Spec: storage.CSIDriverSpec{ + AttachRequired: &attachRequired, + PodInfoOnMount: &podInfoOnMount, + }, + }, + { + ObjectMeta: metav1.ObjectMeta{Name: driverName}, + Spec: storage.CSIDriverSpec{ + AttachRequired: &attachNotRequired, + PodInfoOnMount: ¬PodInfoOnMount, + }, + }, + } + + for _, csiDriver := range successCases { + if errs := ValidateCSIDriver(&csiDriver); len(errs) != 0 { + t.Errorf("expected success: %v", errs) + } + } + errorCases := []storage.CSIDriver{ + { + ObjectMeta: metav1.ObjectMeta{Name: invalidName}, + Spec: storage.CSIDriverSpec{ + AttachRequired: &attachRequired, + PodInfoOnMount: &podInfoOnMount, + }, + }, + { + ObjectMeta: metav1.ObjectMeta{Name: longName}, + Spec: storage.CSIDriverSpec{ + AttachRequired: &attachNotRequired, + PodInfoOnMount: ¬PodInfoOnMount, + }, + }, + } + + for _, csiDriver := range errorCases { + if errs := ValidateCSIDriver(&csiDriver); len(errs) == 0 { + t.Errorf("Expected failure for test: %v", csiDriver) + } + } +} diff --git a/pkg/kubeapiserver/BUILD b/pkg/kubeapiserver/BUILD index 9b0ec00572..b66f74d9ae 100644 --- a/pkg/kubeapiserver/BUILD +++ b/pkg/kubeapiserver/BUILD @@ -21,6 +21,8 @@ go_library( "//pkg/apis/extensions:go_default_library", "//pkg/apis/networking:go_default_library", "//pkg/apis/policy:go_default_library", + "//pkg/apis/storage:go_default_library", + "//pkg/features:go_default_library", "//staging/src/k8s.io/apimachinery/pkg/runtime:go_default_library", "//staging/src/k8s.io/apimachinery/pkg/runtime/schema:go_default_library", "//staging/src/k8s.io/apiserver/pkg/server/options:go_default_library", @@ -28,6 +30,7 @@ go_library( "//staging/src/k8s.io/apiserver/pkg/server/resourceconfig:go_default_library", "//staging/src/k8s.io/apiserver/pkg/server/storage:go_default_library", "//staging/src/k8s.io/apiserver/pkg/storage/storagebackend:go_default_library", + "//staging/src/k8s.io/apiserver/pkg/util/feature:go_default_library", ], ) diff --git a/pkg/kubeapiserver/default_storage_factory_builder.go b/pkg/kubeapiserver/default_storage_factory_builder.go index a3dbefc5a0..6a829e8a9f 100644 --- a/pkg/kubeapiserver/default_storage_factory_builder.go +++ b/pkg/kubeapiserver/default_storage_factory_builder.go @@ -26,6 +26,7 @@ import ( "k8s.io/apiserver/pkg/server/resourceconfig" serverstorage "k8s.io/apiserver/pkg/server/storage" "k8s.io/apiserver/pkg/storage/storagebackend" + utilfeature "k8s.io/apiserver/pkg/util/feature" "k8s.io/kubernetes/pkg/api/legacyscheme" "k8s.io/kubernetes/pkg/apis/apps" "k8s.io/kubernetes/pkg/apis/batch" @@ -34,6 +35,8 @@ import ( "k8s.io/kubernetes/pkg/apis/extensions" "k8s.io/kubernetes/pkg/apis/networking" "k8s.io/kubernetes/pkg/apis/policy" + apisstorage "k8s.io/kubernetes/pkg/apis/storage" + "k8s.io/kubernetes/pkg/features" ) // SpecialDefaultResourcePrefixes are prefixes compiled into Kubernetes. @@ -48,12 +51,23 @@ var SpecialDefaultResourcePrefixes = map[schema.GroupResource]string{ } func NewStorageFactoryConfig() *StorageFactoryConfig { + + resources := []schema.GroupVersionResource{ + batch.Resource("cronjobs").WithVersion("v1beta1"), + } + // add csinodes if CSINodeInfo feature gate is enabled + if utilfeature.DefaultFeatureGate.Enabled(features.CSINodeInfo) { + resources = append(resources, apisstorage.Resource("csinodes").WithVersion("v1beta1")) + } + // add csidrivers if CSIDriverRegistry feature gate is enabled + if utilfeature.DefaultFeatureGate.Enabled(features.CSIDriverRegistry) { + resources = append(resources, apisstorage.Resource("csidrivers").WithVersion("v1beta1")) + } + return &StorageFactoryConfig{ - Serializer: legacyscheme.Codecs, - DefaultResourceEncoding: serverstorage.NewDefaultResourceEncodingConfig(legacyscheme.Scheme), - ResourceEncodingOverrides: []schema.GroupVersionResource{ - batch.Resource("cronjobs").WithVersion("v1beta1"), - }, + Serializer: legacyscheme.Codecs, + DefaultResourceEncoding: serverstorage.NewDefaultResourceEncodingConfig(legacyscheme.Scheme), + ResourceEncodingOverrides: resources, } } diff --git a/pkg/registry/BUILD b/pkg/registry/BUILD index acf2054ffb..5d432d0aa7 100644 --- a/pkg/registry/BUILD +++ b/pkg/registry/BUILD @@ -85,6 +85,8 @@ filegroup( "//pkg/registry/scheduling/rest:all-srcs", "//pkg/registry/settings/podpreset:all-srcs", "//pkg/registry/settings/rest:all-srcs", + "//pkg/registry/storage/csidriver:all-srcs", + "//pkg/registry/storage/csinode:all-srcs", "//pkg/registry/storage/rest:all-srcs", "//pkg/registry/storage/storageclass:all-srcs", "//pkg/registry/storage/volumeattachment:all-srcs", diff --git a/pkg/registry/storage/csidriver/BUILD b/pkg/registry/storage/csidriver/BUILD new file mode 100644 index 0000000000..41953486c8 --- /dev/null +++ b/pkg/registry/storage/csidriver/BUILD @@ -0,0 +1,48 @@ +load("@io_bazel_rules_go//go:def.bzl", "go_library", "go_test") + +go_library( + name = "go_default_library", + srcs = [ + "doc.go", + "strategy.go", + ], + importpath = "k8s.io/kubernetes/pkg/registry/storage/csidriver", + visibility = ["//visibility:public"], + deps = [ + "//pkg/api/legacyscheme:go_default_library", + "//pkg/apis/storage:go_default_library", + "//pkg/apis/storage/validation:go_default_library", + "//staging/src/k8s.io/apimachinery/pkg/runtime:go_default_library", + "//staging/src/k8s.io/apimachinery/pkg/util/validation/field:go_default_library", + "//staging/src/k8s.io/apiserver/pkg/storage/names:go_default_library", + ], +) + +filegroup( + name = "package-srcs", + srcs = glob(["**"]), + tags = ["automanaged"], + visibility = ["//visibility:private"], +) + +filegroup( + name = "all-srcs", + srcs = [ + ":package-srcs", + "//pkg/registry/storage/csidriver/storage:all-srcs", + ], + tags = ["automanaged"], + visibility = ["//visibility:public"], +) + +go_test( + name = "go_default_test", + srcs = ["strategy_test.go"], + embed = [":go_default_library"], + deps = [ + "//pkg/apis/storage:go_default_library", + "//staging/src/k8s.io/apimachinery/pkg/apis/meta/v1:go_default_library", + "//staging/src/k8s.io/apimachinery/pkg/util/validation/field:go_default_library", + "//staging/src/k8s.io/apiserver/pkg/endpoints/request:go_default_library", + ], +) diff --git a/pkg/registry/storage/csidriver/doc.go b/pkg/registry/storage/csidriver/doc.go new file mode 100644 index 0000000000..edb13124fc --- /dev/null +++ b/pkg/registry/storage/csidriver/doc.go @@ -0,0 +1,19 @@ +/* +Copyright 2019 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 csidriver provides Registry interface and its REST +// implementation for storing csidriver api objects. +package csidriver diff --git a/pkg/registry/storage/csidriver/storage/BUILD b/pkg/registry/storage/csidriver/storage/BUILD new file mode 100644 index 0000000000..bdbddb6134 --- /dev/null +++ b/pkg/registry/storage/csidriver/storage/BUILD @@ -0,0 +1,48 @@ +load("@io_bazel_rules_go//go:def.bzl", "go_library", "go_test") + +go_library( + name = "go_default_library", + srcs = ["storage.go"], + importpath = "k8s.io/kubernetes/pkg/registry/storage/csidriver/storage", + visibility = ["//visibility:public"], + deps = [ + "//pkg/apis/storage:go_default_library", + "//pkg/registry/storage/csidriver:go_default_library", + "//staging/src/k8s.io/apimachinery/pkg/runtime:go_default_library", + "//staging/src/k8s.io/apiserver/pkg/registry/generic:go_default_library", + "//staging/src/k8s.io/apiserver/pkg/registry/generic/registry:go_default_library", + ], +) + +filegroup( + name = "package-srcs", + srcs = glob(["**"]), + tags = ["automanaged"], + visibility = ["//visibility:private"], +) + +filegroup( + name = "all-srcs", + srcs = [":package-srcs"], + tags = ["automanaged"], + visibility = ["//visibility:public"], +) + +go_test( + name = "go_default_test", + srcs = ["storage_test.go"], + embed = [":go_default_library"], + deps = [ + "//pkg/api/testapi:go_default_library", + "//pkg/apis/storage:go_default_library", + "//pkg/registry/registrytest:go_default_library", + "//staging/src/k8s.io/api/storage/v1beta1:go_default_library", + "//staging/src/k8s.io/apimachinery/pkg/apis/meta/v1:go_default_library", + "//staging/src/k8s.io/apimachinery/pkg/fields:go_default_library", + "//staging/src/k8s.io/apimachinery/pkg/labels:go_default_library", + "//staging/src/k8s.io/apimachinery/pkg/runtime:go_default_library", + "//staging/src/k8s.io/apiserver/pkg/registry/generic:go_default_library", + "//staging/src/k8s.io/apiserver/pkg/registry/generic/testing:go_default_library", + "//staging/src/k8s.io/apiserver/pkg/storage/etcd/testing:go_default_library", + ], +) diff --git a/pkg/registry/storage/csidriver/storage/storage.go b/pkg/registry/storage/csidriver/storage/storage.go new file mode 100644 index 0000000000..c22747d6f2 --- /dev/null +++ b/pkg/registry/storage/csidriver/storage/storage.go @@ -0,0 +1,57 @@ +/* +Copyright 2019 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" + storageapi "k8s.io/kubernetes/pkg/apis/storage" + "k8s.io/kubernetes/pkg/registry/storage/csidriver" +) + +// CSIDriverStorage includes storage for CSIDrivers and all subresources +type CSIDriverStorage struct { + CSIDriver *REST +} + +// REST object that will work for CSIDrivers +type REST struct { + *genericregistry.Store +} + +// NewStorage returns a RESTStorage object that will work against CSIDrivers +func NewStorage(optsGetter generic.RESTOptionsGetter) *CSIDriverStorage { + store := &genericregistry.Store{ + NewFunc: func() runtime.Object { return &storageapi.CSIDriver{} }, + NewListFunc: func() runtime.Object { return &storageapi.CSIDriverList{} }, + DefaultQualifiedResource: storageapi.Resource("csidrivers"), + + CreateStrategy: csidriver.Strategy, + UpdateStrategy: csidriver.Strategy, + DeleteStrategy: csidriver.Strategy, + ReturnDeletedObject: true, + } + options := &generic.StoreOptions{RESTOptions: optsGetter} + if err := store.CompleteWithOptions(options); err != nil { + panic(err) // TODO: Propagate error up + } + + return &CSIDriverStorage{ + CSIDriver: &REST{store}, + } +} diff --git a/pkg/registry/storage/csidriver/storage/storage_test.go b/pkg/registry/storage/csidriver/storage/storage_test.go new file mode 100644 index 0000000000..2cc848f6dc --- /dev/null +++ b/pkg/registry/storage/csidriver/storage/storage_test.go @@ -0,0 +1,179 @@ +/* +Copyright 2019 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" + + storageapiv1beta1 "k8s.io/api/storage/v1beta1" + 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" + etcdtesting "k8s.io/apiserver/pkg/storage/etcd/testing" + "k8s.io/kubernetes/pkg/api/testapi" + storageapi "k8s.io/kubernetes/pkg/apis/storage" + "k8s.io/kubernetes/pkg/registry/registrytest" +) + +func newStorage(t *testing.T) (*REST, *etcdtesting.EtcdTestServer) { + etcdStorage, server := registrytest.NewEtcdStorage(t, storageapi.GroupName) + restOptions := generic.RESTOptions{ + StorageConfig: etcdStorage, + Decorator: generic.UndecoratedStorage, + DeleteCollectionWorkers: 1, + ResourcePrefix: "csidrivers", + } + csiDriverStorage := NewStorage(restOptions) + return csiDriverStorage.CSIDriver, server +} + +func validNewCSIDriver(name string) *storageapi.CSIDriver { + attachRequired := true + podInfoOnMount := true + return &storageapi.CSIDriver{ + ObjectMeta: metav1.ObjectMeta{ + Name: name, + }, + Spec: storageapi.CSIDriverSpec{ + AttachRequired: &attachRequired, + PodInfoOnMount: &podInfoOnMount, + }, + } +} + +func TestCreate(t *testing.T) { + if *testapi.Storage.GroupVersion() != storageapiv1beta1.SchemeGroupVersion { + // skip the test for all versions exception v1beta1 + return + } + + storage, server := newStorage(t) + defer server.Terminate(t) + defer storage.Store.DestroyFunc() + test := genericregistrytest.New(t, storage.Store).ClusterScope() + csiDriver := validNewCSIDriver("foo") + csiDriver.ObjectMeta = metav1.ObjectMeta{GenerateName: "foo"} + attachNotRequired := false + notPodInfoOnMount := false + test.TestCreate( + // valid + csiDriver, + // invalid + &storageapi.CSIDriver{ + ObjectMeta: metav1.ObjectMeta{Name: "*BadName!"}, + Spec: storageapi.CSIDriverSpec{ + AttachRequired: &attachNotRequired, + PodInfoOnMount: ¬PodInfoOnMount, + }, + }, + ) +} + +func TestUpdate(t *testing.T) { + if *testapi.Storage.GroupVersion() != storageapiv1beta1.SchemeGroupVersion { + // skip the test for all versions exception v1beta1 + return + } + + storage, server := newStorage(t) + defer server.Terminate(t) + defer storage.Store.DestroyFunc() + test := genericregistrytest.New(t, storage.Store).ClusterScope() + notPodInfoOnMount := false + + test.TestUpdate( + // valid + validNewCSIDriver("foo"), + //invalid update + func(obj runtime.Object) runtime.Object { + object := obj.(*storageapi.CSIDriver) + object.Spec.PodInfoOnMount = ¬PodInfoOnMount + return object + }, + ) +} + +func TestDelete(t *testing.T) { + if *testapi.Storage.GroupVersion() != storageapiv1beta1.SchemeGroupVersion { + // skip the test for all versions exception v1beta1 + return + } + + storage, server := newStorage(t) + defer server.Terminate(t) + defer storage.Store.DestroyFunc() + test := genericregistrytest.New(t, storage.Store).ClusterScope().ReturnDeletedObject() + test.TestDelete(validNewCSIDriver("foo")) +} + +func TestGet(t *testing.T) { + if *testapi.Storage.GroupVersion() != storageapiv1beta1.SchemeGroupVersion { + // skip the test for all versions exception v1beta1 + return + } + + storage, server := newStorage(t) + defer server.Terminate(t) + defer storage.Store.DestroyFunc() + test := genericregistrytest.New(t, storage.Store).ClusterScope() + test.TestGet(validNewCSIDriver("foo")) +} + +func TestList(t *testing.T) { + if *testapi.Storage.GroupVersion() != storageapiv1beta1.SchemeGroupVersion { + // skip the test for all versions exception v1beta1 + return + } + + storage, server := newStorage(t) + defer server.Terminate(t) + defer storage.Store.DestroyFunc() + test := genericregistrytest.New(t, storage.Store).ClusterScope() + test.TestList(validNewCSIDriver("foo")) +} + +func TestWatch(t *testing.T) { + if *testapi.Storage.GroupVersion() != storageapiv1beta1.SchemeGroupVersion { + // skip the test for all versions exception v1beta1 + return + } + + storage, server := newStorage(t) + defer server.Terminate(t) + defer storage.Store.DestroyFunc() + test := genericregistrytest.New(t, storage.Store).ClusterScope() + test.TestWatch( + validNewCSIDriver("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"}, + }, + ) +} diff --git a/pkg/registry/storage/csidriver/strategy.go b/pkg/registry/storage/csidriver/strategy.go new file mode 100644 index 0000000000..1f534b2e76 --- /dev/null +++ b/pkg/registry/storage/csidriver/strategy.go @@ -0,0 +1,78 @@ +/* +Copyright 2019 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 csidriver + +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" +) + +// csiDriverStrategy implements behavior for CSIDriver objects +type csiDriverStrategy struct { + runtime.ObjectTyper + names.NameGenerator +} + +// Strategy is the default logic that applies when creating and updating +// CSIDriver objects via the REST API. +var Strategy = csiDriverStrategy{legacyscheme.Scheme, names.SimpleNameGenerator} + +func (csiDriverStrategy) NamespaceScoped() bool { + return false +} + +// ResetBeforeCreate clears the Status field which is not allowed to be set by end users on creation. +func (csiDriverStrategy) PrepareForCreate(ctx context.Context, obj runtime.Object) { +} + +func (csiDriverStrategy) Validate(ctx context.Context, obj runtime.Object) field.ErrorList { + csiDriver := obj.(*storage.CSIDriver) + + errs := validation.ValidateCSIDriver(csiDriver) + errs = append(errs, validation.ValidateCSIDriver(csiDriver)...) + + return errs +} + +// Canonicalize normalizes the object after validation. +func (csiDriverStrategy) Canonicalize(obj runtime.Object) { +} + +func (csiDriverStrategy) AllowCreateOnUpdate() bool { + return false +} + +// PrepareForUpdate sets the Status fields which is not allowed to be set by an end user updating a CSIDriver +func (csiDriverStrategy) PrepareForUpdate(ctx context.Context, obj, old runtime.Object) { +} + +func (csiDriverStrategy) ValidateUpdate(ctx context.Context, obj, old runtime.Object) field.ErrorList { + newCSIDriverObj := obj.(*storage.CSIDriver) + oldCSIDriverObj := old.(*storage.CSIDriver) + errorList := validation.ValidateCSIDriver(newCSIDriverObj) + return append(errorList, validation.ValidateCSIDriverUpdate(newCSIDriverObj, oldCSIDriverObj)...) +} + +func (csiDriverStrategy) AllowUnconditionalUpdate() bool { + return false +} diff --git a/pkg/registry/storage/csidriver/strategy_test.go b/pkg/registry/storage/csidriver/strategy_test.go new file mode 100644 index 0000000000..e5334fc1ed --- /dev/null +++ b/pkg/registry/storage/csidriver/strategy_test.go @@ -0,0 +1,155 @@ +/* +Copyright 2019 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 csidriver + +import ( + "testing" + + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/util/validation/field" + genericapirequest "k8s.io/apiserver/pkg/endpoints/request" + "k8s.io/kubernetes/pkg/apis/storage" +) + +func getValidCSIDriver(name string) *storage.CSIDriver { + attachRequired := true + podInfoOnMount := true + return &storage.CSIDriver{ + ObjectMeta: metav1.ObjectMeta{ + Name: name, + }, + Spec: storage.CSIDriverSpec{ + AttachRequired: &attachRequired, + PodInfoOnMount: &podInfoOnMount, + }, + } +} + +func TestCSIDriverStrategy(t *testing.T) { + ctx := genericapirequest.WithRequestInfo(genericapirequest.NewContext(), &genericapirequest.RequestInfo{ + APIGroup: "storage.k8s.io", + APIVersion: "v1beta1", + Resource: "csidrivers", + }) + if Strategy.NamespaceScoped() { + t.Errorf("CSIDriver must not be namespace scoped") + } + if Strategy.AllowCreateOnUpdate() { + t.Errorf("CSIDriver should not allow create on update") + } + + csiDriver := getValidCSIDriver("valid-csidriver") + + Strategy.PrepareForCreate(ctx, csiDriver) + + errs := Strategy.Validate(ctx, csiDriver) + if len(errs) != 0 { + t.Errorf("unexpected error validating %v", errs) + } + + // Update of spec is disallowed + newCSIDriver := csiDriver.DeepCopy() + attachNotRequired := false + newCSIDriver.Spec.AttachRequired = &attachNotRequired + + Strategy.PrepareForUpdate(ctx, newCSIDriver, csiDriver) + + errs = Strategy.ValidateUpdate(ctx, newCSIDriver, csiDriver) + if len(errs) == 0 { + t.Errorf("Expected a validation error") + } +} + +func TestCSIDriverValidation(t *testing.T) { + attachRequired := true + notAttachRequired := false + podInfoOnMount := true + notPodInfoOnMount := false + + tests := []struct { + name string + csiDriver *storage.CSIDriver + expectError bool + }{ + { + "valid csidriver", + getValidCSIDriver("foo"), + false, + }, + { + "true PodInfoOnMount and AttachRequired", + &storage.CSIDriver{ + ObjectMeta: metav1.ObjectMeta{ + Name: "foo", + }, + Spec: storage.CSIDriverSpec{ + AttachRequired: &attachRequired, + PodInfoOnMount: &podInfoOnMount, + }, + }, + false, + }, + { + "false PodInfoOnMount and AttachRequired", + &storage.CSIDriver{ + ObjectMeta: metav1.ObjectMeta{ + Name: "foo", + }, + Spec: storage.CSIDriverSpec{ + AttachRequired: ¬AttachRequired, + PodInfoOnMount: ¬PodInfoOnMount, + }, + }, + false, + }, + { + "invalid driver name", + &storage.CSIDriver{ + ObjectMeta: metav1.ObjectMeta{ + Name: "*foo#", + }, + Spec: storage.CSIDriverSpec{ + AttachRequired: &attachRequired, + PodInfoOnMount: &podInfoOnMount, + }, + }, + true, + }, + } + + for _, test := range tests { + t.Run(test.name, func(t *testing.T) { + + testValidation := func(csiDriver *storage.CSIDriver, apiVersion string) field.ErrorList { + ctx := genericapirequest.WithRequestInfo(genericapirequest.NewContext(), &genericapirequest.RequestInfo{ + APIGroup: "storage.k8s.io", + APIVersion: "v1beta1", + Resource: "csidrivers", + }) + return Strategy.Validate(ctx, csiDriver) + } + + betaErr := testValidation(test.csiDriver, "v1beta1") + if len(betaErr) > 0 && !test.expectError { + t.Errorf("Validation of v1beta1 object failed: %+v", betaErr) + } + if len(betaErr) == 0 && test.expectError { + t.Errorf("Validation of v1beta1 object unexpectedly succeeded") + } + }) + } +} diff --git a/pkg/registry/storage/csinode/BUILD b/pkg/registry/storage/csinode/BUILD new file mode 100644 index 0000000000..fdf1f063d0 --- /dev/null +++ b/pkg/registry/storage/csinode/BUILD @@ -0,0 +1,48 @@ +load("@io_bazel_rules_go//go:def.bzl", "go_library", "go_test") + +go_library( + name = "go_default_library", + srcs = [ + "doc.go", + "strategy.go", + ], + importpath = "k8s.io/kubernetes/pkg/registry/storage/csinode", + visibility = ["//visibility:public"], + deps = [ + "//pkg/api/legacyscheme:go_default_library", + "//pkg/apis/storage:go_default_library", + "//pkg/apis/storage/validation:go_default_library", + "//staging/src/k8s.io/apimachinery/pkg/runtime:go_default_library", + "//staging/src/k8s.io/apimachinery/pkg/util/validation/field:go_default_library", + "//staging/src/k8s.io/apiserver/pkg/storage/names:go_default_library", + ], +) + +filegroup( + name = "package-srcs", + srcs = glob(["**"]), + tags = ["automanaged"], + visibility = ["//visibility:private"], +) + +filegroup( + name = "all-srcs", + srcs = [ + ":package-srcs", + "//pkg/registry/storage/csinode/storage:all-srcs", + ], + tags = ["automanaged"], + visibility = ["//visibility:public"], +) + +go_test( + name = "go_default_test", + srcs = ["strategy_test.go"], + embed = [":go_default_library"], + deps = [ + "//pkg/apis/storage:go_default_library", + "//staging/src/k8s.io/apimachinery/pkg/apis/meta/v1:go_default_library", + "//staging/src/k8s.io/apimachinery/pkg/util/validation/field:go_default_library", + "//staging/src/k8s.io/apiserver/pkg/endpoints/request:go_default_library", + ], +) diff --git a/pkg/registry/storage/csinode/doc.go b/pkg/registry/storage/csinode/doc.go new file mode 100644 index 0000000000..cfbc7bc3a9 --- /dev/null +++ b/pkg/registry/storage/csinode/doc.go @@ -0,0 +1,19 @@ +/* +Copyright 2019 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 csinode provides Registry interface and its REST +// implementation for storing csinode api objects. +package csinode diff --git a/pkg/registry/storage/csinode/storage/BUILD b/pkg/registry/storage/csinode/storage/BUILD new file mode 100644 index 0000000000..a3daa50f0a --- /dev/null +++ b/pkg/registry/storage/csinode/storage/BUILD @@ -0,0 +1,48 @@ +load("@io_bazel_rules_go//go:def.bzl", "go_library", "go_test") + +go_library( + name = "go_default_library", + srcs = ["storage.go"], + importpath = "k8s.io/kubernetes/pkg/registry/storage/csinode/storage", + visibility = ["//visibility:public"], + deps = [ + "//pkg/apis/storage:go_default_library", + "//pkg/registry/storage/csinode:go_default_library", + "//staging/src/k8s.io/apimachinery/pkg/runtime:go_default_library", + "//staging/src/k8s.io/apiserver/pkg/registry/generic:go_default_library", + "//staging/src/k8s.io/apiserver/pkg/registry/generic/registry:go_default_library", + ], +) + +filegroup( + name = "package-srcs", + srcs = glob(["**"]), + tags = ["automanaged"], + visibility = ["//visibility:private"], +) + +filegroup( + name = "all-srcs", + srcs = [":package-srcs"], + tags = ["automanaged"], + visibility = ["//visibility:public"], +) + +go_test( + name = "go_default_test", + srcs = ["storage_test.go"], + embed = [":go_default_library"], + deps = [ + "//pkg/api/testapi:go_default_library", + "//pkg/apis/storage:go_default_library", + "//pkg/registry/registrytest:go_default_library", + "//staging/src/k8s.io/api/storage/v1beta1:go_default_library", + "//staging/src/k8s.io/apimachinery/pkg/apis/meta/v1:go_default_library", + "//staging/src/k8s.io/apimachinery/pkg/fields:go_default_library", + "//staging/src/k8s.io/apimachinery/pkg/labels:go_default_library", + "//staging/src/k8s.io/apimachinery/pkg/runtime:go_default_library", + "//staging/src/k8s.io/apiserver/pkg/registry/generic:go_default_library", + "//staging/src/k8s.io/apiserver/pkg/registry/generic/testing:go_default_library", + "//staging/src/k8s.io/apiserver/pkg/storage/etcd/testing:go_default_library", + ], +) diff --git a/pkg/registry/storage/csinode/storage/storage.go b/pkg/registry/storage/csinode/storage/storage.go new file mode 100644 index 0000000000..ecdcb5a44b --- /dev/null +++ b/pkg/registry/storage/csinode/storage/storage.go @@ -0,0 +1,57 @@ +/* +Copyright 2019 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" + storageapi "k8s.io/kubernetes/pkg/apis/storage" + "k8s.io/kubernetes/pkg/registry/storage/csinode" +) + +// CSINodeStorage includes storage for CSINodes and all subresources +type CSINodeStorage struct { + CSINode *REST +} + +// REST object that will work for CSINodes +type REST struct { + *genericregistry.Store +} + +// NewStorage returns a RESTStorage object that will work against CSINodes +func NewStorage(optsGetter generic.RESTOptionsGetter) *CSINodeStorage { + store := &genericregistry.Store{ + NewFunc: func() runtime.Object { return &storageapi.CSINode{} }, + NewListFunc: func() runtime.Object { return &storageapi.CSINodeList{} }, + DefaultQualifiedResource: storageapi.Resource("csinodes"), + + CreateStrategy: csinode.Strategy, + UpdateStrategy: csinode.Strategy, + DeleteStrategy: csinode.Strategy, + ReturnDeletedObject: true, + } + options := &generic.StoreOptions{RESTOptions: optsGetter} + if err := store.CompleteWithOptions(options); err != nil { + panic(err) // TODO: Propagate error up + } + + return &CSINodeStorage{ + CSINode: &REST{store}, + } +} diff --git a/pkg/registry/storage/csinode/storage/storage_test.go b/pkg/registry/storage/csinode/storage/storage_test.go new file mode 100644 index 0000000000..5cfb5756d4 --- /dev/null +++ b/pkg/registry/storage/csinode/storage/storage_test.go @@ -0,0 +1,190 @@ +/* +Copyright 2019 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" + + storageapiv1beta1 "k8s.io/api/storage/v1beta1" + 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" + etcdtesting "k8s.io/apiserver/pkg/storage/etcd/testing" + "k8s.io/kubernetes/pkg/api/testapi" + storageapi "k8s.io/kubernetes/pkg/apis/storage" + "k8s.io/kubernetes/pkg/registry/registrytest" +) + +func newStorage(t *testing.T) (*REST, *etcdtesting.EtcdTestServer) { + etcdStorage, server := registrytest.NewEtcdStorage(t, storageapi.GroupName) + restOptions := generic.RESTOptions{ + StorageConfig: etcdStorage, + Decorator: generic.UndecoratedStorage, + DeleteCollectionWorkers: 1, + ResourcePrefix: "csinodes", + } + csiNodeStorage := NewStorage(restOptions) + return csiNodeStorage.CSINode, server +} + +func validNewCSINode(name string) *storageapi.CSINode { + return &storageapi.CSINode{ + ObjectMeta: metav1.ObjectMeta{ + Name: name, + }, + Spec: storageapi.CSINodeSpec{ + Drivers: []storageapi.CSINodeDriver{ + { + Name: "valid-driver-name", + NodeID: "valid-node", + TopologyKeys: []string{"company.com/zone1", "company.com/zone2"}, + }, + }, + }, + } +} + +func TestCreate(t *testing.T) { + if *testapi.Storage.GroupVersion() != storageapiv1beta1.SchemeGroupVersion { + // skip the test for all versions exception v1beta1 + return + } + + storage, server := newStorage(t) + defer server.Terminate(t) + defer storage.Store.DestroyFunc() + test := genericregistrytest.New(t, storage.Store).ClusterScope() + csiNode := validNewCSINode("foo") + csiNode.ObjectMeta = metav1.ObjectMeta{GenerateName: "foo"} + test.TestCreate( + // valid + csiNode, + // invalid + &storageapi.CSINode{ + ObjectMeta: metav1.ObjectMeta{Name: "*BadName!"}, + Spec: storageapi.CSINodeSpec{ + Drivers: []storageapi.CSINodeDriver{ + { + Name: "invalid-name-!@#$%^&*()", + NodeID: "invalid-node-!@#$%^&*()", + TopologyKeys: []string{"company.com/zone1", "company.com/zone2"}, + }, + }, + }, + }, + ) +} + +func TestUpdate(t *testing.T) { + if *testapi.Storage.GroupVersion() != storageapiv1beta1.SchemeGroupVersion { + // skip the test for all versions exception v1beta1 + return + } + + storage, server := newStorage(t) + defer server.Terminate(t) + defer storage.Store.DestroyFunc() + test := genericregistrytest.New(t, storage.Store).ClusterScope() + + test.TestUpdate( + // valid + validNewCSINode("foo"), + // we allow status field to be set in v1beta1 + func(obj runtime.Object) runtime.Object { + object := obj.(*storageapi.CSINode) + //object.Status = *getCSINodeStatus() + return object + }, + //invalid update + func(obj runtime.Object) runtime.Object { + object := obj.(*storageapi.CSINode) + object.Spec.Drivers[0].Name = "invalid-name-!@#$%^&*()" + return object + }, + ) +} + +func TestDelete(t *testing.T) { + if *testapi.Storage.GroupVersion() != storageapiv1beta1.SchemeGroupVersion { + // skip the test for all versions exception v1beta1 + return + } + + storage, server := newStorage(t) + defer server.Terminate(t) + defer storage.Store.DestroyFunc() + test := genericregistrytest.New(t, storage.Store).ClusterScope().ReturnDeletedObject() + test.TestDelete(validNewCSINode("foo")) +} + +func TestGet(t *testing.T) { + if *testapi.Storage.GroupVersion() != storageapiv1beta1.SchemeGroupVersion { + // skip the test for all versions exception v1beta1 + return + } + + storage, server := newStorage(t) + defer server.Terminate(t) + defer storage.Store.DestroyFunc() + test := genericregistrytest.New(t, storage.Store).ClusterScope() + test.TestGet(validNewCSINode("foo")) +} + +func TestList(t *testing.T) { + if *testapi.Storage.GroupVersion() != storageapiv1beta1.SchemeGroupVersion { + // skip the test for all versions exception v1beta1 + return + } + + storage, server := newStorage(t) + defer server.Terminate(t) + defer storage.Store.DestroyFunc() + test := genericregistrytest.New(t, storage.Store).ClusterScope() + test.TestList(validNewCSINode("foo")) +} + +func TestWatch(t *testing.T) { + if *testapi.Storage.GroupVersion() != storageapiv1beta1.SchemeGroupVersion { + // skip the test for all versions exception v1beta1 + return + } + + storage, server := newStorage(t) + defer server.Terminate(t) + defer storage.Store.DestroyFunc() + test := genericregistrytest.New(t, storage.Store).ClusterScope() + test.TestWatch( + validNewCSINode("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"}, + }, + ) +} diff --git a/pkg/registry/storage/csinode/strategy.go b/pkg/registry/storage/csinode/strategy.go new file mode 100644 index 0000000000..f20e6e57b0 --- /dev/null +++ b/pkg/registry/storage/csinode/strategy.go @@ -0,0 +1,78 @@ +/* +Copyright 2019 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 csinode + +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" +) + +// csiNodeStrategy implements behavior for CSINode objects +type csiNodeStrategy struct { + runtime.ObjectTyper + names.NameGenerator +} + +// Strategy is the default logic that applies when creating and updating +// CSINode objects via the REST API. +var Strategy = csiNodeStrategy{legacyscheme.Scheme, names.SimpleNameGenerator} + +func (csiNodeStrategy) NamespaceScoped() bool { + return false +} + +// ResetBeforeCreate clears the Status field which is not allowed to be set by end users on creation. +func (csiNodeStrategy) PrepareForCreate(ctx context.Context, obj runtime.Object) { +} + +func (csiNodeStrategy) Validate(ctx context.Context, obj runtime.Object) field.ErrorList { + csiNode := obj.(*storage.CSINode) + + errs := validation.ValidateCSINode(csiNode) + errs = append(errs, validation.ValidateCSINode(csiNode)...) + + return errs +} + +// Canonicalize normalizes the object after validation. +func (csiNodeStrategy) Canonicalize(obj runtime.Object) { +} + +func (csiNodeStrategy) AllowCreateOnUpdate() bool { + return false +} + +// PrepareForUpdate sets the Status fields which is not allowed to be set by an end user updating a CSINode +func (csiNodeStrategy) PrepareForUpdate(ctx context.Context, obj, old runtime.Object) { +} + +func (csiNodeStrategy) ValidateUpdate(ctx context.Context, obj, old runtime.Object) field.ErrorList { + newCSINodeObj := obj.(*storage.CSINode) + oldCSINodeObj := old.(*storage.CSINode) + errorList := validation.ValidateCSINode(newCSINodeObj) + return append(errorList, validation.ValidateCSINodeUpdate(newCSINodeObj, oldCSINodeObj)...) +} + +func (csiNodeStrategy) AllowUnconditionalUpdate() bool { + return false +} diff --git a/pkg/registry/storage/csinode/strategy_test.go b/pkg/registry/storage/csinode/strategy_test.go new file mode 100644 index 0000000000..14d04f6c15 --- /dev/null +++ b/pkg/registry/storage/csinode/strategy_test.go @@ -0,0 +1,167 @@ +/* +Copyright 2019 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 csinode + +import ( + "testing" + + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/util/validation/field" + genericapirequest "k8s.io/apiserver/pkg/endpoints/request" + "k8s.io/kubernetes/pkg/apis/storage" +) + +func getValidCSINode(name string) *storage.CSINode { + return &storage.CSINode{ + ObjectMeta: metav1.ObjectMeta{ + Name: name, + }, + Spec: storage.CSINodeSpec{ + Drivers: []storage.CSINodeDriver{ + { + Name: "valid-driver-name", + NodeID: "valid-node", + TopologyKeys: []string{"company.com/zone1", "company.com/zone2"}, + }, + }, + }, + } +} + +func TestCSINodeStrategy(t *testing.T) { + ctx := genericapirequest.WithRequestInfo(genericapirequest.NewContext(), &genericapirequest.RequestInfo{ + APIGroup: "storage.k8s.io", + APIVersion: "v1beta1", + Resource: "csinodes", + }) + if Strategy.NamespaceScoped() { + t.Errorf("CSINode must not be namespace scoped") + } + if Strategy.AllowCreateOnUpdate() { + t.Errorf("CSINode should not allow create on update") + } + + csiNode := getValidCSINode("valid-csinode") + + Strategy.PrepareForCreate(ctx, csiNode) + + errs := Strategy.Validate(ctx, csiNode) + if len(errs) != 0 { + t.Errorf("unexpected error validating %v", errs) + } + + // Update of spec is allowed + newCSINode := csiNode.DeepCopy() + newCSINode.Spec.Drivers[0].NodeID = "valid-node-2" + + Strategy.PrepareForUpdate(ctx, newCSINode, csiNode) + + errs = Strategy.ValidateUpdate(ctx, newCSINode, csiNode) + if len(errs) == 0 { + t.Errorf("expected validation error") + } +} + +func TestCSINodeValidation(t *testing.T) { + tests := []struct { + name string + csiNode *storage.CSINode + expectError bool + }{ + { + "valid csinode", + getValidCSINode("foo"), + false, + }, + { + "invalid driver name", + &storage.CSINode{ + ObjectMeta: metav1.ObjectMeta{ + Name: "foo", + }, + Spec: storage.CSINodeSpec{ + Drivers: []storage.CSINodeDriver{ + { + Name: "$csi-driver@", + NodeID: "valid-node", + TopologyKeys: []string{"company.com/zone1", "company.com/zone2"}, + }, + }, + }, + }, + true, + }, + { + "empty node id", + &storage.CSINode{ + ObjectMeta: metav1.ObjectMeta{ + Name: "foo", + }, + Spec: storage.CSINodeSpec{ + Drivers: []storage.CSINodeDriver{ + { + Name: "valid-driver-name", + NodeID: "", + TopologyKeys: []string{"company.com/zone1", "company.com/zone2"}, + }, + }, + }, + }, + true, + }, + { + "invalid topology keys", + &storage.CSINode{ + ObjectMeta: metav1.ObjectMeta{ + Name: "foo", + }, + Spec: storage.CSINodeSpec{ + Drivers: []storage.CSINodeDriver{ + { + Name: "valid-driver-name", + NodeID: "valid-node", + TopologyKeys: []string{"company.com/zone1", ""}, + }, + }, + }, + }, + true, + }, + } + + for _, test := range tests { + t.Run(test.name, func(t *testing.T) { + + testValidation := func(csiNode *storage.CSINode, apiVersion string) field.ErrorList { + ctx := genericapirequest.WithRequestInfo(genericapirequest.NewContext(), &genericapirequest.RequestInfo{ + APIGroup: "storage.k8s.io", + APIVersion: "v1beta1", + Resource: "csinodes", + }) + return Strategy.Validate(ctx, csiNode) + } + + betaErr := testValidation(test.csiNode, "v1beta1") + if len(betaErr) > 0 && !test.expectError { + t.Errorf("Validation of v1beta1 object failed: %+v", betaErr) + } + if len(betaErr) == 0 && test.expectError { + t.Errorf("Validation of v1beta1 object unexpectedly succeeded") + } + }) + } +} diff --git a/pkg/registry/storage/rest/BUILD b/pkg/registry/storage/rest/BUILD index 60121512da..8c773060cf 100644 --- a/pkg/registry/storage/rest/BUILD +++ b/pkg/registry/storage/rest/BUILD @@ -12,6 +12,9 @@ go_library( deps = [ "//pkg/api/legacyscheme:go_default_library", "//pkg/apis/storage:go_default_library", + "//pkg/features:go_default_library", + "//pkg/registry/storage/csidriver/storage:go_default_library", + "//pkg/registry/storage/csinode/storage:go_default_library", "//pkg/registry/storage/storageclass/storage:go_default_library", "//pkg/registry/storage/volumeattachment/storage:go_default_library", "//staging/src/k8s.io/api/storage/v1:go_default_library", @@ -21,6 +24,7 @@ go_library( "//staging/src/k8s.io/apiserver/pkg/registry/rest:go_default_library", "//staging/src/k8s.io/apiserver/pkg/server:go_default_library", "//staging/src/k8s.io/apiserver/pkg/server/storage:go_default_library", + "//staging/src/k8s.io/apiserver/pkg/util/feature:go_default_library", ], ) diff --git a/pkg/registry/storage/rest/storage_storage.go b/pkg/registry/storage/rest/storage_storage.go index f799226001..6cd0efbe88 100644 --- a/pkg/registry/storage/rest/storage_storage.go +++ b/pkg/registry/storage/rest/storage_storage.go @@ -24,8 +24,12 @@ import ( "k8s.io/apiserver/pkg/registry/rest" genericapiserver "k8s.io/apiserver/pkg/server" serverstorage "k8s.io/apiserver/pkg/server/storage" + utilfeature "k8s.io/apiserver/pkg/util/feature" "k8s.io/kubernetes/pkg/api/legacyscheme" storageapi "k8s.io/kubernetes/pkg/apis/storage" + "k8s.io/kubernetes/pkg/features" + csidriverstore "k8s.io/kubernetes/pkg/registry/storage/csidriver/storage" + csinodestore "k8s.io/kubernetes/pkg/registry/storage/csinode/storage" storageclassstore "k8s.io/kubernetes/pkg/registry/storage/storageclass/storage" volumeattachmentstore "k8s.io/kubernetes/pkg/registry/storage/volumeattachment/storage" ) @@ -70,6 +74,18 @@ func (p RESTStorageProvider) v1beta1Storage(apiResourceConfigSource serverstorag volumeAttachmentStorage := volumeattachmentstore.NewStorage(restOptionsGetter) storage["volumeattachments"] = volumeAttachmentStorage.VolumeAttachment + // register csinodes if CSINodeInfo feature gate is enabled + if utilfeature.DefaultFeatureGate.Enabled(features.CSINodeInfo) { + csiNodeStorage := csinodestore.NewStorage(restOptionsGetter) + storage["csinodes"] = csiNodeStorage.CSINode + } + + // register csidrivers if CSIDriverRegistry feature gate is enabled + if utilfeature.DefaultFeatureGate.Enabled(features.CSIDriverRegistry) { + csiDriverStorage := csidriverstore.NewStorage(restOptionsGetter) + storage["csidrivers"] = csiDriverStorage.CSIDriver + } + return storage } diff --git a/staging/src/k8s.io/api/storage/v1beta1/register.go b/staging/src/k8s.io/api/storage/v1beta1/register.go index 06b0f3d529..c270ace57d 100644 --- a/staging/src/k8s.io/api/storage/v1beta1/register.go +++ b/staging/src/k8s.io/api/storage/v1beta1/register.go @@ -49,6 +49,12 @@ func addKnownTypes(scheme *runtime.Scheme) error { &VolumeAttachment{}, &VolumeAttachmentList{}, + + &CSIDriver{}, + &CSIDriverList{}, + + &CSINode{}, + &CSINodeList{}, ) metav1.AddToGroupVersion(scheme, SchemeGroupVersion) diff --git a/staging/src/k8s.io/api/storage/v1beta1/types.go b/staging/src/k8s.io/api/storage/v1beta1/types.go index a955542256..d82e58f903 100644 --- a/staging/src/k8s.io/api/storage/v1beta1/types.go +++ b/staging/src/k8s.io/api/storage/v1beta1/types.go @@ -209,3 +209,158 @@ type VolumeError struct { // +optional Message string `json:"message,omitempty" protobuf:"bytes,2,opt,name=message"` } + +// +genclient +// +genclient:nonNamespaced +// +k8s:deepcopy-gen:interfaces=k8s.io/apimachinery/pkg/runtime.Object + +// CSIDriver captures information about a Container Storage Interface (CSI) +// volume driver deployed on the cluster. +// CSI drivers do not need to create the CSIDriver object directly. Instead they may use the +// cluster-driver-registrar sidecar container. When deployed with a CSI driver it automatically +// creates a CSIDriver object representing the driver. +// Kubernetes attach detach controller uses this object to determine whether attach is required. +// Kubelet uses this object to determine whether pod information needs to be passed on mount. +// CSIDriver objects are non-namespaced. +type CSIDriver struct { + metav1.TypeMeta `json:",inline"` + + // Standard object metadata. + // metadata.Name indicates the name of the CSI driver that this object + // refers to; it MUST be the same name returned by the CSI GetPluginName() + // call for that driver. + // The driver name must be 63 characters or less, beginning and ending with + // an alphanumeric character ([a-z0-9A-Z]) with dashes (-), dots (.), and + // alphanumerics between. + // More info: https://git.k8s.io/community/contributors/devel/api-conventions.md#metadata + metav1.ObjectMeta `json:"metadata,omitempty" protobuf:"bytes,1,opt,name=metadata"` + + // Specification of the CSI Driver. + Spec CSIDriverSpec `json:"spec" protobuf:"bytes,2,opt,name=spec"` +} + +// +k8s:deepcopy-gen:interfaces=k8s.io/apimachinery/pkg/runtime.Object + +// CSIDriverList is a collection of CSIDriver objects. +type CSIDriverList struct { + metav1.TypeMeta `json:",inline"` + + // Standard list metadata + // More info: https://git.k8s.io/community/contributors/devel/api-conventions.md#metadata + // +optional + metav1.ListMeta `json:"metadata,omitempty" protobuf:"bytes,1,opt,name=metadata"` + + // items is the list of CSIDriver + Items []CSIDriver `json:"items" protobuf:"bytes,2,rep,name=items"` +} + +// CSIDriverSpec is the specification of a CSIDriver. +type CSIDriverSpec struct { + // attachRequired indicates this CSI volume driver requires an attach + // operation (because it implements the CSI ControllerPublishVolume() + // method), and that the Kubernetes attach detach controller should call + // the attach volume interface which checks the volumeattachment status + // and waits until the volume is attached before proceeding to mounting. + // The CSI external-attacher coordinates with CSI volume driver and updates + // the volumeattachment status when the attach operation is complete. + // If the CSIDriverRegistry feature gate is enabled and the value is + // specified to false, the attach operation will be skipped. + // Otherwise the attach operation will be called. + // +optional + AttachRequired *bool `json:"attachRequired,omitempty" protobuf:"varint,1,opt,name=attachRequired"` + + // If set to true, podInfoOnMount indicates this CSI volume driver + // requires additional pod information (like podName, podUID, etc.) during + // mount operations. + // If not set or set to false, pod information will not be passed on mount. + // The CSI driver specifies podInfoOnMount as part of driver deployment. + // If true, Kubelet will pass pod information as VolumeContext in the CSI + // NodePublishVolume() calls. + // The CSI driver is responsible for parsing and validating the information + // passed in as VolumeContext. + // The following VolumeConext will be passed if podInfoOnMount is set to true: + // "csi.storage.k8s.io/pod.name": pod.Name + // "csi.storage.k8s.io/pod.namespace": pod.Namespace + // "csi.storage.k8s.io/pod.uid": string(pod.UID) + // +optional + PodInfoOnMount *bool `json:"podInfoOnMount,omitempty" protobuf:"bytes,2,opt,name=podInfoOnMount"` +} + +// +genclient +// +genclient:nonNamespaced +// +k8s:deepcopy-gen:interfaces=k8s.io/apimachinery/pkg/runtime.Object + +// CSINode holds information about all CSI drivers installed on a node. +// CSI drivers do not need to create the CSINode object directly. As long as +// they use the node-driver-registrar sidecar container, the kubelet will +// automatically populate the CSINode object for the CSI driver as part of +// kubelet plugin registration. +// CSINode has the same name as a node. If the object is missing, it means either +// there are no CSI Drivers available on the node, or the Kubelet version is low +// enough that it doesn't create this object. +// CSINode has an OwnerReference that points to the corresponding node object. +type CSINode struct { + metav1.TypeMeta `json:",inline"` + + // metadata.name must be the Kubernetes node name. + metav1.ObjectMeta `json:"metadata,omitempty" protobuf:"bytes,1,opt,name=metadata"` + + // spec is the specification of CSINode + Spec CSINodeSpec `json:"spec" protobuf:"bytes,2,opt,name=spec"` +} + +// CSINodeSpec holds information about the specification of all CSI drivers installed on a node +type CSINodeSpec struct { + // drivers is a list of information of all CSI Drivers existing on a node. + // It can be empty on initialization. + // +patchMergeKey=name + // +patchStrategy=merge + Drivers []CSINodeDriver `json:"drivers" patchStrategy:"merge" patchMergeKey:"name" protobuf:"bytes,1,rep,name=drivers"` +} + +// CSINodeDriver holds information about the specification of one CSI driver installed on a node +type CSINodeDriver struct { + // This is the name of the CSI driver that this object refers to. + // This MUST be the same name returned by the CSI GetPluginName() call for + // that driver. + Name string `json:"name" protobuf:"bytes,1,opt,name=name"` + + // nodeID of the node from the driver point of view. + // This field enables Kubernetes to communicate with storage systems that do + // not share the same nomenclature for nodes. For example, Kubernetes may + // refer to a given node as "node1", but the storage system may refer to + // the same node as "nodeA". When Kubernetes issues a command to the storage + // system to attach a volume to a specific node, it can use this field to + // refer to the node name using the ID that the storage system will + // understand, e.g. "nodeA" instead of "node1". This field is required. + NodeID string `json:"nodeID" protobuf:"bytes,2,opt,name=nodeID"` + + // topologyKeys is the list of keys supported by the driver. + // When a driver is initialized on a cluster, it provides a set of topology + // keys that it understands (e.g. "company.com/zone", "company.com/region"). + // When a driver is initialized on a node, it provides the same topology keys + // along with values. Kubelet will expose these topology keys as labels + // on its own node object. + // When Kubernetes does topology aware provisioning, it can use this list to + // determine which labels it should retrieve from the node object and pass + // back to the driver. + // It is possible for different nodes to use different topology keys. + // This can be empty if driver does not support topology. + // +optional + TopologyKeys []string `json:"topologyKeys" protobuf:"bytes,3,rep,name=topologyKeys"` +} + +// +k8s:deepcopy-gen:interfaces=k8s.io/apimachinery/pkg/runtime.Object + +// CSINodeList is a collection of CSINode objects. +type CSINodeList struct { + metav1.TypeMeta `json:",inline"` + + // Standard list metadata + // More info: https://git.k8s.io/community/contributors/devel/api-conventions.md#metadata + // +optional + metav1.ListMeta `json:"metadata,omitempty" protobuf:"bytes,1,opt,name=metadata"` + + // items is the list of CSINode + Items []CSINode `json:"items" protobuf:"bytes,2,rep,name=items"` +} diff --git a/test/integration/apiserver/print_test.go b/test/integration/apiserver/print_test.go index b3cab84f09..40d1355f21 100644 --- a/test/integration/apiserver/print_test.go +++ b/test/integration/apiserver/print_test.go @@ -133,6 +133,8 @@ var missingHanlders = sets.NewString( "PriorityClass", "PodPreset", "AuditSink", + "CSINode", + "CSIDriver", ) func TestServerSidePrint(t *testing.T) { diff --git a/test/integration/etcd/BUILD b/test/integration/etcd/BUILD index f29d37ba72..ecf1142563 100644 --- a/test/integration/etcd/BUILD +++ b/test/integration/etcd/BUILD @@ -55,6 +55,7 @@ go_library( deps = [ "//cmd/kube-apiserver/app:go_default_library", "//cmd/kube-apiserver/app/options:go_default_library", + "//pkg/features:go_default_library", "//pkg/master:go_default_library", "//staging/src/k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/v1beta1:go_default_library", "//staging/src/k8s.io/apiextensions-apiserver/pkg/client/clientset/clientset:go_default_library", @@ -65,6 +66,7 @@ go_library( "//staging/src/k8s.io/apimachinery/pkg/runtime/schema:go_default_library", "//staging/src/k8s.io/apimachinery/pkg/util/wait:go_default_library", "//staging/src/k8s.io/apiserver/pkg/server/options:go_default_library", + "//staging/src/k8s.io/apiserver/pkg/util/feature:go_default_library", "//staging/src/k8s.io/client-go/discovery/cached/memory:go_default_library", "//staging/src/k8s.io/client-go/dynamic:go_default_library", "//staging/src/k8s.io/client-go/kubernetes:go_default_library", diff --git a/test/integration/etcd/data.go b/test/integration/etcd/data.go index 30f5247fa5..0bbe937cd1 100644 --- a/test/integration/etcd/data.go +++ b/test/integration/etcd/data.go @@ -20,13 +20,15 @@ import ( apiextensionsv1beta1 "k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/v1beta1" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/apimachinery/pkg/runtime/schema" + utilfeature "k8s.io/apiserver/pkg/util/feature" + "k8s.io/kubernetes/pkg/features" ) // GetEtcdStorageData returns etcd data for all persisted objects. // It is exported so that it can be reused across multiple tests. // It returns a new map on every invocation to prevent different tests from mutating shared state. func GetEtcdStorageData() map[schema.GroupVersionResource]StorageData { - return map[schema.GroupVersionResource]StorageData{ + etcdStorageData := map[schema.GroupVersionResource]StorageData{ // k8s.io/kubernetes/pkg/api/v1 gvr("", "v1", "configmaps"): { Stub: `{"data": {"foo": "bar"}, "metadata": {"name": "cm1"}}`, @@ -484,6 +486,26 @@ func GetEtcdStorageData() map[schema.GroupVersionResource]StorageData { }, // -- } + + // k8s.io/kubernetes/pkg/apis/storage/v1beta1 + // add csinodes if CSINodeInfo feature gate is enabled + if utilfeature.DefaultFeatureGate.Enabled(features.CSINodeInfo) { + etcdStorageData[gvr("storage.k8s.io", "v1beta1", "csinodes")] = StorageData{ + Stub: `{"metadata": {"name": "csini1"}, "spec": {"drivers": [{"name": "test-driver", "nodeID": "localhost", "topologyKeys": ["company.com/zone1", "company.com/zone2"]}]}`, + ExpectedEtcdPath: "/registry/csinodes/csini1", + } + } + + // k8s.io/kubernetes/pkg/apis/storage/v1beta1 + // add csidrivers if CSIDriverRegistry feature gate is enabled + if utilfeature.DefaultFeatureGate.Enabled(features.CSIDriverRegistry) { + etcdStorageData[gvr("storage.k8s.io", "v1beta1", "csidrivers")] = StorageData{ + Stub: `{"metadata": {"name": "csid1"}, "spec": {"attachRequired": true, "podInfoOnMount": true}}`, + ExpectedEtcdPath: "/registry/csidrivers/csid1", + } + } + + return etcdStorageData } // StorageData contains information required to create an object and verify its storage in etcd diff --git a/test/integration/framework/master_utils.go b/test/integration/framework/master_utils.go index 7c94935484..9b98835f2b 100644 --- a/test/integration/framework/master_utils.go +++ b/test/integration/framework/master_utils.go @@ -267,6 +267,8 @@ func NewMasterConfig() *master.Config { resourceEncoding.SetResourceEncoding(schema.GroupResource{Group: batch.GroupName, Resource: "cronjobs"}, schema.GroupVersion{Group: batch.GroupName, Version: "v1beta1"}, schema.GroupVersion{Group: batch.GroupName, Version: runtime.APIVersionInternal}) // we also need to set both for the storage group and for volumeattachments, separately resourceEncoding.SetResourceEncoding(schema.GroupResource{Group: storage.GroupName, Resource: "volumeattachments"}, schema.GroupVersion{Group: storage.GroupName, Version: "v1beta1"}, schema.GroupVersion{Group: storage.GroupName, Version: runtime.APIVersionInternal}) + resourceEncoding.SetResourceEncoding(schema.GroupResource{Group: storage.GroupName, Resource: "csinodes"}, schema.GroupVersion{Group: storage.GroupName, Version: "v1beta1"}, schema.GroupVersion{Group: storage.GroupName, Version: runtime.APIVersionInternal}) + resourceEncoding.SetResourceEncoding(schema.GroupResource{Group: storage.GroupName, Resource: "csidrivers"}, schema.GroupVersion{Group: storage.GroupName, Version: "v1beta1"}, schema.GroupVersion{Group: storage.GroupName, Version: runtime.APIVersionInternal}) storageFactory := serverstorage.NewDefaultStorageFactory(etcdOptions.StorageConfig, runtime.ContentTypeJSON, ns, resourceEncoding, master.DefaultAPIResourceConfigSource(), nil) storageFactory.SetSerializer(