Support field selectors for CRDs

Signed-off-by: Andy Goldstein <andy.goldstein@gmail.com>
This commit is contained in:
Andy Goldstein 2017-10-02 15:46:12 -04:00
parent dd99659dc1
commit 2ff87307c2
3 changed files with 262 additions and 3 deletions

View File

@ -340,6 +340,8 @@ func (r *crdHandler) getServingInfoFor(crd *apiextensions.CustomResourceDefiniti
selfLinkPrefix = "/" + path.Join("apis", crd.Spec.Group, crd.Spec.Version, "namespaces") + "/" selfLinkPrefix = "/" + path.Join("apis", crd.Spec.Group, crd.Spec.Version, "namespaces") + "/"
} }
clusterScoped := crd.Spec.Scope == apiextensions.ClusterScoped
requestScope := handlers.RequestScope{ requestScope := handlers.RequestScope{
Namer: handlers.ContextBasedNaming{ Namer: handlers.ContextBasedNaming{
GetContext: func(req *http.Request) apirequest.Context { GetContext: func(req *http.Request) apirequest.Context {
@ -347,7 +349,7 @@ func (r *crdHandler) getServingInfoFor(crd *apiextensions.CustomResourceDefiniti
return ret return ret
}, },
SelfLinker: meta.NewAccessor(), SelfLinker: meta.NewAccessor(),
ClusterScoped: crd.Spec.Scope == apiextensions.ClusterScoped, ClusterScoped: clusterScoped,
SelfLinkPathPrefix: selfLinkPrefix, SelfLinkPathPrefix: selfLinkPrefix,
}, },
ContextFunc: func(req *http.Request) apirequest.Context { ContextFunc: func(req *http.Request) apirequest.Context {
@ -358,8 +360,11 @@ func (r *crdHandler) getServingInfoFor(crd *apiextensions.CustomResourceDefiniti
Serializer: unstructuredNegotiatedSerializer{typer: typer, creator: creator}, Serializer: unstructuredNegotiatedSerializer{typer: typer, creator: creator},
ParameterCodec: parameterCodec, ParameterCodec: parameterCodec,
Creater: creator, Creater: creator,
Convertor: unstructured.UnstructuredObjectConverter{}, Convertor: crdObjectConverter{
UnstructuredObjectConverter: unstructured.UnstructuredObjectConverter{},
clusterScoped: clusterScoped,
},
Defaulter: unstructuredDefaulter{parameterScheme}, Defaulter: unstructuredDefaulter{parameterScheme},
Copier: UnstructuredCopier{}, Copier: UnstructuredCopier{},
Typer: typer, Typer: typer,
@ -390,6 +395,24 @@ func (r *crdHandler) getServingInfoFor(crd *apiextensions.CustomResourceDefiniti
return ret, nil 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{}) { func (c *crdHandler) updateCustomResourceDefinition(oldObj, _ interface{}) {
oldCRD := oldObj.(*apiextensions.CustomResourceDefinition) oldCRD := oldObj.(*apiextensions.CustomResourceDefinition)
glog.V(4).Infof("Updating customresourcedefinition %s", oldCRD.Name) 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" ns := "not-the-default"
testSimpleCRUD(t, ns, noxuDefinition, noxuVersionClient) testSimpleCRUD(t, ns, noxuDefinition, noxuVersionClient)
testFieldSelector(t, ns, noxuDefinition, noxuVersionClient)
} }
func TestClusterScopedCRUD(t *testing.T) { func TestClusterScopedCRUD(t *testing.T) {
@ -73,6 +74,7 @@ func TestClusterScopedCRUD(t *testing.T) {
ns := "" ns := ""
testSimpleCRUD(t, ns, noxuDefinition, noxuVersionClient) testSimpleCRUD(t, ns, noxuDefinition, noxuVersionClient)
testFieldSelector(t, ns, noxuDefinition, noxuVersionClient)
} }
func testSimpleCRUD(t *testing.T, ns string, noxuDefinition *apiextensionsv1beta1.CustomResourceDefinition, noxuVersionClient dynamic.Interface) { 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) { func TestDiscovery(t *testing.T) {
group := "mygroup.example.com" group := "mygroup.example.com"
version := "v1beta1" version := "v1beta1"