mirror of
https://github.com/k3s-io/kubernetes.git
synced 2025-07-24 20:24:09 +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(
|
load(
|
||||||
"@io_bazel_rules_go//go:def.bzl",
|
"@io_bazel_rules_go//go:def.bzl",
|
||||||
"go_library",
|
"go_library",
|
||||||
|
"go_test",
|
||||||
)
|
)
|
||||||
|
|
||||||
go_library(
|
go_library(
|
||||||
@ -78,3 +79,10 @@ filegroup(
|
|||||||
],
|
],
|
||||||
tags = ["automanaged"],
|
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") + "/"
|
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)
|
||||||
|
@ -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"
|
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"
|
||||||
|
Loading…
Reference in New Issue
Block a user