diff --git a/cmd/kube-apiserver/app/aggregator.go b/cmd/kube-apiserver/app/aggregator.go index ed50c5511ac..cc99810ca35 100644 --- a/cmd/kube-apiserver/app/aggregator.go +++ b/cmd/kube-apiserver/app/aggregator.go @@ -223,6 +223,7 @@ var apiVersionPriorities = map[schema.GroupVersion]priority{ {Group: "settings.k8s.io", Version: "v1alpha1"}: {group: 16900, version: 9}, {Group: "storage.k8s.io", Version: "v1"}: {group: 16800, version: 15}, {Group: "storage.k8s.io", Version: "v1beta1"}: {group: 16800, version: 9}, + {Group: "storage.k8s.io", Version: "v1alpha1"}: {group: 16800, version: 1}, {Group: "apiextensions.k8s.io", Version: "v1beta1"}: {group: 16700, version: 9}, {Group: "admissionregistration.k8s.io", Version: "v1alpha1"}: {group: 16700, version: 9}, {Group: "scheduling.k8s.io", Version: "v1alpha1"}: {group: 16600, version: 9}, diff --git a/cmd/kube-apiserver/app/server.go b/cmd/kube-apiserver/app/server.go index c60331ffa30..ff72ade5f5b 100644 --- a/cmd/kube-apiserver/app/server.go +++ b/cmd/kube-apiserver/app/server.go @@ -64,6 +64,7 @@ import ( "k8s.io/kubernetes/pkg/apis/batch" "k8s.io/kubernetes/pkg/apis/extensions" "k8s.io/kubernetes/pkg/apis/networking" + "k8s.io/kubernetes/pkg/apis/storage" "k8s.io/kubernetes/pkg/capabilities" "k8s.io/kubernetes/pkg/client/clientset_generated/internalclientset" informers "k8s.io/kubernetes/pkg/client/informers/informers_generated/internalversion" @@ -556,7 +557,10 @@ func BuildStorageFactory(s *options.ServerRunOptions) (*serverstorage.DefaultSto s.Etcd.StorageConfig, s.Etcd.DefaultStorageMediaType, legacyscheme.Codecs, serverstorage.NewDefaultResourceEncodingConfig(legacyscheme.Registry), storageGroupsToEncodingVersion, // FIXME (soltysh): this GroupVersionResource override should be configurable - []schema.GroupVersionResource{batch.Resource("cronjobs").WithVersion("v1beta1")}, + []schema.GroupVersionResource{ + batch.Resource("cronjobs").WithVersion("v1beta1"), + storage.Resource("volumeattachments").WithVersion("v1alpha1"), + }, master.DefaultAPIResourceConfigSource(), s.APIEnablement.RuntimeConfig) if err != nil { return nil, fmt.Errorf("error in initializing storage factory: %s", err) diff --git a/hack/.golint_failures b/hack/.golint_failures index 4a31103c218..e321126f42c 100644 --- a/hack/.golint_failures +++ b/hack/.golint_failures @@ -73,6 +73,7 @@ pkg/apis/storage pkg/apis/storage/util pkg/apis/storage/v1 pkg/apis/storage/v1/util +pkg/apis/storage/v1alpha1 pkg/apis/storage/v1beta1 pkg/apis/storage/v1beta1/util pkg/auth/authorizer/abac @@ -458,6 +459,7 @@ staging/src/k8s.io/api/rbac/v1beta1 staging/src/k8s.io/api/scheduling/v1alpha1 staging/src/k8s.io/api/settings/v1alpha1 staging/src/k8s.io/api/storage/v1 +staging/src/k8s.io/api/storage/v1alpha1 staging/src/k8s.io/api/storage/v1beta1 staging/src/k8s.io/apiextensions-apiserver/examples/client-go/pkg/apis/cr staging/src/k8s.io/apiextensions-apiserver/examples/client-go/pkg/apis/cr/v1 @@ -666,6 +668,8 @@ staging/src/k8s.io/client-go/kubernetes/typed/settings/v1alpha1 staging/src/k8s.io/client-go/kubernetes/typed/settings/v1alpha1/fake staging/src/k8s.io/client-go/kubernetes/typed/storage/v1 staging/src/k8s.io/client-go/kubernetes/typed/storage/v1/fake +staging/src/k8s.io/client-go/kubernetes/typed/storage/v1alpha1 +staging/src/k8s.io/client-go/kubernetes/typed/storage/v1alpha1/fake staging/src/k8s.io/client-go/kubernetes/typed/storage/v1beta1 staging/src/k8s.io/client-go/kubernetes/typed/storage/v1beta1/fake staging/src/k8s.io/client-go/plugin/pkg/auth/authenticator/token/oidc/testing diff --git a/hack/lib/init.sh b/hack/lib/init.sh index 3d3047eebc3..7e57c90da85 100755 --- a/hack/lib/init.sh +++ b/hack/lib/init.sh @@ -76,6 +76,7 @@ rbac.authorization.k8s.io/v1beta1 \ rbac.authorization.k8s.io/v1alpha1 \ scheduling.k8s.io/v1alpha1 \ settings.k8s.io/v1alpha1 \ +storage.k8s.io/v1alpha1 \ storage.k8s.io/v1beta1 \ storage.k8s.io/v1 \ }" diff --git a/hack/update-generated-protobuf-dockerized.sh b/hack/update-generated-protobuf-dockerized.sh index 5b45586a1ed..cd179476e54 100755 --- a/hack/update-generated-protobuf-dockerized.sh +++ b/hack/update-generated-protobuf-dockerized.sh @@ -67,6 +67,7 @@ PACKAGES=( k8s.io/api/imagepolicy/v1alpha1 k8s.io/api/scheduling/v1alpha1 k8s.io/api/settings/v1alpha1 + k8s.io/api/storage/v1alpha1 k8s.io/api/storage/v1beta1 k8s.io/api/storage/v1 k8s.io/api/admissionregistration/v1alpha1 diff --git a/pkg/apis/storage/install/BUILD b/pkg/apis/storage/install/BUILD index 1aa602f4381..798341862f4 100644 --- a/pkg/apis/storage/install/BUILD +++ b/pkg/apis/storage/install/BUILD @@ -13,6 +13,7 @@ go_library( "//pkg/api/legacyscheme:go_default_library", "//pkg/apis/storage:go_default_library", "//pkg/apis/storage/v1:go_default_library", + "//pkg/apis/storage/v1alpha1:go_default_library", "//pkg/apis/storage/v1beta1:go_default_library", "//vendor/k8s.io/apimachinery/pkg/apimachinery/announced:go_default_library", "//vendor/k8s.io/apimachinery/pkg/apimachinery/registered:go_default_library", diff --git a/pkg/apis/storage/install/install.go b/pkg/apis/storage/install/install.go index 9127e3eb9d4..28fa7865968 100644 --- a/pkg/apis/storage/install/install.go +++ b/pkg/apis/storage/install/install.go @@ -26,6 +26,7 @@ import ( "k8s.io/kubernetes/pkg/api/legacyscheme" "k8s.io/kubernetes/pkg/apis/storage" "k8s.io/kubernetes/pkg/apis/storage/v1" + "k8s.io/kubernetes/pkg/apis/storage/v1alpha1" "k8s.io/kubernetes/pkg/apis/storage/v1beta1" ) @@ -37,14 +38,18 @@ func init() { func Install(groupFactoryRegistry announced.APIGroupFactoryRegistry, registry *registered.APIRegistrationManager, scheme *runtime.Scheme) { if err := announced.NewGroupMetaFactory( &announced.GroupMetaFactoryArgs{ - GroupName: storage.GroupName, - VersionPreferenceOrder: []string{v1.SchemeGroupVersion.Version, v1beta1.SchemeGroupVersion.Version}, - RootScopedKinds: sets.NewString("StorageClass"), + GroupName: storage.GroupName, + VersionPreferenceOrder: []string{v1.SchemeGroupVersion.Version, v1beta1.SchemeGroupVersion.Version, v1alpha1.SchemeGroupVersion.Version}, + RootScopedKinds: sets.NewString( + "StorageClass", + "VolumeAttachment", + ), AddInternalObjectsToScheme: storage.AddToScheme, }, announced.VersionToSchemeFunc{ - v1.SchemeGroupVersion.Version: v1.AddToScheme, - v1beta1.SchemeGroupVersion.Version: v1beta1.AddToScheme, + v1.SchemeGroupVersion.Version: v1.AddToScheme, + v1beta1.SchemeGroupVersion.Version: v1beta1.AddToScheme, + v1alpha1.SchemeGroupVersion.Version: v1alpha1.AddToScheme, }, ).Announce(groupFactoryRegistry).RegisterAndEnable(registry, scheme); err != nil { panic(err) diff --git a/pkg/apis/storage/register.go b/pkg/apis/storage/register.go index aaa619b4d97..7ae2f3efe18 100644 --- a/pkg/apis/storage/register.go +++ b/pkg/apis/storage/register.go @@ -46,6 +46,8 @@ func addKnownTypes(scheme *runtime.Scheme) error { scheme.AddKnownTypes(SchemeGroupVersion, &StorageClass{}, &StorageClassList{}, + &VolumeAttachment{}, + &VolumeAttachmentList{}, ) return nil } diff --git a/pkg/apis/storage/types.go b/pkg/apis/storage/types.go index f80a6308b8b..5a9024a70f9 100644 --- a/pkg/apis/storage/types.go +++ b/pkg/apis/storage/types.go @@ -80,3 +80,110 @@ type StorageClassList struct { // Items is the list of StorageClasses Items []StorageClass } + +// +genclient +// +genclient:nonNamespaced +// +k8s:deepcopy-gen:interfaces=k8s.io/apimachinery/pkg/runtime.Object + +// Captures the intent to attach or detach the specified volume to/from +// the specified node. +// +// VolumeAttachment objects are non-namespaced. +type VolumeAttachment struct { + metav1.TypeMeta + + // Standard object metadata. + // More info: https://git.k8s.io/community/contributors/devel/api-conventions.md#metadata + // +optional + metav1.ObjectMeta + + // Specification of the desired attach/detach volume behavior. + // Populated by the Kubernetes system. + Spec VolumeAttachmentSpec + + // Status of the VolumeAttachment request. + // Populated by the entity completing the attach or detach + // operation, i.e. the external-attacher. + // +optional + Status VolumeAttachmentStatus +} + +// +k8s:deepcopy-gen:interfaces=k8s.io/apimachinery/pkg/runtime.Object + +// VolumeAttachmentList is a collection of VolumeAttachment objects. +type VolumeAttachmentList 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 VolumeAttachments + Items []VolumeAttachment +} + +// The specification of a VolumeAttachment request. +type VolumeAttachmentSpec struct { + // Attacher indicates the name of the volume driver that MUST handle this + // request. This is the name returned by GetPluginName(). + Attacher string + + // Source represents the volume that should be attached. + Source VolumeAttachmentSource + + // The node that the volume should be attached to. + NodeName string +} + +// VolumeAttachmentSource represents a volume that should be attached. +// Right now only PersistenVolumes can be attached via external attacher, +// in future we may allow also inline volumes in pods. +// Exactly one member can be set. +type VolumeAttachmentSource struct { + // Name of the persistent volume to attach. + // +optional + PersistentVolumeName *string + + // Placeholder for *VolumeSource to accommodate inline volumes in pods. +} + +// The status of a VolumeAttachment request. +type VolumeAttachmentStatus struct { + // Indicates the volume is successfully attached. + // This field must only be set by the entity completing the attach + // operation, i.e. the external-attacher. + Attached bool + + // Upon successful attach, this field is populated with any + // information returned by the attach operation that must be passed + // into subsequent WaitForAttach or Mount calls. + // This field must only be set by the entity completing the attach + // operation, i.e. the external-attacher. + // +optional + AttachmentMetadata map[string]string + + // The last error encountered during attach operation, if any. + // This field must only be set by the entity completing the attach + // operation, i.e. the external-attacher. + // +optional + AttachError *VolumeError + + // The last error encountered during detach operation, if any. + // This field must only be set by the entity completing the detach + // operation, i.e. the external-attacher. + // +optional + DetachError *VolumeError +} + +// Captures an error encountered during a volume operation. +type VolumeError struct { + // Time the error was encountered. + // +optional + Time metav1.Time + + // String detailing the error encountered during Attach or Detach operation. + // This string maybe logged, so it should not contain sensitive + // information. + // +optional + Message string +} diff --git a/pkg/apis/storage/v1alpha1/doc.go b/pkg/apis/storage/v1alpha1/doc.go new file mode 100644 index 00000000000..5c1f6059d3e --- /dev/null +++ b/pkg/apis/storage/v1alpha1/doc.go @@ -0,0 +1,22 @@ +/* +Copyright 2017 The Kubernetes Authors. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +// +k8s:conversion-gen=k8s.io/kubernetes/pkg/apis/storage +// +k8s:conversion-gen-external-types=../../../../vendor/k8s.io/api/storage/v1alpha1 +// +groupName=storage.k8s.io +// +k8s:defaulter-gen=TypeMeta +// +k8s:defaulter-gen-input=../../../../vendor/k8s.io/api/storage/v1alpha1 +package v1alpha1 // import "k8s.io/kubernetes/pkg/apis/storage/v1alpha1" diff --git a/pkg/apis/storage/v1alpha1/register.go b/pkg/apis/storage/v1alpha1/register.go new file mode 100644 index 00000000000..699fab12d5b --- /dev/null +++ b/pkg/apis/storage/v1alpha1/register.go @@ -0,0 +1,38 @@ +/* +Copyright 2017 The Kubernetes Authors. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package v1alpha1 + +import ( + storagev1alpha1 "k8s.io/api/storage/v1alpha1" + "k8s.io/apimachinery/pkg/runtime/schema" +) + +// GroupName is the group name use in this package +const GroupName = "storage.k8s.io" + +// SchemeGroupVersion is group version used to register these objects +var SchemeGroupVersion = schema.GroupVersion{Group: GroupName, Version: "v1alpha1"} + +// Resource takes an unqualified resource and returns a Group qualified GroupResource +func Resource(resource string) schema.GroupResource { + return SchemeGroupVersion.WithResource(resource).GroupResource() +} + +var ( + localSchemeBuilder = &storagev1alpha1.SchemeBuilder + AddToScheme = localSchemeBuilder.AddToScheme +) diff --git a/pkg/apis/storage/validation/validation.go b/pkg/apis/storage/validation/validation.go index 5e9803ecd29..4ffc7108808 100644 --- a/pkg/apis/storage/validation/validation.go +++ b/pkg/apis/storage/validation/validation.go @@ -20,6 +20,7 @@ import ( "reflect" "strings" + apiequality "k8s.io/apimachinery/pkg/api/equality" "k8s.io/apimachinery/pkg/util/sets" "k8s.io/apimachinery/pkg/util/validation" "k8s.io/apimachinery/pkg/util/validation/field" @@ -30,6 +31,14 @@ import ( "k8s.io/kubernetes/pkg/features" ) +const ( + maxProvisionerParameterSize = 256 * (1 << 10) // 256 kB + maxProvisionerParameterLen = 512 + + maxAttachedVolumeMetadataSize = 256 * (1 << 10) // 256 kB + maxVolumeErrorMessageSize = 1024 +) + // ValidateStorageClass validates a StorageClass. func ValidateStorageClass(storageClass *storage.StorageClass) field.ErrorList { allErrs := apivalidation.ValidateObjectMeta(&storageClass.ObjectMeta, false, apivalidation.ValidateClassName, field.NewPath("metadata")) @@ -72,9 +81,6 @@ func validateProvisioner(provisioner string, fldPath *field.Path) field.ErrorLis return allErrs } -const maxProvisionerParameterSize = 256 * (1 << 10) // 256 kB -const maxProvisionerParameterLen = 512 - // validateParameters tests that keys are qualified names and that provisionerParameter are < 256kB. func validateParameters(params map[string]string, fldPath *field.Path) field.ErrorList { var totalSize int64 @@ -121,3 +127,94 @@ func validateAllowVolumeExpansion(allowExpand *bool, fldPath *field.Path) field. } return allErrs } + +// ValidateVolumeAttachment validates a VolumeAttachment. +func ValidateVolumeAttachment(volumeAttachment *storage.VolumeAttachment) field.ErrorList { + allErrs := apivalidation.ValidateObjectMeta(&volumeAttachment.ObjectMeta, false, apivalidation.ValidateClassName, field.NewPath("metadata")) + allErrs = append(allErrs, validateVolumeAttachmentSpec(&volumeAttachment.Spec, field.NewPath("spec"))...) + allErrs = append(allErrs, validateVolumeAttachmentStatus(&volumeAttachment.Status, field.NewPath("status"))...) + return allErrs +} + +// ValidateVolumeAttachmentSpec tests that the specified VolumeAttachmentSpec +// has valid data. +func validateVolumeAttachmentSpec( + spec *storage.VolumeAttachmentSpec, fldPath *field.Path) field.ErrorList { + allErrs := field.ErrorList{} + allErrs = append(allErrs, validateAttacher(spec.Attacher, fldPath.Child("attacher"))...) + allErrs = append(allErrs, validateVolumeAttachmentSource(&spec.Source, fldPath.Child("source"))...) + allErrs = append(allErrs, validateNodeName(spec.NodeName, fldPath.Child("nodeName"))...) + return allErrs +} + +// validateAttacher tests if attacher is a valid qualified name. +func validateAttacher(attacher string, fldPath *field.Path) field.ErrorList { + allErrs := field.ErrorList{} + if len(attacher) == 0 { + allErrs = append(allErrs, field.Required(fldPath, attacher)) + } + return allErrs +} + +// validateSource tests if the source is valid for VolumeAttachment. +func validateVolumeAttachmentSource(source *storage.VolumeAttachmentSource, fldPath *field.Path) field.ErrorList { + allErrs := field.ErrorList{} + if source.PersistentVolumeName == nil || len(*source.PersistentVolumeName) == 0 { + allErrs = append(allErrs, field.Required(fldPath, "")) + } + return allErrs +} + +// validateNodeName tests if the nodeName is valid for VolumeAttachment. +func validateNodeName(nodeName string, fldPath *field.Path) field.ErrorList { + allErrs := field.ErrorList{} + for _, msg := range apivalidation.ValidateNodeName(nodeName, false /* prefix */) { + allErrs = append(allErrs, field.Invalid(fldPath, nodeName, msg)) + } + return allErrs +} + +// validaVolumeAttachmentStatus tests if volumeAttachmentStatus is valid. +func validateVolumeAttachmentStatus(status *storage.VolumeAttachmentStatus, fldPath *field.Path) field.ErrorList { + allErrs := field.ErrorList{} + allErrs = append(allErrs, validateAttachmentMetadata(status.AttachmentMetadata, fldPath.Child("attachmentMetadata"))...) + allErrs = append(allErrs, validateVolumeError(status.AttachError, fldPath.Child("attachError"))...) + allErrs = append(allErrs, validateVolumeError(status.DetachError, fldPath.Child("detachError"))...) + return allErrs +} + +func validateAttachmentMetadata(metadata map[string]string, fldPath *field.Path) field.ErrorList { + allErrs := field.ErrorList{} + + var size int64 + for k, v := range metadata { + size += (int64)(len(k)) + (int64)(len(v)) + } + if size > maxAttachedVolumeMetadataSize { + allErrs = append(allErrs, field.TooLong(fldPath, metadata, maxAttachedVolumeMetadataSize)) + } + return allErrs +} + +func validateVolumeError(e *storage.VolumeError, fldPath *field.Path) field.ErrorList { + allErrs := field.ErrorList{} + + if e == nil { + return allErrs + } + if len(e.Message) > maxVolumeErrorMessageSize { + allErrs = append(allErrs, field.TooLong(fldPath.Child("message"), e.Message, maxAttachedVolumeMetadataSize)) + } + return allErrs +} + +// ValidateVolumeAttachmentUpdate validates a VolumeAttachment. +func ValidateVolumeAttachmentUpdate(new, old *storage.VolumeAttachment) field.ErrorList { + allErrs := ValidateVolumeAttachment(new) + + // Spec is read-only + if !apiequality.Semantic.DeepEqual(old.Spec, new.Spec) { + allErrs = append(allErrs, field.Invalid(field.NewPath("spec"), new.Spec, "field is immutable")) + } + return allErrs +} diff --git a/pkg/apis/storage/validation/validation_test.go b/pkg/apis/storage/validation/validation_test.go index 9333f6a2025..1c0542a787e 100644 --- a/pkg/apis/storage/validation/validation_test.go +++ b/pkg/apis/storage/validation/validation_test.go @@ -18,6 +18,7 @@ package validation import ( "fmt" + "strings" "testing" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" @@ -157,3 +158,281 @@ func TestAlphaExpandPersistentVolumesFeatureValidation(t *testing.T) { } } + +func TestVolumeAttachmentValidation(t *testing.T) { + volumeName := "pv-name" + empty := "" + successCases := []storage.VolumeAttachment{ + { + ObjectMeta: metav1.ObjectMeta{Name: "foo"}, + Spec: storage.VolumeAttachmentSpec{ + Attacher: "myattacher", + Source: storage.VolumeAttachmentSource{ + PersistentVolumeName: &volumeName, + }, + NodeName: "mynode", + }, + }, + { + ObjectMeta: metav1.ObjectMeta{Name: "foo-with-status"}, + Spec: storage.VolumeAttachmentSpec{ + Attacher: "myattacher", + Source: storage.VolumeAttachmentSource{ + PersistentVolumeName: &volumeName, + }, + NodeName: "mynode", + }, + Status: storage.VolumeAttachmentStatus{ + Attached: true, + AttachmentMetadata: map[string]string{ + "foo": "bar", + }, + AttachError: &storage.VolumeError{ + Time: metav1.Time{}, + Message: "hello world", + }, + DetachError: &storage.VolumeError{ + Time: metav1.Time{}, + Message: "hello world", + }, + }, + }, + } + + for _, volumeAttachment := range successCases { + if errs := ValidateVolumeAttachment(&volumeAttachment); len(errs) != 0 { + t.Errorf("expected success: %v", errs) + } + } + errorCases := []storage.VolumeAttachment{ + { + // Empty attacher name + ObjectMeta: metav1.ObjectMeta{Name: "foo"}, + Spec: storage.VolumeAttachmentSpec{ + Attacher: "", + NodeName: "mynode", + }, + }, + { + // Invalid attacher name + ObjectMeta: metav1.ObjectMeta{Name: "foo"}, + Spec: storage.VolumeAttachmentSpec{ + Attacher: "invalid!@#$%^&*()", + NodeName: "mynode", + }, + }, + { + // Empty node name + ObjectMeta: metav1.ObjectMeta{Name: "foo"}, + Spec: storage.VolumeAttachmentSpec{ + Attacher: "myattacher", + NodeName: "", + }, + }, + { + // No volume name + ObjectMeta: metav1.ObjectMeta{Name: "foo"}, + Spec: storage.VolumeAttachmentSpec{ + Attacher: "myattacher", + NodeName: "node", + Source: storage.VolumeAttachmentSource{ + PersistentVolumeName: nil, + }, + }, + }, + { + // Empty volume name + ObjectMeta: metav1.ObjectMeta{Name: "foo"}, + Spec: storage.VolumeAttachmentSpec{ + Attacher: "myattacher", + NodeName: "node", + Source: storage.VolumeAttachmentSource{ + PersistentVolumeName: &empty, + }, + }, + }, + { + // Too long error message + ObjectMeta: metav1.ObjectMeta{Name: "foo"}, + Spec: storage.VolumeAttachmentSpec{ + Attacher: "myattacher", + NodeName: "node", + Source: storage.VolumeAttachmentSource{ + PersistentVolumeName: &volumeName, + }, + }, + Status: storage.VolumeAttachmentStatus{ + Attached: true, + AttachmentMetadata: map[string]string{ + "foo": "bar", + }, + AttachError: &storage.VolumeError{ + Time: metav1.Time{}, + Message: "hello world", + }, + DetachError: &storage.VolumeError{ + Time: metav1.Time{}, + Message: strings.Repeat("a", maxVolumeErrorMessageSize+1), + }, + }, + }, + { + // Too long metadata + ObjectMeta: metav1.ObjectMeta{Name: "foo"}, + Spec: storage.VolumeAttachmentSpec{ + Attacher: "myattacher", + NodeName: "node", + Source: storage.VolumeAttachmentSource{ + PersistentVolumeName: &volumeName, + }, + }, + Status: storage.VolumeAttachmentStatus{ + Attached: true, + AttachmentMetadata: map[string]string{ + "foo": strings.Repeat("a", maxAttachedVolumeMetadataSize), + }, + AttachError: &storage.VolumeError{ + Time: metav1.Time{}, + Message: "hello world", + }, + DetachError: &storage.VolumeError{ + Time: metav1.Time{}, + Message: "hello world", + }, + }, + }, + } + + for _, volumeAttachment := range errorCases { + if errs := ValidateVolumeAttachment(&volumeAttachment); len(errs) == 0 { + t.Errorf("Expected failure for test: %v", volumeAttachment) + } + } +} + +func TestVolumeAttachmentUpdateValidation(t *testing.T) { + volumeName := "foo" + newVolumeName := "bar" + + old := storage.VolumeAttachment{ + ObjectMeta: metav1.ObjectMeta{Name: "foo"}, + Spec: storage.VolumeAttachmentSpec{ + Attacher: "myattacher", + Source: storage.VolumeAttachmentSource{ + PersistentVolumeName: &volumeName, + }, + NodeName: "mynode", + }, + } + successCases := []storage.VolumeAttachment{ + { + // no change + ObjectMeta: metav1.ObjectMeta{Name: "foo"}, + Spec: storage.VolumeAttachmentSpec{ + Attacher: "myattacher", + Source: storage.VolumeAttachmentSource{ + PersistentVolumeName: &volumeName, + }, + NodeName: "mynode", + }, + }, + { + // modify status + ObjectMeta: metav1.ObjectMeta{Name: "foo"}, + Spec: storage.VolumeAttachmentSpec{ + Attacher: "myattacher", + Source: storage.VolumeAttachmentSource{ + PersistentVolumeName: &volumeName, + }, + NodeName: "mynode", + }, + Status: storage.VolumeAttachmentStatus{ + Attached: true, + AttachmentMetadata: map[string]string{ + "foo": "bar", + }, + AttachError: &storage.VolumeError{ + Time: metav1.Time{}, + Message: "hello world", + }, + DetachError: &storage.VolumeError{ + Time: metav1.Time{}, + Message: "hello world", + }, + }, + }, + } + + for _, volumeAttachment := range successCases { + if errs := ValidateVolumeAttachmentUpdate(&volumeAttachment, &old); len(errs) != 0 { + t.Errorf("expected success: %v", errs) + } + } + + errorCases := []storage.VolumeAttachment{ + { + // change attacher + ObjectMeta: metav1.ObjectMeta{Name: "foo"}, + Spec: storage.VolumeAttachmentSpec{ + Attacher: "another-attacher", + Source: storage.VolumeAttachmentSource{ + PersistentVolumeName: &volumeName, + }, + NodeName: "mynode", + }, + }, + { + // change volume + ObjectMeta: metav1.ObjectMeta{Name: "foo"}, + Spec: storage.VolumeAttachmentSpec{ + Attacher: "myattacher", + Source: storage.VolumeAttachmentSource{ + PersistentVolumeName: &newVolumeName, + }, + NodeName: "mynode", + }, + }, + { + // change node + ObjectMeta: metav1.ObjectMeta{Name: "foo"}, + Spec: storage.VolumeAttachmentSpec{ + Attacher: "myattacher", + Source: storage.VolumeAttachmentSource{ + PersistentVolumeName: &volumeName, + }, + NodeName: "anothernode", + }, + }, + { + // add invalid status + ObjectMeta: metav1.ObjectMeta{Name: "foo"}, + Spec: storage.VolumeAttachmentSpec{ + Attacher: "myattacher", + Source: storage.VolumeAttachmentSource{ + PersistentVolumeName: &volumeName, + }, + NodeName: "mynode", + }, + Status: storage.VolumeAttachmentStatus{ + Attached: true, + AttachmentMetadata: map[string]string{ + "foo": "bar", + }, + AttachError: &storage.VolumeError{ + Time: metav1.Time{}, + Message: strings.Repeat("a", maxAttachedVolumeMetadataSize), + }, + DetachError: &storage.VolumeError{ + Time: metav1.Time{}, + Message: "hello world", + }, + }, + }, + } + + for _, volumeAttachment := range errorCases { + if errs := ValidateVolumeAttachmentUpdate(&volumeAttachment, &old); len(errs) == 0 { + t.Errorf("Expected failure for test: %v", volumeAttachment) + } + } +} diff --git a/pkg/registry/BUILD b/pkg/registry/BUILD index f821d9b3309..6f8ffe8abeb 100644 --- a/pkg/registry/BUILD +++ b/pkg/registry/BUILD @@ -82,6 +82,7 @@ filegroup( "//pkg/registry/settings/rest:all-srcs", "//pkg/registry/storage/rest:all-srcs", "//pkg/registry/storage/storageclass:all-srcs", + "//pkg/registry/storage/volumeattachment:all-srcs", ], tags = ["automanaged"], ) diff --git a/pkg/registry/storage/rest/BUILD b/pkg/registry/storage/rest/BUILD index 0c04e132e52..d95ee7f5c6a 100644 --- a/pkg/registry/storage/rest/BUILD +++ b/pkg/registry/storage/rest/BUILD @@ -13,6 +13,7 @@ go_library( "//pkg/api/legacyscheme:go_default_library", "//pkg/apis/storage:go_default_library", "//pkg/registry/storage/storageclass/storage:go_default_library", + "//pkg/registry/storage/volumeattachment/storage:go_default_library", "//vendor/k8s.io/api/storage/v1:go_default_library", "//vendor/k8s.io/api/storage/v1beta1:go_default_library", "//vendor/k8s.io/apiserver/pkg/registry/generic:go_default_library", diff --git a/pkg/registry/storage/rest/storage_storage.go b/pkg/registry/storage/rest/storage_storage.go index 32ee2fd9982..c8ab31e4acd 100644 --- a/pkg/registry/storage/rest/storage_storage.go +++ b/pkg/registry/storage/rest/storage_storage.go @@ -18,6 +18,7 @@ package rest import ( storageapiv1 "k8s.io/api/storage/v1" + storageapiv1alpha1 "k8s.io/api/storage/v1alpha1" storageapiv1beta1 "k8s.io/api/storage/v1beta1" "k8s.io/apiserver/pkg/registry/generic" "k8s.io/apiserver/pkg/registry/rest" @@ -26,6 +27,7 @@ import ( "k8s.io/kubernetes/pkg/api/legacyscheme" storageapi "k8s.io/kubernetes/pkg/apis/storage" storageclassstore "k8s.io/kubernetes/pkg/registry/storage/storageclass/storage" + volumeattachmentstore "k8s.io/kubernetes/pkg/registry/storage/volumeattachment/storage" ) type RESTStorageProvider struct { @@ -36,6 +38,10 @@ func (p RESTStorageProvider) NewRESTStorage(apiResourceConfigSource serverstorag // If you add a version here, be sure to add an entry in `k8s.io/kubernetes/cmd/kube-apiserver/app/aggregator.go with specific priorities. // TODO refactor the plumbing to provide the information in the APIGroupInfo + if apiResourceConfigSource.AnyResourcesForVersionEnabled(storageapiv1alpha1.SchemeGroupVersion) { + apiGroupInfo.VersionedResourcesStorageMap[storageapiv1alpha1.SchemeGroupVersion.Version] = p.v1alpha1Storage(apiResourceConfigSource, restOptionsGetter) + apiGroupInfo.GroupMeta.GroupVersion = storageapiv1alpha1.SchemeGroupVersion + } if apiResourceConfigSource.AnyResourcesForVersionEnabled(storageapiv1beta1.SchemeGroupVersion) { apiGroupInfo.VersionedResourcesStorageMap[storageapiv1beta1.SchemeGroupVersion.Version] = p.v1beta1Storage(apiResourceConfigSource, restOptionsGetter) apiGroupInfo.GroupMeta.GroupVersion = storageapiv1beta1.SchemeGroupVersion @@ -48,6 +54,19 @@ func (p RESTStorageProvider) NewRESTStorage(apiResourceConfigSource serverstorag return apiGroupInfo, true } +func (p RESTStorageProvider) v1alpha1Storage(apiResourceConfigSource serverstorage.APIResourceConfigSource, restOptionsGetter generic.RESTOptionsGetter) map[string]rest.Storage { + version := storageapiv1alpha1.SchemeGroupVersion + + storage := map[string]rest.Storage{} + + if apiResourceConfigSource.ResourceEnabled(version.WithResource("volumeattachments")) { + volumeAttachmentStorage := volumeattachmentstore.NewREST(restOptionsGetter) + storage["volumeattachments"] = volumeAttachmentStorage + } + + return storage +} + func (p RESTStorageProvider) v1beta1Storage(apiResourceConfigSource serverstorage.APIResourceConfigSource, restOptionsGetter generic.RESTOptionsGetter) map[string]rest.Storage { version := storageapiv1beta1.SchemeGroupVersion diff --git a/pkg/registry/storage/volumeattachment/BUILD b/pkg/registry/storage/volumeattachment/BUILD new file mode 100644 index 00000000000..7c014068604 --- /dev/null +++ b/pkg/registry/storage/volumeattachment/BUILD @@ -0,0 +1,49 @@ +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/volumeattachment", + visibility = ["//visibility:public"], + deps = [ + "//pkg/api/legacyscheme:go_default_library", + "//pkg/apis/storage:go_default_library", + "//pkg/apis/storage/validation:go_default_library", + "//vendor/k8s.io/apimachinery/pkg/runtime:go_default_library", + "//vendor/k8s.io/apimachinery/pkg/util/validation/field:go_default_library", + "//vendor/k8s.io/apiserver/pkg/endpoints/request:go_default_library", + "//vendor/k8s.io/apiserver/pkg/storage/names:go_default_library", + ], +) + +go_test( + name = "go_default_test", + srcs = ["strategy_test.go"], + importpath = "k8s.io/kubernetes/pkg/registry/storage/volumeattachment", + library = ":go_default_library", + deps = [ + "//pkg/apis/storage:go_default_library", + "//vendor/k8s.io/apimachinery/pkg/apis/meta/v1:go_default_library", + "//vendor/k8s.io/apiserver/pkg/endpoints/request:go_default_library", + ], +) + +filegroup( + name = "package-srcs", + srcs = glob(["**"]), + tags = ["automanaged"], + visibility = ["//visibility:private"], +) + +filegroup( + name = "all-srcs", + srcs = [ + ":package-srcs", + "//pkg/registry/storage/volumeattachment/storage:all-srcs", + ], + tags = ["automanaged"], + visibility = ["//visibility:public"], +) diff --git a/pkg/registry/storage/volumeattachment/doc.go b/pkg/registry/storage/volumeattachment/doc.go new file mode 100644 index 00000000000..91bb452354e --- /dev/null +++ b/pkg/registry/storage/volumeattachment/doc.go @@ -0,0 +1,19 @@ +/* +Copyright 2017 The Kubernetes Authors. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +// Package volumeattachment provides Registry interface and its REST +// implementation for storing volumeattachment api objects. +package volumeattachment diff --git a/pkg/registry/storage/volumeattachment/storage/BUILD b/pkg/registry/storage/volumeattachment/storage/BUILD new file mode 100644 index 00000000000..c05912d09ea --- /dev/null +++ b/pkg/registry/storage/volumeattachment/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/volumeattachment/storage", + visibility = ["//visibility:public"], + deps = [ + "//pkg/apis/storage:go_default_library", + "//pkg/registry/storage/volumeattachment:go_default_library", + "//vendor/k8s.io/apimachinery/pkg/runtime:go_default_library", + "//vendor/k8s.io/apiserver/pkg/registry/generic:go_default_library", + "//vendor/k8s.io/apiserver/pkg/registry/generic/registry:go_default_library", + ], +) + +go_test( + name = "go_default_test", + srcs = ["storage_test.go"], + importpath = "k8s.io/kubernetes/pkg/registry/storage/volumeattachment/storage", + library = ":go_default_library", + deps = [ + "//pkg/api/testapi:go_default_library", + "//pkg/apis/storage:go_default_library", + "//pkg/registry/registrytest:go_default_library", + "//vendor/k8s.io/api/storage/v1alpha1:go_default_library", + "//vendor/k8s.io/apimachinery/pkg/apis/meta/v1:go_default_library", + "//vendor/k8s.io/apimachinery/pkg/fields:go_default_library", + "//vendor/k8s.io/apimachinery/pkg/labels:go_default_library", + "//vendor/k8s.io/apimachinery/pkg/runtime:go_default_library", + "//vendor/k8s.io/apiserver/pkg/registry/generic:go_default_library", + "//vendor/k8s.io/apiserver/pkg/storage/etcd/testing: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"], +) diff --git a/pkg/registry/storage/volumeattachment/storage/storage.go b/pkg/registry/storage/volumeattachment/storage/storage.go new file mode 100644 index 00000000000..28a3208e413 --- /dev/null +++ b/pkg/registry/storage/volumeattachment/storage/storage.go @@ -0,0 +1,50 @@ +/* +Copyright 2017 The Kubernetes Authors. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package 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/volumeattachment" +) + +// REST object that will work against persistent volumes. +type REST struct { + *genericregistry.Store +} + +// NewREST returns a RESTStorage object that will work against persistent volumes. +func NewREST(optsGetter generic.RESTOptionsGetter) *REST { + store := &genericregistry.Store{ + NewFunc: func() runtime.Object { return &storageapi.VolumeAttachment{} }, + NewListFunc: func() runtime.Object { return &storageapi.VolumeAttachmentList{} }, + DefaultQualifiedResource: storageapi.Resource("volumeattachments"), + + CreateStrategy: volumeattachment.Strategy, + UpdateStrategy: volumeattachment.Strategy, + DeleteStrategy: volumeattachment.Strategy, + ReturnDeletedObject: true, + } + options := &generic.StoreOptions{RESTOptions: optsGetter} + if err := store.CompleteWithOptions(options); err != nil { + panic(err) // TODO: Propagate error up + } + + return &REST{store} +} diff --git a/pkg/registry/storage/volumeattachment/storage/storage_test.go b/pkg/registry/storage/volumeattachment/storage/storage_test.go new file mode 100644 index 00000000000..e6de8b54b3e --- /dev/null +++ b/pkg/registry/storage/volumeattachment/storage/storage_test.go @@ -0,0 +1,190 @@ +/* +Copyright 2017 The Kubernetes Authors. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package storage + +import ( + "testing" + + storageapiv1alpha1 "k8s.io/api/storage/v1alpha1" + 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" + 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: "volumeattachments", + } + volumeAttachmentStorage := NewREST(restOptions) + return volumeAttachmentStorage, server +} + +func validNewVolumeAttachment(name string) *storageapi.VolumeAttachment { + pvName := "foo" + return &storageapi.VolumeAttachment{ + ObjectMeta: metav1.ObjectMeta{ + Name: name, + }, + Spec: storageapi.VolumeAttachmentSpec{ + Attacher: "valid-attacher", + Source: storageapi.VolumeAttachmentSource{ + PersistentVolumeName: &pvName, + }, + NodeName: "valid-node", + }, + } +} + +func validChangedVolumeAttachment() *storageapi.VolumeAttachment { + return validNewVolumeAttachment("foo") +} + +func TestCreate(t *testing.T) { + if *testapi.Storage.GroupVersion() != storageapiv1alpha1.SchemeGroupVersion { + // skip the test for all versions exception v1alpha1 + return + } + + storage, server := newStorage(t) + defer server.Terminate(t) + defer storage.Store.DestroyFunc() + test := registrytest.New(t, storage.Store).ClusterScope() + volumeAttachment := validNewVolumeAttachment("foo") + volumeAttachment.ObjectMeta = metav1.ObjectMeta{GenerateName: "foo"} + pvName := "foo" + test.TestCreate( + // valid + volumeAttachment, + // invalid + &storageapi.VolumeAttachment{ + ObjectMeta: metav1.ObjectMeta{Name: "*BadName!"}, + Spec: storageapi.VolumeAttachmentSpec{ + Attacher: "invalid-attacher-!@#$%^&*()", + Source: storageapi.VolumeAttachmentSource{ + PersistentVolumeName: &pvName, + }, + NodeName: "invalid-node-!@#$%^&*()", + }, + }, + ) +} + +func TestUpdate(t *testing.T) { + if *testapi.Storage.GroupVersion() != storageapiv1alpha1.SchemeGroupVersion { + // skip the test for all versions except v1alpha1 + return + } + + storage, server := newStorage(t) + defer server.Terminate(t) + defer storage.Store.DestroyFunc() + test := registrytest.New(t, storage.Store).ClusterScope() + test.TestUpdate( + // valid + validNewVolumeAttachment("foo"), + // updateFunc + func(obj runtime.Object) runtime.Object { + object := obj.(*storageapi.VolumeAttachment) + object.Status.Attached = true + return object + }, + //invalid update + func(obj runtime.Object) runtime.Object { + object := obj.(*storageapi.VolumeAttachment) + object.Spec.Attacher = "invalid-attacher-!@#$%^&*()" + return object + }, + ) +} + +func TestDelete(t *testing.T) { + if *testapi.Storage.GroupVersion() != storageapiv1alpha1.SchemeGroupVersion { + // skip the test for all versions except v1alpha1 + return + } + + storage, server := newStorage(t) + defer server.Terminate(t) + defer storage.Store.DestroyFunc() + test := registrytest.New(t, storage.Store).ClusterScope().ReturnDeletedObject() + test.TestDelete(validNewVolumeAttachment("foo")) +} + +func TestGet(t *testing.T) { + if *testapi.Storage.GroupVersion() != storageapiv1alpha1.SchemeGroupVersion { + // skip the test for all versions except v1alpha1 + return + } + + storage, server := newStorage(t) + defer server.Terminate(t) + defer storage.Store.DestroyFunc() + test := registrytest.New(t, storage.Store).ClusterScope() + test.TestGet(validNewVolumeAttachment("foo")) +} + +func TestList(t *testing.T) { + if *testapi.Storage.GroupVersion() != storageapiv1alpha1.SchemeGroupVersion { + // skip the test for all versions except v1alpha1 + return + } + + storage, server := newStorage(t) + defer server.Terminate(t) + defer storage.Store.DestroyFunc() + test := registrytest.New(t, storage.Store).ClusterScope() + test.TestList(validNewVolumeAttachment("foo")) +} + +func TestWatch(t *testing.T) { + if *testapi.Storage.GroupVersion() != storageapiv1alpha1.SchemeGroupVersion { + // skip the test for all versions except v1alpha1 + return + } + + storage, server := newStorage(t) + defer server.Terminate(t) + defer storage.Store.DestroyFunc() + test := registrytest.New(t, storage.Store).ClusterScope() + test.TestWatch( + validNewVolumeAttachment("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/volumeattachment/strategy.go b/pkg/registry/storage/volumeattachment/strategy.go new file mode 100644 index 00000000000..e319a0d878e --- /dev/null +++ b/pkg/registry/storage/volumeattachment/strategy.go @@ -0,0 +1,73 @@ +/* +Copyright 2017 The Kubernetes Authors. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package volumeattachment + +import ( + "k8s.io/apimachinery/pkg/runtime" + "k8s.io/apimachinery/pkg/util/validation/field" + genericapirequest "k8s.io/apiserver/pkg/endpoints/request" + "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" +) + +// volumeAttachmentStrategy implements behavior for VolumeAttachment objects +type volumeAttachmentStrategy struct { + runtime.ObjectTyper + names.NameGenerator +} + +// Strategy is the default logic that applies when creating and updating +// VolumeAttachment objects via the REST API. +var Strategy = volumeAttachmentStrategy{legacyscheme.Scheme, names.SimpleNameGenerator} + +func (volumeAttachmentStrategy) NamespaceScoped() bool { + return false +} + +// ResetBeforeCreate clears the Status field which is not allowed to be set by end users on creation. +func (volumeAttachmentStrategy) PrepareForCreate(ctx genericapirequest.Context, obj runtime.Object) { +} + +func (volumeAttachmentStrategy) Validate(ctx genericapirequest.Context, obj runtime.Object) field.ErrorList { + volumeAttachment := obj.(*storage.VolumeAttachment) + return validation.ValidateVolumeAttachment(volumeAttachment) +} + +// Canonicalize normalizes the object after validation. +func (volumeAttachmentStrategy) Canonicalize(obj runtime.Object) { +} + +func (volumeAttachmentStrategy) AllowCreateOnUpdate() bool { + return false +} + +// PrepareForUpdate sets the Status fields which is not allowed to be set by an end user updating a PV +func (volumeAttachmentStrategy) PrepareForUpdate(ctx genericapirequest.Context, obj, old runtime.Object) { +} + +func (volumeAttachmentStrategy) ValidateUpdate(ctx genericapirequest.Context, obj, old runtime.Object) field.ErrorList { + newVolumeAttachmentObj := obj.(*storage.VolumeAttachment) + oldVolumeAttachmentObj := old.(*storage.VolumeAttachment) + errorList := validation.ValidateVolumeAttachment(newVolumeAttachmentObj) + return append(errorList, validation.ValidateVolumeAttachmentUpdate(newVolumeAttachmentObj, oldVolumeAttachmentObj)...) +} + +func (volumeAttachmentStrategy) AllowUnconditionalUpdate() bool { + return false +} diff --git a/pkg/registry/storage/volumeattachment/strategy_test.go b/pkg/registry/storage/volumeattachment/strategy_test.go new file mode 100644 index 00000000000..33e8985e79a --- /dev/null +++ b/pkg/registry/storage/volumeattachment/strategy_test.go @@ -0,0 +1,77 @@ +/* +Copyright 2017 The Kubernetes Authors. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package volumeattachment + +import ( + "testing" + + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + genericapirequest "k8s.io/apiserver/pkg/endpoints/request" + "k8s.io/kubernetes/pkg/apis/storage" +) + +func TestVolumeAttachmentStrategy(t *testing.T) { + ctx := genericapirequest.NewDefaultContext() + if Strategy.NamespaceScoped() { + t.Errorf("VolumeAttachment must not be namespace scoped") + } + if Strategy.AllowCreateOnUpdate() { + t.Errorf("VolumeAttachment should not allow create on update") + } + + pvName := "name" + volumeAttachment := &storage.VolumeAttachment{ + ObjectMeta: metav1.ObjectMeta{ + Name: "valid-attachment", + }, + Spec: storage.VolumeAttachmentSpec{ + Attacher: "valid-attacher", + Source: storage.VolumeAttachmentSource{ + PersistentVolumeName: &pvName, + }, + NodeName: "valid-node", + }, + } + + Strategy.PrepareForCreate(ctx, volumeAttachment) + + errs := Strategy.Validate(ctx, volumeAttachment) + if len(errs) != 0 { + t.Errorf("unexpected error validating %v", errs) + } + + newVolumeAttachment := &storage.VolumeAttachment{ + ObjectMeta: metav1.ObjectMeta{ + Name: "valid-attachment-2", + }, + Spec: storage.VolumeAttachmentSpec{ + Attacher: "valid-attacher-2", + Source: storage.VolumeAttachmentSource{ + PersistentVolumeName: &pvName, + }, + NodeName: "valid-node-2", + }, + } + + Strategy.PrepareForUpdate(ctx, newVolumeAttachment, volumeAttachment) + + errs = Strategy.ValidateUpdate(ctx, newVolumeAttachment, volumeAttachment) + if len(errs) == 0 { + t.Errorf("Expected a validation error") + } + +} diff --git a/staging/src/k8s.io/api/storage/v1alpha1/doc.go b/staging/src/k8s.io/api/storage/v1alpha1/doc.go new file mode 100644 index 00000000000..aa94aff7fbb --- /dev/null +++ b/staging/src/k8s.io/api/storage/v1alpha1/doc.go @@ -0,0 +1,20 @@ +/* +Copyright 2017 The Kubernetes Authors. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +// +k8s:deepcopy-gen=package,register +// +groupName=storage.k8s.io +// +k8s:openapi-gen=true +package v1alpha1 // import "k8s.io/api/storage/v1alpha1" diff --git a/staging/src/k8s.io/api/storage/v1alpha1/register.go b/staging/src/k8s.io/api/storage/v1alpha1/register.go new file mode 100644 index 00000000000..7b81ee49c2b --- /dev/null +++ b/staging/src/k8s.io/api/storage/v1alpha1/register.go @@ -0,0 +1,50 @@ +/* +Copyright 2017 The Kubernetes Authors. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package v1alpha1 + +import ( + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/runtime" + "k8s.io/apimachinery/pkg/runtime/schema" +) + +// GroupName is the group name use in this package +const GroupName = "storage.k8s.io" + +// SchemeGroupVersion is group version used to register these objects +var SchemeGroupVersion = schema.GroupVersion{Group: GroupName, Version: "v1alpha1"} + +// Resource takes an unqualified resource and returns a Group qualified GroupResource +func Resource(resource string) schema.GroupResource { + return SchemeGroupVersion.WithResource(resource).GroupResource() +} + +var ( + SchemeBuilder = runtime.NewSchemeBuilder(addKnownTypes) + AddToScheme = SchemeBuilder.AddToScheme +) + +// Adds the list of known types to the given scheme. +func addKnownTypes(scheme *runtime.Scheme) error { + scheme.AddKnownTypes(SchemeGroupVersion, + &VolumeAttachment{}, + &VolumeAttachmentList{}, + ) + + metav1.AddToGroupVersion(scheme, SchemeGroupVersion) + return nil +} diff --git a/staging/src/k8s.io/api/storage/v1alpha1/types.go b/staging/src/k8s.io/api/storage/v1alpha1/types.go new file mode 100644 index 00000000000..964bb5f7b17 --- /dev/null +++ b/staging/src/k8s.io/api/storage/v1alpha1/types.go @@ -0,0 +1,126 @@ +/* +Copyright 2017 The Kubernetes Authors. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package v1alpha1 + +import metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + +// +genclient +// +genclient:nonNamespaced +// +k8s:deepcopy-gen:interfaces=k8s.io/apimachinery/pkg/runtime.Object + +// VolumeAttachment captures the intent to attach or detach the specified volume +// to/from the specified node. +// +// VolumeAttachment objects are non-namespaced. +type VolumeAttachment struct { + metav1.TypeMeta `json:",inline"` + + // Standard object metadata. + // More info: https://git.k8s.io/community/contributors/devel/api-conventions.md#metadata + // +optional + metav1.ObjectMeta `json:"metadata,omitempty" protobuf:"bytes,1,opt,name=metadata"` + + // Specification of the desired attach/detach volume behavior. + // Populated by the Kubernetes system. + Spec VolumeAttachmentSpec `json:"spec" protobuf:"bytes,2,opt,name=spec"` + + // Status of the VolumeAttachment request. + // Populated by the entity completing the attach or detach + // operation, i.e. the external-attacher. + // +optional + Status VolumeAttachmentStatus `json:"status,omitempty" protobuf:"bytes,3,opt,name=status"` +} + +// +k8s:deepcopy-gen:interfaces=k8s.io/apimachinery/pkg/runtime.Object + +// VolumeAttachmentList is a collection of VolumeAttachment objects. +type VolumeAttachmentList 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 VolumeAttachments + Items []VolumeAttachment `json:"items" protobuf:"bytes,2,rep,name=items"` +} + +// VolumeAttachmentSpec is the specification of a VolumeAttachment request. +type VolumeAttachmentSpec struct { + // Attacher indicates the name of the volume driver that MUST handle this + // request. This is the name returned by GetPluginName(). + Attacher string `json:"attacher" protobuf:"bytes,1,opt,name=attacher"` + + // Source represents the volume that should be attached. + Source VolumeAttachmentSource `json:"source" protobuf:"bytes,2,opt,name=source"` + + // The node that the volume should be attached to. + NodeName string `json:"nodeName" protobuf:"bytes,3,opt,name=nodeName"` +} + +// VolumeAttachmentSource represents a volume that should be attached. +// Right now only PersistenVolumes can be attached via external attacher, +// in future we may allow also inline volumes in pods. +// Exactly one member can be set. +type VolumeAttachmentSource struct { + // Name of the persistent volume to attach. + // +optional + PersistentVolumeName *string `json:"persistentVolumeName,omitempty" protobuf:"bytes,1,opt,name=persistentVolumeName"` + + // Placeholder for *VolumeSource to accommodate inline volumes in pods. +} + +// VolumeAttachmentStatus is the status of a VolumeAttachment request. +type VolumeAttachmentStatus struct { + // Indicates the volume is successfully attached. + // This field must only be set by the entity completing the attach + // operation, i.e. the external-attacher. + Attached bool `json:"attached" protobuf:"varint,1,opt,name=attached"` + + // Upon successful attach, this field is populated with any + // information returned by the attach operation that must be passed + // into subsequent WaitForAttach or Mount calls. + // This field must only be set by the entity completing the attach + // operation, i.e. the external-attacher. + // +optional + AttachmentMetadata map[string]string `json:"attachmentMetadata,omitempty" protobuf:"bytes,2,rep,name=attachmentMetadata"` + + // The last error encountered during attach operation, if any. + // This field must only be set by the entity completing the attach + // operation, i.e. the external-attacher. + // +optional + AttachError *VolumeError `json:"attachError,omitempty" protobuf:"bytes,3,opt,name=attachError,casttype=VolumeError"` + + // The last error encountered during detach operation, if any. + // This field must only be set by the entity completing the detach + // operation, i.e. the external-attacher. + // +optional + DetachError *VolumeError `json:"detachError,omitempty" protobuf:"bytes,4,opt,name=detachError,casttype=VolumeError"` +} + +// VolumeError captures an error encountered during a volume operation. +type VolumeError struct { + // Time the error was encountered. + // +optional + Time metav1.Time `json:"time,omitempty" protobuf:"bytes,1,opt,name=time"` + + // String detailing the error encountered during Attach or Detach operation. + // This string maybe logged, so it should not contain sensitive + // information. + // +optional + Message string `json:"message,omitempty" protobuf:"bytes,2,opt,name=message"` +} diff --git a/test/integration/etcd/etcd_storage_path_test.go b/test/integration/etcd/etcd_storage_path_test.go index 95f865568d4..9595d32b02b 100644 --- a/test/integration/etcd/etcd_storage_path_test.go +++ b/test/integration/etcd/etcd_storage_path_test.go @@ -289,6 +289,13 @@ var etcdStorageData = map[schema.GroupVersionResource]struct { }, // -- + // k8s.io/kubernetes/pkg/apis/storage/v1alpha1 + gvr("storage.k8s.io", "v1alpha1", "volumeattachments"): { + stub: `{"metadata": {"name": "va1"}, "spec": {"attacher": "gce", "nodeName": "localhost", "source": {"persistentVolumeName": "pv1"}}}`, + expectedEtcdPath: "/registry/volumeattachments/va1", + }, + // -- + // k8s.io/kubernetes/pkg/apis/storage/v1beta1 gvr("storage.k8s.io", "v1beta1", "storageclasses"): { stub: `{"metadata": {"name": "sc1"}, "provisioner": "aws"}`,