diff --git a/staging/src/k8s.io/apiextensions-apiserver/pkg/apiserver/BUILD b/staging/src/k8s.io/apiextensions-apiserver/pkg/apiserver/BUILD index eb29291c7d4..be54b00ccbf 100644 --- a/staging/src/k8s.io/apiextensions-apiserver/pkg/apiserver/BUILD +++ b/staging/src/k8s.io/apiextensions-apiserver/pkg/apiserver/BUILD @@ -3,6 +3,7 @@ package(default_visibility = ["//visibility:public"]) load( "@io_bazel_rules_go//go:def.bzl", "go_library", + "go_test", ) go_library( @@ -78,3 +79,10 @@ filegroup( ], tags = ["automanaged"], ) + +go_test( + name = "go_default_test", + srcs = ["customresource_handler_test.go"], + library = ":go_default_library", + deps = ["//vendor/k8s.io/apimachinery/pkg/apis/meta/v1/unstructured:go_default_library"], +) diff --git a/staging/src/k8s.io/apiextensions-apiserver/pkg/apiserver/customresource_handler.go b/staging/src/k8s.io/apiextensions-apiserver/pkg/apiserver/customresource_handler.go index a64f0260efe..a9e382cd993 100644 --- a/staging/src/k8s.io/apiextensions-apiserver/pkg/apiserver/customresource_handler.go +++ b/staging/src/k8s.io/apiextensions-apiserver/pkg/apiserver/customresource_handler.go @@ -340,6 +340,8 @@ func (r *crdHandler) getServingInfoFor(crd *apiextensions.CustomResourceDefiniti selfLinkPrefix = "/" + path.Join("apis", crd.Spec.Group, crd.Spec.Version, "namespaces") + "/" } + clusterScoped := crd.Spec.Scope == apiextensions.ClusterScoped + requestScope := handlers.RequestScope{ Namer: handlers.ContextBasedNaming{ GetContext: func(req *http.Request) apirequest.Context { @@ -347,7 +349,7 @@ func (r *crdHandler) getServingInfoFor(crd *apiextensions.CustomResourceDefiniti return ret }, SelfLinker: meta.NewAccessor(), - ClusterScoped: crd.Spec.Scope == apiextensions.ClusterScoped, + ClusterScoped: clusterScoped, SelfLinkPathPrefix: selfLinkPrefix, }, ContextFunc: func(req *http.Request) apirequest.Context { @@ -358,8 +360,11 @@ func (r *crdHandler) getServingInfoFor(crd *apiextensions.CustomResourceDefiniti Serializer: unstructuredNegotiatedSerializer{typer: typer, creator: creator}, ParameterCodec: parameterCodec, - Creater: creator, - Convertor: unstructured.UnstructuredObjectConverter{}, + Creater: creator, + Convertor: crdObjectConverter{ + UnstructuredObjectConverter: unstructured.UnstructuredObjectConverter{}, + clusterScoped: clusterScoped, + }, Defaulter: unstructuredDefaulter{parameterScheme}, Copier: UnstructuredCopier{}, Typer: typer, @@ -390,6 +395,24 @@ func (r *crdHandler) getServingInfoFor(crd *apiextensions.CustomResourceDefiniti return ret, nil } +// crdObjectConverter is a converter that supports field selectors for CRDs. +type crdObjectConverter struct { + unstructured.UnstructuredObjectConverter + clusterScoped bool +} + +func (c crdObjectConverter) ConvertFieldLabel(version, kind, label, value string) (string, string, error) { + // We currently only support metadata.namespace and metadata.name. + switch { + case label == "metadata.name": + return label, value, nil + case !c.clusterScoped && label == "metadata.namespace": + return label, value, nil + default: + return "", "", fmt.Errorf("field label not supported: %s", label) + } +} + func (c *crdHandler) updateCustomResourceDefinition(oldObj, _ interface{}) { oldCRD := oldObj.(*apiextensions.CustomResourceDefinition) glog.V(4).Infof("Updating customresourcedefinition %s", oldCRD.Name) diff --git a/staging/src/k8s.io/apiextensions-apiserver/pkg/apiserver/customresource_handler_test.go b/staging/src/k8s.io/apiextensions-apiserver/pkg/apiserver/customresource_handler_test.go new file mode 100644 index 00000000000..81c3ac7050b --- /dev/null +++ b/staging/src/k8s.io/apiextensions-apiserver/pkg/apiserver/customresource_handler_test.go @@ -0,0 +1,91 @@ +/* +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 apiserver + +import ( + "testing" + + "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" +) + +func TestConvertFieldLabel(t *testing.T) { + tests := []struct { + name string + clusterScoped bool + label string + expectError bool + }{ + { + name: "cluster scoped - name is ok", + clusterScoped: true, + label: "metadata.name", + }, + { + name: "cluster scoped - namespace is not ok", + clusterScoped: true, + label: "metadata.namespace", + expectError: true, + }, + { + name: "cluster scoped - other field is not ok", + clusterScoped: true, + label: "some.other.field", + expectError: true, + }, + { + name: "namespace scoped - name is ok", + label: "metadata.name", + }, + { + name: "namespace scoped - namespace is ok", + label: "metadata.namespace", + }, + { + name: "namespace scoped - other field is not ok", + label: "some.other.field", + expectError: true, + }, + } + + for _, test := range tests { + t.Run(test.name, func(t *testing.T) { + + c := crdObjectConverter{ + UnstructuredObjectConverter: unstructured.UnstructuredObjectConverter{}, + clusterScoped: test.clusterScoped, + } + + label, value, err := c.ConvertFieldLabel("", "", test.label, "value") + if e, a := test.expectError, err != nil; e != a { + t.Fatalf("err: expected %t, got %t", e, a) + } + if test.expectError { + if e, a := "field label not supported: "+test.label, err.Error(); e != a { + t.Errorf("err: expected %s, got %s", e, a) + } + return + } + + if e, a := test.label, label; e != a { + t.Errorf("label: expected %s, got %s", e, a) + } + if e, a := "value", value; e != a { + t.Errorf("value: expected %s, got %s", e, a) + } + }) + } +} diff --git a/staging/src/k8s.io/apiextensions-apiserver/test/integration/basic_test.go b/staging/src/k8s.io/apiextensions-apiserver/test/integration/basic_test.go index 659b118d1ca..03bdd873b85 100644 --- a/staging/src/k8s.io/apiextensions-apiserver/test/integration/basic_test.go +++ b/staging/src/k8s.io/apiextensions-apiserver/test/integration/basic_test.go @@ -56,6 +56,7 @@ func TestNamespaceScopedCRUD(t *testing.T) { ns := "not-the-default" testSimpleCRUD(t, ns, noxuDefinition, noxuVersionClient) + testFieldSelector(t, ns, noxuDefinition, noxuVersionClient) } func TestClusterScopedCRUD(t *testing.T) { @@ -73,6 +74,7 @@ func TestClusterScopedCRUD(t *testing.T) { ns := "" testSimpleCRUD(t, ns, noxuDefinition, noxuVersionClient) + testFieldSelector(t, ns, noxuDefinition, noxuVersionClient) } func testSimpleCRUD(t *testing.T, ns string, noxuDefinition *apiextensionsv1beta1.CustomResourceDefinition, noxuVersionClient dynamic.Interface) { @@ -197,6 +199,149 @@ func testSimpleCRUD(t *testing.T, ns string, noxuDefinition *apiextensionsv1beta } } +func testFieldSelector(t *testing.T, ns string, noxuDefinition *apiextensionsv1beta1.CustomResourceDefinition, noxuVersionClient dynamic.Interface) { + noxuResourceClient := NewNamespacedCustomResourceClient(ns, noxuVersionClient, noxuDefinition) + initialList, err := noxuResourceClient.List(metav1.ListOptions{}) + if err != nil { + t.Fatal(err) + } + if e, a := 0, len(initialList.(*unstructured.UnstructuredList).Items); e != a { + t.Errorf("expected %v, got %v", e, a) + } + initialListTypeMeta, err := meta.TypeAccessor(initialList) + if err != nil { + t.Fatal(err) + } + if e, a := noxuDefinition.Spec.Group+"/"+noxuDefinition.Spec.Version, initialListTypeMeta.GetAPIVersion(); e != a { + t.Errorf("expected %v, got %v", e, a) + } + if e, a := noxuDefinition.Spec.Names.ListKind, initialListTypeMeta.GetKind(); e != a { + t.Errorf("expected %v, got %v", e, a) + } + + initialListListMeta, err := meta.ListAccessor(initialList) + if err != nil { + t.Fatal(err) + } + noxuWatch, err := noxuResourceClient.Watch( + metav1.ListOptions{ + ResourceVersion: initialListListMeta.GetResourceVersion(), + FieldSelector: "metadata.name=foo", + }, + ) + if err != nil { + t.Fatal(err) + } + defer noxuWatch.Stop() + + _, err = instantiateCustomResource(t, testserver.NewNoxuInstance(ns, "bar"), noxuResourceClient, noxuDefinition) + if err != nil { + t.Fatalf("unable to create noxu Instance:%v", err) + } + createdNoxuInstanceFoo, err := instantiateCustomResource(t, testserver.NewNoxuInstance(ns, "foo"), noxuResourceClient, noxuDefinition) + if err != nil { + t.Fatalf("unable to create noxu Instance:%v", err) + } + + select { + case watchEvent := <-noxuWatch.ResultChan(): + if e, a := watch.Added, watchEvent.Type; e != a { + t.Errorf("expected %v, got %v", e, a) + break + } + createdObjectMeta, err := meta.Accessor(watchEvent.Object) + if err != nil { + t.Fatal(err) + } + // it should have a UUID + if len(createdObjectMeta.GetUID()) == 0 { + t.Errorf("missing uuid: %#v", watchEvent.Object) + } + if e, a := ns, createdObjectMeta.GetNamespace(); e != a { + t.Errorf("expected %v, got %v", e, a) + } + if e, a := "foo", createdObjectMeta.GetName(); e != a { + t.Errorf("expected %v, got %v", e, a) + } + createdTypeMeta, err := meta.TypeAccessor(watchEvent.Object) + if err != nil { + t.Fatal(err) + } + if e, a := noxuDefinition.Spec.Group+"/"+noxuDefinition.Spec.Version, createdTypeMeta.GetAPIVersion(); e != a { + t.Errorf("expected %v, got %v", e, a) + } + if e, a := noxuDefinition.Spec.Names.Kind, createdTypeMeta.GetKind(); e != a { + t.Errorf("expected %v, got %v", e, a) + } + + case <-time.After(5 * time.Second): + t.Errorf("missing watch event") + } + + gottenNoxuInstance, err := noxuResourceClient.Get("foo", metav1.GetOptions{}) + if err != nil { + t.Fatal(err) + } + if e, a := createdNoxuInstanceFoo, gottenNoxuInstance; !reflect.DeepEqual(e, a) { + t.Errorf("expected %v, got %v", e, a) + } + + listWithItem, err := noxuResourceClient.List(metav1.ListOptions{FieldSelector: "metadata.name=foo"}) + if err != nil { + t.Fatal(err) + } + if e, a := 1, len(listWithItem.(*unstructured.UnstructuredList).Items); e != a { + t.Errorf("expected %v, got %v", e, a) + } + if e, a := *createdNoxuInstanceFoo, listWithItem.(*unstructured.UnstructuredList).Items[0]; !reflect.DeepEqual(e, a) { + t.Errorf("expected %v, got %v", e, a) + } + + if err := noxuResourceClient.Delete("bar", nil); err != nil { + t.Fatal(err) + } + if err := noxuResourceClient.Delete("foo", nil); err != nil { + t.Fatal(err) + } + + listWithoutItem, err := noxuResourceClient.List(metav1.ListOptions{}) + if err != nil { + t.Fatal(err) + } + if e, a := 0, len(listWithoutItem.(*unstructured.UnstructuredList).Items); e != a { + t.Errorf("expected %v, got %v", e, a) + } + + select { + case watchEvent := <-noxuWatch.ResultChan(): + if e, a := watch.Deleted, watchEvent.Type; e != a { + t.Errorf("expected %v, got %v", e, a) + break + } + deletedObjectMeta, err := meta.Accessor(watchEvent.Object) + if err != nil { + t.Fatal(err) + } + // it should have a UUID + createdObjectMeta, err := meta.Accessor(createdNoxuInstanceFoo) + if err != nil { + t.Fatal(err) + } + if e, a := createdObjectMeta.GetUID(), deletedObjectMeta.GetUID(); e != a { + t.Errorf("expected %v, got %v", e, a) + } + if e, a := ns, createdObjectMeta.GetNamespace(); e != a { + t.Errorf("expected %v, got %v", e, a) + } + if e, a := "foo", createdObjectMeta.GetName(); e != a { + t.Errorf("expected %v, got %v", e, a) + } + + case <-time.After(5 * time.Second): + t.Errorf("missing watch event") + } +} + func TestDiscovery(t *testing.T) { group := "mygroup.example.com" version := "v1beta1"