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 index c5731fca96..299ddc22b4 100644 --- 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 @@ -1,5 +1,5 @@ /* -Copyright 2016 The Kubernetes Authors. +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. diff --git a/staging/src/k8s.io/kube-apiextensions-server/pkg/apis/apiextensions/helpers_test.go b/staging/src/k8s.io/kube-apiextensions-server/pkg/apis/apiextensions/helpers_test.go index 40e65b59a8..1abb498155 100644 --- a/staging/src/k8s.io/kube-apiextensions-server/pkg/apis/apiextensions/helpers_test.go +++ b/staging/src/k8s.io/kube-apiextensions-server/pkg/apis/apiextensions/helpers_test.go @@ -1,5 +1,5 @@ /* -Copyright 2016 The Kubernetes Authors. +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. 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 index b788ec7b0c..45053878f9 100644 --- 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 @@ -19,6 +19,7 @@ package status import ( "fmt" "reflect" + "strings" "time" "github.com/golang/glog" @@ -230,6 +231,11 @@ func equalToAcceptedOrFresh(requestedName, acceptedName string, usedNames sets.S func (c *NamingConditionController) sync(key string) error { inCustomResourceDefinition, err := c.crdLister.Get(key) if apierrors.IsNotFound(err) { + // CRD was deleted and has freed its names. + // Reconsider all other CRDs in the same group. + if err := c.requeueAllOtherGroupCRDs(key); err != nil { + return err + } return nil } if err != nil { @@ -264,14 +270,8 @@ func (c *NamingConditionController) sync(key string) error { // 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) - } + if err := c.requeueAllOtherGroupCRDs(key); err != nil { + return err } return nil @@ -358,3 +358,17 @@ func (c *NamingConditionController) deleteCustomResourceDefinition(obj interface glog.V(4).Infof("Deleting %q", castObj.Name) c.enqueue(castObj) } + +func (c *NamingConditionController) requeueAllOtherGroupCRDs(name string) error { + pluralGroup := strings.SplitN(name, ".", 2) + list, err := c.crdLister.List(labels.Everything()) + if err != nil { + return err + } + for _, curr := range list { + if curr.Spec.Group == pluralGroup[1] && curr.Name != name { + c.queue.Add(curr.Name) + } + } + return nil +} diff --git a/staging/src/k8s.io/kube-apiextensions-server/test/integration/BUILD b/staging/src/k8s.io/kube-apiextensions-server/test/integration/BUILD index ae598a317a..b1fc0c0720 100644 --- a/staging/src/k8s.io/kube-apiextensions-server/test/integration/BUILD +++ b/staging/src/k8s.io/kube-apiextensions-server/test/integration/BUILD @@ -24,6 +24,7 @@ go_test( "//vendor/k8s.io/apimachinery/pkg/apis/meta/v1:go_default_library", "//vendor/k8s.io/apimachinery/pkg/apis/meta/v1/unstructured:go_default_library", "//vendor/k8s.io/apimachinery/pkg/runtime:go_default_library", + "//vendor/k8s.io/apimachinery/pkg/util/wait:go_default_library", "//vendor/k8s.io/apimachinery/pkg/watch:go_default_library", "//vendor/k8s.io/client-go/dynamic:go_default_library", "//vendor/k8s.io/kube-apiextensions-server/pkg/apis/apiextensions/v1alpha1:go_default_library", diff --git a/staging/src/k8s.io/kube-apiextensions-server/test/integration/basic_test.go b/staging/src/k8s.io/kube-apiextensions-server/test/integration/basic_test.go index 7bc0939b15..4816657c36 100644 --- a/staging/src/k8s.io/kube-apiextensions-server/test/integration/basic_test.go +++ b/staging/src/k8s.io/kube-apiextensions-server/test/integration/basic_test.go @@ -26,6 +26,7 @@ import ( metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" "k8s.io/apimachinery/pkg/runtime" + "k8s.io/apimachinery/pkg/util/wait" "k8s.io/apimachinery/pkg/watch" "k8s.io/client-go/dynamic" apiextensionsv1alpha1 "k8s.io/kube-apiextensions-server/pkg/apis/apiextensions/v1alpha1" @@ -309,6 +310,7 @@ func TestSelfLink(t *testing.T) { } defer close(stopCh) + // namespace scoped noxuDefinition := testserver.NewNoxuCustomResourceDefinition(apiextensionsv1alpha1.NamespaceScoped) noxuVersionClient, err := testserver.CreateNewCustomResourceDefinition(noxuDefinition, apiExtensionClient, clientPool) if err != nil { @@ -318,7 +320,7 @@ func TestSelfLink(t *testing.T) { ns := "not-the-default" noxuNamespacedResourceClient := noxuVersionClient.Resource(&metav1.APIResource{ Name: noxuDefinition.Spec.Names.Plural, - Namespaced: true, + Namespaced: noxuDefinition.Spec.Scope == apiextensionsv1alpha1.NamespaceScoped, }, ns) noxuInstanceToCreate := testserver.NewNoxuInstance(ns, "foo") @@ -331,8 +333,27 @@ func TestSelfLink(t *testing.T) { t.Errorf("expected %v, got %v", e, a) } - // TODO add test for cluster scoped self-link when its available + // cluster scoped + curletDefinition := testserver.NewCurletCustomResourceDefinition(apiextensionsv1alpha1.ClusterScoped) + curletVersionClient, err := testserver.CreateNewCustomResourceDefinition(curletDefinition, apiExtensionClient, clientPool) + if err != nil { + t.Fatal(err) + } + curletResourceClient := curletVersionClient.Resource(&metav1.APIResource{ + Name: curletDefinition.Spec.Names.Plural, + Namespaced: curletDefinition.Spec.Scope == apiextensionsv1alpha1.NamespaceScoped, + }, ns) + + curletInstanceToCreate := testserver.NewCurletInstance(ns, "foo") + createdCurletInstance, err := curletResourceClient.Create(curletInstanceToCreate) + if err != nil { + t.Fatal(err) + } + + if e, a := "/apis/mygroup.example.com/v1alpha1/foo", createdCurletInstance.GetSelfLink(); e != a { + t.Errorf("expected %v, got %v", e, a) + } } func TestPreserveInt(t *testing.T) { @@ -515,3 +536,64 @@ func checkNamespacesWatchHelper(t *testing.T, ns string, namespacedwatch watch.I namespacedAddEvent++ } } + +func TestNameConflict(t *testing.T) { + stopCh, apiExtensionClient, clientPool, err := testserver.StartDefaultServer() + if err != nil { + t.Fatal(err) + } + defer close(stopCh) + + noxuDefinition := testserver.NewNoxuCustomResourceDefinition(apiextensionsv1alpha1.NamespaceScoped) + _, err = testserver.CreateNewCustomResourceDefinition(noxuDefinition, apiExtensionClient, clientPool) + if err != nil { + t.Fatal(err) + } + + noxu2Definition := testserver.NewNoxu2CustomResourceDefinition(apiextensionsv1alpha1.NamespaceScoped) + _, err = apiExtensionClient.Apiextensions().CustomResourceDefinitions().Create(noxu2Definition) + if err != nil { + t.Fatal(err) + } + + // A NameConflict occurs + err = wait.Poll(500*time.Millisecond, wait.ForeverTestTimeout, func() (bool, error) { + crd, err := testserver.GetCustomResourceDefinition(noxu2Definition, apiExtensionClient) + if err != nil { + return false, err + } + + for _, condition := range crd.Status.Conditions { + if condition.Type == apiextensionsv1alpha1.NamesAccepted && condition.Status == apiextensionsv1alpha1.ConditionFalse { + return true, nil + } + } + return false, nil + }) + if err != nil { + t.Fatal(err) + } + + err = testserver.DeleteCustomResourceDefinition(noxuDefinition, apiExtensionClient) + if err != nil { + t.Fatal(err) + } + + // Names are now accepted + err = wait.Poll(500*time.Millisecond, wait.ForeverTestTimeout, func() (bool, error) { + crd, err := testserver.GetCustomResourceDefinition(noxu2Definition, apiExtensionClient) + if err != nil { + return false, err + } + + for _, condition := range crd.Status.Conditions { + if condition.Type == apiextensionsv1alpha1.NamesAccepted && condition.Status == apiextensionsv1alpha1.ConditionTrue { + return true, nil + } + } + return false, nil + }) + if err != nil { + t.Fatal(err) + } +} diff --git a/staging/src/k8s.io/kube-apiextensions-server/test/integration/testserver/resources.go b/staging/src/k8s.io/kube-apiextensions-server/test/integration/testserver/resources.go index 519cb5e9d8..720338f25f 100644 --- a/staging/src/k8s.io/kube-apiextensions-server/test/integration/testserver/resources.go +++ b/staging/src/k8s.io/kube-apiextensions-server/test/integration/testserver/resources.go @@ -67,6 +67,24 @@ func NewNoxuInstance(namespace, name string) *unstructured.Unstructured { } } +func NewNoxu2CustomResourceDefinition(scope apiextensionsv1alpha1.ResourceScope) *apiextensionsv1alpha1.CustomResourceDefinition { + return &apiextensionsv1alpha1.CustomResourceDefinition{ + ObjectMeta: metav1.ObjectMeta{Name: "noxus2.mygroup.example.com"}, + Spec: apiextensionsv1alpha1.CustomResourceDefinitionSpec{ + Group: "mygroup.example.com", + Version: "v1alpha1", + Names: apiextensionsv1alpha1.CustomResourceDefinitionNames{ + Plural: "noxus2", + Singular: "nonenglishnoxu2", + Kind: "WishIHadChosenNoxu2", + ShortNames: []string{"foo", "bar", "abc", "def"}, + ListKind: "Noxu2ItemList", + }, + Scope: scope, + }, + } +} + func NewCurletCustomResourceDefinition(scope apiextensionsv1alpha1.ResourceScope) *apiextensionsv1alpha1.CustomResourceDefinition { return &apiextensionsv1alpha1.CustomResourceDefinition{ ObjectMeta: metav1.ObjectMeta{Name: "curlets.mygroup.example.com"}, @@ -100,20 +118,20 @@ func NewCurletInstance(namespace, name string) *unstructured.Unstructured { } } -func CreateNewCustomResourceDefinition(customResourceDefinition *apiextensionsv1alpha1.CustomResourceDefinition, apiExtensionsClient clientset.Interface, clientPool dynamic.ClientPool) (*dynamic.Client, error) { - _, err := apiExtensionsClient.Apiextensions().CustomResourceDefinitions().Create(customResourceDefinition) +func CreateNewCustomResourceDefinition(crd *apiextensionsv1alpha1.CustomResourceDefinition, apiExtensionsClient clientset.Interface, clientPool dynamic.ClientPool) (*dynamic.Client, error) { + _, err := apiExtensionsClient.Apiextensions().CustomResourceDefinitions().Create(crd) if err != nil { return nil, err } // wait until the resource appears in discovery - err = wait.PollImmediate(30*time.Millisecond, 30*time.Second, func() (bool, error) { - resourceList, err := apiExtensionsClient.Discovery().ServerResourcesForGroupVersion(customResourceDefinition.Spec.Group + "/" + customResourceDefinition.Spec.Version) + err = wait.PollImmediate(500*time.Millisecond, 30*time.Second, func() (bool, error) { + resourceList, err := apiExtensionsClient.Discovery().ServerResourcesForGroupVersion(crd.Spec.Group + "/" + crd.Spec.Version) if err != nil { return false, nil } for _, resource := range resourceList.APIResources { - if resource.Name == customResourceDefinition.Spec.Names.Plural { + if resource.Name == crd.Spec.Names.Plural { return true, nil } } @@ -123,29 +141,36 @@ func CreateNewCustomResourceDefinition(customResourceDefinition *apiextensionsv1 return nil, err } - dynamicClient, err := clientPool.ClientForGroupVersionResource(schema.GroupVersionResource{Group: customResourceDefinition.Spec.Group, Version: customResourceDefinition.Spec.Version, Resource: customResourceDefinition.Spec.Names.Plural}) + dynamicClient, err := clientPool.ClientForGroupVersionResource(schema.GroupVersionResource{Group: crd.Spec.Group, Version: crd.Spec.Version, Resource: crd.Spec.Names.Plural}) if err != nil { return nil, err } return dynamicClient, nil } -func DeleteCustomResourceDefinition(customResource *apiextensionsv1alpha1.CustomResourceDefinition, apiExtensionsClient clientset.Interface) error { - if err := apiExtensionsClient.Apiextensions().CustomResourceDefinitions().Delete(customResource.Name, nil); err != nil { +func DeleteCustomResourceDefinition(crd *apiextensionsv1alpha1.CustomResourceDefinition, apiExtensionsClient clientset.Interface) error { + if err := apiExtensionsClient.Apiextensions().CustomResourceDefinitions().Delete(crd.Name, nil); err != nil { return err } - err := wait.PollImmediate(30*time.Millisecond, 30*time.Second, func() (bool, error) { - if _, err := apiExtensionsClient.Discovery().ServerResourcesForGroupVersion(customResource.Spec.Group + "/" + customResource.Spec.Version); err != nil { + err := wait.PollImmediate(500*time.Millisecond, 30*time.Second, func() (bool, error) { + groupResource, err := apiExtensionsClient.Discovery().ServerResourcesForGroupVersion(crd.Spec.Group + "/" + crd.Spec.Version) + if err != nil { if errors.IsNotFound(err) { return true, nil + } return false, err } - return false, nil + for _, g := range groupResource.APIResources { + if g.Name == crd.Spec.Names.Plural { + return false, nil + } + } + return true, nil }) return err } -func GetCustomResourceDefinition(customResource *apiextensionsv1alpha1.CustomResourceDefinition, apiExtensionsClient clientset.Interface) (*apiextensionsv1alpha1.CustomResourceDefinition, error) { - return apiExtensionsClient.Apiextensions().CustomResourceDefinitions().Get(customResource.Name, metav1.GetOptions{}) +func GetCustomResourceDefinition(crd *apiextensionsv1alpha1.CustomResourceDefinition, apiExtensionsClient clientset.Interface) (*apiextensionsv1alpha1.CustomResourceDefinition, error) { + return apiExtensionsClient.Apiextensions().CustomResourceDefinitions().Get(crd.Name, metav1.GetOptions{}) }