diff --git a/staging/src/k8s.io/client-go/tools/cache/BUILD b/staging/src/k8s.io/client-go/tools/cache/BUILD index 8df1c876168..115cffc741e 100644 --- a/staging/src/k8s.io/client-go/tools/cache/BUILD +++ b/staging/src/k8s.io/client-go/tools/cache/BUILD @@ -53,6 +53,7 @@ go_library( "index.go", "listers.go", "listwatch.go", + "mutation_cache.go", "mutation_detector.go", "reflector.go", "shared_informer.go", @@ -63,6 +64,7 @@ go_library( tags = ["automanaged"], deps = [ "//vendor/github.com/golang/glog:go_default_library", + "//vendor/github.com/hashicorp/golang-lru:go_default_library", "//vendor/k8s.io/apimachinery/pkg/api/errors:go_default_library", "//vendor/k8s.io/apimachinery/pkg/api/meta:go_default_library", "//vendor/k8s.io/apimachinery/pkg/apis/meta/v1:go_default_library", diff --git a/staging/src/k8s.io/kube-apiextensions-server/artifacts/customresource-01/noxu-resource-definition.yaml b/staging/src/k8s.io/kube-apiextensions-server/artifacts/customresource-01/noxu-resource-definition.yaml index ec7f4d858de..8011eee8fa8 100644 --- a/staging/src/k8s.io/kube-apiextensions-server/artifacts/customresource-01/noxu-resource-definition.yaml +++ b/staging/src/k8s.io/kube-apiextensions-server/artifacts/customresource-01/noxu-resource-definition.yaml @@ -1,5 +1,5 @@ apiVersion: apiextensions.k8s.io/v1alpha1 -kind: CustomResource +kind: CustomResourceDefinition metadata: name: noxus.mygroup.example.com spec: diff --git a/staging/src/k8s.io/kube-apiextensions-server/pkg/apis/apiextensions/BUILD b/staging/src/k8s.io/kube-apiextensions-server/pkg/apis/apiextensions/BUILD index 6e2604388ad..0e128d1a316 100644 --- a/staging/src/k8s.io/kube-apiextensions-server/pkg/apis/apiextensions/BUILD +++ b/staging/src/k8s.io/kube-apiextensions-server/pkg/apis/apiextensions/BUILD @@ -11,6 +11,7 @@ go_library( name = "go_default_library", srcs = [ "doc.go", + "helpers.go", "register.go", "types.go", "zz_generated.deepcopy.go", diff --git a/staging/src/k8s.io/kube-apiextensions-server/pkg/apis/apiextensions/helpers.go b/staging/src/k8s.io/kube-apiextensions-server/pkg/apis/apiextensions/helpers.go new file mode 100644 index 00000000000..b495fd55d6f --- /dev/null +++ b/staging/src/k8s.io/kube-apiextensions-server/pkg/apis/apiextensions/helpers.go @@ -0,0 +1,78 @@ +/* +Copyright 2016 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 apiextensions + +// SetCRDCondition sets the status condition. It either overwrites the existing one or +// creates a new one +func SetCRDCondition(customResourceDefinition *CustomResourceDefinition, newCondition CustomResourceDefinitionCondition) { + existingCondition := FindCRDCondition(customResourceDefinition, newCondition.Type) + if existingCondition == nil { + customResourceDefinition.Status.Conditions = append(customResourceDefinition.Status.Conditions, newCondition) + return + } + + if existingCondition.Status != newCondition.Status { + existingCondition.Status = newCondition.Status + existingCondition.LastTransitionTime = newCondition.LastTransitionTime + } + + existingCondition.Reason = newCondition.Reason + existingCondition.Message = newCondition.Message +} + +// FindCRDCondition returns the condition you're looking for or nil +func FindCRDCondition(customResourceDefinition *CustomResourceDefinition, conditionType CustomResourceDefinitionConditionType) *CustomResourceDefinitionCondition { + for i := range customResourceDefinition.Status.Conditions { + if customResourceDefinition.Status.Conditions[i].Type == conditionType { + return &customResourceDefinition.Status.Conditions[i] + } + } + + return nil +} + +// IsCRDConditionTrue indicates if the condition is present and strictly true +func IsCRDConditionTrue(customResourceDefinition *CustomResourceDefinition, conditionType CustomResourceDefinitionConditionType) bool { + return IsCRDConditionPresentAndEqual(customResourceDefinition, conditionType, ConditionTrue) +} + +// IsCRDConditionFalse indicates if the condition is present and false true +func IsCRDConditionFalse(customResourceDefinition *CustomResourceDefinition, conditionType CustomResourceDefinitionConditionType) bool { + return IsCRDConditionPresentAndEqual(customResourceDefinition, conditionType, ConditionFalse) +} + +// IsCRDConditionPresentAndEqual indicates if the condition is present and equal to the arg +func IsCRDConditionPresentAndEqual(customResourceDefinition *CustomResourceDefinition, conditionType CustomResourceDefinitionConditionType, status ConditionStatus) bool { + for _, condition := range customResourceDefinition.Status.Conditions { + if condition.Type == conditionType { + return condition.Status == status + } + } + return false +} + +// IsCRDConditionEquivalent returns true if the lhs and rhs are equivalent except for times +func IsCRDConditionEquivalent(lhs, rhs *CustomResourceDefinitionCondition) bool { + if lhs == nil && rhs == nil { + return true + } + if lhs == nil || rhs == nil { + return false + } + + return lhs.Message == rhs.Message && lhs.Reason == rhs.Reason && lhs.Status == rhs.Status && lhs.Type == rhs.Type +} diff --git a/staging/src/k8s.io/kube-apiextensions-server/pkg/apiserver/BUILD b/staging/src/k8s.io/kube-apiextensions-server/pkg/apiserver/BUILD index 20af1f2c264..235e4f2263b 100644 --- a/staging/src/k8s.io/kube-apiextensions-server/pkg/apiserver/BUILD +++ b/staging/src/k8s.io/kube-apiextensions-server/pkg/apiserver/BUILD @@ -54,6 +54,7 @@ go_library( "//vendor/k8s.io/kube-apiextensions-server/pkg/client/informers/internalversion:go_default_library", "//vendor/k8s.io/kube-apiextensions-server/pkg/client/informers/internalversion/apiextensions/internalversion:go_default_library", "//vendor/k8s.io/kube-apiextensions-server/pkg/client/listers/apiextensions/internalversion:go_default_library", + "//vendor/k8s.io/kube-apiextensions-server/pkg/controller/status:go_default_library", "//vendor/k8s.io/kube-apiextensions-server/pkg/registry/customresource:go_default_library", "//vendor/k8s.io/kube-apiextensions-server/pkg/registry/customresourcedefinition:go_default_library", ], diff --git a/staging/src/k8s.io/kube-apiextensions-server/pkg/apiserver/apiserver.go b/staging/src/k8s.io/kube-apiextensions-server/pkg/apiserver/apiserver.go index fcdf485dc4e..008dee48466 100644 --- a/staging/src/k8s.io/kube-apiextensions-server/pkg/apiserver/apiserver.go +++ b/staging/src/k8s.io/kube-apiextensions-server/pkg/apiserver/apiserver.go @@ -37,6 +37,7 @@ import ( "k8s.io/kube-apiextensions-server/pkg/apis/apiextensions/v1alpha1" "k8s.io/kube-apiextensions-server/pkg/client/clientset/internalclientset" internalinformers "k8s.io/kube-apiextensions-server/pkg/client/informers/internalversion" + "k8s.io/kube-apiextensions-server/pkg/controller/status" "k8s.io/kube-apiextensions-server/pkg/registry/customresourcedefinition" // make sure the generated client works @@ -113,8 +114,10 @@ func (c completedConfig) New(delegationTarget genericapiserver.DelegationTarget) apiGroupInfo := genericapiserver.NewDefaultAPIGroupInfo(apiextensions.GroupName, registry, Scheme, metav1.ParameterCodec, Codecs) apiGroupInfo.GroupMeta.GroupVersion = v1alpha1.SchemeGroupVersion + customResourceDefintionStorage := customresourcedefinition.NewREST(Scheme, c.GenericConfig.RESTOptionsGetter) v1alpha1storage := map[string]rest.Storage{} - v1alpha1storage["customresourcedefinitions"] = customresourcedefinition.NewREST(Scheme, c.GenericConfig.RESTOptionsGetter) + v1alpha1storage["customresourcedefinitions"] = customResourceDefintionStorage + v1alpha1storage["customresourcedefinitions/status"] = customresourcedefinition.NewStatusREST(Scheme, customResourceDefintionStorage) apiGroupInfo.VersionedResourcesStorageMap["v1alpha1"] = v1alpha1storage if err := s.GenericAPIServer.InstallAPIGroup(&apiGroupInfo); err != nil { @@ -153,6 +156,7 @@ func (c completedConfig) New(delegationTarget genericapiserver.DelegationTarget) s.GenericAPIServer.Handler.PostGoRestfulMux.HandlePrefix("/apis/", customResourceDefinitionHandler) customResourceDefinitionController := NewDiscoveryController(customResourceDefinitionInformers.Apiextensions().InternalVersion().CustomResourceDefinitions(), versionDiscoveryHandler, groupDiscoveryHandler) + namingController := status.NewNamingConditionController(customResourceDefinitionInformers.Apiextensions().InternalVersion().CustomResourceDefinitions(), customResourceDefinitionClient) s.GenericAPIServer.AddPostStartHook("start-apiextensions-informers", func(context genericapiserver.PostStartHookContext) error { customResourceDefinitionInformers.Start(context.StopCh) @@ -160,6 +164,7 @@ func (c completedConfig) New(delegationTarget genericapiserver.DelegationTarget) }) s.GenericAPIServer.AddPostStartHook("start-apiextensions-controllers", func(context genericapiserver.PostStartHookContext) error { go customResourceDefinitionController.Run(context.StopCh) + go namingController.Run(context.StopCh) return nil }) diff --git a/staging/src/k8s.io/kube-apiextensions-server/pkg/apiserver/customresource_discovery_controller.go b/staging/src/k8s.io/kube-apiextensions-server/pkg/apiserver/customresource_discovery_controller.go index 2dd808dd65e..39cdb9c6ee3 100644 --- a/staging/src/k8s.io/kube-apiextensions-server/pkg/apiserver/customresource_discovery_controller.go +++ b/staging/src/k8s.io/kube-apiextensions-server/pkg/apiserver/customresource_discovery_controller.go @@ -40,8 +40,8 @@ type DiscoveryController struct { versionHandler *versionDiscoveryHandler groupHandler *groupDiscoveryHandler - customResourceDefinitionLister listers.CustomResourceDefinitionLister - customResourceDefinitionsSynced cache.InformerSynced + crdLister listers.CustomResourceDefinitionLister + crdsSynced cache.InformerSynced // To allow injection for testing. syncFn func(version schema.GroupVersion) error @@ -49,17 +49,17 @@ type DiscoveryController struct { queue workqueue.RateLimitingInterface } -func NewDiscoveryController(customResourceDefinitionInformer informers.CustomResourceDefinitionInformer, versionHandler *versionDiscoveryHandler, groupHandler *groupDiscoveryHandler) *DiscoveryController { +func NewDiscoveryController(crdInformer informers.CustomResourceDefinitionInformer, versionHandler *versionDiscoveryHandler, groupHandler *groupDiscoveryHandler) *DiscoveryController { c := &DiscoveryController{ - versionHandler: versionHandler, - groupHandler: groupHandler, - customResourceDefinitionLister: customResourceDefinitionInformer.Lister(), - customResourceDefinitionsSynced: customResourceDefinitionInformer.Informer().HasSynced, + versionHandler: versionHandler, + groupHandler: groupHandler, + crdLister: crdInformer.Lister(), + crdsSynced: crdInformer.Informer().HasSynced, queue: workqueue.NewNamedRateLimitingQueue(workqueue.DefaultControllerRateLimiter(), "DiscoveryController"), } - customResourceDefinitionInformer.Informer().AddEventHandler(cache.ResourceEventHandlerFuncs{ + crdInformer.Informer().AddEventHandler(cache.ResourceEventHandlerFuncs{ AddFunc: c.addCustomResourceDefinition, UpdateFunc: c.updateCustomResourceDefinition, DeleteFunc: c.deleteCustomResourceDefinition, @@ -75,36 +75,39 @@ func (c *DiscoveryController) sync(version schema.GroupVersion) error { apiVersionsForDiscovery := []metav1.GroupVersionForDiscovery{} apiResourcesForDiscovery := []metav1.APIResource{} - customResourceDefinitions, err := c.customResourceDefinitionLister.List(labels.Everything()) + crds, err := c.crdLister.List(labels.Everything()) if err != nil { return err } foundVersion := false foundGroup := false - for _, customResourceDefinition := range customResourceDefinitions { - // TODO add status checking + for _, crd := range crds { + // if we can't definitively determine that our names are good, don't serve it + if !apiextensions.IsCRDConditionFalse(crd, apiextensions.NameConflict) { + continue + } - if customResourceDefinition.Spec.Group != version.Group { + if crd.Spec.Group != version.Group { continue } foundGroup = true apiVersionsForDiscovery = append(apiVersionsForDiscovery, metav1.GroupVersionForDiscovery{ - GroupVersion: customResourceDefinition.Spec.Group + "/" + customResourceDefinition.Spec.Version, - Version: customResourceDefinition.Spec.Version, + GroupVersion: crd.Spec.Group + "/" + crd.Spec.Version, + Version: crd.Spec.Version, }) - if customResourceDefinition.Spec.Version != version.Version { + if crd.Spec.Version != version.Version { continue } foundVersion = true apiResourcesForDiscovery = append(apiResourcesForDiscovery, metav1.APIResource{ - Name: customResourceDefinition.Spec.Names.Plural, - SingularName: customResourceDefinition.Spec.Names.Singular, - Namespaced: customResourceDefinition.Spec.Scope == apiextensions.NamespaceScoped, - Kind: customResourceDefinition.Spec.Names.Kind, + Name: crd.Status.AcceptedNames.Plural, + SingularName: crd.Status.AcceptedNames.Singular, + Namespaced: crd.Spec.Scope == apiextensions.NamespaceScoped, + Kind: crd.Status.AcceptedNames.Kind, Verbs: metav1.Verbs([]string{"delete", "deletecollection", "get", "list", "patch", "create", "update", "watch"}), - ShortNames: customResourceDefinition.Spec.Names.ShortNames, + ShortNames: crd.Status.AcceptedNames.ShortNames, }) } @@ -140,7 +143,7 @@ func (c *DiscoveryController) Run(stopCh <-chan struct{}) { glog.Infof("Starting DiscoveryController") - if !cache.WaitForCacheSync(stopCh, c.customResourceDefinitionsSynced) { + if !cache.WaitForCacheSync(stopCh, c.crdsSynced) { utilruntime.HandleError(fmt.Errorf("timed out waiting for caches to sync")) return } diff --git a/staging/src/k8s.io/kube-apiextensions-server/pkg/apiserver/customresource_handler.go b/staging/src/k8s.io/kube-apiextensions-server/pkg/apiserver/customresource_handler.go index d4883f7c933..81feb20fcb4 100644 --- a/staging/src/k8s.io/kube-apiextensions-server/pkg/apiserver/customresource_handler.go +++ b/staging/src/k8s.io/kube-apiextensions-server/pkg/apiserver/customresource_handler.go @@ -48,58 +48,58 @@ import ( "k8s.io/kube-apiextensions-server/pkg/registry/customresource" ) -// customResourceDefinitionHandler serves the `/apis` endpoint. +// crdHandler serves the `/apis` endpoint. // This is registered as a filter so that it never collides with any explictly registered endpoints -type customResourceDefinitionHandler struct { +type crdHandler struct { versionDiscoveryHandler *versionDiscoveryHandler groupDiscoveryHandler *groupDiscoveryHandler customStorageLock sync.Mutex - // customStorage contains a customResourceDefinitionStorageMap + // customStorage contains a crdStorageMap customStorage atomic.Value requestContextMapper apirequest.RequestContextMapper - customResourceDefinitionLister listers.CustomResourceDefinitionLister + crdLister listers.CustomResourceDefinitionLister delegate http.Handler restOptionsGetter generic.RESTOptionsGetter admission admission.Interface } -// customResourceDefinitionInfo stores enough information to serve the storage for the custom resource -type customResourceDefinitionInfo struct { +// crdInfo stores enough information to serve the storage for the custom resource +type crdInfo struct { storage *customresource.REST requestScope handlers.RequestScope } -// customResourceDefinitionStorageMap goes from customresourcedefinition to its storage -type customResourceDefinitionStorageMap map[types.UID]*customResourceDefinitionInfo +// crdStorageMap goes from customresourcedefinition to its storage +type crdStorageMap map[types.UID]*crdInfo func NewCustomResourceDefinitionHandler( versionDiscoveryHandler *versionDiscoveryHandler, groupDiscoveryHandler *groupDiscoveryHandler, requestContextMapper apirequest.RequestContextMapper, - customResourceDefinitionLister listers.CustomResourceDefinitionLister, + crdLister listers.CustomResourceDefinitionLister, delegate http.Handler, restOptionsGetter generic.RESTOptionsGetter, - admission admission.Interface) *customResourceDefinitionHandler { - ret := &customResourceDefinitionHandler{ - versionDiscoveryHandler: versionDiscoveryHandler, - groupDiscoveryHandler: groupDiscoveryHandler, - customStorage: atomic.Value{}, - requestContextMapper: requestContextMapper, - customResourceDefinitionLister: customResourceDefinitionLister, - delegate: delegate, - restOptionsGetter: restOptionsGetter, - admission: admission, + admission admission.Interface) *crdHandler { + ret := &crdHandler{ + versionDiscoveryHandler: versionDiscoveryHandler, + groupDiscoveryHandler: groupDiscoveryHandler, + customStorage: atomic.Value{}, + requestContextMapper: requestContextMapper, + crdLister: crdLister, + delegate: delegate, + restOptionsGetter: restOptionsGetter, + admission: admission, } - ret.customStorage.Store(customResourceDefinitionStorageMap{}) + ret.customStorage.Store(crdStorageMap{}) return ret } -func (r *customResourceDefinitionHandler) ServeHTTP(w http.ResponseWriter, req *http.Request) { +func (r *crdHandler) ServeHTTP(w http.ResponseWriter, req *http.Request) { ctx, ok := r.requestContextMapper.Get(req) if !ok { // programmer error @@ -134,8 +134,8 @@ func (r *customResourceDefinitionHandler) ServeHTTP(w http.ResponseWriter, req * return } - customResourceDefinitionName := requestInfo.Resource + "." + requestInfo.APIGroup - customResourceDefinition, err := r.customResourceDefinitionLister.Get(customResourceDefinitionName) + crdName := requestInfo.Resource + "." + requestInfo.APIGroup + crd, err := r.crdLister.Get(crdName) if apierrors.IsNotFound(err) { r.delegate.ServeHTTP(w, req) return @@ -144,15 +144,18 @@ func (r *customResourceDefinitionHandler) ServeHTTP(w http.ResponseWriter, req * http.Error(w, err.Error(), http.StatusInternalServerError) return } - if customResourceDefinition.Spec.Version != requestInfo.APIVersion { + if crd.Spec.Version != requestInfo.APIVersion { r.delegate.ServeHTTP(w, req) return } - // TODO this is the point to do the condition checks + // if we can't definitively determine that our names are good, delegate + if !apiextensions.IsCRDConditionFalse(crd, apiextensions.NameConflict) { + r.delegate.ServeHTTP(w, req) + } - customResourceDefinitionInfo := r.getServingInfoFor(customResourceDefinition) - storage := customResourceDefinitionInfo.storage - requestScope := customResourceDefinitionInfo.requestScope + crdInfo := r.getServingInfoFor(crd) + storage := crdInfo.storage + requestScope := crdInfo.requestScope minRequestTimeout := 1 * time.Minute switch requestInfo.Verb { @@ -195,12 +198,12 @@ func (r *customResourceDefinitionHandler) ServeHTTP(w http.ResponseWriter, req * } // removeDeadStorage removes REST storage that isn't being used -func (r *customResourceDefinitionHandler) removeDeadStorage() { +func (r *crdHandler) removeDeadStorage() { // these don't have to be live. A snapshot is fine // if we wrongly delete, that's ok. The rest storage will be recreated on the next request // if we wrongly miss one, that's ok. We'll get it next time - storageMap := r.customStorage.Load().(customResourceDefinitionStorageMap) - allCustomResourceDefinitions, err := r.customResourceDefinitionLister.List(labels.Everything()) + storageMap := r.customStorage.Load().(crdStorageMap) + allCustomResourceDefinitions, err := r.crdLister.List(labels.Everything()) if err != nil { utilruntime.HandleError(err) return @@ -208,8 +211,8 @@ func (r *customResourceDefinitionHandler) removeDeadStorage() { for uid := range storageMap { found := false - for _, customResourceDefinition := range allCustomResourceDefinitions { - if customResourceDefinition.UID == uid { + for _, crd := range allCustomResourceDefinitions { + if crd.UID == uid { found = true break } @@ -225,9 +228,9 @@ func (r *customResourceDefinitionHandler) removeDeadStorage() { r.customStorage.Store(storageMap) } -func (r *customResourceDefinitionHandler) getServingInfoFor(customResourceDefinition *apiextensions.CustomResourceDefinition) *customResourceDefinitionInfo { - storageMap := r.customStorage.Load().(customResourceDefinitionStorageMap) - ret, ok := storageMap[customResourceDefinition.UID] +func (r *crdHandler) getServingInfoFor(crd *apiextensions.CustomResourceDefinition) *crdInfo { + storageMap := r.customStorage.Load().(crdStorageMap) + ret, ok := storageMap[crd.UID] if ok { return ret } @@ -235,21 +238,21 @@ func (r *customResourceDefinitionHandler) getServingInfoFor(customResourceDefini r.customStorageLock.Lock() defer r.customStorageLock.Unlock() - ret, ok = storageMap[customResourceDefinition.UID] + ret, ok = storageMap[crd.UID] if ok { return ret } storage := customresource.NewREST( - schema.GroupResource{Group: customResourceDefinition.Spec.Group, Resource: customResourceDefinition.Spec.Names.Plural}, - schema.GroupVersionKind{Group: customResourceDefinition.Spec.Group, Version: customResourceDefinition.Spec.Version, Kind: customResourceDefinition.Spec.Names.ListKind}, + schema.GroupResource{Group: crd.Spec.Group, Resource: crd.Spec.Names.Plural}, + schema.GroupVersionKind{Group: crd.Spec.Group, Version: crd.Spec.Version, Kind: crd.Spec.Names.ListKind}, UnstructuredCopier{}, - customresource.NewStrategy(discovery.NewUnstructuredObjectTyper(nil), customResourceDefinition.Spec.Scope == apiextensions.NamespaceScoped), + customresource.NewStrategy(discovery.NewUnstructuredObjectTyper(nil), crd.Spec.Scope == apiextensions.NamespaceScoped), r.restOptionsGetter, ) parameterScheme := runtime.NewScheme() - parameterScheme.AddUnversionedTypes(schema.GroupVersion{Group: customResourceDefinition.Spec.Group, Version: customResourceDefinition.Spec.Version}, + parameterScheme.AddUnversionedTypes(schema.GroupVersion{Group: crd.Spec.Group, Version: crd.Spec.Version}, &metav1.ListOptions{}, &metav1.ExportOptions{}, &metav1.GetOptions{}, @@ -259,11 +262,11 @@ func (r *customResourceDefinitionHandler) getServingInfoFor(customResourceDefini parameterCodec := runtime.NewParameterCodec(parameterScheme) selfLinkPrefix := "" - switch customResourceDefinition.Spec.Scope { + switch crd.Spec.Scope { case apiextensions.ClusterScoped: - selfLinkPrefix = "/" + path.Join("apis", customResourceDefinition.Spec.Group, customResourceDefinition.Spec.Version) + "/" + selfLinkPrefix = "/" + path.Join("apis", crd.Spec.Group, crd.Spec.Version) + "/" case apiextensions.NamespaceScoped: - selfLinkPrefix = "/" + path.Join("apis", customResourceDefinition.Spec.Group, customResourceDefinition.Spec.Version, "namespaces") + "/" + selfLinkPrefix = "/" + path.Join("apis", crd.Spec.Group, crd.Spec.Version, "namespaces") + "/" } requestScope := handlers.RequestScope{ @@ -273,7 +276,7 @@ func (r *customResourceDefinitionHandler) getServingInfoFor(customResourceDefini return ret }, SelfLinker: meta.NewAccessor(), - ClusterScoped: customResourceDefinition.Spec.Scope == apiextensions.ClusterScoped, + ClusterScoped: crd.Spec.Scope == apiextensions.ClusterScoped, SelfLinkPathPrefix: selfLinkPrefix, }, ContextFunc: func(req *http.Request) apirequest.Context { @@ -291,18 +294,18 @@ func (r *customResourceDefinitionHandler) getServingInfoFor(customResourceDefini Typer: discovery.NewUnstructuredObjectTyper(nil), UnsafeConvertor: unstructured.UnstructuredObjectConverter{}, - Resource: schema.GroupVersionResource{Group: customResourceDefinition.Spec.Group, Version: customResourceDefinition.Spec.Version, Resource: customResourceDefinition.Spec.Names.Plural}, - Kind: schema.GroupVersionKind{Group: customResourceDefinition.Spec.Group, Version: customResourceDefinition.Spec.Version, Kind: customResourceDefinition.Spec.Names.Kind}, + Resource: schema.GroupVersionResource{Group: crd.Spec.Group, Version: crd.Spec.Version, Resource: crd.Spec.Names.Plural}, + Kind: schema.GroupVersionKind{Group: crd.Spec.Group, Version: crd.Spec.Version, Kind: crd.Spec.Names.Kind}, Subresource: "", MetaGroupVersion: metav1.SchemeGroupVersion, } - ret = &customResourceDefinitionInfo{ + ret = &crdInfo{ storage: storage, requestScope: requestScope, } - storageMap[customResourceDefinition.UID] = ret + storageMap[crd.UID] = ret r.customStorage.Store(storageMap) return ret } diff --git a/staging/src/k8s.io/kube-apiextensions-server/pkg/controller/status/BUILD b/staging/src/k8s.io/kube-apiextensions-server/pkg/controller/status/BUILD new file mode 100644 index 00000000000..c1760d34d65 --- /dev/null +++ b/staging/src/k8s.io/kube-apiextensions-server/pkg/controller/status/BUILD @@ -0,0 +1,44 @@ +package(default_visibility = ["//visibility:public"]) + +licenses(["notice"]) + +load( + "@io_bazel_rules_go//go:def.bzl", + "go_library", + "go_test", +) + +go_test( + name = "go_default_test", + srcs = ["naming_controller_test.go"], + library = ":go_default_library", + tags = ["automanaged"], + deps = [ + "//vendor/k8s.io/apimachinery/pkg/apis/meta/v1:go_default_library", + "//vendor/k8s.io/client-go/tools/cache:go_default_library", + "//vendor/k8s.io/kube-apiextensions-server/pkg/apis/apiextensions:go_default_library", + "//vendor/k8s.io/kube-apiextensions-server/pkg/client/listers/apiextensions/internalversion:go_default_library", + ], +) + +go_library( + name = "go_default_library", + srcs = ["naming_controller.go"], + tags = ["automanaged"], + deps = [ + "//vendor/github.com/golang/glog:go_default_library", + "//vendor/k8s.io/apimachinery/pkg/api/errors:go_default_library", + "//vendor/k8s.io/apimachinery/pkg/conversion:go_default_library", + "//vendor/k8s.io/apimachinery/pkg/labels:go_default_library", + "//vendor/k8s.io/apimachinery/pkg/util/errors:go_default_library", + "//vendor/k8s.io/apimachinery/pkg/util/runtime:go_default_library", + "//vendor/k8s.io/apimachinery/pkg/util/sets:go_default_library", + "//vendor/k8s.io/apimachinery/pkg/util/wait:go_default_library", + "//vendor/k8s.io/client-go/tools/cache:go_default_library", + "//vendor/k8s.io/client-go/util/workqueue:go_default_library", + "//vendor/k8s.io/kube-apiextensions-server/pkg/apis/apiextensions:go_default_library", + "//vendor/k8s.io/kube-apiextensions-server/pkg/client/clientset/internalclientset/typed/apiextensions/internalversion:go_default_library", + "//vendor/k8s.io/kube-apiextensions-server/pkg/client/informers/internalversion/apiextensions/internalversion:go_default_library", + "//vendor/k8s.io/kube-apiextensions-server/pkg/client/listers/apiextensions/internalversion:go_default_library", + ], +) diff --git a/staging/src/k8s.io/kube-apiextensions-server/pkg/controller/status/naming_controller.go b/staging/src/k8s.io/kube-apiextensions-server/pkg/controller/status/naming_controller.go new file mode 100644 index 00000000000..77f3c887ec5 --- /dev/null +++ b/staging/src/k8s.io/kube-apiextensions-server/pkg/controller/status/naming_controller.go @@ -0,0 +1,337 @@ +/* +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 status + +import ( + "fmt" + "reflect" + "time" + + "github.com/golang/glog" + + apierrors "k8s.io/apimachinery/pkg/api/errors" + "k8s.io/apimachinery/pkg/conversion" + "k8s.io/apimachinery/pkg/labels" + utilerrors "k8s.io/apimachinery/pkg/util/errors" + utilruntime "k8s.io/apimachinery/pkg/util/runtime" + "k8s.io/apimachinery/pkg/util/sets" + "k8s.io/apimachinery/pkg/util/wait" + "k8s.io/client-go/tools/cache" + "k8s.io/client-go/util/workqueue" + + "k8s.io/kube-apiextensions-server/pkg/apis/apiextensions" + client "k8s.io/kube-apiextensions-server/pkg/client/clientset/internalclientset/typed/apiextensions/internalversion" + informers "k8s.io/kube-apiextensions-server/pkg/client/informers/internalversion/apiextensions/internalversion" + listers "k8s.io/kube-apiextensions-server/pkg/client/listers/apiextensions/internalversion" +) + +var cloner = conversion.NewCloner() + +// This controller is reserving names. To avoid conflicts, be sure to run only one instance of the worker at a time. +// This could eventually be lifted, but starting simple. +type NamingConditionController struct { + crdClient client.CustomResourceDefinitionsGetter + + crdLister listers.CustomResourceDefinitionLister + crdSynced cache.InformerSynced + // crdMutationCache backs our lister and keeps track of committed updates to avoid racy + // write/lookup cycles. It's got 100 slots by default, so it unlikely to overrun + // TODO to revisit this if naming conflicts are found to occur in the wild + crdMutationCache cache.MutationCache + + // To allow injection for testing. + syncFn func(key string) error + + queue workqueue.RateLimitingInterface +} + +func NewNamingConditionController( + crdInformer informers.CustomResourceDefinitionInformer, + crdClient client.CustomResourceDefinitionsGetter, +) *NamingConditionController { + c := &NamingConditionController{ + crdClient: crdClient, + crdLister: crdInformer.Lister(), + crdSynced: crdInformer.Informer().HasSynced, + queue: workqueue.NewNamedRateLimitingQueue(workqueue.DefaultControllerRateLimiter(), "CustomResourceDefinition-NamingConditionController"), + } + + informerIndexer := crdInformer.Informer().GetIndexer() + c.crdMutationCache = cache.NewIntegerResourceVersionMutationCache(informerIndexer, informerIndexer) + + crdInformer.Informer().AddEventHandler(cache.ResourceEventHandlerFuncs{ + AddFunc: c.addCustomResourceDefinition, + UpdateFunc: c.updateCustomResourceDefinition, + DeleteFunc: c.deleteCustomResourceDefinition, + }) + + c.syncFn = c.sync + + return c +} + +func (c *NamingConditionController) getAcceptedNamesForGroup(group string) (allResources sets.String, allKinds sets.String) { + allResources = sets.String{} + allKinds = sets.String{} + + list, err := c.crdLister.List(labels.Everything()) + if err != nil { + panic(err) + } + + for _, curr := range list { + if curr.Spec.Group != group { + continue + } + + // for each item here, see if we have a mutation cache entry that is more recent + // this makes sure that if we tight loop on update and run, our mutation cache will show + // us the version of the objects we just updated to. + item := curr + obj, exists, err := c.crdMutationCache.GetByKey(curr.Name) + if exists && err == nil { + item = obj.(*apiextensions.CustomResourceDefinition) + } + + allResources.Insert(item.Status.AcceptedNames.Plural) + allResources.Insert(item.Status.AcceptedNames.Singular) + allResources.Insert(item.Status.AcceptedNames.ShortNames...) + + allKinds.Insert(item.Status.AcceptedNames.Kind) + allKinds.Insert(item.Status.AcceptedNames.ListKind) + } + + return allResources, allKinds +} + +func (c *NamingConditionController) calculateNames(in *apiextensions.CustomResourceDefinition) (apiextensions.CustomResourceDefinitionNames, apiextensions.CustomResourceDefinitionCondition) { + // Get the names that have already been claimed + allResources, allKinds := c.getAcceptedNamesForGroup(in.Spec.Group) + + condition := apiextensions.CustomResourceDefinitionCondition{ + Type: apiextensions.NameConflict, + Status: apiextensions.ConditionUnknown, + } + + requestedNames := in.Spec.Names + acceptedNames := in.Status.AcceptedNames + newNames := in.Status.AcceptedNames + + // Check each name for mismatches. If there's a mismatch between spec and status, then try to deconflict. + // Continue on errors so that the status is the best match possible + if err := equalToAcceptedOrFresh(requestedNames.Plural, acceptedNames.Plural, allResources); err != nil { + condition.Status = apiextensions.ConditionTrue + condition.Reason = "Plural" + condition.Message = err.Error() + } else { + newNames.Plural = requestedNames.Plural + } + if err := equalToAcceptedOrFresh(requestedNames.Singular, acceptedNames.Singular, allResources); err != nil { + condition.Status = apiextensions.ConditionTrue + condition.Reason = "Singular" + condition.Message = err.Error() + } else { + newNames.Singular = requestedNames.Singular + } + if !reflect.DeepEqual(requestedNames.ShortNames, acceptedNames.ShortNames) { + errs := []error{} + existingShortNames := sets.NewString(acceptedNames.ShortNames...) + for _, shortName := range requestedNames.ShortNames { + // if the shortname is already ours, then we're fine + if existingShortNames.Has(shortName) { + continue + } + if err := equalToAcceptedOrFresh(shortName, "", allResources); err != nil { + errs = append(errs, err) + } + + } + if err := utilerrors.NewAggregate(errs); err != nil { + condition.Status = apiextensions.ConditionTrue + condition.Reason = "ShortNames" + condition.Message = err.Error() + } else { + newNames.ShortNames = requestedNames.ShortNames + } + } + + if err := equalToAcceptedOrFresh(requestedNames.Kind, acceptedNames.Kind, allKinds); err != nil { + condition.Status = apiextensions.ConditionTrue + condition.Reason = "Kind" + condition.Message = err.Error() + } else { + newNames.Kind = requestedNames.Kind + } + if err := equalToAcceptedOrFresh(requestedNames.ListKind, acceptedNames.ListKind, allKinds); err != nil { + condition.Status = apiextensions.ConditionTrue + condition.Reason = "ListKind" + condition.Message = err.Error() + } else { + newNames.ListKind = requestedNames.ListKind + } + + // if we haven't changed the condition, then our names must be good. + if condition.Status == apiextensions.ConditionUnknown { + condition.Status = apiextensions.ConditionFalse + condition.Reason = "NoConflicts" + condition.Message = "no conflicts found" + } + + return newNames, condition +} + +func equalToAcceptedOrFresh(requestedName, acceptedName string, usedNames sets.String) error { + if requestedName == acceptedName { + return nil + } + if !usedNames.Has(requestedName) { + return nil + } + + return fmt.Errorf("%q is already in use", requestedName) +} + +func (c *NamingConditionController) sync(key string) error { + inCustomResourceDefinition, err := c.crdLister.Get(key) + if apierrors.IsNotFound(err) { + return nil + } + if err != nil { + return err + } + + acceptedNames, namingCondition := c.calculateNames(inCustomResourceDefinition) + // nothing to do if accepted names and NameConflict condition didn't change + if reflect.DeepEqual(inCustomResourceDefinition.Status.AcceptedNames, acceptedNames) && + apiextensions.IsCRDConditionEquivalent( + &namingCondition, + apiextensions.FindCRDCondition(inCustomResourceDefinition, apiextensions.NameConflict)) { + return nil + } + + crd := &apiextensions.CustomResourceDefinition{} + if err := apiextensions.DeepCopy_apiextensions_CustomResourceDefinition(inCustomResourceDefinition, crd, cloner); err != nil { + return err + } + + crd.Status.AcceptedNames = acceptedNames + apiextensions.SetCRDCondition(crd, namingCondition) + + updatedObj, err := c.crdClient.CustomResourceDefinitions().UpdateStatus(crd) + if err != nil { + return err + } + + // if the update was successful, go ahead and add the entry to the mutation cache + c.crdMutationCache.Mutation(updatedObj) + + // we updated our status, so we may be releasing a name. When this happens, we need to rekick everything in our group + // if we fail to rekick, just return as normal. We'll get everything on a resync + list, err := c.crdLister.List(labels.Everything()) + if err != nil { + return nil + } + for _, curr := range list { + if curr.Spec.Group == crd.Spec.Group { + c.queue.Add(curr.Name) + } + } + + return nil +} + +func (c *NamingConditionController) Run(stopCh <-chan struct{}) { + defer utilruntime.HandleCrash() + defer c.queue.ShutDown() + + glog.Infof("Starting NamingConditionController") + defer glog.Infof("Shutting down NamingConditionController") + + if !cache.WaitForCacheSync(stopCh, c.crdSynced) { + return + } + + // only start one worker thread since its a slow moving API and the naming conflict resolution bits aren't thread-safe + go wait.Until(c.runWorker, time.Second, stopCh) + + <-stopCh +} + +func (c *NamingConditionController) runWorker() { + for c.processNextWorkItem() { + } +} + +// processNextWorkItem deals with one key off the queue. It returns false when it's time to quit. +func (c *NamingConditionController) processNextWorkItem() bool { + key, quit := c.queue.Get() + if quit { + return false + } + defer c.queue.Done(key) + + err := c.syncFn(key.(string)) + if err == nil { + c.queue.Forget(key) + return true + } + + utilruntime.HandleError(fmt.Errorf("%v failed with: %v", key, err)) + c.queue.AddRateLimited(key) + + return true +} + +func (c *NamingConditionController) enqueue(obj *apiextensions.CustomResourceDefinition) { + key, err := cache.DeletionHandlingMetaNamespaceKeyFunc(obj) + if err != nil { + utilruntime.HandleError(fmt.Errorf("Couldn't get key for object %#v: %v", obj, err)) + return + } + + c.queue.Add(key) +} + +func (c *NamingConditionController) addCustomResourceDefinition(obj interface{}) { + castObj := obj.(*apiextensions.CustomResourceDefinition) + glog.V(4).Infof("Adding %s", castObj.Name) + c.enqueue(castObj) +} + +func (c *NamingConditionController) updateCustomResourceDefinition(obj, _ interface{}) { + castObj := obj.(*apiextensions.CustomResourceDefinition) + glog.V(4).Infof("Updating %s", castObj.Name) + c.enqueue(castObj) +} + +func (c *NamingConditionController) deleteCustomResourceDefinition(obj interface{}) { + castObj, ok := obj.(*apiextensions.CustomResourceDefinition) + if !ok { + tombstone, ok := obj.(cache.DeletedFinalStateUnknown) + if !ok { + glog.Errorf("Couldn't get object from tombstone %#v", obj) + return + } + castObj, ok = tombstone.Obj.(*apiextensions.CustomResourceDefinition) + if !ok { + glog.Errorf("Tombstone contained object that is not expected %#v", obj) + return + } + } + glog.V(4).Infof("Deleting %q", castObj.Name) + c.enqueue(castObj) +} diff --git a/staging/src/k8s.io/kube-apiextensions-server/pkg/controller/status/naming_controller_test.go b/staging/src/k8s.io/kube-apiextensions-server/pkg/controller/status/naming_controller_test.go new file mode 100644 index 00000000000..774015187f5 --- /dev/null +++ b/staging/src/k8s.io/kube-apiextensions-server/pkg/controller/status/naming_controller_test.go @@ -0,0 +1,256 @@ +/* +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 status + +import ( + "reflect" + "strings" + "testing" + + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/client-go/tools/cache" + "k8s.io/kube-apiextensions-server/pkg/apis/apiextensions" + listers "k8s.io/kube-apiextensions-server/pkg/client/listers/apiextensions/internalversion" +) + +type crdBuilder struct { + curr apiextensions.CustomResourceDefinition +} + +func newCRD(name string) *crdBuilder { + tokens := strings.SplitN(name, ".", 2) + return &crdBuilder{ + curr: apiextensions.CustomResourceDefinition{ + ObjectMeta: metav1.ObjectMeta{Name: name}, + Spec: apiextensions.CustomResourceDefinitionSpec{ + Group: tokens[1], + Names: apiextensions.CustomResourceDefinitionNames{ + Plural: tokens[0], + }, + }, + }, + } +} + +func (b *crdBuilder) SpecNames(plural, singular, kind, listKind string, shortNames ...string) *crdBuilder { + b.curr.Spec.Names.Plural = plural + b.curr.Spec.Names.Singular = singular + b.curr.Spec.Names.Kind = kind + b.curr.Spec.Names.ListKind = listKind + b.curr.Spec.Names.ShortNames = shortNames + + return b +} + +func (b *crdBuilder) StatusNames(plural, singular, kind, listKind string, shortNames ...string) *crdBuilder { + b.curr.Status.AcceptedNames.Plural = plural + b.curr.Status.AcceptedNames.Singular = singular + b.curr.Status.AcceptedNames.Kind = kind + b.curr.Status.AcceptedNames.ListKind = listKind + b.curr.Status.AcceptedNames.ShortNames = shortNames + + return b +} + +func names(plural, singular, kind, listKind string, shortNames ...string) apiextensions.CustomResourceDefinitionNames { + ret := apiextensions.CustomResourceDefinitionNames{ + Plural: plural, + Singular: singular, + Kind: kind, + ListKind: listKind, + ShortNames: shortNames, + } + return ret +} + +func (b *crdBuilder) NewOrDie() *apiextensions.CustomResourceDefinition { + return &b.curr +} + +var goodCondition = apiextensions.CustomResourceDefinitionCondition{ + Type: apiextensions.NameConflict, + Status: apiextensions.ConditionFalse, + Reason: "NoConflicts", + Message: "no conflicts found", +} + +func badCondition(reason, message string) apiextensions.CustomResourceDefinitionCondition { + return apiextensions.CustomResourceDefinitionCondition{ + Type: apiextensions.NameConflict, + Status: apiextensions.ConditionTrue, + Reason: reason, + Message: message, + } +} + +func TestSync(t *testing.T) { + tests := []struct { + name string + + in *apiextensions.CustomResourceDefinition + existing []*apiextensions.CustomResourceDefinition + expectedNames apiextensions.CustomResourceDefinitionNames + expectedCondition apiextensions.CustomResourceDefinitionCondition + }{ + { + name: "first resource", + in: newCRD("alfa.bravo.com").NewOrDie(), + existing: []*apiextensions.CustomResourceDefinition{}, + expectedNames: apiextensions.CustomResourceDefinitionNames{ + Plural: "alfa", + }, + expectedCondition: goodCondition, + }, + { + name: "different groups", + in: newCRD("alfa.bravo.com").SpecNames("alfa", "delta-singular", "echo-kind", "foxtrot-listkind", "golf-shortname-1", "hotel-shortname-2").NewOrDie(), + existing: []*apiextensions.CustomResourceDefinition{ + newCRD("alfa.charlie.com").StatusNames("alfa", "delta-singular", "echo-kind", "foxtrot-listkind", "golf-shortname-1", "hotel-shortname-2").NewOrDie(), + }, + expectedNames: names("alfa", "delta-singular", "echo-kind", "foxtrot-listkind", "golf-shortname-1", "hotel-shortname-2"), + expectedCondition: goodCondition, + }, + { + name: "conflict plural to singular", + in: newCRD("alfa.bravo.com").SpecNames("alfa", "delta-singular", "echo-kind", "foxtrot-listkind", "golf-shortname-1", "hotel-shortname-2").NewOrDie(), + existing: []*apiextensions.CustomResourceDefinition{ + newCRD("india.bravo.com").StatusNames("india", "alfa", "", "").NewOrDie(), + }, + expectedNames: names("", "delta-singular", "echo-kind", "foxtrot-listkind", "golf-shortname-1", "hotel-shortname-2"), + expectedCondition: badCondition("Plural", `"alfa" is already in use`), + }, + { + name: "conflict singular to shortName", + in: newCRD("alfa.bravo.com").SpecNames("alfa", "delta-singular", "echo-kind", "foxtrot-listkind", "golf-shortname-1", "hotel-shortname-2").NewOrDie(), + existing: []*apiextensions.CustomResourceDefinition{ + newCRD("india.bravo.com").StatusNames("india", "indias", "", "", "delta-singular").NewOrDie(), + }, + expectedNames: names("alfa", "", "echo-kind", "foxtrot-listkind", "golf-shortname-1", "hotel-shortname-2"), + expectedCondition: badCondition("Singular", `"delta-singular" is already in use`), + }, + { + name: "conflict on shortName to shortName", + in: newCRD("alfa.bravo.com").SpecNames("alfa", "delta-singular", "echo-kind", "foxtrot-listkind", "golf-shortname-1", "hotel-shortname-2").NewOrDie(), + existing: []*apiextensions.CustomResourceDefinition{ + newCRD("india.bravo.com").StatusNames("india", "indias", "", "", "hotel-shortname-2").NewOrDie(), + }, + expectedNames: names("alfa", "delta-singular", "echo-kind", "foxtrot-listkind"), + expectedCondition: badCondition("ShortNames", `"hotel-shortname-2" is already in use`), + }, + { + name: "conflict on kind to listkind", + in: newCRD("alfa.bravo.com").SpecNames("alfa", "delta-singular", "echo-kind", "foxtrot-listkind", "golf-shortname-1", "hotel-shortname-2").NewOrDie(), + existing: []*apiextensions.CustomResourceDefinition{ + newCRD("india.bravo.com").StatusNames("india", "indias", "", "echo-kind").NewOrDie(), + }, + expectedNames: names("alfa", "delta-singular", "", "foxtrot-listkind", "golf-shortname-1", "hotel-shortname-2"), + expectedCondition: badCondition("Kind", `"echo-kind" is already in use`), + }, + { + name: "conflict on listkind to kind", + in: newCRD("alfa.bravo.com").SpecNames("alfa", "delta-singular", "echo-kind", "foxtrot-listkind", "golf-shortname-1", "hotel-shortname-2").NewOrDie(), + existing: []*apiextensions.CustomResourceDefinition{ + newCRD("india.bravo.com").StatusNames("india", "indias", "foxtrot-listkind", "").NewOrDie(), + }, + expectedNames: names("alfa", "delta-singular", "echo-kind", "", "golf-shortname-1", "hotel-shortname-2"), + expectedCondition: badCondition("ListKind", `"foxtrot-listkind" is already in use`), + }, + { + name: "no conflict on resource and kind", + in: newCRD("alfa.bravo.com").SpecNames("alfa", "delta-singular", "echo-kind", "foxtrot-listkind", "golf-shortname-1", "hotel-shortname-2").NewOrDie(), + existing: []*apiextensions.CustomResourceDefinition{ + newCRD("india.bravo.com").StatusNames("india", "echo-kind", "", "").NewOrDie(), + }, + expectedNames: names("alfa", "delta-singular", "echo-kind", "foxtrot-listkind", "golf-shortname-1", "hotel-shortname-2"), + expectedCondition: goodCondition, + }, + { + name: "merge on conflicts", + in: newCRD("alfa.bravo.com"). + SpecNames("alfa", "delta-singular", "echo-kind", "foxtrot-listkind", "golf-shortname-1", "hotel-shortname-2"). + StatusNames("zulu", "yankee-singular", "xray-kind", "whiskey-listkind", "victor-shortname-1", "uniform-shortname-2"). + NewOrDie(), + existing: []*apiextensions.CustomResourceDefinition{ + newCRD("india.bravo.com").StatusNames("india", "indias", "foxtrot-listkind", "", "delta-singular").NewOrDie(), + }, + expectedNames: names("alfa", "yankee-singular", "echo-kind", "whiskey-listkind", "golf-shortname-1", "hotel-shortname-2"), + expectedCondition: badCondition("ListKind", `"foxtrot-listkind" is already in use`), + }, + { + name: "merge on conflicts shortNames as one", + in: newCRD("alfa.bravo.com"). + SpecNames("alfa", "delta-singular", "echo-kind", "foxtrot-listkind", "golf-shortname-1", "hotel-shortname-2"). + StatusNames("zulu", "yankee-singular", "xray-kind", "whiskey-listkind", "victor-shortname-1", "uniform-shortname-2"). + NewOrDie(), + existing: []*apiextensions.CustomResourceDefinition{ + newCRD("india.bravo.com").StatusNames("india", "indias", "foxtrot-listkind", "", "delta-singular", "golf-shortname-1").NewOrDie(), + }, + expectedNames: names("alfa", "yankee-singular", "echo-kind", "whiskey-listkind", "victor-shortname-1", "uniform-shortname-2"), + expectedCondition: badCondition("ListKind", `"foxtrot-listkind" is already in use`), + }, + { + name: "no conflicts on self", + in: newCRD("alfa.bravo.com"). + SpecNames("alfa", "delta-singular", "echo-kind", "foxtrot-listkind", "golf-shortname-1", "hotel-shortname-2"). + StatusNames("alfa", "delta-singular", "echo-kind", "foxtrot-listkind", "golf-shortname-1", "hotel-shortname-2"). + NewOrDie(), + existing: []*apiextensions.CustomResourceDefinition{ + newCRD("alfa.bravo.com"). + SpecNames("alfa", "delta-singular", "echo-kind", "foxtrot-listkind", "golf-shortname-1", "hotel-shortname-2"). + StatusNames("alfa", "delta-singular", "echo-kind", "foxtrot-listkind", "golf-shortname-1", "hotel-shortname-2"). + NewOrDie(), + }, + expectedNames: names("alfa", "delta-singular", "echo-kind", "foxtrot-listkind", "golf-shortname-1", "hotel-shortname-2"), + expectedCondition: goodCondition, + }, + { + name: "no conflicts on self, remove shortname", + in: newCRD("alfa.bravo.com"). + SpecNames("alfa", "delta-singular", "echo-kind", "foxtrot-listkind", "golf-shortname-1"). + StatusNames("alfa", "delta-singular", "echo-kind", "foxtrot-listkind", "golf-shortname-1", "hotel-shortname-2"). + NewOrDie(), + existing: []*apiextensions.CustomResourceDefinition{ + newCRD("alfa.bravo.com"). + SpecNames("alfa", "delta-singular", "echo-kind", "foxtrot-listkind", "golf-shortname-1", "hotel-shortname-2"). + StatusNames("alfa", "delta-singular", "echo-kind", "foxtrot-listkind", "golf-shortname-1", "hotel-shortname-2"). + NewOrDie(), + }, + expectedNames: names("alfa", "delta-singular", "echo-kind", "foxtrot-listkind", "golf-shortname-1"), + expectedCondition: goodCondition, + }, + } + + for _, tc := range tests { + crdIndexer := cache.NewIndexer(cache.MetaNamespaceKeyFunc, cache.Indexers{cache.NamespaceIndex: cache.MetaNamespaceIndexFunc}) + for _, obj := range tc.existing { + crdIndexer.Add(obj) + } + + c := NamingConditionController{ + crdLister: listers.NewCustomResourceDefinitionLister(crdIndexer), + crdMutationCache: cache.NewIntegerResourceVersionMutationCache(crdIndexer, crdIndexer), + } + actualNames, actualCondition := c.calculateNames(tc.in) + + if e, a := tc.expectedNames, actualNames; !reflect.DeepEqual(e, a) { + t.Errorf("%v expected %v, got %#v", tc.name, e, a) + } + if e, a := tc.expectedCondition, actualCondition; !apiextensions.IsCRDConditionEquivalent(&e, &a) { + t.Errorf("%v expected %v, got %v", tc.name, e, a) + } + } +} diff --git a/staging/src/k8s.io/kube-apiextensions-server/pkg/registry/customresourcedefinition/BUILD b/staging/src/k8s.io/kube-apiextensions-server/pkg/registry/customresourcedefinition/BUILD index 9ab5ce3118e..ade49d817d3 100644 --- a/staging/src/k8s.io/kube-apiextensions-server/pkg/registry/customresourcedefinition/BUILD +++ b/staging/src/k8s.io/kube-apiextensions-server/pkg/registry/customresourcedefinition/BUILD @@ -22,6 +22,7 @@ go_library( "//vendor/k8s.io/apiserver/pkg/endpoints/request:go_default_library", "//vendor/k8s.io/apiserver/pkg/registry/generic:go_default_library", "//vendor/k8s.io/apiserver/pkg/registry/generic/registry:go_default_library", + "//vendor/k8s.io/apiserver/pkg/registry/rest:go_default_library", "//vendor/k8s.io/apiserver/pkg/storage:go_default_library", "//vendor/k8s.io/apiserver/pkg/storage/names:go_default_library", "//vendor/k8s.io/kube-apiextensions-server/pkg/apis/apiextensions:go_default_library", diff --git a/staging/src/k8s.io/kube-apiextensions-server/pkg/registry/customresourcedefinition/etcd.go b/staging/src/k8s.io/kube-apiextensions-server/pkg/registry/customresourcedefinition/etcd.go index 4d1e56687ec..d19bffd1842 100644 --- a/staging/src/k8s.io/kube-apiextensions-server/pkg/registry/customresourcedefinition/etcd.go +++ b/staging/src/k8s.io/kube-apiextensions-server/pkg/registry/customresourcedefinition/etcd.go @@ -18,8 +18,10 @@ package customresourcedefinition import ( "k8s.io/apimachinery/pkg/runtime" + genericapirequest "k8s.io/apiserver/pkg/endpoints/request" "k8s.io/apiserver/pkg/registry/generic" genericregistry "k8s.io/apiserver/pkg/registry/generic/registry" + "k8s.io/apiserver/pkg/registry/rest" "k8s.io/kube-apiextensions-server/pkg/apis/apiextensions" ) @@ -52,3 +54,28 @@ func NewREST(scheme *runtime.Scheme, optsGetter generic.RESTOptionsGetter) *REST } return &REST{store} } + +// NewStatusREST makes a RESTStorage for status that has more limited options. +// It is based on the original REST so that we can share the same underlying store +func NewStatusREST(scheme *runtime.Scheme, rest *REST) *StatusREST { + statusStore := *rest.Store + statusStore.CreateStrategy = nil + statusStore.DeleteStrategy = nil + statusStore.UpdateStrategy = NewStatusStrategy(scheme) + return &StatusREST{store: &statusStore} +} + +type StatusREST struct { + store *genericregistry.Store +} + +var _ = rest.Updater(&StatusREST{}) + +func (r *StatusREST) New() runtime.Object { + return &apiextensions.CustomResourceDefinition{} +} + +// Update alters the status subset of an object. +func (r *StatusREST) Update(ctx genericapirequest.Context, name string, objInfo rest.UpdatedObjectInfo) (runtime.Object, bool, error) { + return r.store.Update(ctx, name, objInfo) +} diff --git a/staging/src/k8s.io/kube-apiextensions-server/pkg/registry/customresourcedefinition/strategy.go b/staging/src/k8s.io/kube-apiextensions-server/pkg/registry/customresourcedefinition/strategy.go index cee33421138..c29fe8144c2 100644 --- a/staging/src/k8s.io/kube-apiextensions-server/pkg/registry/customresourcedefinition/strategy.go +++ b/staging/src/k8s.io/kube-apiextensions-server/pkg/registry/customresourcedefinition/strategy.go @@ -32,44 +32,82 @@ import ( "k8s.io/kube-apiextensions-server/pkg/apis/apiextensions/validation" ) -type apiServerStrategy struct { +type strategy struct { runtime.ObjectTyper names.NameGenerator } -func NewStrategy(typer runtime.ObjectTyper) apiServerStrategy { - return apiServerStrategy{typer, names.SimpleNameGenerator} +func NewStrategy(typer runtime.ObjectTyper) strategy { + return strategy{typer, names.SimpleNameGenerator} } -func (apiServerStrategy) NamespaceScoped() bool { +func (strategy) NamespaceScoped() bool { return false } -func (apiServerStrategy) PrepareForCreate(ctx genericapirequest.Context, obj runtime.Object) { +func (strategy) PrepareForCreate(ctx genericapirequest.Context, obj runtime.Object) { } -func (apiServerStrategy) PrepareForUpdate(ctx genericapirequest.Context, obj, old runtime.Object) { +func (strategy) PrepareForUpdate(ctx genericapirequest.Context, obj, old runtime.Object) { } -func (apiServerStrategy) Validate(ctx genericapirequest.Context, obj runtime.Object) field.ErrorList { +func (strategy) Validate(ctx genericapirequest.Context, obj runtime.Object) field.ErrorList { return validation.ValidateCustomResourceDefinition(obj.(*apiextensions.CustomResourceDefinition)) } -func (apiServerStrategy) AllowCreateOnUpdate() bool { +func (strategy) AllowCreateOnUpdate() bool { return false } -func (apiServerStrategy) AllowUnconditionalUpdate() bool { +func (strategy) AllowUnconditionalUpdate() bool { return false } -func (apiServerStrategy) Canonicalize(obj runtime.Object) { +func (strategy) Canonicalize(obj runtime.Object) { } -func (apiServerStrategy) ValidateUpdate(ctx genericapirequest.Context, obj, old runtime.Object) field.ErrorList { +func (strategy) ValidateUpdate(ctx genericapirequest.Context, obj, old runtime.Object) field.ErrorList { return validation.ValidateCustomResourceDefinitionUpdate(obj.(*apiextensions.CustomResourceDefinition), old.(*apiextensions.CustomResourceDefinition)) } +type statusStrategy struct { + runtime.ObjectTyper + names.NameGenerator +} + +func NewStatusStrategy(typer runtime.ObjectTyper) statusStrategy { + return statusStrategy{typer, names.SimpleNameGenerator} +} + +func (statusStrategy) NamespaceScoped() bool { + return false +} + +func (statusStrategy) PrepareForUpdate(ctx genericapirequest.Context, obj, old runtime.Object) { + newObj := obj.(*apiextensions.CustomResourceDefinition) + oldObj := old.(*apiextensions.CustomResourceDefinition) + newObj.Spec = oldObj.Spec + newObj.Labels = oldObj.Labels + newObj.Annotations = oldObj.Annotations + newObj.Finalizers = oldObj.Finalizers + newObj.OwnerReferences = oldObj.OwnerReferences +} + +func (statusStrategy) AllowCreateOnUpdate() bool { + return false +} + +func (statusStrategy) AllowUnconditionalUpdate() bool { + return false +} + +func (statusStrategy) Canonicalize(obj runtime.Object) { +} + +func (statusStrategy) ValidateUpdate(ctx genericapirequest.Context, obj, old runtime.Object) field.ErrorList { + return validation.ValidateUpdateCustomResourceDefinitionStatus(obj.(*apiextensions.CustomResourceDefinition), old.(*apiextensions.CustomResourceDefinition)) +} + func GetAttrs(obj runtime.Object) (labels.Set, fields.Set, error) { apiserver, ok := obj.(*apiextensions.CustomResourceDefinition) if !ok {