mirror of
https://github.com/k3s-io/kubernetes.git
synced 2025-07-29 06:27:05 +00:00
start using customresourcedefinition.status
This commit is contained in:
parent
f88c7725b4
commit
cb604f756a
@ -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",
|
||||
|
@ -1,5 +1,5 @@
|
||||
apiVersion: apiextensions.k8s.io/v1alpha1
|
||||
kind: CustomResource
|
||||
kind: CustomResourceDefinition
|
||||
metadata:
|
||||
name: noxus.mygroup.example.com
|
||||
spec:
|
||||
|
@ -11,6 +11,7 @@ go_library(
|
||||
name = "go_default_library",
|
||||
srcs = [
|
||||
"doc.go",
|
||||
"helpers.go",
|
||||
"register.go",
|
||||
"types.go",
|
||||
"zz_generated.deepcopy.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
|
||||
}
|
@ -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",
|
||||
],
|
||||
|
@ -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
|
||||
})
|
||||
|
||||
|
@ -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
|
||||
}
|
||||
|
@ -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
|
||||
}
|
||||
|
@ -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",
|
||||
],
|
||||
)
|
@ -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)
|
||||
}
|
@ -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)
|
||||
}
|
||||
}
|
||||
}
|
@ -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",
|
||||
|
@ -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)
|
||||
}
|
||||
|
@ -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 {
|
||||
|
Loading…
Reference in New Issue
Block a user