From cade5c04737810a5510cda8c31ea2bd026085907 Mon Sep 17 00:00:00 2001 From: Clayton Coleman Date: Wed, 5 Jun 2019 14:28:51 -0400 Subject: [PATCH] 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. Kubernetes-commit: bc89c37f32aa6cfd0f9ca975d9221d0a89320623 --- metadata/fake/simple.go | 390 +++++++++++++++++++++ metadata/fake/simple_test.go | 201 +++++++++++ metadata/metadatainformer/informer.go | 156 +++++++++ metadata/metadatainformer/informer_test.go | 170 +++++++++ metadata/metadatainformer/interface.go | 34 ++ metadata/metadatalister/interface.go | 40 +++ metadata/metadatalister/lister.go | 91 +++++ metadata/metadatalister/lister_test.go | 256 ++++++++++++++ metadata/metadatalister/shim.go | 87 +++++ 9 files changed, 1425 insertions(+) create mode 100644 metadata/fake/simple.go create mode 100644 metadata/fake/simple_test.go create mode 100644 metadata/metadatainformer/informer.go create mode 100644 metadata/metadatainformer/informer_test.go create mode 100644 metadata/metadatainformer/interface.go create mode 100644 metadata/metadatalister/interface.go create mode 100644 metadata/metadatalister/lister.go create mode 100644 metadata/metadatalister/lister_test.go create mode 100644 metadata/metadatalister/shim.go diff --git a/metadata/fake/simple.go b/metadata/fake/simple.go new file mode 100644 index 00000000..0667e9e1 --- /dev/null +++ b/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/metadata/fake/simple_test.go b/metadata/fake/simple_test.go new file mode 100644 index 00000000..ece08f3b --- /dev/null +++ b/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/metadata/metadatainformer/informer.go b/metadata/metadatainformer/informer.go new file mode 100644 index 00000000..4c9efed1 --- /dev/null +++ b/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/metadata/metadatainformer/informer_test.go b/metadata/metadatainformer/informer_test.go new file mode 100644 index 00000000..ef3b186e --- /dev/null +++ b/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/metadata/metadatainformer/interface.go b/metadata/metadatainformer/interface.go new file mode 100644 index 00000000..732e565c --- /dev/null +++ b/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/metadata/metadatalister/interface.go b/metadata/metadatalister/interface.go new file mode 100644 index 00000000..bb354858 --- /dev/null +++ b/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/metadata/metadatalister/lister.go b/metadata/metadatalister/lister.go new file mode 100644 index 00000000..faeccc0f --- /dev/null +++ b/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/metadata/metadatalister/lister_test.go b/metadata/metadatalister/lister_test.go new file mode 100644 index 00000000..7c32c1e7 --- /dev/null +++ b/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/metadata/metadatalister/shim.go b/metadata/metadatalister/shim.go new file mode 100644 index 00000000..f31c6072 --- /dev/null +++ b/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) +}