diff --git a/cmd/kube-apiserver/app/aggregator.go b/cmd/kube-apiserver/app/aggregator.go index 14af4103ea5..f4a1bcc5a89 100644 --- a/cmd/kube-apiserver/app/aggregator.go +++ b/cmd/kube-apiserver/app/aggregator.go @@ -275,6 +275,7 @@ var apiVersionPriorities = map[schema.GroupVersion]priority{ {Group: "discovery.k8s.io", Version: "v1beta1"}: {group: 16200, version: 12}, {Group: "discovery.k8s.io", Version: "v1alpha1"}: {group: 16200, version: 9}, {Group: "flowcontrol.apiserver.k8s.io", Version: "v1alpha1"}: {group: 16100, version: 9}, + {Group: "internal.apiserver.k8s.io", Version: "v1alpha1"}: {group: 16000, version: 9}, // Append a new group to the end of the list if unsure. // You can use min(existing group)-100 as the initial value for a group. // Version can be set to 9 (to have space around) for a new group. diff --git a/pkg/apis/apiserverinternal/install/install.go b/pkg/apis/apiserverinternal/install/install.go index cf82ccfe730..9a3905644e0 100644 --- a/pkg/apis/apiserverinternal/install/install.go +++ b/pkg/apis/apiserverinternal/install/install.go @@ -21,10 +21,15 @@ package install import ( "k8s.io/apimachinery/pkg/runtime" utilruntime "k8s.io/apimachinery/pkg/util/runtime" + "k8s.io/kubernetes/pkg/api/legacyscheme" "k8s.io/kubernetes/pkg/apis/apiserverinternal" "k8s.io/kubernetes/pkg/apis/apiserverinternal/v1alpha1" ) +func init() { + Install(legacyscheme.Scheme) +} + // Install registers the API group and adds types to a scheme func Install(scheme *runtime.Scheme) { utilruntime.Must(apiserverinternal.AddToScheme(scheme)) diff --git a/pkg/apis/apiserverinternal/validation/BUILD b/pkg/apis/apiserverinternal/validation/BUILD index b8b4a51f1d7..f7e6cef73ab 100644 --- a/pkg/apis/apiserverinternal/validation/BUILD +++ b/pkg/apis/apiserverinternal/validation/BUILD @@ -7,6 +7,7 @@ go_library( visibility = ["//visibility:public"], deps = [ "//pkg/apis/apiserverinternal:go_default_library", + "//pkg/apis/core/validation:go_default_library", "//staging/src/k8s.io/apimachinery/pkg/api/validation:go_default_library", "//staging/src/k8s.io/apimachinery/pkg/util/validation:go_default_library", "//staging/src/k8s.io/apimachinery/pkg/util/validation/field:go_default_library", diff --git a/pkg/apis/apiserverinternal/validation/validation.go b/pkg/apis/apiserverinternal/validation/validation.go index b045d24ba70..6f2e73c91fe 100644 --- a/pkg/apis/apiserverinternal/validation/validation.go +++ b/pkg/apis/apiserverinternal/validation/validation.go @@ -25,10 +25,42 @@ import ( utilvalidation "k8s.io/apimachinery/pkg/util/validation" "k8s.io/apimachinery/pkg/util/validation/field" "k8s.io/kubernetes/pkg/apis/apiserverinternal" + apivalidation "k8s.io/kubernetes/pkg/apis/core/validation" ) // ValidateStorageVersion validate the storage version object. func ValidateStorageVersion(sv *apiserverinternal.StorageVersion) field.ErrorList { + var allErrs field.ErrorList + allErrs = append(allErrs, apivalidation.ValidateObjectMeta(&sv.ObjectMeta, false, ValidateStorageVersionName, field.NewPath("metadata"))...) + allErrs = append(allErrs, validateStorageVersionStatus(sv.Status, field.NewPath("status"))...) + return allErrs +} + +// ValidateStorageVersionName is a ValidateNameFunc for storage version names +func ValidateStorageVersionName(name string, prefix bool) []string { + var allErrs []string + idx := strings.LastIndex(name, ".") + if idx < 0 { + allErrs = append(allErrs, "name must be in the form of .") + } else { + for _, msg := range utilvalidation.IsDNS1123Subdomain(name[:idx]) { + allErrs = append(allErrs, "the group segment "+msg) + } + for _, msg := range utilvalidation.IsDNS1035Label(name[idx+1:]) { + allErrs = append(allErrs, "the resource segment "+msg) + } + } + return allErrs +} + +// ValidateStorageVersionUpdate tests if an update to a StorageVersion is valid. +func ValidateStorageVersionUpdate(sv, oldSV *apiserverinternal.StorageVersion) field.ErrorList { + // no error since StorageVersionSpec is an empty spec + return field.ErrorList{} +} + +// ValidateStorageVersionStatusUpdate tests if an update to a StorageVersionStatus is valid. +func ValidateStorageVersionStatusUpdate(sv, oldSV *apiserverinternal.StorageVersion) field.ErrorList { var allErrs field.ErrorList allErrs = append(allErrs, validateStorageVersionStatus(sv.Status, field.NewPath("status"))...) return allErrs diff --git a/pkg/apis/apiserverinternal/validation/validation_test.go b/pkg/apis/apiserverinternal/validation/validation_test.go index c349d1828cf..87b8cd7fe80 100644 --- a/pkg/apis/apiserverinternal/validation/validation_test.go +++ b/pkg/apis/apiserverinternal/validation/validation_test.go @@ -284,3 +284,66 @@ func TestValidateStorageVersionCondition(t *testing.T) { } } } + +func TestValidateStorageVersionName(t *testing.T) { + cases := []struct { + name string + expectedErr string + }{ + { + name: "", + expectedErr: `name must be in the form of .`, + }, + { + name: "pods", + expectedErr: `name must be in the form of .`, + }, + { + name: "core.pods", + expectedErr: "", + }, + { + name: "authentication.k8s.io.tokenreviews", + expectedErr: "", + }, + { + name: strings.Repeat("x", 253) + ".tokenreviews", + expectedErr: "", + }, + { + name: strings.Repeat("x", 254) + ".tokenreviews", + expectedErr: `the group segment must be no more than 253 characters`, + }, + { + name: "authentication.k8s.io." + strings.Repeat("x", 63), + expectedErr: "", + }, + { + name: "authentication.k8s.io." + strings.Repeat("x", 64), + expectedErr: `the resource segment must be no more than 63 characters`, + }, + } + for _, tc := range cases { + errs := ValidateStorageVersionName(tc.name, false) + if errs == nil && len(tc.expectedErr) == 0 { + continue + } + if errs != nil && len(tc.expectedErr) == 0 { + t.Errorf("unexpected error %v", errs) + continue + } + if errs == nil && len(tc.expectedErr) != 0 { + t.Errorf("unexpected empty error") + continue + } + found := false + for _, msg := range errs { + if msg == tc.expectedErr { + found = true + } + } + if !found { + t.Errorf("expected error to contain %s, got %v", tc.expectedErr, errs) + } + } +} diff --git a/pkg/controlplane/master.go b/pkg/controlplane/master.go index cf53bdf4bb1..85be9a77946 100644 --- a/pkg/controlplane/master.go +++ b/pkg/controlplane/master.go @@ -27,6 +27,7 @@ import ( admissionregistrationv1 "k8s.io/api/admissionregistration/v1" admissionregistrationv1beta1 "k8s.io/api/admissionregistration/v1beta1" + apiserverinternalv1alpha1 "k8s.io/api/apiserverinternal/v1alpha1" appsv1 "k8s.io/api/apps/v1" authenticationv1 "k8s.io/api/authentication/v1" authenticationv1beta1 "k8s.io/api/authentication/v1beta1" @@ -93,6 +94,7 @@ import ( // RESTStorage installers admissionregistrationrest "k8s.io/kubernetes/pkg/registry/admissionregistration/rest" + apiserverinternalrest "k8s.io/kubernetes/pkg/registry/apiserverinternal/rest" appsrest "k8s.io/kubernetes/pkg/registry/apps/rest" authenticationrest "k8s.io/kubernetes/pkg/registry/authentication/rest" authorizationrest "k8s.io/kubernetes/pkg/registry/authorization/rest" @@ -415,6 +417,7 @@ func (c completedConfig) New(delegationTarget genericapiserver.DelegationTarget) // TODO: describe the priority all the way down in the RESTStorageProviders and plumb it back through the various discovery // handlers that we have. restStorageProviders := []RESTStorageProvider{ + apiserverinternalrest.StorageProvider{}, authenticationrest.RESTStorageProvider{Authenticator: c.GenericConfig.Authentication.Authenticator, APIAudiences: c.GenericConfig.Authentication.APIAudiences}, authorizationrest.RESTStorageProvider{Authorizer: c.GenericConfig.Authorization.Authorizer, RuleResolver: c.GenericConfig.RuleResolver}, autoscalingrest.RESTStorageProvider{}, @@ -633,6 +636,7 @@ func DefaultAPIResourceConfigSource() *serverstorage.ResourceConfig { ) // disable alpha versions explicitly so we have a full list of what's possible to serve ret.DisableVersions( + apiserverinternalv1alpha1.SchemeGroupVersion, batchapiv2alpha1.SchemeGroupVersion, nodev1alpha1.SchemeGroupVersion, rbacv1alpha1.SchemeGroupVersion, diff --git a/pkg/printers/internalversion/printers.go b/pkg/printers/internalversion/printers.go index 9aea4aee9f6..a5ad60763ae 100644 --- a/pkg/printers/internalversion/printers.go +++ b/pkg/printers/internalversion/printers.go @@ -25,6 +25,7 @@ import ( "strings" "time" + apiserverinternalv1alpha1 "k8s.io/api/apiserverinternal/v1alpha1" appsv1beta1 "k8s.io/api/apps/v1beta1" autoscalingv2beta1 "k8s.io/api/autoscaling/v2beta1" batchv1 "k8s.io/api/batch/v1" @@ -48,6 +49,7 @@ import ( "k8s.io/apimachinery/pkg/util/sets" utilfeature "k8s.io/apiserver/pkg/util/feature" "k8s.io/kubernetes/pkg/apis/admissionregistration" + "k8s.io/kubernetes/pkg/apis/apiserverinternal" "k8s.io/kubernetes/pkg/apis/apps" "k8s.io/kubernetes/pkg/apis/autoscaling" "k8s.io/kubernetes/pkg/apis/batch" @@ -571,6 +573,15 @@ func AddHandlers(h printers.PrintHandler) { } h.TableHandler(priorityLevelColumnDefinitions, printPriorityLevelConfiguration) h.TableHandler(priorityLevelColumnDefinitions, printPriorityLevelConfigurationList) + + storageVersionColumnDefinitions := []metav1.TableColumnDefinition{ + {Name: "Name", Type: "string", Format: "name", Description: metav1.ObjectMeta{}.SwaggerDoc()["name"]}, + {Name: "CommonEncodingVersion", Type: "string", Description: apiserverinternalv1alpha1.StorageVersionStatus{}.SwaggerDoc()["commonEncodingVersion"]}, + {Name: "StorageVersions", Type: "string", Description: apiserverinternalv1alpha1.StorageVersionStatus{}.SwaggerDoc()["storageVersions"]}, + {Name: "Age", Type: "string", Description: metav1.ObjectMeta{}.SwaggerDoc()["creationTimestamp"]}, + } + h.TableHandler(storageVersionColumnDefinitions, printStorageVersion) + h.TableHandler(storageVersionColumnDefinitions, printStorageVersionList) } // Pass ports=nil for all ports. @@ -2477,6 +2488,46 @@ func printFlowSchemaList(list *flowcontrol.FlowSchemaList, options printers.Gene return rows, nil } +func printStorageVersion(obj *apiserverinternal.StorageVersion, options printers.GenerateOptions) ([]metav1.TableRow, error) { + row := metav1.TableRow{ + Object: runtime.RawExtension{Object: obj}, + } + commonEncodingVersion := "" + if obj.Status.CommonEncodingVersion != nil { + commonEncodingVersion = *obj.Status.CommonEncodingVersion + } + row.Cells = append(row.Cells, obj.Name, commonEncodingVersion, formatStorageVersions(obj.Status.StorageVersions), translateTimestampSince(obj.CreationTimestamp)) + return []metav1.TableRow{row}, nil +} + +func formatStorageVersions(storageVersions []apiserverinternal.ServerStorageVersion) string { + list := []string{} + max := 3 + more := false + count := 0 + for _, sv := range storageVersions { + if len(list) < max { + list = append(list, fmt.Sprintf("%s=%s", sv.APIServerID, sv.EncodingVersion)) + } else if len(list) == max { + more = true + } + count++ + } + return listWithMoreString(list, more, count, max) +} + +func printStorageVersionList(list *apiserverinternal.StorageVersionList, options printers.GenerateOptions) ([]metav1.TableRow, error) { + rows := make([]metav1.TableRow, 0, len(list.Items)) + for i := range list.Items { + r, err := printStorageVersion(&list.Items[i], options) + if err != nil { + return nil, err + } + rows = append(rows, r...) + } + return rows, nil +} + func printPriorityLevelConfiguration(obj *flowcontrol.PriorityLevelConfiguration, options printers.GenerateOptions) ([]metav1.TableRow, error) { row := metav1.TableRow{ Object: runtime.RawExtension{Object: obj}, diff --git a/pkg/printers/internalversion/printers_test.go b/pkg/printers/internalversion/printers_test.go index 1ecde89ad60..9765a88599e 100644 --- a/pkg/printers/internalversion/printers_test.go +++ b/pkg/printers/internalversion/printers_test.go @@ -27,6 +27,7 @@ import ( "k8s.io/apimachinery/pkg/runtime/schema" "k8s.io/apimachinery/pkg/util/diff" "k8s.io/apimachinery/pkg/util/intstr" + "k8s.io/kubernetes/pkg/apis/apiserverinternal" "k8s.io/kubernetes/pkg/apis/apps" "k8s.io/kubernetes/pkg/apis/autoscaling" "k8s.io/kubernetes/pkg/apis/batch" @@ -5311,3 +5312,125 @@ func TestPrintPriorityLevelConfiguration(t *testing.T) { } } } + +func TestPrintStorageVersion(t *testing.T) { + commonEncodingVersion := "v1" + tests := []struct { + sv apiserverinternal.StorageVersion + expected []metav1.TableRow + }{ + { + sv: apiserverinternal.StorageVersion{ + ObjectMeta: metav1.ObjectMeta{ + Name: "empty", + CreationTimestamp: metav1.Time{Time: time.Now().Add(1.9e9)}, + }, + Status: apiserverinternal.StorageVersionStatus{}, + }, + // Columns: Name, CommonEncodingVersion, StorageVersions, Age + expected: []metav1.TableRow{{Cells: []interface{}{"empty", "", "", "0s"}}}, + }, + { + sv: apiserverinternal.StorageVersion{ + ObjectMeta: metav1.ObjectMeta{ + Name: "valid", + CreationTimestamp: metav1.Time{Time: time.Now().Add(1.9e9)}, + }, + Status: apiserverinternal.StorageVersionStatus{ + StorageVersions: []apiserverinternal.ServerStorageVersion{ + { + APIServerID: "1", + EncodingVersion: "v1", + DecodableVersions: []string{"v1"}, + }, + { + APIServerID: "2", + EncodingVersion: "v1", + DecodableVersions: []string{"v1", "v2"}, + }, + }, + CommonEncodingVersion: &commonEncodingVersion, + }, + }, + // Columns: Name, CommonEncodingVersion, StorageVersions, Age + expected: []metav1.TableRow{{Cells: []interface{}{"valid", "v1", "1=v1,2=v1", "0s"}}}, + }, + { + sv: apiserverinternal.StorageVersion{ + ObjectMeta: metav1.ObjectMeta{ + Name: "disagree", + CreationTimestamp: metav1.Time{Time: time.Now().Add(1.9e9)}, + }, + Status: apiserverinternal.StorageVersionStatus{ + StorageVersions: []apiserverinternal.ServerStorageVersion{ + { + APIServerID: "1", + EncodingVersion: "v1", + DecodableVersions: []string{"v1"}, + }, + { + APIServerID: "2", + EncodingVersion: "v1", + DecodableVersions: []string{"v1", "v2"}, + }, + { + APIServerID: "3", + EncodingVersion: "v2", + DecodableVersions: []string{"v2"}, + }, + }, + }, + }, + // Columns: Name, CommonEncodingVersion, StorageVersions, Age + expected: []metav1.TableRow{{Cells: []interface{}{"disagree", "", "1=v1,2=v1,3=v2", "0s"}}}, + }, + { + sv: apiserverinternal.StorageVersion{ + ObjectMeta: metav1.ObjectMeta{ + Name: "agreeWithMore", + CreationTimestamp: metav1.Time{Time: time.Now().Add(1.9e9)}, + }, + Status: apiserverinternal.StorageVersionStatus{ + StorageVersions: []apiserverinternal.ServerStorageVersion{ + { + APIServerID: "1", + EncodingVersion: "v1", + DecodableVersions: []string{"v1"}, + }, + { + APIServerID: "2", + EncodingVersion: "v1", + DecodableVersions: []string{"v1", "v2"}, + }, + { + APIServerID: "3", + EncodingVersion: "v1", + DecodableVersions: []string{"v1", "v2"}, + }, + { + APIServerID: "4", + EncodingVersion: "v1", + DecodableVersions: []string{"v1", "v2", "v3alpha1"}, + }, + }, + CommonEncodingVersion: &commonEncodingVersion, + }, + }, + // Columns: Name, CommonEncodingVersion, StorageVersions, Age + expected: []metav1.TableRow{{Cells: []interface{}{"agreeWithMore", "v1", "1=v1,2=v1,3=v1 + 1 more...", "0s"}}}, + }, + } + + for i, test := range tests { + rows, err := printStorageVersion(&test.sv, printers.GenerateOptions{}) + if err != nil { + t.Fatal(err) + } + for i := range rows { + rows[i].Object.Object = nil + } + if !reflect.DeepEqual(test.expected, rows) { + t.Errorf("%d mismatch: %s", i, diff.ObjectReflectDiff(test.expected, rows)) + } + } +} diff --git a/pkg/registry/apiserverinternal/rest/storage.go b/pkg/registry/apiserverinternal/rest/storage.go new file mode 100644 index 00000000000..e69e8cc56c1 --- /dev/null +++ b/pkg/registry/apiserverinternal/rest/storage.go @@ -0,0 +1,62 @@ +/* +Copyright 2020 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 rest + +import ( + apiserverv1alpha1 "k8s.io/api/apiserverinternal/v1alpha1" + "k8s.io/apiserver/pkg/registry/generic" + "k8s.io/apiserver/pkg/registry/rest" + genericapiserver "k8s.io/apiserver/pkg/server" + serverstorage "k8s.io/apiserver/pkg/server/storage" + "k8s.io/kubernetes/pkg/api/legacyscheme" + "k8s.io/kubernetes/pkg/apis/apiserverinternal" + storageversionstorage "k8s.io/kubernetes/pkg/registry/apiserverinternal/storageversion/storage" +) + +// StorageProvider is a REST storage provider for internal.apiserver.k8s.io +type StorageProvider struct{} + +// NewRESTStorage returns a StorageProvider +func (p StorageProvider) NewRESTStorage(apiResourceConfigSource serverstorage.APIResourceConfigSource, restOptionsGetter generic.RESTOptionsGetter) (genericapiserver.APIGroupInfo, bool, error) { + apiGroupInfo := genericapiserver.NewDefaultAPIGroupInfo(apiserverinternal.GroupName, legacyscheme.Scheme, legacyscheme.ParameterCodec, legacyscheme.Codecs) + + if apiResourceConfigSource.VersionEnabled(apiserverv1alpha1.SchemeGroupVersion) { + storageMap, err := p.v1alpha1Storage(apiResourceConfigSource, restOptionsGetter) + if err != nil { + return genericapiserver.APIGroupInfo{}, false, err + } + apiGroupInfo.VersionedResourcesStorageMap[apiserverv1alpha1.SchemeGroupVersion.Version] = storageMap + } + return apiGroupInfo, true, nil +} + +func (p StorageProvider) v1alpha1Storage(apiResourceConfigSource serverstorage.APIResourceConfigSource, restOptionsGetter generic.RESTOptionsGetter) (map[string]rest.Storage, error) { + storage := map[string]rest.Storage{} + s, status, err := storageversionstorage.NewREST(restOptionsGetter) + if err != nil { + return nil, err + } + storage["storageversions"] = s + storage["storageversions/status"] = status + + return storage, nil +} + +// GroupName is the group name for the storage provider +func (p StorageProvider) GroupName() string { + return apiserverinternal.GroupName +} diff --git a/pkg/registry/apiserverinternal/storageversion/doc.go b/pkg/registry/apiserverinternal/storageversion/doc.go new file mode 100644 index 00000000000..eb2775dc11f --- /dev/null +++ b/pkg/registry/apiserverinternal/storageversion/doc.go @@ -0,0 +1,19 @@ +/* +Copyright 2020 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 storageversion provides Registry interface and it's RESTStorage +// implementation for storing StorageVersion api objects. +package storageversion // import "k8s.io/kubernetes/pkg/registry/apiserverinternal/storageversion" diff --git a/pkg/registry/apiserverinternal/storageversion/storage/storage.go b/pkg/registry/apiserverinternal/storageversion/storage/storage.go new file mode 100644 index 00000000000..a6157b7b5a3 --- /dev/null +++ b/pkg/registry/apiserverinternal/storageversion/storage/storage.go @@ -0,0 +1,83 @@ +/* +Copyright 2020 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 ( + "context" + + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/runtime" + "k8s.io/apiserver/pkg/registry/generic" + genericregistry "k8s.io/apiserver/pkg/registry/generic/registry" + "k8s.io/apiserver/pkg/registry/rest" + "k8s.io/kubernetes/pkg/apis/apiserverinternal" + "k8s.io/kubernetes/pkg/printers" + printersinternal "k8s.io/kubernetes/pkg/printers/internalversion" + printerstorage "k8s.io/kubernetes/pkg/printers/storage" + strategy "k8s.io/kubernetes/pkg/registry/apiserverinternal/storageversion" +) + +// REST implements a RESTStorage for storage version against etcd +type REST struct { + *genericregistry.Store +} + +// NewREST returns a RESTStorage object that will work against storageVersions +func NewREST(optsGetter generic.RESTOptionsGetter) (*REST, *StatusREST, error) { + store := &genericregistry.Store{ + NewFunc: func() runtime.Object { return &apiserverinternal.StorageVersion{} }, + NewListFunc: func() runtime.Object { return &apiserverinternal.StorageVersionList{} }, + ObjectNameFunc: func(obj runtime.Object) (string, error) { + return obj.(*apiserverinternal.StorageVersion).Name, nil + }, + DefaultQualifiedResource: apiserverinternal.Resource("storageversions"), + + CreateStrategy: strategy.Strategy, + UpdateStrategy: strategy.Strategy, + DeleteStrategy: strategy.Strategy, + TableConvertor: printerstorage.TableConvertor{TableGenerator: printers.NewTableGenerator().With(printersinternal.AddHandlers)}, + } + options := &generic.StoreOptions{RESTOptions: optsGetter} + if err := store.CompleteWithOptions(options); err != nil { + return nil, nil, err + } + statusStore := *store + statusStore.UpdateStrategy = strategy.StatusStrategy + return &REST{store}, &StatusREST{store: &statusStore}, nil +} + +// StatusREST implements the REST endpoint for changing the status of a storageVersion +type StatusREST struct { + store *genericregistry.Store +} + +// New creates a new StorageVersion object. +func (r *StatusREST) New() runtime.Object { + return &apiserverinternal.StorageVersion{} +} + +// Get retrieves the object from the storage. It is required to support Patch. +func (r *StatusREST) Get(ctx context.Context, name string, options *metav1.GetOptions) (runtime.Object, error) { + return r.store.Get(ctx, name, options) +} + +// Update alters the status subset of an object. +func (r *StatusREST) Update(ctx context.Context, name string, objInfo rest.UpdatedObjectInfo, createValidation rest.ValidateObjectFunc, updateValidation rest.ValidateObjectUpdateFunc, forceAllowCreate bool, options *metav1.UpdateOptions) (runtime.Object, bool, error) { + // We are explicitly setting forceAllowCreate to false in the call to the underlying storage because + // subresources should never allow create on update. + return r.store.Update(ctx, name, objInfo, createValidation, updateValidation, false, options) +} diff --git a/pkg/registry/apiserverinternal/storageversion/strategy.go b/pkg/registry/apiserverinternal/storageversion/strategy.go new file mode 100644 index 00000000000..5a1f2982760 --- /dev/null +++ b/pkg/registry/apiserverinternal/storageversion/strategy.go @@ -0,0 +1,103 @@ +/* +Copyright 2020 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 storageversion + +import ( + "context" + + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "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/apiserverinternal" + "k8s.io/kubernetes/pkg/apis/apiserverinternal/validation" +) + +// storageVersionStrategy implements verification logic for StorageVersion. +type storageVersionStrategy struct { + runtime.ObjectTyper + names.NameGenerator +} + +// Strategy is the default logic that applies when creating and updating StorageVersion objects. +var Strategy = storageVersionStrategy{legacyscheme.Scheme, names.SimpleNameGenerator} + +// NamespaceScoped returns false because all StorageVersion's need to be cluster scoped +func (storageVersionStrategy) NamespaceScoped() bool { + return false +} + +// PrepareForCreate clears the status of an StorageVersion before creation. +func (storageVersionStrategy) PrepareForCreate(ctx context.Context, obj runtime.Object) { + sv := obj.(*apiserverinternal.StorageVersion) + sv.Status = apiserverinternal.StorageVersionStatus{} +} + +// PrepareForUpdate clears fields that are not allowed to be set by end users on update. +func (storageVersionStrategy) PrepareForUpdate(ctx context.Context, obj, old runtime.Object) { + sv := obj.(*apiserverinternal.StorageVersion) + sv.Status = old.(*apiserverinternal.StorageVersion).Status +} + +// Validate validates a new storageVersion. +func (storageVersionStrategy) Validate(ctx context.Context, obj runtime.Object) field.ErrorList { + sv := obj.(*apiserverinternal.StorageVersion) + return validation.ValidateStorageVersion(sv) +} + +// Canonicalize normalizes the object after validation. +func (storageVersionStrategy) Canonicalize(obj runtime.Object) { +} + +// Does not allow creating a StorageVersion object with a PUT request. +func (storageVersionStrategy) AllowCreateOnUpdate() bool { + return false +} + +// ValidateUpdate is the default update validation for an end user. +func (storageVersionStrategy) ValidateUpdate(ctx context.Context, obj, old runtime.Object) field.ErrorList { + newStorageVersion := obj.(*apiserverinternal.StorageVersion) + oldStorageVersion := old.(*apiserverinternal.StorageVersion) + validationErrorList := validation.ValidateStorageVersionUpdate(newStorageVersion, oldStorageVersion) + return validationErrorList +} + +// AllowUnconditionalUpdate is the default update policy for storageVersion objects. Status update should +// only be allowed if version match. +func (storageVersionStrategy) AllowUnconditionalUpdate() bool { + return false +} + +type storageVersionStatusStrategy struct { + storageVersionStrategy +} + +// StatusStrategy is the default logic invoked when updating object status. +var StatusStrategy = storageVersionStatusStrategy{Strategy} + +func (storageVersionStatusStrategy) PrepareForUpdate(ctx context.Context, obj, old runtime.Object) { + newSV := obj.(*apiserverinternal.StorageVersion) + oldSV := old.(*apiserverinternal.StorageVersion) + + newSV.Spec = oldSV.Spec + metav1.ResetObjectMetaForStatus(&newSV.ObjectMeta, &oldSV.ObjectMeta) +} + +func (storageVersionStatusStrategy) ValidateUpdate(ctx context.Context, obj, old runtime.Object) field.ErrorList { + return validation.ValidateStorageVersionStatusUpdate(obj.(*apiserverinternal.StorageVersion), old.(*apiserverinternal.StorageVersion)) +} diff --git a/staging/src/k8s.io/api/apiserverinternal/v1alpha1/types.go b/staging/src/k8s.io/api/apiserverinternal/v1alpha1/types.go index 0d80fcfb9fb..880091b6f82 100644 --- a/staging/src/k8s.io/api/apiserverinternal/v1alpha1/types.go +++ b/staging/src/k8s.io/api/apiserverinternal/v1alpha1/types.go @@ -47,7 +47,7 @@ type StorageVersionStatus struct { // The reported versions per API server instance. // +optional // +listType=map - // +listMapKey=apiserverID + // +listMapKey=apiServerID StorageVersions []ServerStorageVersion `json:"storageVersions,omitempty" protobuf:"bytes,1,opt,name=storageVersions"` // If all API server instances agree on the same encoding storage version, // then this field is set to that version. Otherwise this field is left empty. diff --git a/test/integration/apiserver/apply/status_test.go b/test/integration/apiserver/apply/status_test.go index b23610db392..32b82735a3b 100644 --- a/test/integration/apiserver/apply/status_test.go +++ b/test/integration/apiserver/apply/status_test.go @@ -57,6 +57,7 @@ var statusData = map[schema.GroupVersionResource]string{ gvr("policy", "v1beta1", "poddisruptionbudgets"): `{"status": {"currentHealthy": 5}}`, gvr("certificates.k8s.io", "v1beta1", "certificatesigningrequests"): `{"status": {"conditions": [{"type": "MyStatus"}]}}`, gvr("certificates.k8s.io", "v1", "certificatesigningrequests"): `{"status": {"conditions": [{"type": "MyStatus", "status": "True"}]}}`, + gvr("internal.apiserver.k8s.io", "v1alpha1", "storageversions"): `{"status": {"commonEncodingVersion":"v1","storageVersions":[{"apiServerID":"1","decodableVersions":["v1","v2"],"encodingVersion":"v1"}],"conditions":[{"type":"AllEncodingVersionsEqual","status":"True","lastTransitionTime":"2020-01-01T00:00:00Z","reason":"allEncodingVersionsEqual","message":"all encoding versions are set to v1"}]}}`, } const statusDefault = `{"status": {"conditions": [{"type": "MyStatus", "status":"true"}]}}` @@ -69,6 +70,12 @@ var ignoreList = map[schema.GroupVersionResource]struct{}{ gvr("apiregistration.k8s.io", "v1", "apiservices"): {}, } +// Some status-only APIs have empty object on creation. Therefore we don't expect create_test +// managedFields for these APIs +var ignoreCreateManagementList = map[schema.GroupVersionResource]struct{}{ + gvr("internal.apiserver.k8s.io", "v1alpha1", "storageversions"): {}, +} + func gvr(g, v, r string) schema.GroupVersionResource { return schema.GroupVersionResource{Group: g, Version: v, Resource: r} } @@ -200,7 +207,11 @@ func TestApplyStatus(t *testing.T) { t.Fatalf("Couldn't find apply_status_test: %v", managedFields) } if !findManager(managedFields, "create_test") { - t.Fatalf("Couldn't find create_test: %v", managedFields) + if _, ok := ignoreCreateManagementList[mapping.Resource]; !ok { + t.Fatalf("Couldn't find create_test: %v", managedFields) + } + } else if _, ok := ignoreCreateManagementList[mapping.Resource]; ok { + t.Fatalf("found create_test in ignoreCreateManagementList resource: %v", managedFields) } if err := rsc.Delete(context.TODO(), name, *metav1.NewDeleteOptions(0)); err != nil { diff --git a/test/integration/apiserver/print_test.go b/test/integration/apiserver/print_test.go index 53f59dcaa1d..e0520200144 100644 --- a/test/integration/apiserver/print_test.go +++ b/test/integration/apiserver/print_test.go @@ -27,6 +27,7 @@ import ( "testing" "time" + apiserverinternalv1alpha1 "k8s.io/api/apiserverinternal/v1alpha1" batchv2alpha1 "k8s.io/api/batch/v2alpha1" discoveryv1alpha1 "k8s.io/api/discovery/v1alpha1" discoveryv1beta1 "k8s.io/api/discovery/v1beta1" @@ -172,6 +173,7 @@ func TestServerSidePrint(t *testing.T) { extensionsv1beta1.SchemeGroupVersion, nodev1alpha1.SchemeGroupVersion, flowcontrolv1alpha1.SchemeGroupVersion, + apiserverinternalv1alpha1.SchemeGroupVersion, }, []schema.GroupVersionResource{}, ) diff --git a/test/integration/etcd/data.go b/test/integration/etcd/data.go index c096ef7de62..b8525f4c3dd 100644 --- a/test/integration/etcd/data.go +++ b/test/integration/etcd/data.go @@ -508,6 +508,13 @@ func GetEtcdStorageDataForNamespace(namespace string) map[schema.GroupVersionRes ExpectedEtcdPath: "/registry/runtimeclasses/rc2", }, // -- + + // k8s.io/apiserver/pkg/apis/apiserverinternal/v1alpha1 + gvr("internal.apiserver.k8s.io", "v1alpha1", "storageversions"): { + Stub: `{"metadata":{"name":"sv1.test"},"spec":{}}`, + ExpectedEtcdPath: "/registry/storageversions/sv1.test", + }, + // -- } // add csinodes if CSINodeInfo feature gate is enabled