From 20c034c1785b34a893f7f96d91e5b849ac52ff31 Mon Sep 17 00:00:00 2001 From: David Eads Date: Thu, 29 Oct 2020 15:53:34 -0400 Subject: [PATCH] add GVK to fake dynamic client to match actual behavior Kubernetes-commit: f4383458432cd67714e9ce0acde56a2ed5c24a21 --- dynamic/dynamicinformer/informer_test.go | 14 ++++- dynamic/fake/simple.go | 64 +++++++++++++++++---- dynamic/fake/simple_test.go | 71 +++++++++++++++++++++++- 3 files changed, 135 insertions(+), 14 deletions(-) diff --git a/dynamic/dynamicinformer/informer_test.go b/dynamic/dynamicinformer/informer_test.go index b3b10ef7e3..98acdb40fc 100644 --- a/dynamic/dynamicinformer/informer_test.go +++ b/dynamic/dynamicinformer/informer_test.go @@ -95,7 +95,12 @@ func TestFilteredDynamicSharedInformerFactory(t *testing.T) { if ts.existingObj != nil { objs = append(objs, ts.existingObj) } - fakeClient := fake.NewSimpleDynamicClient(scheme, objs...) + // don't adjust the scheme to include deploymentlist. This is testing whether an informer can be created against using + // a client that doesn't have a type registered in the scheme. + gvrToListKind := map[schema.GroupVersionResource]string{ + {Group: "apps", Version: "v1", Resource: "deployments"}: "DeploymentList", + } + fakeClient := fake.NewSimpleDynamicClientWithCustomListKinds(scheme, gvrToListKind, objs...) target := dynamicinformer.NewFilteredDynamicSharedInformerFactory(fakeClient, 0, ts.informNS, nil) // act @@ -214,7 +219,12 @@ func TestDynamicSharedInformerFactory(t *testing.T) { if ts.existingObj != nil { objs = append(objs, ts.existingObj) } - fakeClient := fake.NewSimpleDynamicClient(scheme, objs...) + // don't adjust the scheme to include deploymentlist. This is testing whether an informer can be created against using + // a client that doesn't have a type registered in the scheme. + gvrToListKind := map[schema.GroupVersionResource]string{ + {Group: "extensions", Version: "v1beta1", Resource: "deployments"}: "DeploymentList", + } + fakeClient := fake.NewSimpleDynamicClientWithCustomListKinds(scheme, gvrToListKind, objs...) target := dynamicinformer.NewDynamicSharedInformerFactory(fakeClient, 0) // act diff --git a/dynamic/fake/simple.go b/dynamic/fake/simple.go index 001780970b..622995776a 100644 --- a/dynamic/fake/simple.go +++ b/dynamic/fake/simple.go @@ -18,6 +18,7 @@ package fake import ( "context" + "fmt" "strings" "k8s.io/apimachinery/pkg/api/meta" @@ -34,11 +35,45 @@ import ( ) func NewSimpleDynamicClient(scheme *runtime.Scheme, objects ...runtime.Object) *FakeDynamicClient { - // In order to use List with this client, you have to have the v1.List registered in your scheme. Neat thing though - // it does NOT have to be the *same* list. UnstructuredList returned from this fake client will NOT have apiVersion and kind set, - // but each Unstructured object in Items will preserve their respective apiVersion and kind. As a result, schema conversion for - // *List kinds will not work and conversion of each Unstructured object in Items will be required instead. - scheme.AddKnownTypeWithName(schema.GroupVersionKind{Group: "fake-dynamic-client-group", Version: "v1", Kind: "List"}, &unstructured.UnstructuredList{}) + return NewSimpleDynamicClientWithCustomListKinds(scheme, nil, objects...) +} + +// NewSimpleDynamicClientWithCustomListKinds try not to use this. In general you want to have the scheme have the List types registered +// and allow the default guessing for resources match. Sometimes that doesn't work, so you can specify a custom mapping here. +func NewSimpleDynamicClientWithCustomListKinds(scheme *runtime.Scheme, gvrToListKind map[schema.GroupVersionResource]string, objects ...runtime.Object) *FakeDynamicClient { + // In order to use List with this client, you have to have your lists registered so that the object tracker will find them + // in the scheme to support the t.scheme.New(listGVK) call when it's building the return value. + // Since the base fake client needs the listGVK passed through the action (in cases where there are no instances, it + // cannot look up the actual hits), we need to know a mapping of GVR to listGVK here. For GETs and other types of calls, + // there is no return value that contains a GVK, so it doesn't have to know the mapping in advance. + + // first we attempt to invert known List types from the scheme to auto guess the resource with unsafe guesses + // this covers common usage of registering types in scheme and passing them + completeGVRToListKind := map[schema.GroupVersionResource]string{} + for listGVK := range scheme.AllKnownTypes() { + if !strings.HasSuffix(listGVK.Kind, "List") { + continue + } + nonListGVK := listGVK.GroupVersion().WithKind(listGVK.Kind[:len(listGVK.Kind)-4]) + plural, _ := meta.UnsafeGuessKindToResource(nonListGVK) + completeGVRToListKind[plural] = listGVK.Kind + } + + for gvr, listKind := range gvrToListKind { + if !strings.HasSuffix(listKind, "List") { + panic("coding error, listGVK must end in List or this fake client doesn't work right") + } + listGVK := gvr.GroupVersion().WithKind(listKind) + + // if we already have this type registered, just skip it + if _, err := scheme.New(listGVK); err == nil { + completeGVRToListKind[gvr] = listKind + continue + } + + scheme.AddKnownTypeWithName(listGVK, &unstructured.UnstructuredList{}) + completeGVRToListKind[gvr] = listKind + } codecs := serializer.NewCodecFactory(scheme) o := testing.NewObjectTracker(scheme, codecs.UniversalDecoder()) @@ -48,7 +83,7 @@ func NewSimpleDynamicClient(scheme *runtime.Scheme, objects ...runtime.Object) * } } - cs := &FakeDynamicClient{scheme: scheme} + cs := &FakeDynamicClient{scheme: scheme, gvrToListKind: completeGVRToListKind} cs.AddReactor("*", "*", testing.ObjectReaction(o)) cs.AddWatchReactor("*", func(action testing.Action) (handled bool, ret watch.Interface, err error) { gvr := action.GetResource() @@ -68,19 +103,21 @@ func NewSimpleDynamicClient(scheme *runtime.Scheme, objects ...runtime.Object) * // you want to test easier. type FakeDynamicClient struct { testing.Fake - scheme *runtime.Scheme + scheme *runtime.Scheme + gvrToListKind map[schema.GroupVersionResource]string } type dynamicResourceClient struct { client *FakeDynamicClient namespace string resource schema.GroupVersionResource + listKind string } var _ dynamic.Interface = &FakeDynamicClient{} func (c *FakeDynamicClient) Resource(resource schema.GroupVersionResource) dynamic.NamespaceableResourceInterface { - return &dynamicResourceClient{client: c, resource: resource} + return &dynamicResourceClient{client: c, resource: resource, listKind: c.gvrToListKind[resource]} } func (c *dynamicResourceClient) Namespace(ns string) dynamic.ResourceInterface { @@ -276,16 +313,22 @@ func (c *dynamicResourceClient) Get(ctx context.Context, name string, opts metav } func (c *dynamicResourceClient) List(ctx context.Context, opts metav1.ListOptions) (*unstructured.UnstructuredList, error) { + if len(c.listKind) == 0 { + panic(fmt.Sprintf("coding error: you must register resource to list kind for every resource you're going to LIST when creating the client. See NewSimpleDynamicClientWithCustomListKinds or register the list into the scheme: %v out of %v", c.resource, c.client.gvrToListKind)) + } + listGVK := c.resource.GroupVersion().WithKind(c.listKind) + listForFakeClientGVK := c.resource.GroupVersion().WithKind(c.listKind[:len(c.listKind)-4]) /*base library appends List*/ + 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-dynamic-client-group", Version: "v1", Kind: "" /*List is appended by the tracker automatically*/}, opts), &metav1.Status{Status: "dynamic list fail"}) + Invokes(testing.NewRootListAction(c.resource, listForFakeClientGVK, opts), &metav1.Status{Status: "dynamic list fail"}) case len(c.namespace) > 0: obj, err = c.client.Fake. - Invokes(testing.NewListAction(c.resource, schema.GroupVersionKind{Group: "fake-dynamic-client-group", Version: "v1", Kind: "" /*List is appended by the tracker automatically*/}, c.namespace, opts), &metav1.Status{Status: "dynamic list fail"}) + Invokes(testing.NewListAction(c.resource, listForFakeClientGVK, c.namespace, opts), &metav1.Status{Status: "dynamic list fail"}) } @@ -309,6 +352,7 @@ func (c *dynamicResourceClient) List(ctx context.Context, opts metav1.ListOption list := &unstructured.UnstructuredList{} list.SetResourceVersion(entireList.GetResourceVersion()) + list.GetObjectKind().SetGroupVersionKind(listGVK) for i := range entireList.Items { item := &entireList.Items[i] metadata, err := meta.Accessor(item) diff --git a/dynamic/fake/simple_test.go b/dynamic/fake/simple_test.go index 2babc07557..c7f8c1af3c 100644 --- a/dynamic/fake/simple_test.go +++ b/dynamic/fake/simple_test.go @@ -59,10 +59,74 @@ func newUnstructuredWithSpec(spec map[string]interface{}) *unstructured.Unstruct return u } +func TestGet(t *testing.T) { + scheme := runtime.NewScheme() + + client := NewSimpleDynamicClient(scheme, newUnstructured("group/version", "TheKind", "ns-foo", "name-foo")) + get, err := client.Resource(schema.GroupVersionResource{Group: "group", Version: "version", Resource: "thekinds"}).Namespace("ns-foo").Get(context.TODO(), "name-foo", metav1.GetOptions{}) + if err != nil { + t.Fatal(err) + } + + expected := &unstructured.Unstructured{ + Object: map[string]interface{}{ + "apiVersion": "group/version", + "kind": "TheKind", + "metadata": map[string]interface{}{ + "name": "name-foo", + "namespace": "ns-foo", + }, + }, + } + if !equality.Semantic.DeepEqual(get, expected) { + t.Fatal(diff.ObjectGoPrintDiff(expected, get)) + } +} + +func TestListDecoding(t *testing.T) { + // this the duplication of logic from the real List API. This will prove that our dynamic client actually returns the gvk + uncastObj, err := runtime.Decode(unstructured.UnstructuredJSONScheme, []byte(`{"apiVersion": "group/version", "kind": "TheKindList", "items":[]}`)) + if err != nil { + t.Fatal(err) + } + list := uncastObj.(*unstructured.UnstructuredList) + expectedList := &unstructured.UnstructuredList{ + Object: map[string]interface{}{ + "apiVersion": "group/version", + "kind": "TheKindList", + }, + Items: []unstructured.Unstructured{}, + } + if !equality.Semantic.DeepEqual(list, expectedList) { + t.Fatal(diff.ObjectGoPrintDiff(expectedList, list)) + } +} + +func TestGetDecoding(t *testing.T) { + // this the duplication of logic from the real Get API. This will prove that our dynamic client actually returns the gvk + uncastObj, err := runtime.Decode(unstructured.UnstructuredJSONScheme, []byte(`{"apiVersion": "group/version", "kind": "TheKind"}`)) + if err != nil { + t.Fatal(err) + } + get := uncastObj.(*unstructured.Unstructured) + expectedObj := &unstructured.Unstructured{ + Object: map[string]interface{}{ + "apiVersion": "group/version", + "kind": "TheKind", + }, + } + if !equality.Semantic.DeepEqual(get, expectedObj) { + t.Fatal(diff.ObjectGoPrintDiff(expectedObj, get)) + } +} + func TestList(t *testing.T) { scheme := runtime.NewScheme() - client := NewSimpleDynamicClient(scheme, + client := NewSimpleDynamicClientWithCustomListKinds(scheme, + map[schema.GroupVersionResource]string{ + {Group: "group", Version: "version", Resource: "thekinds"}: "TheKindList", + }, newUnstructured("group/version", "TheKind", "ns-foo", "name-foo"), newUnstructured("group2/version", "TheKind", "ns-foo", "name2-foo"), newUnstructured("group/version", "TheKind", "ns-foo", "name-bar"), @@ -87,7 +151,10 @@ func TestList(t *testing.T) { func Test_ListKind(t *testing.T) { scheme := runtime.NewScheme() - client := NewSimpleDynamicClient(scheme, + client := NewSimpleDynamicClientWithCustomListKinds(scheme, + map[schema.GroupVersionResource]string{ + {Group: "group", Version: "version", Resource: "thekinds"}: "TheKindList", + }, &unstructured.UnstructuredList{ Object: map[string]interface{}{ "apiVersion": "group/version",