Merge pull request #53345 from ncdc/crd-add-fieldSelector-support

Automatic merge from submit-queue. If you want to cherry-pick this change to another branch, please follow the instructions <a href="https://github.com/kubernetes/community/blob/master/contributors/devel/cherry-picks.md">here</a>.

Support field selectors for CRDs

Signed-off-by: Andy Goldstein <andy.goldstein@gmail.com>



**What this PR does / why we need it**: allow field selectors to be used with custom resources

**Which issue this PR fixes** *(optional, in `fixes #<issue number>(, fixes #<issue_number>, ...)` format, will close that issue when PR gets merged)*: fixes #51046, fixes #49424

**Special notes for your reviewer**:

**Release note**:

```release-note
Custom resources served through CustomResourceDefinition now support field selectors for `metadata.name` and `metadata.namespace`.
```
This commit is contained in:
Kubernetes Submit Queue 2017-10-04 11:47:43 -07:00 committed by GitHub
commit e9a0b157d5
4 changed files with 270 additions and 3 deletions

View File

@ -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"],
)

View File

@ -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)

View File

@ -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)
}
})
}
}

View File

@ -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"