diff --git a/pkg/api/defaulting_test.go b/pkg/api/defaulting_test.go index b440d68a359..ad92b12f780 100644 --- a/pkg/api/defaulting_test.go +++ b/pkg/api/defaulting_test.go @@ -138,6 +138,10 @@ func TestDefaulting(t *testing.T) { {Group: "admissionregistration.k8s.io", Version: "v1alpha1", Kind: "ExternalAdmissionHookConfigurationList"}: {}, {Group: "networking.k8s.io", Version: "v1", Kind: "NetworkPolicy"}: {}, {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: "v1", Kind: "StorageClass"}: {}, + {Group: "storage.k8s.io", Version: "v1", Kind: "StorageClassList"}: {}, } f := fuzz.New().NilChance(.5).NumElements(1, 1).RandSource(rand.NewSource(1)) diff --git a/pkg/api/testing/fuzzer.go b/pkg/api/testing/fuzzer.go index cb1d85ecb8a..844b9f79d2b 100644 --- a/pkg/api/testing/fuzzer.go +++ b/pkg/api/testing/fuzzer.go @@ -40,6 +40,7 @@ import ( extensionsv1beta1 "k8s.io/kubernetes/pkg/apis/extensions/v1beta1" policyfuzzer "k8s.io/kubernetes/pkg/apis/policy/fuzzer" rbacfuzzer "k8s.io/kubernetes/pkg/apis/rbac/fuzzer" + storagefuzzer "k8s.io/kubernetes/pkg/apis/storage/fuzzer" ) // overrideGenericFuncs override some generic fuzzer funcs from k8s.io/apiserver in order to have more realistic @@ -100,4 +101,5 @@ var FuzzerFuncs = fuzzer.MergeFuzzerFuncs( policyfuzzer.Funcs, certificatesfuzzer.Funcs, admissionregistrationfuzzer.Funcs, + storagefuzzer.Funcs, ) diff --git a/pkg/apis/storage/fuzzer/fuzzer.go b/pkg/apis/storage/fuzzer/fuzzer.go index a0a68bdf9ff..e8a36514947 100644 --- a/pkg/apis/storage/fuzzer/fuzzer.go +++ b/pkg/apis/storage/fuzzer/fuzzer.go @@ -17,10 +17,20 @@ limitations under the License. package fuzzer import ( + fuzz "github.com/google/gofuzz" + runtimeserializer "k8s.io/apimachinery/pkg/runtime/serializer" + "k8s.io/kubernetes/pkg/api" + "k8s.io/kubernetes/pkg/apis/storage" ) // Funcs returns the fuzzer functions for the storage api group. var Funcs = func(codecs runtimeserializer.CodecFactory) []interface{} { - return []interface{}{} + return []interface{}{ + func(obj *storage.StorageClass, c fuzz.Continue) { + c.FuzzNoCustom(obj) // fuzz self without calling this function again + reclamationPolicies := []api.PersistentVolumeReclaimPolicy{api.PersistentVolumeReclaimDelete, api.PersistentVolumeReclaimRetain} + obj.ReclaimPolicy = &reclamationPolicies[c.Rand.Intn(len(reclamationPolicies))] + }, + } } diff --git a/pkg/apis/storage/types.go b/pkg/apis/storage/types.go index ded08718782..26955afde3b 100644 --- a/pkg/apis/storage/types.go +++ b/pkg/apis/storage/types.go @@ -16,7 +16,10 @@ limitations under the License. package storage -import metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" +import ( + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/kubernetes/pkg/api" +) // +genclient // +genclient:nonNamespaced @@ -46,6 +49,11 @@ type StorageClass struct { // 512, with a cumulative max size of 256K // +optional Parameters map[string]string + + // reclaimPolicy is the reclaim policy that dynamically provisioned + // PersistentVolumes of this storage class are created with + // +optional + ReclaimPolicy *api.PersistentVolumeReclaimPolicy } // +k8s:deepcopy-gen:interfaces=k8s.io/apimachinery/pkg/runtime.Object diff --git a/pkg/apis/storage/v1/defaults.go b/pkg/apis/storage/v1/defaults.go new file mode 100644 index 00000000000..2e7c51c632e --- /dev/null +++ b/pkg/apis/storage/v1/defaults.go @@ -0,0 +1,34 @@ +/* +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 v1 + +import ( + "k8s.io/api/core/v1" + storagev1 "k8s.io/api/storage/v1" + "k8s.io/apimachinery/pkg/runtime" +) + +func addDefaultingFuncs(scheme *runtime.Scheme) error { + return RegisterDefaults(scheme) +} + +func SetDefaults_StorageClass(obj *storagev1.StorageClass) { + if obj.ReclaimPolicy == nil { + obj.ReclaimPolicy = new(v1.PersistentVolumeReclaimPolicy) + *obj.ReclaimPolicy = v1.PersistentVolumeReclaimDelete + } +} diff --git a/pkg/apis/storage/v1/register.go b/pkg/apis/storage/v1/register.go index bda48325c97..f56f75d58b4 100644 --- a/pkg/apis/storage/v1/register.go +++ b/pkg/apis/storage/v1/register.go @@ -41,5 +41,5 @@ func init() { // We only register manually written functions here. The registration of the // generated functions takes place in the generated files. The separation // makes the code compile even when the generated files are missing. - localSchemeBuilder.Register(RegisterDefaults) + localSchemeBuilder.Register(addDefaultingFuncs) } diff --git a/pkg/apis/storage/v1beta1/defaults.go b/pkg/apis/storage/v1beta1/defaults.go new file mode 100644 index 00000000000..e50599bf273 --- /dev/null +++ b/pkg/apis/storage/v1beta1/defaults.go @@ -0,0 +1,34 @@ +/* +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 v1beta1 + +import ( + "k8s.io/api/core/v1" + storagev1beta1 "k8s.io/api/storage/v1beta1" + "k8s.io/apimachinery/pkg/runtime" +) + +func addDefaultingFuncs(scheme *runtime.Scheme) error { + return RegisterDefaults(scheme) +} + +func SetDefaults_StorageClass(obj *storagev1beta1.StorageClass) { + if obj.ReclaimPolicy == nil { + obj.ReclaimPolicy = new(v1.PersistentVolumeReclaimPolicy) + *obj.ReclaimPolicy = v1.PersistentVolumeReclaimDelete + } +} diff --git a/pkg/apis/storage/v1beta1/register.go b/pkg/apis/storage/v1beta1/register.go index 9dbb95ec2b9..961f75c0037 100644 --- a/pkg/apis/storage/v1beta1/register.go +++ b/pkg/apis/storage/v1beta1/register.go @@ -41,5 +41,5 @@ func init() { // We only register manually written functions here. The registration of the // generated functions takes place in the generated files. The separation // makes the code compile even when the generated files are missing. - localSchemeBuilder.Register(RegisterDefaults) + localSchemeBuilder.Register(addDefaultingFuncs) } diff --git a/pkg/apis/storage/validation/validation.go b/pkg/apis/storage/validation/validation.go index 029694f0523..84dc042faec 100644 --- a/pkg/apis/storage/validation/validation.go +++ b/pkg/apis/storage/validation/validation.go @@ -20,8 +20,10 @@ import ( "reflect" "strings" + "k8s.io/apimachinery/pkg/util/sets" "k8s.io/apimachinery/pkg/util/validation" "k8s.io/apimachinery/pkg/util/validation/field" + "k8s.io/kubernetes/pkg/api" apivalidation "k8s.io/kubernetes/pkg/api/validation" "k8s.io/kubernetes/pkg/apis/storage" ) @@ -31,6 +33,7 @@ func ValidateStorageClass(storageClass *storage.StorageClass) field.ErrorList { allErrs := apivalidation.ValidateObjectMeta(&storageClass.ObjectMeta, false, apivalidation.ValidateClassName, field.NewPath("metadata")) allErrs = append(allErrs, validateProvisioner(storageClass.Provisioner, field.NewPath("provisioner"))...) allErrs = append(allErrs, validateParameters(storageClass.Parameters, field.NewPath("parameters"))...) + allErrs = append(allErrs, validateReclaimPolicy(storageClass.ReclaimPolicy, field.NewPath("reclaimPolicy"))...) return allErrs } @@ -45,6 +48,10 @@ func ValidateStorageClassUpdate(storageClass, oldStorageClass *storage.StorageCl if storageClass.Provisioner != oldStorageClass.Provisioner { allErrs = append(allErrs, field.Forbidden(field.NewPath("provisioner"), "updates to provisioner are forbidden.")) } + + if *storageClass.ReclaimPolicy != *oldStorageClass.ReclaimPolicy { + allErrs = append(allErrs, field.Forbidden(field.NewPath("reclaimPolicy"), "updates to reclaimPolicy are forbidden.")) + } return allErrs } @@ -87,3 +94,17 @@ func validateParameters(params map[string]string, fldPath *field.Path) field.Err } return allErrs } + +var supportedReclaimPolicy = sets.NewString(string(api.PersistentVolumeReclaimDelete), string(api.PersistentVolumeReclaimRetain)) + +// validateReclaimPolicy tests that the reclaim policy is one of the supported. It is up to the volume plugin to reject +// provisioning for storage classes with impossible reclaim policies, e.g. EBS is not Recyclable +func validateReclaimPolicy(reclaimPolicy *api.PersistentVolumeReclaimPolicy, fldPath *field.Path) field.ErrorList { + allErrs := field.ErrorList{} + if len(string(*reclaimPolicy)) > 0 { + if !supportedReclaimPolicy.Has(string(*reclaimPolicy)) { + allErrs = append(allErrs, field.NotSupported(fldPath, reclaimPolicy, supportedReclaimPolicy.List())) + } + } + return allErrs +} diff --git a/pkg/apis/storage/validation/validation_test.go b/pkg/apis/storage/validation/validation_test.go index d113ea3922b..07b020b52eb 100644 --- a/pkg/apis/storage/validation/validation_test.go +++ b/pkg/apis/storage/validation/validation_test.go @@ -21,21 +21,27 @@ import ( "testing" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/kubernetes/pkg/api" "k8s.io/kubernetes/pkg/apis/storage" ) func TestValidateStorageClass(t *testing.T) { + deleteReclaimPolicy := api.PersistentVolumeReclaimPolicy("Delete") + retainReclaimPolicy := api.PersistentVolumeReclaimPolicy("Retain") + recycleReclaimPolicy := api.PersistentVolumeReclaimPolicy("Recycle") successCases := []storage.StorageClass{ { // empty parameters - ObjectMeta: metav1.ObjectMeta{Name: "foo"}, - Provisioner: "kubernetes.io/foo-provisioner", - Parameters: map[string]string{}, + ObjectMeta: metav1.ObjectMeta{Name: "foo"}, + Provisioner: "kubernetes.io/foo-provisioner", + Parameters: map[string]string{}, + ReclaimPolicy: &deleteReclaimPolicy, }, { // nil parameters - ObjectMeta: metav1.ObjectMeta{Name: "foo"}, - Provisioner: "kubernetes.io/foo-provisioner", + ObjectMeta: metav1.ObjectMeta{Name: "foo"}, + Provisioner: "kubernetes.io/foo-provisioner", + ReclaimPolicy: &deleteReclaimPolicy, }, { // some parameters @@ -46,6 +52,13 @@ func TestValidateStorageClass(t *testing.T) { "foo-parameter": "free-form-string", "foo-parameter2": "{\"embedded\": \"json\", \"with\": {\"structures\":\"inside\"}}", }, + ReclaimPolicy: &deleteReclaimPolicy, + }, + { + // retain reclaimPolicy + ObjectMeta: metav1.ObjectMeta{Name: "foo"}, + Provisioner: "kubernetes.io/foo-provisioner", + ReclaimPolicy: &retainReclaimPolicy, }, } @@ -68,12 +81,14 @@ func TestValidateStorageClass(t *testing.T) { errorCases := map[string]storage.StorageClass{ "namespace is present": { - ObjectMeta: metav1.ObjectMeta{Name: "foo", Namespace: "bar"}, - Provisioner: "kubernetes.io/foo-provisioner", + ObjectMeta: metav1.ObjectMeta{Name: "foo", Namespace: "bar"}, + Provisioner: "kubernetes.io/foo-provisioner", + ReclaimPolicy: &deleteReclaimPolicy, }, "invalid provisioner": { - ObjectMeta: metav1.ObjectMeta{Name: "foo"}, - Provisioner: "kubernetes.io/invalid/provisioner", + ObjectMeta: metav1.ObjectMeta{Name: "foo"}, + Provisioner: "kubernetes.io/invalid/provisioner", + ReclaimPolicy: &deleteReclaimPolicy, }, "invalid empty parameter name": { ObjectMeta: metav1.ObjectMeta{Name: "foo"}, @@ -81,15 +96,23 @@ func TestValidateStorageClass(t *testing.T) { Parameters: map[string]string{ "": "value", }, + ReclaimPolicy: &deleteReclaimPolicy, }, "provisioner: Required value": { - ObjectMeta: metav1.ObjectMeta{Name: "foo"}, - Provisioner: "", + ObjectMeta: metav1.ObjectMeta{Name: "foo"}, + Provisioner: "", + ReclaimPolicy: &deleteReclaimPolicy, }, "too long parameters": { - ObjectMeta: metav1.ObjectMeta{Name: "foo"}, - Provisioner: "kubernetes.io/foo", - Parameters: longParameters, + ObjectMeta: metav1.ObjectMeta{Name: "foo"}, + Provisioner: "kubernetes.io/foo", + Parameters: longParameters, + ReclaimPolicy: &deleteReclaimPolicy, + }, + "invalid reclaimpolicy": { + ObjectMeta: metav1.ObjectMeta{Name: "foo"}, + Provisioner: "kubernetes.io/foo", + ReclaimPolicy: &recycleReclaimPolicy, }, } diff --git a/pkg/controller/volume/persistentvolume/provision_test.go b/pkg/controller/volume/persistentvolume/provision_test.go index f881a920c90..8465136074c 100644 --- a/pkg/controller/volume/persistentvolume/provision_test.go +++ b/pkg/controller/volume/persistentvolume/provision_test.go @@ -31,6 +31,7 @@ var class1Parameters = map[string]string{ var class2Parameters = map[string]string{ "param2": "value2", } +var deleteReclaimPolicy = v1.PersistentVolumeReclaimDelete var storageClasses = []*storage.StorageClass{ { TypeMeta: metav1.TypeMeta{ @@ -41,8 +42,9 @@ var storageClasses = []*storage.StorageClass{ Name: "gold", }, - Provisioner: mockPluginName, - Parameters: class1Parameters, + Provisioner: mockPluginName, + Parameters: class1Parameters, + ReclaimPolicy: &deleteReclaimPolicy, }, { TypeMeta: metav1.TypeMeta{ @@ -51,8 +53,9 @@ var storageClasses = []*storage.StorageClass{ ObjectMeta: metav1.ObjectMeta{ Name: "silver", }, - Provisioner: mockPluginName, - Parameters: class2Parameters, + Provisioner: mockPluginName, + Parameters: class2Parameters, + ReclaimPolicy: &deleteReclaimPolicy, }, { TypeMeta: metav1.TypeMeta{ @@ -61,8 +64,9 @@ var storageClasses = []*storage.StorageClass{ ObjectMeta: metav1.ObjectMeta{ Name: "external", }, - Provisioner: "vendor.com/my-volume", - Parameters: class1Parameters, + Provisioner: "vendor.com/my-volume", + Parameters: class1Parameters, + ReclaimPolicy: &deleteReclaimPolicy, }, { TypeMeta: metav1.TypeMeta{ @@ -71,8 +75,9 @@ var storageClasses = []*storage.StorageClass{ ObjectMeta: metav1.ObjectMeta{ Name: "unknown-internal", }, - Provisioner: "kubernetes.io/unknown", - Parameters: class1Parameters, + Provisioner: "kubernetes.io/unknown", + Parameters: class1Parameters, + ReclaimPolicy: &deleteReclaimPolicy, }, } diff --git a/pkg/controller/volume/persistentvolume/pv_controller.go b/pkg/controller/volume/persistentvolume/pv_controller.go index e516c9f9df9..16ad055d83a 100644 --- a/pkg/controller/volume/persistentvolume/pv_controller.go +++ b/pkg/controller/volume/persistentvolume/pv_controller.go @@ -1309,7 +1309,7 @@ func (ctrl *PersistentVolumeController) provisionClaimOperation(claimObj interfa tags[CloudVolumeCreatedForVolumeNameTag] = pvName options := vol.VolumeOptions{ - PersistentVolumeReclaimPolicy: v1.PersistentVolumeReclaimDelete, + PersistentVolumeReclaimPolicy: *storageClass.ReclaimPolicy, CloudTags: &tags, ClusterName: ctrl.clusterName, PVName: pvName, diff --git a/pkg/registry/storage/storageclass/storage/storage_test.go b/pkg/registry/storage/storageclass/storage/storage_test.go index e6f8f37f35d..f9858b9a2bd 100644 --- a/pkg/registry/storage/storageclass/storage/storage_test.go +++ b/pkg/registry/storage/storageclass/storage/storage_test.go @@ -25,6 +25,7 @@ import ( "k8s.io/apimachinery/pkg/runtime" "k8s.io/apiserver/pkg/registry/generic" etcdtesting "k8s.io/apiserver/pkg/storage/etcd/testing" + "k8s.io/kubernetes/pkg/api" storageapi "k8s.io/kubernetes/pkg/apis/storage" "k8s.io/kubernetes/pkg/registry/registrytest" ) @@ -42,6 +43,7 @@ func newStorage(t *testing.T) (*REST, *etcdtesting.EtcdTestServer) { } func validNewStorageClass(name string) *storageapi.StorageClass { + deleteReclaimPolicy := api.PersistentVolumeReclaimDelete return &storageapi.StorageClass{ ObjectMeta: metav1.ObjectMeta{ Name: name, @@ -50,6 +52,7 @@ func validNewStorageClass(name string) *storageapi.StorageClass { Parameters: map[string]string{ "foo": "bar", }, + ReclaimPolicy: &deleteReclaimPolicy, } } @@ -64,12 +67,14 @@ func TestCreate(t *testing.T) { test := registrytest.New(t, storage.Store).ClusterScope() storageClass := validNewStorageClass("foo") storageClass.ObjectMeta = metav1.ObjectMeta{GenerateName: "foo"} + deleteReclaimPolicy := api.PersistentVolumeReclaimDelete test.TestCreate( // valid storageClass, // invalid &storageapi.StorageClass{ - ObjectMeta: metav1.ObjectMeta{Name: "*BadName!"}, + ObjectMeta: metav1.ObjectMeta{Name: "*BadName!"}, + ReclaimPolicy: &deleteReclaimPolicy, }, ) } diff --git a/pkg/registry/storage/storageclass/strategy_test.go b/pkg/registry/storage/storageclass/strategy_test.go index 2bcbc21b944..da57f0460c9 100644 --- a/pkg/registry/storage/storageclass/strategy_test.go +++ b/pkg/registry/storage/storageclass/strategy_test.go @@ -21,6 +21,7 @@ import ( metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" genericapirequest "k8s.io/apiserver/pkg/endpoints/request" + "k8s.io/kubernetes/pkg/api" "k8s.io/kubernetes/pkg/apis/storage" ) @@ -33,6 +34,7 @@ func TestStorageClassStrategy(t *testing.T) { t.Errorf("StorageClass should not allow create on update") } + deleteReclaimPolicy := api.PersistentVolumeReclaimDelete storageClass := &storage.StorageClass{ ObjectMeta: metav1.ObjectMeta{ Name: "valid-class", @@ -41,6 +43,7 @@ func TestStorageClassStrategy(t *testing.T) { Parameters: map[string]string{ "foo": "bar", }, + ReclaimPolicy: &deleteReclaimPolicy, } Strategy.PrepareForCreate(ctx, storageClass) @@ -59,6 +62,7 @@ func TestStorageClassStrategy(t *testing.T) { Parameters: map[string]string{ "foo": "bar", }, + ReclaimPolicy: &deleteReclaimPolicy, } Strategy.PrepareForUpdate(ctx, newStorageClass, storageClass) diff --git a/staging/src/k8s.io/api/storage/v1/types.go b/staging/src/k8s.io/api/storage/v1/types.go index caa71907108..02b17795bb0 100644 --- a/staging/src/k8s.io/api/storage/v1/types.go +++ b/staging/src/k8s.io/api/storage/v1/types.go @@ -17,6 +17,7 @@ limitations under the License. package v1 import ( + "k8s.io/api/core/v1" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" ) @@ -43,6 +44,11 @@ type StorageClass struct { // create volumes of this storage class. // +optional Parameters map[string]string `json:"parameters,omitempty" protobuf:"bytes,3,rep,name=parameters"` + + // Dynamically provisioned PersistentVolumes of this storage class are + // created with this reclaimPolicy. Defaults to Delete. + // +optional + ReclaimPolicy *v1.PersistentVolumeReclaimPolicy `json:"reclaimPolicy,omitempty" protobuf:"bytes,4,opt,name=reclaimPolicy,casttype=k8s.io/api/core/v1.PersistentVolumeReclaimPolicy"` } // +k8s:deepcopy-gen:interfaces=k8s.io/apimachinery/pkg/runtime.Object diff --git a/staging/src/k8s.io/api/storage/v1beta1/types.go b/staging/src/k8s.io/api/storage/v1beta1/types.go index 7a15aa0f2ea..aa828172a44 100644 --- a/staging/src/k8s.io/api/storage/v1beta1/types.go +++ b/staging/src/k8s.io/api/storage/v1beta1/types.go @@ -17,6 +17,7 @@ limitations under the License. package v1beta1 import ( + "k8s.io/api/core/v1" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" ) @@ -43,6 +44,11 @@ type StorageClass struct { // create volumes of this storage class. // +optional Parameters map[string]string `json:"parameters,omitempty" protobuf:"bytes,3,rep,name=parameters"` + + // Dynamically provisioned PersistentVolumes of this storage class are + // created with this reclaimPolicy. Defaults to Delete. + // +optional + ReclaimPolicy *v1.PersistentVolumeReclaimPolicy `json:"reclaimPolicy,omitempty" protobuf:"bytes,4,opt,name=reclaimPolicy,casttype=k8s.io/api/core/v1.PersistentVolumeReclaimPolicy"` } // +k8s:deepcopy-gen:interfaces=k8s.io/apimachinery/pkg/runtime.Object