From b83dc9a7d9da365f8a1939e827c70b112de17f92 Mon Sep 17 00:00:00 2001 From: Clayton Coleman Date: Wed, 3 Apr 2019 12:12:11 -0400 Subject: [PATCH 1/2] 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. Kubernetes-commit: baf091e9dbad00db39e246815f9d7a21d148044f --- testing/fixture.go | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/testing/fixture.go b/testing/fixture.go index b3ace307..98f82326 100644 --- a/testing/fixture.go +++ b/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 cade5c04737810a5510cda8c31ea2bd026085907 Mon Sep 17 00:00:00 2001 From: Clayton Coleman Date: Wed, 5 Jun 2019 14:28:51 -0400 Subject: [PATCH 2/2] 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) +}