mirror of
https://github.com/k3s-io/kubernetes.git
synced 2025-07-23 11:50:44 +00:00
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:
commit
e9a0b157d5
@ -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"],
|
||||
)
|
||||
|
@ -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)
|
||||
|
@ -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)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
@ -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"
|
||||
|
Loading…
Reference in New Issue
Block a user