From c6c0ca0e1e60953904542b283b18d26e2947cb93 Mon Sep 17 00:00:00 2001 From: dprotaso Date: Wed, 16 Jun 2021 14:51:00 -0400 Subject: [PATCH] Simplify use of the fake dynamic client With the introduction of GVK to the fake dynamic client it made using the fake much more cumbersome. Specifically: - requires manual registration of list types - mismatch between scheme types and passed in fixtures would result in errors The PR changes the constructor method NewSimpleDynamicClient to do the following: - rewire the schemes to unstructured types - typed fixtures are converted to unstructured types - automatically register fixture gvks with the scheme This should make the dynamic client 'flexible' with it's inputs like it was before Kubernetes-commit: 418fa71b6b1d1fba930daaba1f8ecf55070b4bdf --- dynamic/fake/simple.go | 68 ++++++++++++++- dynamic/fake/simple_test.go | 164 ++++++++++++++++++++++++++++++++++++ 2 files changed, 231 insertions(+), 1 deletion(-) diff --git a/dynamic/fake/simple.go b/dynamic/fake/simple.go index 82f805d1f2..dee16b245a 100644 --- a/dynamic/fake/simple.go +++ b/dynamic/fake/simple.go @@ -35,7 +35,35 @@ import ( ) func NewSimpleDynamicClient(scheme *runtime.Scheme, objects ...runtime.Object) *FakeDynamicClient { - return NewSimpleDynamicClientWithCustomListKinds(scheme, nil, objects...) + unstructuredScheme := runtime.NewScheme() + for gvk := range scheme.AllKnownTypes() { + if unstructuredScheme.Recognizes(gvk) { + continue + } + if strings.HasSuffix(gvk.Kind, "List") { + unstructuredScheme.AddKnownTypeWithName(gvk, &unstructured.UnstructuredList{}) + continue + } + unstructuredScheme.AddKnownTypeWithName(gvk, &unstructured.Unstructured{}) + } + + objects, err := convertObjectsToUnstructured(scheme, objects) + if err != nil { + panic(err) + } + + for _, obj := range objects { + gvk := obj.GetObjectKind().GroupVersionKind() + if !unstructuredScheme.Recognizes(gvk) { + unstructuredScheme.AddKnownTypeWithName(gvk, &unstructured.Unstructured{}) + } + gvk.Kind += "List" + if !unstructuredScheme.Recognizes(gvk) { + unstructuredScheme.AddKnownTypeWithName(gvk, &unstructured.UnstructuredList{}) + } + } + + return NewSimpleDynamicClientWithCustomListKinds(unstructuredScheme, nil, objects...) } // NewSimpleDynamicClientWithCustomListKinds try not to use this. In general you want to have the scheme have the List types registered @@ -425,3 +453,41 @@ func (c *dynamicResourceClient) Patch(ctx context.Context, name string, pt types } return ret, err } + +func convertObjectsToUnstructured(s *runtime.Scheme, objs []runtime.Object) ([]runtime.Object, error) { + ul := make([]runtime.Object, 0, len(objs)) + + for _, obj := range objs { + u, err := convertToUnstructured(s, obj) + if err != nil { + return nil, err + } + + ul = append(ul, u) + } + return ul, nil +} + +func convertToUnstructured(s *runtime.Scheme, obj runtime.Object) (runtime.Object, error) { + var ( + err error + u unstructured.Unstructured + ) + + u.Object, err = runtime.DefaultUnstructuredConverter.ToUnstructured(obj) + if err != nil { + return nil, fmt.Errorf("failed to convert to unstructured: %w", err) + } + + gvk := u.GroupVersionKind() + if gvk.Group == "" || gvk.Kind == "" { + gvks, _, err := s.ObjectKinds(obj) + if err != nil { + return nil, fmt.Errorf("failed to convert to unstructured - unable to get GVK %w", err) + } + apiv, k := gvks[0].ToAPIVersionAndKind() + u.SetAPIVersion(apiv) + u.SetKind(k) + } + return &u, nil +} diff --git a/dynamic/fake/simple_test.go b/dynamic/fake/simple_test.go index c7f8c1af3c..bedacf94ac 100644 --- a/dynamic/fake/simple_test.go +++ b/dynamic/fake/simple_test.go @@ -21,6 +21,7 @@ import ( "fmt" "testing" + "github.com/google/go-cmp/cmp" "k8s.io/apimachinery/pkg/api/equality" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" @@ -303,3 +304,166 @@ func TestPatch(t *testing.T) { t.Run(tc.name, tc.runner) } } + +// This test ensures list works when the fake dynamic client is seeded with a typed scheme and +// unstructured type fixtures +func TestListWithUnstructuredObjectsAndTypedScheme(t *testing.T) { + gvr := schema.GroupVersionResource{Group: testGroup, Version: testVersion, Resource: testResource} + gvk := gvr.GroupVersion().WithKind(testKind) + + listGVK := gvk + listGVK.Kind += "List" + + u := unstructured.Unstructured{} + u.SetGroupVersionKind(gvk) + u.SetName("name") + u.SetNamespace("namespace") + + typedScheme := runtime.NewScheme() + typedScheme.AddKnownTypeWithName(gvk, &mockResource{}) + typedScheme.AddKnownTypeWithName(listGVK, &mockResourceList{}) + + client := NewSimpleDynamicClient(typedScheme, &u) + list, err := client.Resource(gvr).Namespace("namespace").List(context.Background(), metav1.ListOptions{}) + + if err != nil { + t.Error("error listing", err) + } + + expectedList := &unstructured.UnstructuredList{} + expectedList.SetGroupVersionKind(listGVK) + expectedList.SetResourceVersion("") // by product of the fake setting resource version + expectedList.Items = append(expectedList.Items, u) + + if diff := cmp.Diff(expectedList, list); diff != "" { + t.Fatal("unexpected diff (-want, +got): ", diff) + } +} + +func TestListWithNoFixturesAndTypedScheme(t *testing.T) { + gvr := schema.GroupVersionResource{Group: testGroup, Version: testVersion, Resource: testResource} + gvk := gvr.GroupVersion().WithKind(testKind) + + listGVK := gvk + listGVK.Kind += "List" + + typedScheme := runtime.NewScheme() + typedScheme.AddKnownTypeWithName(gvk, &mockResource{}) + typedScheme.AddKnownTypeWithName(listGVK, &mockResourceList{}) + + client := NewSimpleDynamicClient(typedScheme) + list, err := client.Resource(gvr).Namespace("namespace").List(context.Background(), metav1.ListOptions{}) + + if err != nil { + t.Error("error listing", err) + } + + expectedList := &unstructured.UnstructuredList{} + expectedList.SetGroupVersionKind(listGVK) + expectedList.SetResourceVersion("") // by product of the fake setting resource version + + if diff := cmp.Diff(expectedList, list); diff != "" { + t.Fatal("unexpected diff (-want, +got): ", diff) + } +} + +// This test ensures list works when the dynamic client is seeded with an empty scheme and +// unstructured typed fixtures +func TestListWithNoScheme(t *testing.T) { + gvr := schema.GroupVersionResource{Group: testGroup, Version: testVersion, Resource: testResource} + gvk := gvr.GroupVersion().WithKind(testKind) + + listGVK := gvk + listGVK.Kind += "List" + + u := unstructured.Unstructured{} + u.SetGroupVersionKind(gvk) + u.SetName("name") + u.SetNamespace("namespace") + + emptyScheme := runtime.NewScheme() + + client := NewSimpleDynamicClient(emptyScheme, &u) + list, err := client.Resource(gvr).Namespace("namespace").List(context.Background(), metav1.ListOptions{}) + + if err != nil { + t.Error("error listing", err) + } + + expectedList := &unstructured.UnstructuredList{} + expectedList.SetGroupVersionKind(listGVK) + expectedList.SetResourceVersion("") // by product of the fake setting resource version + expectedList.Items = append(expectedList.Items, u) + + if diff := cmp.Diff(expectedList, list); diff != "" { + t.Fatal("unexpected diff (-want, +got): ", diff) + } +} + +// This test ensures list works when the dynamic client is seeded with an empty scheme and +// unstructured typed fixtures +func TestListWithTypedFixtures(t *testing.T) { + gvr := schema.GroupVersionResource{Group: testGroup, Version: testVersion, Resource: testResource} + gvk := gvr.GroupVersion().WithKind(testKind) + + listGVK := gvk + listGVK.Kind += "List" + + r := mockResource{} + r.SetGroupVersionKind(gvk) + r.SetName("name") + r.SetNamespace("namespace") + + u := unstructured.Unstructured{} + u.SetGroupVersionKind(r.GetObjectKind().GroupVersionKind()) + u.SetName(r.GetName()) + u.SetNamespace(r.GetNamespace()) + // Needed see: https://github.com/kubernetes/kubernetes/issues/67610 + unstructured.SetNestedField(u.Object, nil, "metadata", "creationTimestamp") + + typedScheme := runtime.NewScheme() + typedScheme.AddKnownTypeWithName(gvk, &mockResource{}) + typedScheme.AddKnownTypeWithName(listGVK, &mockResourceList{}) + + client := NewSimpleDynamicClient(typedScheme, &r) + list, err := client.Resource(gvr).Namespace("namespace").List(context.Background(), metav1.ListOptions{}) + + if err != nil { + t.Error("error listing", err) + } + + expectedList := &unstructured.UnstructuredList{} + expectedList.SetGroupVersionKind(listGVK) + expectedList.SetResourceVersion("") // by product of the fake setting resource version + expectedList.Items = []unstructured.Unstructured{u} + + if diff := cmp.Diff(expectedList, list); diff != "" { + t.Fatal("unexpected diff (-want, +got): ", diff) + } +} + +type ( + mockResource struct { + metav1.TypeMeta `json:",inline"` + metav1.ObjectMeta `json:"metadata"` + } + mockResourceList struct { + metav1.TypeMeta `json:",inline"` + metav1.ListMeta `json:"metadata"` + + Items []mockResource + } +) + +func (l *mockResourceList) DeepCopyObject() runtime.Object { + o := *l + return &o +} + +func (r *mockResource) DeepCopyObject() runtime.Object { + o := *r + return &o +} + +var _ runtime.Object = (*mockResource)(nil) +var _ runtime.Object = (*mockResourceList)(nil)