From baf091e9dbad00db39e246815f9d7a21d148044f Mon Sep 17 00:00:00 2001 From: Clayton Coleman Date: Wed, 3 Apr 2019 12:12:11 -0400 Subject: [PATCH 1/3] Fake ObjectReaction should handle PartialObjectMetadata special When a client requests a PartialObjectMetadata returned from the ObjectReaction type, if the object has a GVK set use that instead of what the schema returns, since the majority of clients getting partial object metadata will be doing so using the metadata client or server side conversion. --- staging/src/k8s.io/client-go/testing/fixture.go | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/staging/src/k8s.io/client-go/testing/fixture.go b/staging/src/k8s.io/client-go/testing/fixture.go index b3ace307f83..98f82326730 100644 --- a/staging/src/k8s.io/client-go/testing/fixture.go +++ b/staging/src/k8s.io/client-go/testing/fixture.go @@ -318,6 +318,11 @@ func (t *tracker) Add(obj runtime.Object) error { if err != nil { return err } + + if partial, ok := obj.(*metav1.PartialObjectMetadata); ok && len(partial.TypeMeta.APIVersion) > 0 { + gvks = []schema.GroupVersionKind{partial.TypeMeta.GroupVersionKind()} + } + if len(gvks) == 0 { return fmt.Errorf("no registered kinds for %v", obj) } From bc89c37f32aa6cfd0f9ca975d9221d0a89320623 Mon Sep 17 00:00:00 2001 From: Clayton Coleman Date: Wed, 5 Jun 2019 14:28:51 -0400 Subject: [PATCH 2/3] Add fake client, informer factory, and lister to metadata client These will be used by the garbage collector controller and others that use higher level primitives. --- hack/.golint_failures | 1 + staging/src/k8s.io/client-go/metadata/BUILD | 7 +- .../src/k8s.io/client-go/metadata/fake/BUILD | 53 +++ .../k8s.io/client-go/metadata/fake/simple.go | 390 ++++++++++++++++++ .../client-go/metadata/fake/simple_test.go | 201 +++++++++ .../client-go/metadata/metadatainformer/BUILD | 52 +++ .../metadata/metadatainformer/informer.go | 156 +++++++ .../metadatainformer/informer_test.go | 170 ++++++++ .../metadata/metadatainformer/interface.go | 34 ++ .../client-go/metadata/metadatalister/BUILD | 49 +++ .../metadata/metadatalister/interface.go | 40 ++ .../metadata/metadatalister/lister.go | 91 ++++ .../metadata/metadatalister/lister_test.go | 256 ++++++++++++ .../client-go/metadata/metadatalister/shim.go | 87 ++++ 14 files changed, 1586 insertions(+), 1 deletion(-) create mode 100644 staging/src/k8s.io/client-go/metadata/fake/BUILD create mode 100644 staging/src/k8s.io/client-go/metadata/fake/simple.go create mode 100644 staging/src/k8s.io/client-go/metadata/fake/simple_test.go create mode 100644 staging/src/k8s.io/client-go/metadata/metadatainformer/BUILD create mode 100644 staging/src/k8s.io/client-go/metadata/metadatainformer/informer.go create mode 100644 staging/src/k8s.io/client-go/metadata/metadatainformer/informer_test.go create mode 100644 staging/src/k8s.io/client-go/metadata/metadatainformer/interface.go create mode 100644 staging/src/k8s.io/client-go/metadata/metadatalister/BUILD create mode 100644 staging/src/k8s.io/client-go/metadata/metadatalister/interface.go create mode 100644 staging/src/k8s.io/client-go/metadata/metadatalister/lister.go create mode 100644 staging/src/k8s.io/client-go/metadata/metadatalister/lister_test.go create mode 100644 staging/src/k8s.io/client-go/metadata/metadatalister/shim.go diff --git a/hack/.golint_failures b/hack/.golint_failures index 833ba16772f..fbb47eed3f5 100644 --- a/hack/.golint_failures +++ b/hack/.golint_failures @@ -492,6 +492,7 @@ staging/src/k8s.io/client-go/kubernetes/typed/core/v1 staging/src/k8s.io/client-go/kubernetes/typed/core/v1/fake staging/src/k8s.io/client-go/kubernetes/typed/extensions/v1beta1/fake staging/src/k8s.io/client-go/kubernetes/typed/policy/v1beta1/fake +staging/src/k8s.io/client-go/metadata/fake staging/src/k8s.io/client-go/plugin/pkg/client/auth/oidc staging/src/k8s.io/client-go/rest staging/src/k8s.io/client-go/rest/fake diff --git a/staging/src/k8s.io/client-go/metadata/BUILD b/staging/src/k8s.io/client-go/metadata/BUILD index c5cb5ae76d7..705fdcec2d8 100644 --- a/staging/src/k8s.io/client-go/metadata/BUILD +++ b/staging/src/k8s.io/client-go/metadata/BUILD @@ -45,7 +45,12 @@ filegroup( filegroup( name = "all-srcs", - srcs = [":package-srcs"], + srcs = [ + ":package-srcs", + "//staging/src/k8s.io/client-go/metadata/fake:all-srcs", + "//staging/src/k8s.io/client-go/metadata/metadatainformer:all-srcs", + "//staging/src/k8s.io/client-go/metadata/metadatalister:all-srcs", + ], tags = ["automanaged"], visibility = ["//visibility:public"], ) diff --git a/staging/src/k8s.io/client-go/metadata/fake/BUILD b/staging/src/k8s.io/client-go/metadata/fake/BUILD new file mode 100644 index 00000000000..590b08a85b6 --- /dev/null +++ b/staging/src/k8s.io/client-go/metadata/fake/BUILD @@ -0,0 +1,53 @@ +package(default_visibility = ["//visibility:public"]) + +load( + "@io_bazel_rules_go//go:def.bzl", + "go_library", + "go_test", +) + +go_library( + name = "go_default_library", + srcs = ["simple.go"], + importmap = "k8s.io/kubernetes/vendor/k8s.io/client-go/metadata/fake", + importpath = "k8s.io/client-go/metadata/fake", + deps = [ + "//staging/src/k8s.io/apimachinery/pkg/api/meta:go_default_library", + "//staging/src/k8s.io/apimachinery/pkg/apis/meta/v1:go_default_library", + "//staging/src/k8s.io/apimachinery/pkg/labels:go_default_library", + "//staging/src/k8s.io/apimachinery/pkg/runtime:go_default_library", + "//staging/src/k8s.io/apimachinery/pkg/runtime/schema:go_default_library", + "//staging/src/k8s.io/apimachinery/pkg/runtime/serializer:go_default_library", + "//staging/src/k8s.io/apimachinery/pkg/types:go_default_library", + "//staging/src/k8s.io/apimachinery/pkg/watch:go_default_library", + "//staging/src/k8s.io/client-go/metadata:go_default_library", + "//staging/src/k8s.io/client-go/testing:go_default_library", + ], +) + +filegroup( + name = "package-srcs", + srcs = glob(["**"]), + tags = ["automanaged"], + visibility = ["//visibility:private"], +) + +filegroup( + name = "all-srcs", + srcs = [":package-srcs"], + tags = ["automanaged"], +) + +go_test( + name = "go_default_test", + srcs = ["simple_test.go"], + embed = [":go_default_library"], + deps = [ + "//staging/src/k8s.io/apimachinery/pkg/api/equality:go_default_library", + "//staging/src/k8s.io/apimachinery/pkg/apis/meta/v1:go_default_library", + "//staging/src/k8s.io/apimachinery/pkg/runtime:go_default_library", + "//staging/src/k8s.io/apimachinery/pkg/runtime/schema:go_default_library", + "//staging/src/k8s.io/apimachinery/pkg/types:go_default_library", + "//staging/src/k8s.io/apimachinery/pkg/util/diff:go_default_library", + ], +) diff --git a/staging/src/k8s.io/client-go/metadata/fake/simple.go b/staging/src/k8s.io/client-go/metadata/fake/simple.go new file mode 100644 index 00000000000..0667e9e184d --- /dev/null +++ b/staging/src/k8s.io/client-go/metadata/fake/simple.go @@ -0,0 +1,390 @@ +/* +Copyright 2018 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 fake + +import ( + "fmt" + "strings" + + "k8s.io/apimachinery/pkg/api/meta" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/labels" + "k8s.io/apimachinery/pkg/runtime" + "k8s.io/apimachinery/pkg/runtime/schema" + "k8s.io/apimachinery/pkg/runtime/serializer" + "k8s.io/apimachinery/pkg/types" + "k8s.io/apimachinery/pkg/watch" + "k8s.io/client-go/metadata" + "k8s.io/client-go/testing" +) + +// MetadataClient assists in creating fake objects for use when testing, since metadata.Getter +// does not expose create +type MetadataClient interface { + metadata.Getter + CreateFake(obj *metav1.PartialObjectMetadata, opts metav1.CreateOptions, subresources ...string) (*metav1.PartialObjectMetadata, error) + UpdateFake(obj *metav1.PartialObjectMetadata, opts metav1.UpdateOptions, subresources ...string) (*metav1.PartialObjectMetadata, error) +} + +// NewSimpleMetadataClient creates a new client that will use the provided scheme and respond with the +// provided objects when requests are made. It will track actions made to the client which can be checked +// with GetActions(). +func NewSimpleMetadataClient(scheme *runtime.Scheme, objects ...runtime.Object) *FakeMetadataClient { + gvkFakeList := schema.GroupVersionKind{Group: "fake-metadata-client-group", Version: "v1", Kind: "List"} + if !scheme.Recognizes(gvkFakeList) { + // In order to use List with this client, you have to have the v1.List registered in your scheme, since this is a test + // type we modify the input scheme + scheme.AddKnownTypeWithName(gvkFakeList, &metav1.List{}) + } + + codecs := serializer.NewCodecFactory(scheme) + o := testing.NewObjectTracker(scheme, codecs.UniversalDeserializer()) + for _, obj := range objects { + if err := o.Add(obj); err != nil { + panic(err) + } + } + + cs := &FakeMetadataClient{scheme: scheme} + cs.AddReactor("*", "*", testing.ObjectReaction(o)) + cs.AddWatchReactor("*", func(action testing.Action) (handled bool, ret watch.Interface, err error) { + gvr := action.GetResource() + ns := action.GetNamespace() + watch, err := o.Watch(gvr, ns) + if err != nil { + return false, nil, err + } + return true, watch, nil + }) + + return cs +} + +// FakeMetadataClient implements clientset.Interface. Meant to be embedded into a +// struct to get a default implementation. This makes faking out just the method +// you want to test easier. +type FakeMetadataClient struct { + testing.Fake + scheme *runtime.Scheme +} + +type metadataResourceClient struct { + client *FakeMetadataClient + namespace string + resource schema.GroupVersionResource +} + +var _ metadata.Interface = &FakeMetadataClient{} + +// Resource returns an interface for accessing the provided resource. +func (c *FakeMetadataClient) Resource(resource schema.GroupVersionResource) metadata.Getter { + return &metadataResourceClient{client: c, resource: resource} +} + +// Namespace returns an interface for accessing the current resource in the specified +// namespace. +func (c *metadataResourceClient) Namespace(ns string) metadata.ResourceInterface { + ret := *c + ret.namespace = ns + return &ret +} + +// CreateFake records the object creation and processes it via the reactor. +func (c *metadataResourceClient) CreateFake(obj *metav1.PartialObjectMetadata, opts metav1.CreateOptions, subresources ...string) (*metav1.PartialObjectMetadata, error) { + var uncastRet runtime.Object + var err error + switch { + case len(c.namespace) == 0 && len(subresources) == 0: + uncastRet, err = c.client.Fake. + Invokes(testing.NewRootCreateAction(c.resource, obj), obj) + + case len(c.namespace) == 0 && len(subresources) > 0: + accessor, err := meta.Accessor(obj) + if err != nil { + return nil, err + } + name := accessor.GetName() + uncastRet, err = c.client.Fake. + Invokes(testing.NewRootCreateSubresourceAction(c.resource, name, strings.Join(subresources, "/"), obj), obj) + + case len(c.namespace) > 0 && len(subresources) == 0: + uncastRet, err = c.client.Fake. + Invokes(testing.NewCreateAction(c.resource, c.namespace, obj), obj) + + case len(c.namespace) > 0 && len(subresources) > 0: + accessor, err := meta.Accessor(obj) + if err != nil { + return nil, err + } + name := accessor.GetName() + uncastRet, err = c.client.Fake. + Invokes(testing.NewCreateSubresourceAction(c.resource, name, strings.Join(subresources, "/"), c.namespace, obj), obj) + + } + + if err != nil { + return nil, err + } + if uncastRet == nil { + return nil, err + } + ret, ok := uncastRet.(*metav1.PartialObjectMetadata) + if !ok { + return nil, fmt.Errorf("unexpected return value type %T", uncastRet) + } + return ret, err +} + +// UpdateFake records the object update and processes it via the reactor. +func (c *metadataResourceClient) UpdateFake(obj *metav1.PartialObjectMetadata, opts metav1.UpdateOptions, subresources ...string) (*metav1.PartialObjectMetadata, error) { + var uncastRet runtime.Object + var err error + switch { + case len(c.namespace) == 0 && len(subresources) == 0: + uncastRet, err = c.client.Fake. + Invokes(testing.NewRootUpdateAction(c.resource, obj), obj) + + case len(c.namespace) == 0 && len(subresources) > 0: + uncastRet, err = c.client.Fake. + Invokes(testing.NewRootUpdateSubresourceAction(c.resource, strings.Join(subresources, "/"), obj), obj) + + case len(c.namespace) > 0 && len(subresources) == 0: + uncastRet, err = c.client.Fake. + Invokes(testing.NewUpdateAction(c.resource, c.namespace, obj), obj) + + case len(c.namespace) > 0 && len(subresources) > 0: + uncastRet, err = c.client.Fake. + Invokes(testing.NewUpdateSubresourceAction(c.resource, strings.Join(subresources, "/"), c.namespace, obj), obj) + + } + + if err != nil { + return nil, err + } + if uncastRet == nil { + return nil, err + } + ret, ok := uncastRet.(*metav1.PartialObjectMetadata) + if !ok { + return nil, fmt.Errorf("unexpected return value type %T", uncastRet) + } + return ret, err +} + +// UpdateStatus records the object status update and processes it via the reactor. +func (c *metadataResourceClient) UpdateStatus(obj *metav1.PartialObjectMetadata, opts metav1.UpdateOptions) (*metav1.PartialObjectMetadata, error) { + var uncastRet runtime.Object + var err error + switch { + case len(c.namespace) == 0: + uncastRet, err = c.client.Fake. + Invokes(testing.NewRootUpdateSubresourceAction(c.resource, "status", obj), obj) + + case len(c.namespace) > 0: + uncastRet, err = c.client.Fake. + Invokes(testing.NewUpdateSubresourceAction(c.resource, "status", c.namespace, obj), obj) + + } + + if err != nil { + return nil, err + } + if uncastRet == nil { + return nil, err + } + ret, ok := uncastRet.(*metav1.PartialObjectMetadata) + if !ok { + return nil, fmt.Errorf("unexpected return value type %T", uncastRet) + } + return ret, err +} + +// Delete records the object deletion and processes it via the reactor. +func (c *metadataResourceClient) Delete(name string, opts *metav1.DeleteOptions, subresources ...string) error { + var err error + switch { + case len(c.namespace) == 0 && len(subresources) == 0: + _, err = c.client.Fake. + Invokes(testing.NewRootDeleteAction(c.resource, name), &metav1.Status{Status: "metadata delete fail"}) + + case len(c.namespace) == 0 && len(subresources) > 0: + _, err = c.client.Fake. + Invokes(testing.NewRootDeleteSubresourceAction(c.resource, strings.Join(subresources, "/"), name), &metav1.Status{Status: "metadata delete fail"}) + + case len(c.namespace) > 0 && len(subresources) == 0: + _, err = c.client.Fake. + Invokes(testing.NewDeleteAction(c.resource, c.namespace, name), &metav1.Status{Status: "metadata delete fail"}) + + case len(c.namespace) > 0 && len(subresources) > 0: + _, err = c.client.Fake. + Invokes(testing.NewDeleteSubresourceAction(c.resource, strings.Join(subresources, "/"), c.namespace, name), &metav1.Status{Status: "metadata delete fail"}) + } + + return err +} + +// DeleteCollection records the object collection deletion and processes it via the reactor. +func (c *metadataResourceClient) DeleteCollection(opts *metav1.DeleteOptions, listOptions metav1.ListOptions) error { + var err error + switch { + case len(c.namespace) == 0: + action := testing.NewRootDeleteCollectionAction(c.resource, listOptions) + _, err = c.client.Fake.Invokes(action, &metav1.Status{Status: "metadata deletecollection fail"}) + + case len(c.namespace) > 0: + action := testing.NewDeleteCollectionAction(c.resource, c.namespace, listOptions) + _, err = c.client.Fake.Invokes(action, &metav1.Status{Status: "metadata deletecollection fail"}) + + } + + return err +} + +// Get records the object retrieval and processes it via the reactor. +func (c *metadataResourceClient) Get(name string, opts metav1.GetOptions, subresources ...string) (*metav1.PartialObjectMetadata, error) { + var uncastRet runtime.Object + var err error + switch { + case len(c.namespace) == 0 && len(subresources) == 0: + uncastRet, err = c.client.Fake. + Invokes(testing.NewRootGetAction(c.resource, name), &metav1.Status{Status: "metadata get fail"}) + + case len(c.namespace) == 0 && len(subresources) > 0: + uncastRet, err = c.client.Fake. + Invokes(testing.NewRootGetSubresourceAction(c.resource, strings.Join(subresources, "/"), name), &metav1.Status{Status: "metadata get fail"}) + + case len(c.namespace) > 0 && len(subresources) == 0: + uncastRet, err = c.client.Fake. + Invokes(testing.NewGetAction(c.resource, c.namespace, name), &metav1.Status{Status: "metadata get fail"}) + + case len(c.namespace) > 0 && len(subresources) > 0: + uncastRet, err = c.client.Fake. + Invokes(testing.NewGetSubresourceAction(c.resource, c.namespace, strings.Join(subresources, "/"), name), &metav1.Status{Status: "metadata get fail"}) + } + + if err != nil { + return nil, err + } + if uncastRet == nil { + return nil, err + } + ret, ok := uncastRet.(*metav1.PartialObjectMetadata) + if !ok { + return nil, fmt.Errorf("unexpected return value type %T", uncastRet) + } + return ret, err +} + +// List records the object deletion and processes it via the reactor. +func (c *metadataResourceClient) List(opts metav1.ListOptions) (*metav1.PartialObjectMetadataList, error) { + var obj runtime.Object + var err error + switch { + case len(c.namespace) == 0: + obj, err = c.client.Fake. + Invokes(testing.NewRootListAction(c.resource, schema.GroupVersionKind{Group: "fake-metadata-client-group", Version: "v1", Kind: "" /*List is appended by the tracker automatically*/}, opts), &metav1.Status{Status: "metadata list fail"}) + + case len(c.namespace) > 0: + obj, err = c.client.Fake. + Invokes(testing.NewListAction(c.resource, schema.GroupVersionKind{Group: "fake-metadata-client-group", Version: "v1", Kind: "" /*List is appended by the tracker automatically*/}, c.namespace, opts), &metav1.Status{Status: "metadata list fail"}) + + } + + if obj == nil { + return nil, err + } + + label, _, _ := testing.ExtractFromListOptions(opts) + if label == nil { + label = labels.Everything() + } + + inputList, ok := obj.(*metav1.List) + if !ok { + return nil, fmt.Errorf("incoming object is incorrect type %T", obj) + } + fmt.Printf("DEBUG: %#v\n", inputList) + + list := &metav1.PartialObjectMetadataList{ + ListMeta: inputList.ListMeta, + } + for i := range inputList.Items { + item, ok := inputList.Items[i].Object.(*metav1.PartialObjectMetadata) + if !ok { + return nil, fmt.Errorf("item %d in list %T is %T", i, inputList, inputList.Items[i].Object) + } + metadata, err := meta.Accessor(item) + if err != nil { + return nil, err + } + if label.Matches(labels.Set(metadata.GetLabels())) { + list.Items = append(list.Items, *item) + } + } + return list, nil +} + +func (c *metadataResourceClient) Watch(opts metav1.ListOptions) (watch.Interface, error) { + switch { + case len(c.namespace) == 0: + return c.client.Fake. + InvokesWatch(testing.NewRootWatchAction(c.resource, opts)) + + case len(c.namespace) > 0: + return c.client.Fake. + InvokesWatch(testing.NewWatchAction(c.resource, c.namespace, opts)) + + } + + panic("math broke") +} + +// Patch records the object patch and processes it via the reactor. +func (c *metadataResourceClient) Patch(name string, pt types.PatchType, data []byte, opts metav1.PatchOptions, subresources ...string) (*metav1.PartialObjectMetadata, error) { + var uncastRet runtime.Object + var err error + switch { + case len(c.namespace) == 0 && len(subresources) == 0: + uncastRet, err = c.client.Fake. + Invokes(testing.NewRootPatchAction(c.resource, name, pt, data), &metav1.Status{Status: "metadata patch fail"}) + + case len(c.namespace) == 0 && len(subresources) > 0: + uncastRet, err = c.client.Fake. + Invokes(testing.NewRootPatchSubresourceAction(c.resource, name, pt, data, subresources...), &metav1.Status{Status: "metadata patch fail"}) + + case len(c.namespace) > 0 && len(subresources) == 0: + uncastRet, err = c.client.Fake. + Invokes(testing.NewPatchAction(c.resource, c.namespace, name, pt, data), &metav1.Status{Status: "metadata patch fail"}) + + case len(c.namespace) > 0 && len(subresources) > 0: + uncastRet, err = c.client.Fake. + Invokes(testing.NewPatchSubresourceAction(c.resource, c.namespace, name, pt, data, subresources...), &metav1.Status{Status: "metadata patch fail"}) + + } + + if err != nil { + return nil, err + } + if uncastRet == nil { + return nil, err + } + ret, ok := uncastRet.(*metav1.PartialObjectMetadata) + if !ok { + return nil, fmt.Errorf("unexpected return value type %T", uncastRet) + } + return ret, err +} diff --git a/staging/src/k8s.io/client-go/metadata/fake/simple_test.go b/staging/src/k8s.io/client-go/metadata/fake/simple_test.go new file mode 100644 index 00000000000..ece08f3ba22 --- /dev/null +++ b/staging/src/k8s.io/client-go/metadata/fake/simple_test.go @@ -0,0 +1,201 @@ +/* +Copyright 2018 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 fake + +import ( + "fmt" + "testing" + + "k8s.io/apimachinery/pkg/api/equality" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/runtime" + "k8s.io/apimachinery/pkg/runtime/schema" + "k8s.io/apimachinery/pkg/types" + "k8s.io/apimachinery/pkg/util/diff" +) + +const ( + testGroup = "testgroup" + testVersion = "testversion" + testResource = "testkinds" + testNamespace = "testns" + testName = "testname" + testKind = "TestKind" + testAPIVersion = "testgroup/testversion" +) + +var scheme *runtime.Scheme + +func init() { + scheme = runtime.NewScheme() + metav1.AddMetaToScheme(scheme) +} + +func newPartialObjectMetadata(apiVersion, kind, namespace, name string) *metav1.PartialObjectMetadata { + return &metav1.PartialObjectMetadata{ + TypeMeta: metav1.TypeMeta{ + APIVersion: apiVersion, + Kind: kind, + }, + ObjectMeta: metav1.ObjectMeta{ + Namespace: namespace, + Name: name, + }, + } +} + +func newPartialObjectMetadataWithAnnotations(annotations map[string]string) *metav1.PartialObjectMetadata { + u := newPartialObjectMetadata(testAPIVersion, testKind, testNamespace, testName) + u.Annotations = annotations + return u +} + +func TestList(t *testing.T) { + client := NewSimpleMetadataClient(scheme, + newPartialObjectMetadata("group/version", "TheKind", "ns-foo", "name-foo"), + newPartialObjectMetadata("group2/version", "TheKind", "ns-foo", "name2-foo"), + newPartialObjectMetadata("group/version", "TheKind", "ns-foo", "name-bar"), + newPartialObjectMetadata("group/version", "TheKind", "ns-foo", "name-baz"), + newPartialObjectMetadata("group2/version", "TheKind", "ns-foo", "name2-baz"), + ) + listFirst, err := client.Resource(schema.GroupVersionResource{Group: "group", Version: "version", Resource: "thekinds"}).List(metav1.ListOptions{}) + if err != nil { + t.Fatal(err) + } + + expected := []metav1.PartialObjectMetadata{ + *newPartialObjectMetadata("group/version", "TheKind", "ns-foo", "name-foo"), + *newPartialObjectMetadata("group/version", "TheKind", "ns-foo", "name-bar"), + *newPartialObjectMetadata("group/version", "TheKind", "ns-foo", "name-baz"), + } + if !equality.Semantic.DeepEqual(listFirst.Items, expected) { + t.Fatal(diff.ObjectGoPrintDiff(expected, listFirst.Items)) + } +} + +type patchTestCase struct { + name string + object runtime.Object + patchType types.PatchType + patchBytes []byte + wantErrMsg string + expectedPatchedObject runtime.Object +} + +func (tc *patchTestCase) runner(t *testing.T) { + client := NewSimpleMetadataClient(scheme, tc.object) + resourceInterface := client.Resource(schema.GroupVersionResource{Group: testGroup, Version: testVersion, Resource: testResource}).Namespace(testNamespace) + + got, recErr := resourceInterface.Patch(testName, tc.patchType, tc.patchBytes, metav1.PatchOptions{}) + + if err := tc.verifyErr(recErr); err != nil { + t.Error(err) + } + + if err := tc.verifyResult(got); err != nil { + t.Error(err) + } + +} + +// verifyErr verifies that the given error returned from Patch is the error +// expected by the test case. +func (tc *patchTestCase) verifyErr(err error) error { + if tc.wantErrMsg != "" && err == nil { + return fmt.Errorf("want error, got nil") + } + + if tc.wantErrMsg == "" && err != nil { + return fmt.Errorf("want no error, got %v", err) + } + + if err != nil { + if want, got := tc.wantErrMsg, err.Error(); want != got { + return fmt.Errorf("incorrect error: want: %q got: %q", want, got) + } + } + return nil +} + +func (tc *patchTestCase) verifyResult(result *metav1.PartialObjectMetadata) error { + if tc.expectedPatchedObject == nil && result == nil { + return nil + } + if !equality.Semantic.DeepEqual(result, tc.expectedPatchedObject) { + return fmt.Errorf("unexpected diff in received object: %s", diff.ObjectGoPrintDiff(tc.expectedPatchedObject, result)) + } + return nil +} + +func TestPatch(t *testing.T) { + testCases := []patchTestCase{ + { + name: "jsonpatch fails with merge type", + object: newPartialObjectMetadataWithAnnotations(map[string]string{"foo": "bar"}), + patchType: types.StrategicMergePatchType, + patchBytes: []byte(`[]`), + wantErrMsg: "invalid JSON document", + }, { + name: "jsonpatch works with empty patch", + object: newPartialObjectMetadataWithAnnotations(map[string]string{"foo": "bar"}), + patchType: types.JSONPatchType, + // No-op + patchBytes: []byte(`[]`), + expectedPatchedObject: newPartialObjectMetadataWithAnnotations(map[string]string{"foo": "bar"}), + }, { + name: "jsonpatch works with simple change patch", + object: newPartialObjectMetadataWithAnnotations(map[string]string{"foo": "bar"}), + patchType: types.JSONPatchType, + // change spec.foo from bar to foobar + patchBytes: []byte(`[{"op": "replace", "path": "/metadata/annotations/foo", "value": "foobar"}]`), + expectedPatchedObject: newPartialObjectMetadataWithAnnotations(map[string]string{"foo": "foobar"}), + }, { + name: "jsonpatch works with simple addition", + object: newPartialObjectMetadataWithAnnotations(map[string]string{"foo": "bar"}), + patchType: types.JSONPatchType, + // add spec.newvalue = dummy + patchBytes: []byte(`[{"op": "add", "path": "/metadata/annotations/newvalue", "value": "dummy"}]`), + expectedPatchedObject: newPartialObjectMetadataWithAnnotations(map[string]string{"foo": "bar", "newvalue": "dummy"}), + }, { + name: "jsonpatch works with simple deletion", + object: newPartialObjectMetadataWithAnnotations(map[string]string{"foo": "bar", "toremove": "shouldnotbehere"}), + patchType: types.JSONPatchType, + // remove spec.newvalue = dummy + patchBytes: []byte(`[{"op": "remove", "path": "/metadata/annotations/toremove"}]`), + expectedPatchedObject: newPartialObjectMetadataWithAnnotations(map[string]string{"foo": "bar"}), + }, { + name: "strategic merge patch fails with JSONPatch", + object: newPartialObjectMetadataWithAnnotations(map[string]string{"foo": "bar"}), + patchType: types.StrategicMergePatchType, + // add spec.newvalue = dummy + patchBytes: []byte(`[{"op": "add", "path": "/metadata/annotations/newvalue", "value": "dummy"}]`), + wantErrMsg: "invalid JSON document", + }, { + name: "merge patch works with simple replacement", + object: newPartialObjectMetadataWithAnnotations(map[string]string{"foo": "bar"}), + patchType: types.MergePatchType, + patchBytes: []byte(`{ "metadata": {"annotations": { "foo": "baz" } } }`), + expectedPatchedObject: newPartialObjectMetadataWithAnnotations(map[string]string{"foo": "baz"}), + }, + // TODO: Add tests for strategic merge using v1.Pod for example to ensure the test cases + // demonstrate expected use cases. + } + + for _, tc := range testCases { + t.Run(tc.name, tc.runner) + } +} diff --git a/staging/src/k8s.io/client-go/metadata/metadatainformer/BUILD b/staging/src/k8s.io/client-go/metadata/metadatainformer/BUILD new file mode 100644 index 00000000000..0d783026e6b --- /dev/null +++ b/staging/src/k8s.io/client-go/metadata/metadatainformer/BUILD @@ -0,0 +1,52 @@ +load("@io_bazel_rules_go//go:def.bzl", "go_library", "go_test") + +go_library( + name = "go_default_library", + srcs = [ + "informer.go", + "interface.go", + ], + importmap = "k8s.io/kubernetes/vendor/k8s.io/client-go/metadata/metadatainformer", + importpath = "k8s.io/client-go/metadata/metadatainformer", + visibility = ["//visibility:public"], + deps = [ + "//staging/src/k8s.io/apimachinery/pkg/apis/meta/v1:go_default_library", + "//staging/src/k8s.io/apimachinery/pkg/runtime:go_default_library", + "//staging/src/k8s.io/apimachinery/pkg/runtime/schema:go_default_library", + "//staging/src/k8s.io/apimachinery/pkg/watch:go_default_library", + "//staging/src/k8s.io/client-go/informers:go_default_library", + "//staging/src/k8s.io/client-go/metadata:go_default_library", + "//staging/src/k8s.io/client-go/metadata/metadatalister:go_default_library", + "//staging/src/k8s.io/client-go/tools/cache:go_default_library", + ], +) + +go_test( + name = "go_default_test", + srcs = ["informer_test.go"], + embed = [":go_default_library"], + deps = [ + "//staging/src/k8s.io/apimachinery/pkg/api/equality:go_default_library", + "//staging/src/k8s.io/apimachinery/pkg/apis/meta/v1:go_default_library", + "//staging/src/k8s.io/apimachinery/pkg/runtime:go_default_library", + "//staging/src/k8s.io/apimachinery/pkg/runtime/schema:go_default_library", + "//staging/src/k8s.io/apimachinery/pkg/util/diff:go_default_library", + "//staging/src/k8s.io/client-go/metadata/fake:go_default_library", + "//staging/src/k8s.io/client-go/tools/cache:go_default_library", + "//vendor/k8s.io/klog:go_default_library", + ], +) + +filegroup( + name = "package-srcs", + srcs = glob(["**"]), + tags = ["automanaged"], + visibility = ["//visibility:private"], +) + +filegroup( + name = "all-srcs", + srcs = [":package-srcs"], + tags = ["automanaged"], + visibility = ["//visibility:public"], +) diff --git a/staging/src/k8s.io/client-go/metadata/metadatainformer/informer.go b/staging/src/k8s.io/client-go/metadata/metadatainformer/informer.go new file mode 100644 index 00000000000..4c9efed1b08 --- /dev/null +++ b/staging/src/k8s.io/client-go/metadata/metadatainformer/informer.go @@ -0,0 +1,156 @@ +/* +Copyright 2018 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 metadatainformer + +import ( + "sync" + "time" + + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/runtime" + "k8s.io/apimachinery/pkg/runtime/schema" + "k8s.io/apimachinery/pkg/watch" + "k8s.io/client-go/informers" + "k8s.io/client-go/metadata" + "k8s.io/client-go/metadata/metadatalister" + "k8s.io/client-go/tools/cache" +) + +// NewSharedInformerFactory constructs a new instance of metadataSharedInformerFactory for all namespaces. +func NewSharedInformerFactory(client metadata.Interface, defaultResync time.Duration) SharedInformerFactory { + return NewFilteredSharedInformerFactory(client, defaultResync, metav1.NamespaceAll, nil) +} + +// NewFilteredSharedInformerFactory constructs a new instance of metadataSharedInformerFactory. +// Listers obtained via this factory will be subject to the same filters as specified here. +func NewFilteredSharedInformerFactory(client metadata.Interface, defaultResync time.Duration, namespace string, tweakListOptions TweakListOptionsFunc) SharedInformerFactory { + return &metadataSharedInformerFactory{ + client: client, + defaultResync: defaultResync, + namespace: namespace, + informers: map[schema.GroupVersionResource]informers.GenericInformer{}, + startedInformers: make(map[schema.GroupVersionResource]bool), + tweakListOptions: tweakListOptions, + } +} + +type metadataSharedInformerFactory struct { + client metadata.Interface + defaultResync time.Duration + namespace string + + lock sync.Mutex + informers map[schema.GroupVersionResource]informers.GenericInformer + // startedInformers is used for tracking which informers have been started. + // This allows Start() to be called multiple times safely. + startedInformers map[schema.GroupVersionResource]bool + tweakListOptions TweakListOptionsFunc +} + +var _ SharedInformerFactory = &metadataSharedInformerFactory{} + +func (f *metadataSharedInformerFactory) ForResource(gvr schema.GroupVersionResource) informers.GenericInformer { + f.lock.Lock() + defer f.lock.Unlock() + + key := gvr + informer, exists := f.informers[key] + if exists { + return informer + } + + informer = NewFilteredMetadataInformer(f.client, gvr, f.namespace, f.defaultResync, cache.Indexers{cache.NamespaceIndex: cache.MetaNamespaceIndexFunc}, f.tweakListOptions) + f.informers[key] = informer + + return informer +} + +// Start initializes all requested informers. +func (f *metadataSharedInformerFactory) Start(stopCh <-chan struct{}) { + f.lock.Lock() + defer f.lock.Unlock() + + for informerType, informer := range f.informers { + if !f.startedInformers[informerType] { + go informer.Informer().Run(stopCh) + f.startedInformers[informerType] = true + } + } +} + +// WaitForCacheSync waits for all started informers' cache were synced. +func (f *metadataSharedInformerFactory) WaitForCacheSync(stopCh <-chan struct{}) map[schema.GroupVersionResource]bool { + informers := func() map[schema.GroupVersionResource]cache.SharedIndexInformer { + f.lock.Lock() + defer f.lock.Unlock() + + informers := map[schema.GroupVersionResource]cache.SharedIndexInformer{} + for informerType, informer := range f.informers { + if f.startedInformers[informerType] { + informers[informerType] = informer.Informer() + } + } + return informers + }() + + res := map[schema.GroupVersionResource]bool{} + for informType, informer := range informers { + res[informType] = cache.WaitForCacheSync(stopCh, informer.HasSynced) + } + return res +} + +// NewFilteredMetadataInformer constructs a new informer for a metadata type. +func NewFilteredMetadataInformer(client metadata.Interface, gvr schema.GroupVersionResource, namespace string, resyncPeriod time.Duration, indexers cache.Indexers, tweakListOptions TweakListOptionsFunc) informers.GenericInformer { + return &metadataInformer{ + gvr: gvr, + informer: cache.NewSharedIndexInformer( + &cache.ListWatch{ + ListFunc: func(options metav1.ListOptions) (runtime.Object, error) { + if tweakListOptions != nil { + tweakListOptions(&options) + } + return client.Resource(gvr).Namespace(namespace).List(options) + }, + WatchFunc: func(options metav1.ListOptions) (watch.Interface, error) { + if tweakListOptions != nil { + tweakListOptions(&options) + } + return client.Resource(gvr).Namespace(namespace).Watch(options) + }, + }, + &metav1.PartialObjectMetadata{}, + resyncPeriod, + indexers, + ), + } +} + +type metadataInformer struct { + informer cache.SharedIndexInformer + gvr schema.GroupVersionResource +} + +var _ informers.GenericInformer = &metadataInformer{} + +func (d *metadataInformer) Informer() cache.SharedIndexInformer { + return d.informer +} + +func (d *metadataInformer) Lister() cache.GenericLister { + return metadatalister.NewRuntimeObjectShim(metadatalister.New(d.informer.GetIndexer(), d.gvr)) +} diff --git a/staging/src/k8s.io/client-go/metadata/metadatainformer/informer_test.go b/staging/src/k8s.io/client-go/metadata/metadatainformer/informer_test.go new file mode 100644 index 00000000000..ef3b186e76d --- /dev/null +++ b/staging/src/k8s.io/client-go/metadata/metadatainformer/informer_test.go @@ -0,0 +1,170 @@ +/* +Copyright 2018 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 metadatainformer + +import ( + "context" + "flag" + "testing" + "time" + + "k8s.io/klog" + + "k8s.io/apimachinery/pkg/api/equality" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/runtime" + "k8s.io/apimachinery/pkg/runtime/schema" + "k8s.io/apimachinery/pkg/util/diff" + "k8s.io/client-go/metadata/fake" + "k8s.io/client-go/tools/cache" +) + +func init() { + klog.InitFlags(flag.CommandLine) + flag.CommandLine.Lookup("v").Value.Set("5") + flag.CommandLine.Lookup("alsologtostderr").Value.Set("true") +} + +func TestMetadataSharedInformerFactory(t *testing.T) { + scenarios := []struct { + name string + existingObj *metav1.PartialObjectMetadata + gvr schema.GroupVersionResource + ns string + trigger func(gvr schema.GroupVersionResource, ns string, fakeClient *fake.FakeMetadataClient, testObject *metav1.PartialObjectMetadata) *metav1.PartialObjectMetadata + handler func(rcvCh chan<- *metav1.PartialObjectMetadata) *cache.ResourceEventHandlerFuncs + }{ + // scenario 1 + { + name: "scenario 1: test if adding an object triggers AddFunc", + ns: "ns-foo", + gvr: schema.GroupVersionResource{Group: "extensions", Version: "v1beta1", Resource: "deployments"}, + trigger: func(gvr schema.GroupVersionResource, ns string, fakeClient *fake.FakeMetadataClient, _ *metav1.PartialObjectMetadata) *metav1.PartialObjectMetadata { + testObject := newPartialObjectMetadata("extensions/v1beta1", "Deployment", "ns-foo", "name-foo") + createdObj, err := fakeClient.Resource(gvr).Namespace(ns).(fake.MetadataClient).CreateFake(testObject, metav1.CreateOptions{}) + if err != nil { + t.Error(err) + } + return createdObj + }, + handler: func(rcvCh chan<- *metav1.PartialObjectMetadata) *cache.ResourceEventHandlerFuncs { + return &cache.ResourceEventHandlerFuncs{ + AddFunc: func(obj interface{}) { + rcvCh <- obj.(*metav1.PartialObjectMetadata) + }, + } + }, + }, + + // scenario 2 + { + name: "scenario 2: tests if updating an object triggers UpdateFunc", + ns: "ns-foo", + gvr: schema.GroupVersionResource{Group: "extensions", Version: "v1beta1", Resource: "deployments"}, + existingObj: newPartialObjectMetadata("extensions/v1beta1", "Deployment", "ns-foo", "name-foo"), + trigger: func(gvr schema.GroupVersionResource, ns string, fakeClient *fake.FakeMetadataClient, testObject *metav1.PartialObjectMetadata) *metav1.PartialObjectMetadata { + if testObject.Annotations == nil { + testObject.Annotations = make(map[string]string) + } + testObject.Annotations["test"] = "updatedName" + updatedObj, err := fakeClient.Resource(gvr).Namespace(ns).(fake.MetadataClient).UpdateFake(testObject, metav1.UpdateOptions{}) + if err != nil { + t.Error(err) + } + return updatedObj + }, + handler: func(rcvCh chan<- *metav1.PartialObjectMetadata) *cache.ResourceEventHandlerFuncs { + return &cache.ResourceEventHandlerFuncs{ + UpdateFunc: func(old, updated interface{}) { + rcvCh <- updated.(*metav1.PartialObjectMetadata) + }, + } + }, + }, + + // scenario 3 + { + name: "scenario 3: test if deleting an object triggers DeleteFunc", + ns: "ns-foo", + gvr: schema.GroupVersionResource{Group: "extensions", Version: "v1beta1", Resource: "deployments"}, + existingObj: newPartialObjectMetadata("extensions/v1beta1", "Deployment", "ns-foo", "name-foo"), + trigger: func(gvr schema.GroupVersionResource, ns string, fakeClient *fake.FakeMetadataClient, testObject *metav1.PartialObjectMetadata) *metav1.PartialObjectMetadata { + err := fakeClient.Resource(gvr).Namespace(ns).Delete(testObject.GetName(), &metav1.DeleteOptions{}) + if err != nil { + t.Error(err) + } + return testObject + }, + handler: func(rcvCh chan<- *metav1.PartialObjectMetadata) *cache.ResourceEventHandlerFuncs { + return &cache.ResourceEventHandlerFuncs{ + DeleteFunc: func(obj interface{}) { + rcvCh <- obj.(*metav1.PartialObjectMetadata) + }, + } + }, + }, + } + + for _, ts := range scenarios { + t.Run(ts.name, func(t *testing.T) { + // test data + timeout := time.Duration(3 * time.Second) + ctx, cancel := context.WithTimeout(context.Background(), timeout) + defer cancel() + scheme := runtime.NewScheme() + metav1.AddMetaToScheme(scheme) + informerReciveObjectCh := make(chan *metav1.PartialObjectMetadata, 1) + objs := []runtime.Object{} + if ts.existingObj != nil { + objs = append(objs, ts.existingObj) + } + fakeClient := fake.NewSimpleMetadataClient(scheme, objs...) + target := NewSharedInformerFactory(fakeClient, 0) + + // act + informerListerForGvr := target.ForResource(ts.gvr) + informerListerForGvr.Informer().AddEventHandler(ts.handler(informerReciveObjectCh)) + target.Start(ctx.Done()) + if synced := target.WaitForCacheSync(ctx.Done()); !synced[ts.gvr] { + t.Fatalf("informer for %s hasn't synced", ts.gvr) + } + + testObject := ts.trigger(ts.gvr, ts.ns, fakeClient, ts.existingObj) + select { + case objFromInformer := <-informerReciveObjectCh: + if !equality.Semantic.DeepEqual(testObject, objFromInformer) { + t.Fatalf("%v", diff.ObjectDiff(testObject, objFromInformer)) + } + case <-ctx.Done(): + t.Errorf("tested informer haven't received an object, waited %v", timeout) + } + }) + } +} + +func newPartialObjectMetadata(apiVersion, kind, namespace, name string) *metav1.PartialObjectMetadata { + return &metav1.PartialObjectMetadata{ + TypeMeta: metav1.TypeMeta{ + APIVersion: apiVersion, + Kind: kind, + }, + ObjectMeta: metav1.ObjectMeta{ + Namespace: namespace, + Name: name, + }, + } +} diff --git a/staging/src/k8s.io/client-go/metadata/metadatainformer/interface.go b/staging/src/k8s.io/client-go/metadata/metadatainformer/interface.go new file mode 100644 index 00000000000..732e565c7da --- /dev/null +++ b/staging/src/k8s.io/client-go/metadata/metadatainformer/interface.go @@ -0,0 +1,34 @@ +/* +Copyright 2018 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 metadatainformer + +import ( + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/runtime/schema" + "k8s.io/client-go/informers" +) + +// SharedInformerFactory provides access to a shared informer and lister for dynamic client +type SharedInformerFactory interface { + Start(stopCh <-chan struct{}) + ForResource(gvr schema.GroupVersionResource) informers.GenericInformer + WaitForCacheSync(stopCh <-chan struct{}) map[schema.GroupVersionResource]bool +} + +// TweakListOptionsFunc defines the signature of a helper function +// that wants to provide more listing options to API +type TweakListOptionsFunc func(*metav1.ListOptions) diff --git a/staging/src/k8s.io/client-go/metadata/metadatalister/BUILD b/staging/src/k8s.io/client-go/metadata/metadatalister/BUILD new file mode 100644 index 00000000000..c64a1ae1c44 --- /dev/null +++ b/staging/src/k8s.io/client-go/metadata/metadatalister/BUILD @@ -0,0 +1,49 @@ +load("@io_bazel_rules_go//go:def.bzl", "go_library", "go_test") + +go_library( + name = "go_default_library", + srcs = [ + "interface.go", + "lister.go", + "shim.go", + ], + importmap = "k8s.io/kubernetes/vendor/k8s.io/client-go/metadata/metadatalister", + importpath = "k8s.io/client-go/metadata/metadatalister", + visibility = ["//visibility:public"], + deps = [ + "//staging/src/k8s.io/apimachinery/pkg/api/errors:go_default_library", + "//staging/src/k8s.io/apimachinery/pkg/apis/meta/v1:go_default_library", + "//staging/src/k8s.io/apimachinery/pkg/labels:go_default_library", + "//staging/src/k8s.io/apimachinery/pkg/runtime:go_default_library", + "//staging/src/k8s.io/apimachinery/pkg/runtime/schema:go_default_library", + "//staging/src/k8s.io/client-go/tools/cache:go_default_library", + ], +) + +go_test( + name = "go_default_test", + srcs = ["lister_test.go"], + embed = [":go_default_library"], + deps = [ + "//staging/src/k8s.io/apimachinery/pkg/apis/meta/v1:go_default_library", + "//staging/src/k8s.io/apimachinery/pkg/labels:go_default_library", + "//staging/src/k8s.io/apimachinery/pkg/runtime:go_default_library", + "//staging/src/k8s.io/apimachinery/pkg/runtime/schema:go_default_library", + "//staging/src/k8s.io/apimachinery/pkg/util/diff:go_default_library", + "//staging/src/k8s.io/client-go/tools/cache:go_default_library", + ], +) + +filegroup( + name = "package-srcs", + srcs = glob(["**"]), + tags = ["automanaged"], + visibility = ["//visibility:private"], +) + +filegroup( + name = "all-srcs", + srcs = [":package-srcs"], + tags = ["automanaged"], + visibility = ["//visibility:public"], +) diff --git a/staging/src/k8s.io/client-go/metadata/metadatalister/interface.go b/staging/src/k8s.io/client-go/metadata/metadatalister/interface.go new file mode 100644 index 00000000000..bb35485895b --- /dev/null +++ b/staging/src/k8s.io/client-go/metadata/metadatalister/interface.go @@ -0,0 +1,40 @@ +/* +Copyright 2018 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 metadatalister + +import ( + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/labels" +) + +// Lister helps list resources. +type Lister interface { + // List lists all resources in the indexer. + List(selector labels.Selector) (ret []*metav1.PartialObjectMetadata, err error) + // Get retrieves a resource from the indexer with the given name + Get(name string) (*metav1.PartialObjectMetadata, error) + // Namespace returns an object that can list and get resources in a given namespace. + Namespace(namespace string) NamespaceLister +} + +// NamespaceLister helps list and get resources. +type NamespaceLister interface { + // List lists all resources in the indexer for a given namespace. + List(selector labels.Selector) (ret []*metav1.PartialObjectMetadata, err error) + // Get retrieves a resource from the indexer for a given namespace and name. + Get(name string) (*metav1.PartialObjectMetadata, error) +} diff --git a/staging/src/k8s.io/client-go/metadata/metadatalister/lister.go b/staging/src/k8s.io/client-go/metadata/metadatalister/lister.go new file mode 100644 index 00000000000..faeccc0fc23 --- /dev/null +++ b/staging/src/k8s.io/client-go/metadata/metadatalister/lister.go @@ -0,0 +1,91 @@ +/* +Copyright 2018 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 metadatalister + +import ( + "k8s.io/apimachinery/pkg/api/errors" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/labels" + "k8s.io/apimachinery/pkg/runtime/schema" + "k8s.io/client-go/tools/cache" +) + +var _ Lister = &metadataLister{} +var _ NamespaceLister = &metadataNamespaceLister{} + +// metadataLister implements the Lister interface. +type metadataLister struct { + indexer cache.Indexer + gvr schema.GroupVersionResource +} + +// New returns a new Lister. +func New(indexer cache.Indexer, gvr schema.GroupVersionResource) Lister { + return &metadataLister{indexer: indexer, gvr: gvr} +} + +// List lists all resources in the indexer. +func (l *metadataLister) List(selector labels.Selector) (ret []*metav1.PartialObjectMetadata, err error) { + err = cache.ListAll(l.indexer, selector, func(m interface{}) { + ret = append(ret, m.(*metav1.PartialObjectMetadata)) + }) + return ret, err +} + +// Get retrieves a resource from the indexer with the given name +func (l *metadataLister) Get(name string) (*metav1.PartialObjectMetadata, error) { + obj, exists, err := l.indexer.GetByKey(name) + if err != nil { + return nil, err + } + if !exists { + return nil, errors.NewNotFound(l.gvr.GroupResource(), name) + } + return obj.(*metav1.PartialObjectMetadata), nil +} + +// Namespace returns an object that can list and get resources from a given namespace. +func (l *metadataLister) Namespace(namespace string) NamespaceLister { + return &metadataNamespaceLister{indexer: l.indexer, namespace: namespace, gvr: l.gvr} +} + +// metadataNamespaceLister implements the NamespaceLister interface. +type metadataNamespaceLister struct { + indexer cache.Indexer + namespace string + gvr schema.GroupVersionResource +} + +// List lists all resources in the indexer for a given namespace. +func (l *metadataNamespaceLister) List(selector labels.Selector) (ret []*metav1.PartialObjectMetadata, err error) { + err = cache.ListAllByNamespace(l.indexer, l.namespace, selector, func(m interface{}) { + ret = append(ret, m.(*metav1.PartialObjectMetadata)) + }) + return ret, err +} + +// Get retrieves a resource from the indexer for a given namespace and name. +func (l *metadataNamespaceLister) Get(name string) (*metav1.PartialObjectMetadata, error) { + obj, exists, err := l.indexer.GetByKey(l.namespace + "/" + name) + if err != nil { + return nil, err + } + if !exists { + return nil, errors.NewNotFound(l.gvr.GroupResource(), name) + } + return obj.(*metav1.PartialObjectMetadata), nil +} diff --git a/staging/src/k8s.io/client-go/metadata/metadatalister/lister_test.go b/staging/src/k8s.io/client-go/metadata/metadatalister/lister_test.go new file mode 100644 index 00000000000..7c32c1e7f38 --- /dev/null +++ b/staging/src/k8s.io/client-go/metadata/metadatalister/lister_test.go @@ -0,0 +1,256 @@ +/* +Copyright 2018 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 metadatalister + +import ( + "reflect" + "testing" + + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/labels" + "k8s.io/apimachinery/pkg/runtime" + "k8s.io/apimachinery/pkg/runtime/schema" + "k8s.io/apimachinery/pkg/util/diff" + "k8s.io/client-go/tools/cache" +) + +func TestNamespaceGetMethod(t *testing.T) { + tests := []struct { + name string + existingObjects []runtime.Object + namespaceToSync string + gvrToSync schema.GroupVersionResource + objectToGet string + expectedObject *metav1.PartialObjectMetadata + expectError bool + }{ + { + name: "scenario 1: gets name-foo1 resource from the indexer from ns-foo namespace", + existingObjects: []runtime.Object{ + newPartialObjectMetadata("group/version", "TheKind", "ns-foo", "name-foo"), + newPartialObjectMetadata("group/version", "TheKind", "ns-foo", "name-foo1"), + newPartialObjectMetadata("group/version", "TheKind", "ns-bar", "name-bar"), + }, + namespaceToSync: "ns-foo", + gvrToSync: schema.GroupVersionResource{Group: "group", Version: "version", Resource: "TheKinds"}, + objectToGet: "name-foo1", + expectedObject: newPartialObjectMetadata("group/version", "TheKind", "ns-foo", "name-foo1"), + }, + { + name: "scenario 2: gets name-foo-non-existing resource from the indexer from ns-foo namespace", + existingObjects: []runtime.Object{ + newPartialObjectMetadata("group/version", "TheKind", "ns-foo", "name-foo"), + newPartialObjectMetadata("group/version", "TheKind", "ns-foo", "name-foo1"), + newPartialObjectMetadata("group/version", "TheKind", "ns-bar", "name-bar"), + }, + namespaceToSync: "ns-foo", + gvrToSync: schema.GroupVersionResource{Group: "group", Version: "version", Resource: "TheKinds"}, + objectToGet: "name-foo-non-existing", + expectError: true, + }, + } + + for _, test := range tests { + t.Run(test.name, func(t *testing.T) { + // test data + indexer := cache.NewIndexer(cache.MetaNamespaceKeyFunc, cache.Indexers{}) + for _, obj := range test.existingObjects { + err := indexer.Add(obj) + if err != nil { + t.Fatal(err) + } + } + // act + target := New(indexer, test.gvrToSync).Namespace(test.namespaceToSync) + actualObject, err := target.Get(test.objectToGet) + + // validate + if test.expectError { + if err == nil { + t.Fatal("expected to get an error but non was returned") + } + return + } + if err != nil { + t.Fatal(err) + } + if !reflect.DeepEqual(test.expectedObject, actualObject) { + t.Fatalf("unexpected object has been returned expected = %v actual = %v, diff = %v", test.expectedObject, actualObject, diff.ObjectDiff(test.expectedObject, actualObject)) + } + }) + } +} + +func TestNamespaceListMethod(t *testing.T) { + // test data + objs := []runtime.Object{ + newPartialObjectMetadata("group/version", "TheKind", "ns-foo", "name-foo"), + newPartialObjectMetadata("group/version", "TheKind", "ns-foo", "name-foo1"), + newPartialObjectMetadata("group/version", "TheKind", "ns-bar", "name-bar"), + } + indexer := cache.NewIndexer(cache.MetaNamespaceKeyFunc, cache.Indexers{}) + for _, obj := range objs { + err := indexer.Add(obj) + if err != nil { + t.Fatal(err) + } + } + expectedOutput := []*metav1.PartialObjectMetadata{ + newPartialObjectMetadata("group/version", "TheKind", "ns-foo", "name-foo"), + newPartialObjectMetadata("group/version", "TheKind", "ns-foo", "name-foo1"), + } + namespaceToList := "ns-foo" + + // act + target := New(indexer, schema.GroupVersionResource{Group: "group", Version: "version", Resource: "TheKinds"}).Namespace(namespaceToList) + actualOutput, err := target.List(labels.Everything()) + + // validate + if err != nil { + t.Fatal(err) + } + assertListOrDie(expectedOutput, actualOutput, t) +} + +func TestListerGetMethod(t *testing.T) { + tests := []struct { + name string + existingObjects []runtime.Object + namespaceToSync string + gvrToSync schema.GroupVersionResource + objectToGet string + expectedObject *metav1.PartialObjectMetadata + expectError bool + }{ + { + name: "scenario 1: gets name-foo1 resource from the indexer", + existingObjects: []runtime.Object{ + newPartialObjectMetadata("group/version", "TheKind", "ns-foo", "name-foo"), + newPartialObjectMetadata("group/version", "TheKind", "", "name-foo1"), + newPartialObjectMetadata("group/version", "TheKind", "ns-bar", "name-bar"), + }, + namespaceToSync: "", + gvrToSync: schema.GroupVersionResource{Group: "group", Version: "version", Resource: "TheKinds"}, + objectToGet: "name-foo1", + expectedObject: newPartialObjectMetadata("group/version", "TheKind", "", "name-foo1"), + }, + { + name: "scenario 2: doesn't get name-foo resource from the indexer from ns-foo namespace", + existingObjects: []runtime.Object{ + newPartialObjectMetadata("group/version", "TheKind", "ns-foo", "name-foo"), + newPartialObjectMetadata("group/version", "TheKind", "ns-foo", "name-foo1"), + newPartialObjectMetadata("group/version", "TheKind", "ns-bar", "name-bar"), + }, + namespaceToSync: "ns-foo", + gvrToSync: schema.GroupVersionResource{Group: "group", Version: "version", Resource: "TheKinds"}, + objectToGet: "name-foo", + expectError: true, + }, + } + + for _, test := range tests { + t.Run(test.name, func(t *testing.T) { + // test data + indexer := cache.NewIndexer(cache.MetaNamespaceKeyFunc, cache.Indexers{}) + for _, obj := range test.existingObjects { + err := indexer.Add(obj) + if err != nil { + t.Fatal(err) + } + } + // act + target := New(indexer, test.gvrToSync) + actualObject, err := target.Get(test.objectToGet) + + // validate + if test.expectError { + if err == nil { + t.Fatal("expected to get an error but non was returned") + } + return + } + if err != nil { + t.Fatal(err) + } + if !reflect.DeepEqual(test.expectedObject, actualObject) { + t.Fatalf("unexpected object has been returned expected = %v actual = %v, diff = %v", test.expectedObject, actualObject, diff.ObjectDiff(test.expectedObject, actualObject)) + } + }) + } +} + +func TestListerListMethod(t *testing.T) { + // test data + objs := []runtime.Object{ + newPartialObjectMetadata("group/version", "TheKind", "ns-foo", "name-foo"), + newPartialObjectMetadata("group/version", "TheKind", "ns-foo", "name-bar"), + } + indexer := cache.NewIndexer(cache.MetaNamespaceKeyFunc, cache.Indexers{}) + for _, obj := range objs { + err := indexer.Add(obj) + if err != nil { + t.Fatal(err) + } + } + expectedOutput := []*metav1.PartialObjectMetadata{ + newPartialObjectMetadata("group/version", "TheKind", "ns-foo", "name-foo"), + newPartialObjectMetadata("group/version", "TheKind", "ns-foo", "name-bar"), + } + + // act + target := New(indexer, schema.GroupVersionResource{Group: "group", Version: "version", Resource: "TheKinds"}) + actualOutput, err := target.List(labels.Everything()) + + // validate + if err != nil { + t.Fatal(err) + } + assertListOrDie(expectedOutput, actualOutput, t) +} + +func assertListOrDie(expected, actual []*metav1.PartialObjectMetadata, t *testing.T) { + if len(actual) != len(expected) { + t.Fatalf("unexpected number of items returned, expected = %d, actual = %d", len(expected), len(actual)) + } + for _, expectedObject := range expected { + found := false + for _, actualObject := range actual { + if actualObject.GetName() == expectedObject.GetName() { + if !reflect.DeepEqual(expectedObject, actualObject) { + t.Fatalf("unexpected object has been returned expected = %v actual = %v, diff = %v", expectedObject, actualObject, diff.ObjectDiff(expectedObject, actualObject)) + } + found = true + } + } + if !found { + t.Fatalf("the resource with the name = %s was not found in the returned output", expectedObject.GetName()) + } + } +} + +func newPartialObjectMetadata(apiVersion, kind, namespace, name string) *metav1.PartialObjectMetadata { + return &metav1.PartialObjectMetadata{ + TypeMeta: metav1.TypeMeta{ + APIVersion: apiVersion, + Kind: kind, + }, + ObjectMeta: metav1.ObjectMeta{ + Namespace: namespace, + Name: name, + }, + } +} diff --git a/staging/src/k8s.io/client-go/metadata/metadatalister/shim.go b/staging/src/k8s.io/client-go/metadata/metadatalister/shim.go new file mode 100644 index 00000000000..f31c6072589 --- /dev/null +++ b/staging/src/k8s.io/client-go/metadata/metadatalister/shim.go @@ -0,0 +1,87 @@ +/* +Copyright 2018 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 metadatalister + +import ( + "k8s.io/apimachinery/pkg/labels" + "k8s.io/apimachinery/pkg/runtime" + "k8s.io/client-go/tools/cache" +) + +var _ cache.GenericLister = &metadataListerShim{} +var _ cache.GenericNamespaceLister = &metadataNamespaceListerShim{} + +// metadataListerShim implements the cache.GenericLister interface. +type metadataListerShim struct { + lister Lister +} + +// NewRuntimeObjectShim returns a new shim for Lister. +// It wraps Lister so that it implements cache.GenericLister interface +func NewRuntimeObjectShim(lister Lister) cache.GenericLister { + return &metadataListerShim{lister: lister} +} + +// List will return all objects across namespaces +func (s *metadataListerShim) List(selector labels.Selector) (ret []runtime.Object, err error) { + objs, err := s.lister.List(selector) + if err != nil { + return nil, err + } + + ret = make([]runtime.Object, len(objs)) + for index, obj := range objs { + ret[index] = obj + } + return ret, err +} + +// Get will attempt to retrieve assuming that name==key +func (s *metadataListerShim) Get(name string) (runtime.Object, error) { + return s.lister.Get(name) +} + +func (s *metadataListerShim) ByNamespace(namespace string) cache.GenericNamespaceLister { + return &metadataNamespaceListerShim{ + namespaceLister: s.lister.Namespace(namespace), + } +} + +// metadataNamespaceListerShim implements the NamespaceLister interface. +// It wraps NamespaceLister so that it implements cache.GenericNamespaceLister interface +type metadataNamespaceListerShim struct { + namespaceLister NamespaceLister +} + +// List will return all objects in this namespace +func (ns *metadataNamespaceListerShim) List(selector labels.Selector) (ret []runtime.Object, err error) { + objs, err := ns.namespaceLister.List(selector) + if err != nil { + return nil, err + } + + ret = make([]runtime.Object, len(objs)) + for index, obj := range objs { + ret[index] = obj + } + return ret, err +} + +// Get will attempt to retrieve by namespace and name +func (ns *metadataNamespaceListerShim) Get(name string) (runtime.Object, error) { + return ns.namespaceLister.Get(name) +} From 50fd47258d97299a34b41c98a0ce23bcceee2326 Mon Sep 17 00:00:00 2001 From: Clayton Coleman Date: Wed, 5 Jun 2019 15:19:55 -0400 Subject: [PATCH 3/3] Switch the namespace controller to use the metadata client The metadata client uses protobuf and returns only a subset of object data (the metadata) which allows operations that act only on objects generically to work much faster. Use the metadata client in the namespace controller to reduce the amount of work the namespace controller has to do in large namespaces. --- cmd/kube-controller-manager/app/BUILD | 1 + cmd/kube-controller-manager/app/core.go | 7 ++-- pkg/controller/.import-restrictions | 1 + pkg/controller/namespace/BUILD | 2 +- pkg/controller/namespace/deletion/BUILD | 4 +- .../deletion/namespaced_resources_deleter.go | 25 ++++++------ .../namespaced_resources_deleter_test.go | 39 ++++++++++--------- .../namespace/namespace_controller.go | 8 ++-- test/e2e_node/services/BUILD | 2 +- .../e2e_node/services/namespace_controller.go | 6 +-- 10 files changed, 49 insertions(+), 46 deletions(-) diff --git a/cmd/kube-controller-manager/app/BUILD b/cmd/kube-controller-manager/app/BUILD index 37042d5270b..b1f12174587 100644 --- a/cmd/kube-controller-manager/app/BUILD +++ b/cmd/kube-controller-manager/app/BUILD @@ -124,6 +124,7 @@ go_library( "//staging/src/k8s.io/client-go/dynamic/dynamicinformer:go_default_library", "//staging/src/k8s.io/client-go/informers:go_default_library", "//staging/src/k8s.io/client-go/kubernetes:go_default_library", + "//staging/src/k8s.io/client-go/metadata:go_default_library", "//staging/src/k8s.io/client-go/rest:go_default_library", "//staging/src/k8s.io/client-go/restmapper:go_default_library", "//staging/src/k8s.io/client-go/scale:go_default_library", diff --git a/cmd/kube-controller-manager/app/core.go b/cmd/kube-controller-manager/app/core.go index 1d9612f2028..c2744f11a70 100644 --- a/cmd/kube-controller-manager/app/core.go +++ b/cmd/kube-controller-manager/app/core.go @@ -29,12 +29,13 @@ import ( "k8s.io/klog" - "k8s.io/api/core/v1" + v1 "k8s.io/api/core/v1" "k8s.io/apimachinery/pkg/runtime/schema" utilfeature "k8s.io/apiserver/pkg/util/feature" cacheddiscovery "k8s.io/client-go/discovery/cached/memory" "k8s.io/client-go/dynamic" clientset "k8s.io/client-go/kubernetes" + "k8s.io/client-go/metadata" restclient "k8s.io/client-go/rest" "k8s.io/kubernetes/pkg/controller" cloudcontroller "k8s.io/kubernetes/pkg/controller/cloud" @@ -371,7 +372,7 @@ func startNamespaceController(ctx ControllerContext) (http.Handler, bool, error) func startModifiedNamespaceController(ctx ControllerContext, namespaceKubeClient clientset.Interface, nsKubeconfig *restclient.Config) (http.Handler, bool, error) { - dynamicClient, err := dynamic.NewForConfig(nsKubeconfig) + metadataClient, err := metadata.NewForConfig(nsKubeconfig) if err != nil { return nil, true, err } @@ -380,7 +381,7 @@ func startModifiedNamespaceController(ctx ControllerContext, namespaceKubeClient namespaceController := namespacecontroller.NewNamespaceController( namespaceKubeClient, - dynamicClient, + metadataClient, discoverResourcesFn, ctx.InformerFactory.Core().V1().Namespaces(), ctx.ComponentConfig.NamespaceController.NamespaceSyncPeriod.Duration, diff --git a/pkg/controller/.import-restrictions b/pkg/controller/.import-restrictions index 62d91266fd7..8bceedaca31 100644 --- a/pkg/controller/.import-restrictions +++ b/pkg/controller/.import-restrictions @@ -149,6 +149,7 @@ "k8s.io/client-go/listers/policy/v1beta1", "k8s.io/client-go/listers/rbac/v1", "k8s.io/client-go/listers/storage/v1", + "k8s.io/client-go/metadata", "k8s.io/client-go/pkg/version", "k8s.io/client-go/rest", "k8s.io/client-go/scale", diff --git a/pkg/controller/namespace/BUILD b/pkg/controller/namespace/BUILD index 383657ad4e1..caec5746a95 100644 --- a/pkg/controller/namespace/BUILD +++ b/pkg/controller/namespace/BUILD @@ -21,10 +21,10 @@ go_library( "//staging/src/k8s.io/apimachinery/pkg/apis/meta/v1:go_default_library", "//staging/src/k8s.io/apimachinery/pkg/util/runtime:go_default_library", "//staging/src/k8s.io/apimachinery/pkg/util/wait:go_default_library", - "//staging/src/k8s.io/client-go/dynamic:go_default_library", "//staging/src/k8s.io/client-go/informers/core/v1:go_default_library", "//staging/src/k8s.io/client-go/kubernetes:go_default_library", "//staging/src/k8s.io/client-go/listers/core/v1:go_default_library", + "//staging/src/k8s.io/client-go/metadata:go_default_library", "//staging/src/k8s.io/client-go/tools/cache:go_default_library", "//staging/src/k8s.io/client-go/util/workqueue:go_default_library", "//vendor/k8s.io/klog:go_default_library", diff --git a/pkg/controller/namespace/deletion/BUILD b/pkg/controller/namespace/deletion/BUILD index 0c058ee50df..06578b0ea9e 100644 --- a/pkg/controller/namespace/deletion/BUILD +++ b/pkg/controller/namespace/deletion/BUILD @@ -14,14 +14,13 @@ go_library( "//staging/src/k8s.io/api/core/v1:go_default_library", "//staging/src/k8s.io/apimachinery/pkg/api/errors:go_default_library", "//staging/src/k8s.io/apimachinery/pkg/apis/meta/v1:go_default_library", - "//staging/src/k8s.io/apimachinery/pkg/apis/meta/v1/unstructured:go_default_library", "//staging/src/k8s.io/apimachinery/pkg/runtime/schema:go_default_library", "//staging/src/k8s.io/apimachinery/pkg/util/errors:go_default_library", "//staging/src/k8s.io/apimachinery/pkg/util/runtime:go_default_library", "//staging/src/k8s.io/apimachinery/pkg/util/sets:go_default_library", "//staging/src/k8s.io/client-go/discovery:go_default_library", - "//staging/src/k8s.io/client-go/dynamic:go_default_library", "//staging/src/k8s.io/client-go/kubernetes/typed/core/v1:go_default_library", + "//staging/src/k8s.io/client-go/metadata:go_default_library", "//vendor/k8s.io/klog:go_default_library", ], ) @@ -41,6 +40,7 @@ go_test( "//staging/src/k8s.io/client-go/discovery:go_default_library", "//staging/src/k8s.io/client-go/dynamic:go_default_library", "//staging/src/k8s.io/client-go/kubernetes/fake:go_default_library", + "//staging/src/k8s.io/client-go/metadata:go_default_library", "//staging/src/k8s.io/client-go/rest:go_default_library", "//staging/src/k8s.io/client-go/testing:go_default_library", ], diff --git a/pkg/controller/namespace/deletion/namespaced_resources_deleter.go b/pkg/controller/namespace/deletion/namespaced_resources_deleter.go index 6e7152c1c77..1e702fba6fb 100644 --- a/pkg/controller/namespace/deletion/namespaced_resources_deleter.go +++ b/pkg/controller/namespace/deletion/namespaced_resources_deleter.go @@ -24,17 +24,16 @@ import ( "k8s.io/klog" - "k8s.io/api/core/v1" + v1 "k8s.io/api/core/v1" "k8s.io/apimachinery/pkg/api/errors" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" - "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" "k8s.io/apimachinery/pkg/runtime/schema" utilerrors "k8s.io/apimachinery/pkg/util/errors" utilruntime "k8s.io/apimachinery/pkg/util/runtime" "k8s.io/apimachinery/pkg/util/sets" "k8s.io/client-go/discovery" - "k8s.io/client-go/dynamic" v1clientset "k8s.io/client-go/kubernetes/typed/core/v1" + "k8s.io/client-go/metadata" ) // NamespacedResourcesDeleterInterface is the interface to delete a namespace with all resources in it. @@ -44,13 +43,13 @@ type NamespacedResourcesDeleterInterface interface { // NewNamespacedResourcesDeleter returns a new NamespacedResourcesDeleter. func NewNamespacedResourcesDeleter(nsClient v1clientset.NamespaceInterface, - dynamicClient dynamic.Interface, podsGetter v1clientset.PodsGetter, + metadataClient metadata.Interface, podsGetter v1clientset.PodsGetter, discoverResourcesFn func() ([]*metav1.APIResourceList, error), finalizerToken v1.FinalizerName, deleteNamespaceWhenDone bool) NamespacedResourcesDeleterInterface { d := &namespacedResourcesDeleter{ - nsClient: nsClient, - dynamicClient: dynamicClient, - podsGetter: podsGetter, + nsClient: nsClient, + metadataClient: metadataClient, + podsGetter: podsGetter, opCache: &operationNotSupportedCache{ m: make(map[operationKey]bool), }, @@ -69,7 +68,7 @@ type namespacedResourcesDeleter struct { // Client to manipulate the namespace. nsClient v1clientset.NamespaceInterface // Dynamic client to list and delete all namespaced resources. - dynamicClient dynamic.Interface + metadataClient metadata.Interface // Interface to get PodInterface. podsGetter v1clientset.PodsGetter // Cache of what operations are not supported on each group version resource. @@ -345,7 +344,7 @@ func (d *namespacedResourcesDeleter) deleteCollection(gvr schema.GroupVersionRes // namespace itself. background := metav1.DeletePropagationBackground opts := &metav1.DeleteOptions{PropagationPolicy: &background} - err := d.dynamicClient.Resource(gvr).Namespace(namespace).DeleteCollection(opts, metav1.ListOptions{}) + err := d.metadataClient.Resource(gvr).Namespace(namespace).DeleteCollection(opts, metav1.ListOptions{}) if err == nil { return true, nil @@ -372,7 +371,7 @@ func (d *namespacedResourcesDeleter) deleteCollection(gvr schema.GroupVersionRes // the list of items in the collection (if found) // a boolean if the operation is supported // an error if the operation is supported but could not be completed. -func (d *namespacedResourcesDeleter) listCollection(gvr schema.GroupVersionResource, namespace string) (*unstructured.UnstructuredList, bool, error) { +func (d *namespacedResourcesDeleter) listCollection(gvr schema.GroupVersionResource, namespace string) (*metav1.PartialObjectMetadataList, bool, error) { klog.V(5).Infof("namespace controller - listCollection - namespace: %s, gvr: %v", namespace, gvr) key := operationKey{operation: operationList, gvr: gvr} @@ -381,9 +380,9 @@ func (d *namespacedResourcesDeleter) listCollection(gvr schema.GroupVersionResou return nil, false, nil } - unstructuredList, err := d.dynamicClient.Resource(gvr).Namespace(namespace).List(metav1.ListOptions{}) + partialList, err := d.metadataClient.Resource(gvr).Namespace(namespace).List(metav1.ListOptions{}) if err == nil { - return unstructuredList, true, nil + return partialList, true, nil } // this is strange, but we need to special case for both MethodNotSupported and NotFound errors @@ -415,7 +414,7 @@ func (d *namespacedResourcesDeleter) deleteEachItem(gvr schema.GroupVersionResou for _, item := range unstructuredList.Items { background := metav1.DeletePropagationBackground opts := &metav1.DeleteOptions{PropagationPolicy: &background} - if err = d.dynamicClient.Resource(gvr).Namespace(namespace).Delete(item.GetName(), opts); err != nil && !errors.IsNotFound(err) && !errors.IsMethodNotSupported(err) { + if err = d.metadataClient.Resource(gvr).Namespace(namespace).Delete(item.GetName(), opts); err != nil && !errors.IsNotFound(err) && !errors.IsMethodNotSupported(err) { return err } } diff --git a/pkg/controller/namespace/deletion/namespaced_resources_deleter_test.go b/pkg/controller/namespace/deletion/namespaced_resources_deleter_test.go index 079e280199b..7b2bf098821 100644 --- a/pkg/controller/namespace/deletion/namespaced_resources_deleter_test.go +++ b/pkg/controller/namespace/deletion/namespaced_resources_deleter_test.go @@ -25,7 +25,7 @@ import ( "sync" "testing" - "k8s.io/api/core/v1" + v1 "k8s.io/api/core/v1" "k8s.io/apimachinery/pkg/api/errors" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/apimachinery/pkg/runtime" @@ -34,6 +34,7 @@ import ( "k8s.io/client-go/discovery" "k8s.io/client-go/dynamic" "k8s.io/client-go/kubernetes/fake" + "k8s.io/client-go/metadata" restclient "k8s.io/client-go/rest" core "k8s.io/client-go/testing" api "k8s.io/kubernetes/pkg/apis/core" @@ -115,7 +116,7 @@ func testSyncNamespaceThatIsTerminating(t *testing.T, versions *metav1.APIVersio } // when doing a delete all of content, we will do a GET of a collection, and DELETE of a collection by default - dynamicClientActionSet := sets.NewString() + metadataClientActionSet := sets.NewString() resources := testResources() groupVersionResources, _ := discovery.GroupVersionResources(resources) for groupVersionResource := range groupVersionResources { @@ -127,15 +128,15 @@ func testSyncNamespaceThatIsTerminating(t *testing.T, versions *metav1.APIVersio namespaceName, groupVersionResource.Resource, }...) - dynamicClientActionSet.Insert((&fakeAction{method: "GET", path: urlPath}).String()) - dynamicClientActionSet.Insert((&fakeAction{method: "DELETE", path: urlPath}).String()) + metadataClientActionSet.Insert((&fakeAction{method: "GET", path: urlPath}).String()) + metadataClientActionSet.Insert((&fakeAction{method: "DELETE", path: urlPath}).String()) } scenarios := map[string]struct { - testNamespace *v1.Namespace - kubeClientActionSet sets.String - dynamicClientActionSet sets.String - gvrError error + testNamespace *v1.Namespace + kubeClientActionSet sets.String + metadataClientActionSet sets.String + gvrError error }{ "pending-finalize": { testNamespace: testNamespacePendingFinalize, @@ -145,7 +146,7 @@ func testSyncNamespaceThatIsTerminating(t *testing.T, versions *metav1.APIVersio strings.Join([]string{"list", "pods", ""}, "-"), strings.Join([]string{"delete", "namespaces", ""}, "-"), ), - dynamicClientActionSet: dynamicClientActionSet, + metadataClientActionSet: metadataClientActionSet, }, "complete-finalize": { testNamespace: testNamespaceFinalizeComplete, @@ -153,7 +154,7 @@ func testSyncNamespaceThatIsTerminating(t *testing.T, versions *metav1.APIVersio strings.Join([]string{"get", "namespaces", ""}, "-"), strings.Join([]string{"delete", "namespaces", ""}, "-"), ), - dynamicClientActionSet: sets.NewString(), + metadataClientActionSet: sets.NewString(), }, "groupVersionResourceErr": { testNamespace: testNamespaceFinalizeComplete, @@ -161,8 +162,8 @@ func testSyncNamespaceThatIsTerminating(t *testing.T, versions *metav1.APIVersio strings.Join([]string{"get", "namespaces", ""}, "-"), strings.Join([]string{"delete", "namespaces", ""}, "-"), ), - dynamicClientActionSet: sets.NewString(), - gvrError: fmt.Errorf("test error"), + metadataClientActionSet: sets.NewString(), + gvrError: fmt.Errorf("test error"), }, } @@ -172,7 +173,7 @@ func testSyncNamespaceThatIsTerminating(t *testing.T, versions *metav1.APIVersio defer srv.Close() mockClient := fake.NewSimpleClientset(testInput.testNamespace) - dynamicClient, err := dynamic.NewForConfig(clientConfig) + metadataClient, err := metadata.NewForConfig(clientConfig) if err != nil { t.Fatal(err) } @@ -180,7 +181,7 @@ func testSyncNamespaceThatIsTerminating(t *testing.T, versions *metav1.APIVersio fn := func() ([]*metav1.APIResourceList, error) { return resources, nil } - d := NewNamespacedResourcesDeleter(mockClient.CoreV1().Namespaces(), dynamicClient, mockClient.CoreV1(), fn, v1.FinalizerKubernetes, true) + d := NewNamespacedResourcesDeleter(mockClient.CoreV1().Namespaces(), metadataClient, mockClient.CoreV1(), fn, v1.FinalizerKubernetes, true) if err := d.Delete(testInput.testNamespace.Name); err != nil { t.Errorf("scenario %s - Unexpected error when synching namespace %v", scenario, err) } @@ -195,14 +196,14 @@ func testSyncNamespaceThatIsTerminating(t *testing.T, versions *metav1.APIVersio testInput.kubeClientActionSet, actionSet, testInput.kubeClientActionSet.Difference(actionSet)) } - // validate traffic from dynamic client + // validate traffic from metadata client actionSet = sets.NewString() for _, action := range testHandler.actions { actionSet.Insert(action.String()) } - if !actionSet.Equal(testInput.dynamicClientActionSet) { - t.Errorf("scenario %s - dynamic client expected actions:\n%v\n but got:\n%v\nDifference:\n%v", scenario, - testInput.dynamicClientActionSet, actionSet, testInput.dynamicClientActionSet.Difference(actionSet)) + if !actionSet.Equal(testInput.metadataClientActionSet) { + t.Errorf("scenario %s - metadata client expected actions:\n%v\n but got:\n%v\nDifference:\n%v", scenario, + testInput.metadataClientActionSet, actionSet, testInput.metadataClientActionSet.Difference(actionSet)) } } } @@ -307,7 +308,7 @@ func (f *fakeActionHandler) ServeHTTP(response http.ResponseWriter, request *htt f.actions = append(f.actions, fakeAction{method: request.Method, path: request.URL.Path}) response.Header().Set("Content-Type", runtime.ContentTypeJSON) response.WriteHeader(f.statusCode) - response.Write([]byte("{\"kind\": \"List\",\"items\":null}")) + response.Write([]byte("{\"apiVersion\": \"v1\", \"kind\": \"List\",\"items\":null}")) } // testResources returns a mocked up set of resources across different api groups for testing namespace controller. diff --git a/pkg/controller/namespace/namespace_controller.go b/pkg/controller/namespace/namespace_controller.go index 53d4225066d..b5d374d4f56 100644 --- a/pkg/controller/namespace/namespace_controller.go +++ b/pkg/controller/namespace/namespace_controller.go @@ -20,15 +20,15 @@ import ( "fmt" "time" - "k8s.io/api/core/v1" + v1 "k8s.io/api/core/v1" "k8s.io/apimachinery/pkg/api/errors" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" utilruntime "k8s.io/apimachinery/pkg/util/runtime" "k8s.io/apimachinery/pkg/util/wait" - "k8s.io/client-go/dynamic" coreinformers "k8s.io/client-go/informers/core/v1" clientset "k8s.io/client-go/kubernetes" corelisters "k8s.io/client-go/listers/core/v1" + "k8s.io/client-go/metadata" "k8s.io/client-go/tools/cache" "k8s.io/client-go/util/workqueue" "k8s.io/kubernetes/pkg/controller" @@ -63,7 +63,7 @@ type NamespaceController struct { // NewNamespaceController creates a new NamespaceController func NewNamespaceController( kubeClient clientset.Interface, - dynamicClient dynamic.Interface, + metadataClient metadata.Interface, discoverResourcesFn func() ([]*metav1.APIResourceList, error), namespaceInformer coreinformers.NamespaceInformer, resyncPeriod time.Duration, @@ -72,7 +72,7 @@ func NewNamespaceController( // create the controller so we can inject the enqueue function namespaceController := &NamespaceController{ queue: workqueue.NewNamedRateLimitingQueue(workqueue.DefaultControllerRateLimiter(), "namespace"), - namespacedResourcesDeleter: deletion.NewNamespacedResourcesDeleter(kubeClient.CoreV1().Namespaces(), dynamicClient, kubeClient.CoreV1(), discoverResourcesFn, finalizerToken, true), + namespacedResourcesDeleter: deletion.NewNamespacedResourcesDeleter(kubeClient.CoreV1().Namespaces(), metadataClient, kubeClient.CoreV1(), discoverResourcesFn, finalizerToken, true), } if kubeClient != nil && kubeClient.CoreV1().RESTClient().GetRateLimiter() != nil { diff --git a/test/e2e_node/services/BUILD b/test/e2e_node/services/BUILD index 58fe7dbc9f4..bcda62a6a90 100644 --- a/test/e2e_node/services/BUILD +++ b/test/e2e_node/services/BUILD @@ -31,9 +31,9 @@ go_library( "//staging/src/k8s.io/apiserver/pkg/storage/etcd3/testing:go_default_library", "//staging/src/k8s.io/apiserver/pkg/storage/storagebackend:go_default_library", "//staging/src/k8s.io/apiserver/pkg/util/feature:go_default_library", - "//staging/src/k8s.io/client-go/dynamic:go_default_library", "//staging/src/k8s.io/client-go/informers:go_default_library", "//staging/src/k8s.io/client-go/kubernetes:go_default_library", + "//staging/src/k8s.io/client-go/metadata:go_default_library", "//staging/src/k8s.io/client-go/rest:go_default_library", "//staging/src/k8s.io/component-base/cli/flag:go_default_library", "//staging/src/k8s.io/kubelet/config/v1beta1:go_default_library", diff --git a/test/e2e_node/services/namespace_controller.go b/test/e2e_node/services/namespace_controller.go index 7b8e1514be3..68d8134ebfc 100644 --- a/test/e2e_node/services/namespace_controller.go +++ b/test/e2e_node/services/namespace_controller.go @@ -20,9 +20,9 @@ import ( "time" "k8s.io/api/core/v1" - "k8s.io/client-go/dynamic" "k8s.io/client-go/informers" clientset "k8s.io/client-go/kubernetes" + "k8s.io/client-go/metadata" restclient "k8s.io/client-go/rest" namespacecontroller "k8s.io/kubernetes/pkg/controller/namespace" ) @@ -59,7 +59,7 @@ func (n *NamespaceController) Start() error { if err != nil { return err } - dynamicClient, err := dynamic.NewForConfig(config) + metadataClient, err := metadata.NewForConfig(config) if err != nil { return err } @@ -67,7 +67,7 @@ func (n *NamespaceController) Start() error { informerFactory := informers.NewSharedInformerFactory(client, ncResyncPeriod) nc := namespacecontroller.NewNamespaceController( client, - dynamicClient, + metadataClient, discoverResourcesFn, informerFactory.Core().V1().Namespaces(), ncResyncPeriod, v1.FinalizerKubernetes,