From 1a51302c8be51d81adabb7107ba067fb0e9674dc Mon Sep 17 00:00:00 2001 From: JimDevil Date: Mon, 24 Feb 2025 17:16:16 +0800 Subject: [PATCH] test(clusterlink-proxy): add controller unit test 1. add unit test for proxy resourceCache controller 2.change the scope of ResourceCache CRD to Cluster Signed-off-by: JimDevil <709192853@qq.com> --- .../proxy/app/clusterlink-proxy.go | 5 + deploy/crds/kosmos.io_resourcecaches.yaml | 4 +- .../proxy/controller/controller.go | 3 + .../proxy/controller/controller_test.go | 281 +++++++-- pkg/clusterlink/proxy/testing/constant.go | 29 + .../proxy/testing/mock_delegate.go | 42 ++ pkg/clusterlink/proxy/testing/mock_store.go | 82 +++ .../k8s.io/client-go/dynamic/fake/simple.go | 539 ++++++++++++++++++ 8 files changed, 927 insertions(+), 58 deletions(-) create mode 100644 pkg/clusterlink/proxy/testing/constant.go create mode 100644 pkg/clusterlink/proxy/testing/mock_delegate.go create mode 100644 pkg/clusterlink/proxy/testing/mock_store.go create mode 100644 vendor/k8s.io/client-go/dynamic/fake/simple.go diff --git a/cmd/clusterlink/proxy/app/clusterlink-proxy.go b/cmd/clusterlink/proxy/app/clusterlink-proxy.go index 4684c2b73..22161a4ae 100644 --- a/cmd/clusterlink/proxy/app/clusterlink-proxy.go +++ b/cmd/clusterlink/proxy/app/clusterlink-proxy.go @@ -78,5 +78,10 @@ func run(ctx context.Context, opts *options.Options) error { return nil }) + server.GenericAPIServer.AddPreShutdownHookOrDie("stop-karmada-proxy-controller", func() error { + config.ExtraConfig.ProxyController.Stop() + return nil + }) + return server.GenericAPIServer.PrepareRun().Run(ctx.Done()) } diff --git a/deploy/crds/kosmos.io_resourcecaches.yaml b/deploy/crds/kosmos.io_resourcecaches.yaml index a952049f8..8f5babe1c 100644 --- a/deploy/crds/kosmos.io_resourcecaches.yaml +++ b/deploy/crds/kosmos.io_resourcecaches.yaml @@ -3,7 +3,7 @@ apiVersion: apiextensions.k8s.io/v1 kind: CustomResourceDefinition metadata: annotations: - controller-gen.kubebuilder.io/version: v0.14.0 + controller-gen.kubebuilder.io/version: v0.11.0 name: resourcecaches.kosmos.io spec: group: kosmos.io @@ -12,7 +12,7 @@ spec: listKind: ResourceCacheList plural: resourcecaches singular: resourcecache - scope: Namespaced + scope: Cluster versions: - name: v1alpha1 schema: diff --git a/pkg/clusterlink/proxy/controller/controller.go b/pkg/clusterlink/proxy/controller/controller.go index dc3d3e005..580767f2d 100644 --- a/pkg/clusterlink/proxy/controller/controller.go +++ b/pkg/clusterlink/proxy/controller/controller.go @@ -177,6 +177,9 @@ func (rc *ResourceCacheController) getGroupVersionResource(restMapper meta.RESTM return restMapping.Resource, nil } +func (rc *ResourceCacheController) Stop() { + rc.store.Stop() +} func (rc *ResourceCacheController) Run(stopCh <-chan struct{}, workers int) { defer utilruntime.HandleCrash() defer rc.queue.ShutDown() diff --git a/pkg/clusterlink/proxy/controller/controller_test.go b/pkg/clusterlink/proxy/controller/controller_test.go index 37edc99ac..54f737751 100644 --- a/pkg/clusterlink/proxy/controller/controller_test.go +++ b/pkg/clusterlink/proxy/controller/controller_test.go @@ -1,90 +1,259 @@ package controller import ( - "strings" + "context" + "net/http" + "net/http/httptest" + "reflect" "testing" + "time" + "github.com/google/go-cmp/cmp" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" - dynfake "k8s.io/client-go/dynamic" + "k8s.io/apimachinery/pkg/runtime" + "k8s.io/apimachinery/pkg/runtime/schema" + dyfake "k8s.io/client-go/dynamic/fake" + "k8s.io/client-go/kubernetes/scheme" "k8s.io/client-go/rest" - "k8s.io/client-go/restmapper" + v1alpha1 "github.com/kosmos.io/kosmos/pkg/apis/kosmos/v1alpha1" + "github.com/kosmos.io/kosmos/pkg/clusterlink/proxy/delegate" + proxytest "github.com/kosmos.io/kosmos/pkg/clusterlink/proxy/testing" fakekosmosclient "github.com/kosmos.io/kosmos/pkg/generated/clientset/versioned/fake" - informerfactory "github.com/kosmos.io/kosmos/pkg/generated/informers/externalversions" + kosmosInformer "github.com/kosmos.io/kosmos/pkg/generated/informers/externalversions" + "github.com/kosmos.io/kosmos/pkg/utils" ) -var apiGroupResources = []*restmapper.APIGroupResources{ - { - Group: metav1.APIGroup{ - Name: "apps", - Versions: []metav1.GroupVersionForDiscovery{ - {GroupVersion: "apps/v1", Version: "v1"}, - }, - PreferredVersion: metav1.GroupVersionForDiscovery{ - GroupVersion: "apps/v1", Version: "v1", - }, - }, - VersionedResources: map[string][]metav1.APIResource{ - "v1": { - {Name: "deployments", SingularName: "deployment", Namespaced: true, Kind: "Deployment"}, +func TestNewResourceCacheController(t *testing.T) { + restConfig := &rest.Config{ + Host: "https://localhost:6443", + } + rc := &v1alpha1.ResourceCache{ + ObjectMeta: metav1.ObjectMeta{Name: "rc"}, + Spec: v1alpha1.ResourceCacheSpec{ + ResourceCacheSelectors: []v1alpha1.ResourceCacheSelector{ + proxytest.PodResourceCacheSelector, }, }, - }, - { - Group: metav1.APIGroup{ - Name: "", - Versions: []metav1.GroupVersionForDiscovery{ - {GroupVersion: "v1", Version: "v1"}, + } + kosmosFactory := kosmosInformer.NewSharedInformerFactory(fakekosmosclient.NewSimpleClientset(rc), 0) + o := NewControllerOption{ + DynamicClient: dyfake.NewSimpleDynamicClientWithCustomListKinds(runtime.NewScheme(), map[schema.GroupVersionResource]string{ + proxytest.PodGVR: "PodList", + }), + KosmosFactory: kosmosFactory, + RestConfig: restConfig, + RestMapper: proxytest.RestMapper, + } + proxyCtl, err := NewResourceCacheController(o) + if err != nil { + t.Error(err) + return + } + if proxyCtl == nil { + t.Error("proxyCtl is nil") + return + } + stopCh := make(chan struct{}) + defer close(stopCh) + kosmosFactory.Start(stopCh) + // start proxyctl + go func() { + proxyCtl.Run(stopCh, 1) + defer proxyCtl.Stop() + }() + kosmosFactory.WaitForCacheSync(stopCh) + time.Sleep(time.Second) + hasPod := proxyCtl.store.HasResource(proxytest.PodGVR) + if !hasPod { + t.Error("has no pod resource cached") + return + } +} + +func TestResourceCacheController_syncResourceCache(t *testing.T) { + newMultiNs := func(namespaces ...string) *utils.MultiNamespace { + multiNs := utils.NewMultiNamespace() + if len(namespaces) == 0 { + multiNs.Add(metav1.NamespaceAll) + return multiNs + } + for _, ns := range namespaces { + multiNs.Add(ns) + } + return multiNs + } + + tests := []struct { + name string + input []runtime.Object + want map[string]*utils.MultiNamespace + }{ + { + name: "cache pod resource with two namespace", + input: []runtime.Object{ + &v1alpha1.ResourceCache{ + ObjectMeta: metav1.ObjectMeta{Name: "rc1"}, + Spec: v1alpha1.ResourceCacheSpec{ + ResourceCacheSelectors: []v1alpha1.ResourceCacheSelector{ + proxytest.PodResourceCacheSelector, + }, + }, + }, }, - PreferredVersion: metav1.GroupVersionForDiscovery{ - GroupVersion: "v1", Version: "v1", + want: map[string]*utils.MultiNamespace{ + "pods": newMultiNs("ns1", "ns2"), }, }, - VersionedResources: map[string][]metav1.APIResource{ - "v1": { - {Name: "pods", SingularName: "pod", Namespaced: true, Kind: "Pod"}, + { + name: "cache pod twice in two ResourceCache with different namespace", + input: []runtime.Object{ + &v1alpha1.ResourceCache{ + ObjectMeta: metav1.ObjectMeta{Name: "rc1"}, + Spec: v1alpha1.ResourceCacheSpec{ + ResourceCacheSelectors: []v1alpha1.ResourceCacheSelector{ + proxytest.PodSelectorWithNS1, + }, + }, + }, + &v1alpha1.ResourceCache{ + ObjectMeta: metav1.ObjectMeta{Name: "rc2"}, + Spec: v1alpha1.ResourceCacheSpec{ + ResourceCacheSelectors: []v1alpha1.ResourceCacheSelector{ + proxytest.PodSelectorWithNS2, + }, + }, + }, + }, + want: map[string]*utils.MultiNamespace{ + "pods": newMultiNs("ns1", "ns2"), }, }, - }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + actual := map[string]*utils.MultiNamespace{} + kosmosClientSet := fakekosmosclient.NewSimpleClientset(tt.input...) + kosmosFactory := kosmosInformer.NewSharedInformerFactory(kosmosClientSet, 0) + ctl := &ResourceCacheController{ + restMapper: proxytest.RestMapper, + resourceCacheLister: kosmosFactory.Kosmos().V1alpha1().ResourceCaches().Lister(), + store: &proxytest.MockStore{ + UpdateCacheFunc: func(resources map[schema.GroupVersionResource]*utils.MultiNamespace) error { + for k, v := range resources { + actual[k.Resource] = v + } + return nil + }, + }, + } + stopCh := make(chan struct{}) + kosmosFactory.Start(stopCh) + kosmosFactory.WaitForCacheSync(stopCh) + err := ctl.syncResourceCache("test") + if err != nil { + t.Error(err) + return + } + if !reflect.DeepEqual(actual, tt.want) { + t.Errorf("diff: %v", cmp.Diff(actual, tt.want)) + } + }) + } } -func TestNewResourceCacheController(t *testing.T) { - type args struct { - option NewControllerOption - } - dyClient, _ := dynfake.NewForConfig(&rest.Config{}) - o := NewControllerOption{ - DynamicClient: dyClient, - KosmosFactory: informerfactory.NewSharedInformerFactory(fakekosmosclient.NewSimpleClientset(), 0), - RestConfig: &rest.Config{}, - RestMapper: restmapper.NewDiscoveryRESTMapper(apiGroupResources), +func TestResourceCacheController_Connect(t *testing.T) { + store := &proxytest.MockStore{ + HasResourceFunc: func(gvr schema.GroupVersionResource) bool { return gvr == proxytest.PodGVR }, } tests := []struct { - name string - args args - want *ResourceCacheController - wantErr bool - errMsg string + name string + plugins []*proxytest.MockDelegate + wantErr bool + wantCalled []bool }{ { - name: "NewResourceCacheController", - args: args{ - option: o, + name: "call first", + plugins: []*proxytest.MockDelegate{ + { + MockOrder: 0, + IsSupportRequest: true, + }, + { + MockOrder: 1, + IsSupportRequest: true, + }, + }, + wantErr: false, + wantCalled: []bool{true, false}, + }, + { + name: "call second", + plugins: []*proxytest.MockDelegate{ + { + MockOrder: 0, + IsSupportRequest: false, + }, + { + MockOrder: 1, + IsSupportRequest: true, + }, }, - wantErr: false, + wantErr: false, + wantCalled: []bool{false, true}, + }, + { + name: "call fail", + plugins: []*proxytest.MockDelegate{ + { + MockOrder: 0, + IsSupportRequest: false, + }, + { + MockOrder: 1, + IsSupportRequest: false, + }, + }, + wantErr: true, + wantCalled: []bool{false, false}, }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { - _, err := NewResourceCacheController(tt.args.option) - if err == nil && tt.wantErr { - t.Fatal("expected an error, but got none") + ctl := &ResourceCacheController{ + delegate: delegate.NewDelegateChain(proxytest.ConvertPluginSlice(tt.plugins)), + negotiatedSerializer: scheme.Codecs.WithoutConversion(), + store: store, + } + + conn, err := ctl.Connect(context.TODO(), "/api/v1/pods", nil) + if err != nil { + t.Fatal(err) } - if err != nil && !tt.wantErr { - t.Errorf("unexpected error, got: %v", err) + + req, err := http.NewRequest(http.MethodGet, "/prefix/api/v1/pods", nil) + if err != nil { + t.Fatal(err) } - if err != nil && tt.wantErr && !strings.Contains(err.Error(), tt.errMsg) { - t.Errorf("expected error message %s to be in %s", tt.errMsg, err.Error()) + + recorder := httptest.NewRecorder() + conn.ServeHTTP(recorder, req) + + response := recorder.Result() + + if (response.StatusCode != 200) != tt.wantErr { + t.Errorf("http request returned status code = %v, want error = %v", + response.StatusCode, tt.wantErr) + } + + if len(tt.plugins) != len(tt.wantCalled) { + panic("len(tt.plugins) != len(tt.wantCalled), please fix test cases") + } + + for i, n := 0, len(tt.plugins); i < n; i++ { + if tt.plugins[i].Called != tt.wantCalled[i] { + t.Errorf("plugin[%v].Called = %v, want = %v", i, tt.plugins[i].Called, tt.wantCalled[i]) + } } }) } diff --git a/pkg/clusterlink/proxy/testing/constant.go b/pkg/clusterlink/proxy/testing/constant.go new file mode 100644 index 000000000..f59446dd5 --- /dev/null +++ b/pkg/clusterlink/proxy/testing/constant.go @@ -0,0 +1,29 @@ +package testing + +import ( + corev1 "k8s.io/api/core/v1" + "k8s.io/apimachinery/pkg/api/meta" + "k8s.io/apimachinery/pkg/runtime/schema" + + v1alpha1 "github.com/kosmos.io/kosmos/pkg/apis/kosmos/v1alpha1" +) + +var ( + PodGVK = corev1.SchemeGroupVersion.WithKind("Pod") + SecretGVR = corev1.SchemeGroupVersion.WithKind("Secret") + RestMapper *meta.DefaultRESTMapper + + PodGVR = corev1.SchemeGroupVersion.WithResource("pods") + + PodSelectorWithNS1 = v1alpha1.ResourceCacheSelector{APIVersion: PodGVK.GroupVersion().String(), Kind: PodGVK.Kind, Namespace: []string{"ns1"}} + + PodSelectorWithNS2 = v1alpha1.ResourceCacheSelector{APIVersion: PodGVK.GroupVersion().String(), Kind: PodGVK.Kind, Namespace: []string{"ns2"}} + + PodResourceCacheSelector = v1alpha1.ResourceCacheSelector{APIVersion: PodGVK.GroupVersion().String(), Kind: PodGVK.Kind, Namespace: []string{"ns1", "ns2"}} +) + +func init() { + RestMapper = meta.NewDefaultRESTMapper([]schema.GroupVersion{corev1.SchemeGroupVersion}) + RestMapper.Add(PodGVK, meta.RESTScopeNamespace) + RestMapper.Add(SecretGVR, meta.RESTScopeNamespace) +} diff --git a/pkg/clusterlink/proxy/testing/mock_delegate.go b/pkg/clusterlink/proxy/testing/mock_delegate.go new file mode 100644 index 000000000..703ab6c8e --- /dev/null +++ b/pkg/clusterlink/proxy/testing/mock_delegate.go @@ -0,0 +1,42 @@ +package testing + +import ( + "context" + "net/http" + + "github.com/kosmos.io/kosmos/pkg/clusterlink/proxy/delegate" +) + +type MockDelegate struct { + MockOrder int + IsSupportRequest bool + Called bool +} + +// Connect implements delegate.Delegate. +func (m *MockDelegate) Connect(_ context.Context, _ delegate.ProxyRequest) (http.Handler, error) { + return http.HandlerFunc(func(http.ResponseWriter, *http.Request) { + m.Called = true + }), nil +} + +// Order implements delegate.Delegate. +func (m *MockDelegate) Order() int { + return m.MockOrder +} + +// SupportRequest implements delegate.Delegate. +func (m *MockDelegate) SupportRequest(_ delegate.ProxyRequest) bool { + return m.IsSupportRequest +} + +var _ delegate.Delegate = (*MockDelegate)(nil) + +func ConvertPluginSlice(in []*MockDelegate) []delegate.Delegate { + out := make([]delegate.Delegate, 0, len(in)) + for _, plugin := range in { + out = append(out, plugin) + } + + return out +} diff --git a/pkg/clusterlink/proxy/testing/mock_store.go b/pkg/clusterlink/proxy/testing/mock_store.go new file mode 100644 index 000000000..de8d86d8b --- /dev/null +++ b/pkg/clusterlink/proxy/testing/mock_store.go @@ -0,0 +1,82 @@ +package testing + +import ( + "context" + + "k8s.io/apimachinery/pkg/apis/meta/internalversion" + v1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/runtime" + "k8s.io/apimachinery/pkg/runtime/schema" + "k8s.io/apimachinery/pkg/watch" + + "github.com/kosmos.io/kosmos/pkg/clusterlink/proxy/store" + "github.com/kosmos.io/kosmos/pkg/utils" +) + +type MockStore struct { + GetResourceFromCacheFunc func(ctx context.Context, gvr schema.GroupVersionResource, namespace string, name string) (runtime.Object, string, error) + HasResourceFunc func(resource schema.GroupVersionResource) bool + ListFunc func(ctx context.Context, gvr schema.GroupVersionResource, options *internalversion.ListOptions) (runtime.Object, error) + UpdateCacheFunc func(resources map[schema.GroupVersionResource]*utils.MultiNamespace) error + StopFunc func() + WatchFunc func(ctx context.Context, gvr schema.GroupVersionResource, options *internalversion.ListOptions) (watch.Interface, error) + GetFunc func(ctx context.Context, gvr schema.GroupVersionResource, name string, options *v1.GetOptions) (runtime.Object, error) +} + +// Get implements store.Store. +func (m *MockStore) Get(ctx context.Context, gvr schema.GroupVersionResource, name string, options *v1.GetOptions) (runtime.Object, error) { + if m.GetFunc != nil { + return m.GetFunc(ctx, gvr, name, options) + } + panic("unimplemented") +} + +// GetResourceFromCache implements store.Store. +func (m *MockStore) GetResourceFromCache(ctx context.Context, gvr schema.GroupVersionResource, namespace string, name string) (runtime.Object, string, error) { + if m.GetResourceFromCacheFunc != nil { + return m.GetResourceFromCacheFunc(ctx, gvr, namespace, name) + } + panic("unimplemented") +} + +// HasResource implements store.Store. +func (m *MockStore) HasResource(resource schema.GroupVersionResource) bool { + if m.HasResourceFunc != nil { + return m.HasResourceFunc(resource) + } + panic("unimplemented") +} + +// List implements store.Store. +func (m *MockStore) List(ctx context.Context, gvr schema.GroupVersionResource, options *internalversion.ListOptions) (runtime.Object, error) { + if m.ListFunc != nil { + return m.ListFunc(ctx, gvr, options) + } + panic("unimplemented") +} + +// Stop implements store.Store. +func (m *MockStore) Stop() { + if m.StopFunc != nil { + m.StopFunc() + } + panic("unimplemented") +} + +// UpdateCache implements store.Store. +func (m *MockStore) UpdateCache(resources map[schema.GroupVersionResource]*utils.MultiNamespace) error { + if m.UpdateCacheFunc != nil { + return m.UpdateCacheFunc(resources) + } + panic("unimplemented") +} + +// Watch implements store.Store. +func (m *MockStore) Watch(ctx context.Context, gvr schema.GroupVersionResource, options *internalversion.ListOptions) (watch.Interface, error) { + if m.WatchFunc != nil { + return m.WatchFunc(ctx, gvr, options) + } + panic("unimplemented") +} + +var _ store.Store = &MockStore{} diff --git a/vendor/k8s.io/client-go/dynamic/fake/simple.go b/vendor/k8s.io/client-go/dynamic/fake/simple.go new file mode 100644 index 000000000..5d0a6f69f --- /dev/null +++ b/vendor/k8s.io/client-go/dynamic/fake/simple.go @@ -0,0 +1,539 @@ +/* +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 ( + "context" + "fmt" + "strings" + + "k8s.io/apimachinery/pkg/api/meta" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" + "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/dynamic" + "k8s.io/client-go/testing" +) + +func NewSimpleDynamicClient(scheme *runtime.Scheme, objects ...runtime.Object) *FakeDynamicClient { + 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 +// 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()) + for _, obj := range objects { + if err := o.Add(obj); err != nil { + panic(err) + } + } + + cs := &FakeDynamicClient{scheme: scheme, gvrToListKind: completeGVRToListKind, tracker: o} + 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 +} + +// Clientset 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 FakeDynamicClient struct { + testing.Fake + scheme *runtime.Scheme + gvrToListKind map[schema.GroupVersionResource]string + tracker testing.ObjectTracker +} + +type dynamicResourceClient struct { + client *FakeDynamicClient + namespace string + resource schema.GroupVersionResource + listKind string +} + +var ( + _ dynamic.Interface = &FakeDynamicClient{} + _ testing.FakeClient = &FakeDynamicClient{} +) + +func (c *FakeDynamicClient) Tracker() testing.ObjectTracker { + return c.tracker +} + +func (c *FakeDynamicClient) Resource(resource schema.GroupVersionResource) dynamic.NamespaceableResourceInterface { + return &dynamicResourceClient{client: c, resource: resource, listKind: c.gvrToListKind[resource]} +} + +func (c *dynamicResourceClient) Namespace(ns string) dynamic.ResourceInterface { + ret := *c + ret.namespace = ns + return &ret +} + +func (c *dynamicResourceClient) Create(ctx context.Context, obj *unstructured.Unstructured, opts metav1.CreateOptions, subresources ...string) (*unstructured.Unstructured, 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: + var accessor metav1.Object // avoid shadowing err + 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: + var accessor metav1.Object // avoid shadowing err + 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 := &unstructured.Unstructured{} + if err := c.client.scheme.Convert(uncastRet, ret, nil); err != nil { + return nil, err + } + return ret, err +} + +func (c *dynamicResourceClient) Update(ctx context.Context, obj *unstructured.Unstructured, opts metav1.UpdateOptions, subresources ...string) (*unstructured.Unstructured, 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 := &unstructured.Unstructured{} + if err := c.client.scheme.Convert(uncastRet, ret, nil); err != nil { + return nil, err + } + return ret, err +} + +func (c *dynamicResourceClient) UpdateStatus(ctx context.Context, obj *unstructured.Unstructured, opts metav1.UpdateOptions) (*unstructured.Unstructured, 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 := &unstructured.Unstructured{} + if err := c.client.scheme.Convert(uncastRet, ret, nil); err != nil { + return nil, err + } + return ret, err +} + +func (c *dynamicResourceClient) Delete(ctx context.Context, 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: "dynamic 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: "dynamic 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: "dynamic 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: "dynamic delete fail"}) + } + + return err +} + +func (c *dynamicResourceClient) DeleteCollection(ctx context.Context, 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: "dynamic deletecollection fail"}) + + case len(c.namespace) > 0: + action := testing.NewDeleteCollectionAction(c.resource, c.namespace, listOptions) + _, err = c.client.Fake.Invokes(action, &metav1.Status{Status: "dynamic deletecollection fail"}) + + } + + return err +} + +func (c *dynamicResourceClient) Get(ctx context.Context, name string, opts metav1.GetOptions, subresources ...string) (*unstructured.Unstructured, 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: "dynamic 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: "dynamic 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: "dynamic 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: "dynamic get fail"}) + } + + if err != nil { + return nil, err + } + if uncastRet == nil { + return nil, err + } + + ret := &unstructured.Unstructured{} + if err := c.client.scheme.Convert(uncastRet, ret, nil); err != nil { + return nil, err + } + return ret, err +} + +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, listForFakeClientGVK, opts), &metav1.Status{Status: "dynamic list fail"}) + + case len(c.namespace) > 0: + obj, err = c.client.Fake. + Invokes(testing.NewListAction(c.resource, listForFakeClientGVK, c.namespace, opts), &metav1.Status{Status: "dynamic list fail"}) + + } + + if obj == nil { + return nil, err + } + + label, _, _ := testing.ExtractFromListOptions(opts) + if label == nil { + label = labels.Everything() + } + + retUnstructured := &unstructured.Unstructured{} + if err := c.client.scheme.Convert(obj, retUnstructured, nil); err != nil { + return nil, err + } + entireList, err := retUnstructured.ToList() + if err != nil { + return nil, err + } + + list := &unstructured.UnstructuredList{} + list.SetRemainingItemCount(entireList.GetRemainingItemCount()) + list.SetResourceVersion(entireList.GetResourceVersion()) + list.SetContinue(entireList.GetContinue()) + list.GetObjectKind().SetGroupVersionKind(listGVK) + for i := range entireList.Items { + item := &entireList.Items[i] + 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 *dynamicResourceClient) Watch(ctx context.Context, 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") +} + +// TODO: opts are currently ignored. +func (c *dynamicResourceClient) Patch(ctx context.Context, name string, pt types.PatchType, data []byte, opts metav1.PatchOptions, subresources ...string) (*unstructured.Unstructured, 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: "dynamic 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: "dynamic 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: "dynamic 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: "dynamic patch fail"}) + + } + + if err != nil { + return nil, err + } + if uncastRet == nil { + return nil, err + } + + ret := &unstructured.Unstructured{} + if err := c.client.scheme.Convert(uncastRet, ret, nil); err != nil { + return nil, err + } + return ret, err +} + +// TODO: opts are currently ignored. +func (c *dynamicResourceClient) Apply(ctx context.Context, name string, obj *unstructured.Unstructured, options metav1.ApplyOptions, subresources ...string) (*unstructured.Unstructured, error) { + outBytes, err := runtime.Encode(unstructured.UnstructuredJSONScheme, obj) + if err != nil { + return nil, err + } + var uncastRet runtime.Object + switch { + case len(c.namespace) == 0 && len(subresources) == 0: + uncastRet, err = c.client.Fake. + Invokes(testing.NewRootPatchAction(c.resource, name, types.ApplyPatchType, outBytes), &metav1.Status{Status: "dynamic patch fail"}) + + case len(c.namespace) == 0 && len(subresources) > 0: + uncastRet, err = c.client.Fake. + Invokes(testing.NewRootPatchSubresourceAction(c.resource, name, types.ApplyPatchType, outBytes, subresources...), &metav1.Status{Status: "dynamic patch fail"}) + + case len(c.namespace) > 0 && len(subresources) == 0: + uncastRet, err = c.client.Fake. + Invokes(testing.NewPatchAction(c.resource, c.namespace, name, types.ApplyPatchType, outBytes), &metav1.Status{Status: "dynamic patch fail"}) + + case len(c.namespace) > 0 && len(subresources) > 0: + uncastRet, err = c.client.Fake. + Invokes(testing.NewPatchSubresourceAction(c.resource, c.namespace, name, types.ApplyPatchType, outBytes, subresources...), &metav1.Status{Status: "dynamic patch fail"}) + + } + + if err != nil { + return nil, err + } + if uncastRet == nil { + return nil, err + } + + ret := &unstructured.Unstructured{} + if err := c.client.scheme.Convert(uncastRet, ret, nil); err != nil { + return nil, err + } + return ret, nil +} + +func (c *dynamicResourceClient) ApplyStatus(ctx context.Context, name string, obj *unstructured.Unstructured, options metav1.ApplyOptions) (*unstructured.Unstructured, error) { + return c.Apply(ctx, name, obj, options, "status") +} + +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 +}