From 9f798872336ab4baa0dbc88da5df2cea0523c449 Mon Sep 17 00:00:00 2001 From: Ashish Naware Date: Fri, 4 Oct 2024 18:52:39 -0700 Subject: [PATCH] install watch reaction function to fake client Signed-off-by: Ashish Naware --- testing/aliases.go | 2 ++ testing/client.go | 72 ++++++++++++++++++++++++++++++++++++++++ testing/config.go | 8 +++++ testing/reconciler.go | 6 ++-- testing/subreconciler.go | 6 ++-- testing/webhook.go | 6 ++-- 6 files changed, 94 insertions(+), 6 deletions(-) diff --git a/testing/aliases.go b/testing/aliases.go index 8b055b1..38c1bd7 100644 --- a/testing/aliases.go +++ b/testing/aliases.go @@ -22,6 +22,8 @@ import ( type Reactor = clientgotesting.Reactor type ReactionFunc = clientgotesting.ReactionFunc +type WatchReactor = clientgotesting.WatchReactor +type WatchReactionFunc = clientgotesting.WatchReactionFunc type Action = clientgotesting.Action type GetAction = clientgotesting.GetAction diff --git a/testing/client.go b/testing/client.go index bd8764c..765cde5 100644 --- a/testing/client.go +++ b/testing/client.go @@ -23,10 +23,13 @@ import ( "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/runtime" "k8s.io/apimachinery/pkg/runtime/schema" + "k8s.io/apimachinery/pkg/watch" clientgotesting "k8s.io/client-go/testing" ref "k8s.io/client-go/tools/reference" + "reconciler.io/runtime/duck" "sigs.k8s.io/controller-runtime/pkg/client" ) @@ -50,6 +53,7 @@ type clientWrapper struct { StatusPatchActions []PatchAction genCount int reactionChain []Reactor + watchReactionChain []WatchReactor } var _ TestClient = (*clientWrapper)(nil) @@ -67,6 +71,7 @@ func NewFakeClientWrapper(client client.Client, tracker clientgotesting.ObjectTr StatusPatchActions: []PatchAction{}, genCount: 0, reactionChain: []Reactor{}, + watchReactionChain: []WatchReactor{}, } // generate names on create c.AddReactor("create", "*", func(action Action) (bool, runtime.Object, error) { @@ -115,6 +120,10 @@ func (w *clientWrapper) PrependReactor(verb, kind string, reaction ReactionFunc) w.reactionChain = append([]Reactor{&clientgotesting.SimpleReactor{Verb: verb, Resource: kind, Reaction: reaction}}, w.reactionChain...) } +func (w *clientWrapper) PrependWatchReactor(kind string, reaction WatchReactionFunc) { + w.watchReactionChain = append([]WatchReactor{&clientgotesting.SimpleWatchReactor{Resource: kind, Reaction: reaction}}, w.watchReactionChain...) +} + func (w *clientWrapper) objmeta(obj runtime.Object) (schema.GroupVersionResource, string, string, error) { objref, err := ref.GetReference(w.Scheme(), obj) if err != nil { @@ -140,6 +149,20 @@ func (w *clientWrapper) react(action Action) error { return nil } +func (w *clientWrapper) reactWatcherFunc(action Action) error { + for _, reactor := range w.watchReactionChain { + if !reactor.Handles(action) { + continue + } + handled, _, err := reactor.React(action) + if !handled { + continue + } + return err + } + return nil +} + func (w *clientWrapper) Scheme() *runtime.Scheme { return w.client.Scheme() } @@ -307,6 +330,55 @@ func (w *clientWrapper) DeleteAllOf(ctx context.Context, obj client.Object, opts return w.client.DeleteAllOf(ctx, obj, opts...) } +func (w *clientWrapper) Watch(ctx context.Context, list client.ObjectList, opts ...client.ListOption) (watch.Interface, error) { + + ww, ok := w.client.(client.WithWatch) + if !ok { + panic(fmt.Errorf("unable to call Watch with wrapped client that does not implement client.WithWatch")) + } + + gvr, namespace, name, err := w.objmeta(list) + if err != nil { + return nil, err + } + + // call reactor chain + err = w.reactWatcherFunc(clientgotesting.NewGetAction(gvr, namespace, name)) + if err != nil { + return nil, err + } + err = w.reactWatcherFunc(clientgotesting.NewCreateAction(gvr, namespace, list)) + if err != nil { + return nil, err + } + err = w.reactWatcherFunc(clientgotesting.NewDeleteAction(gvr, namespace, name)) + if err != nil { + return nil, err + } + err = w.reactWatcherFunc(clientgotesting.NewUpdateAction(gvr, namespace, list)) + if err != nil { + return nil, err + } + + if !duck.IsDuck(list, w.Scheme()) { + return ww.Watch(ctx, list, opts...) + } + + uObj, err := runtime.DefaultUnstructuredConverter.ToUnstructured(list) + if err != nil { + return nil, err + } + u := &unstructured.UnstructuredList{Object: uObj} + watcher, err := ww.Watch(ctx, u, opts...) + if err != nil { + return nil, err + } + if err := runtime.DefaultUnstructuredConverter.FromUnstructured(u.Object, list); err != nil { + return nil, err + } + return watcher, nil +} + func (w *clientWrapper) Status() client.StatusWriter { return &statusWriterWrapper{ statusWriter: w.client.Status(), diff --git a/testing/config.go b/testing/config.go index 8a63d6f..fb73c1e 100644 --- a/testing/config.go +++ b/testing/config.go @@ -69,6 +69,9 @@ type ExpectConfig struct { // WithReactors installs each ReactionFunc into each fake clientset. ReactionFuncs intercept // each call to the clientset providing the ability to mutate the resource or inject an error. WithReactors []ReactionFunc + // WithWatchReactors installs WatchReactionFunc into the fake clientset. This provides ability + // to simulate events in the watcher. + WithWatchReactors []WatchReactionFunc // GivenTracks provide a set of tracked resources to seed the tracker with GivenTracks []TrackRequest @@ -119,6 +122,11 @@ func (c *ExpectConfig) init() { reactor := c.WithReactors[len(c.WithReactors)-1-i] c.client.PrependReactor("*", "*", reactor) } + for i := range c.WithWatchReactors { + // in reverse order since we prepend + watchReactor := c.WithWatchReactors[len(c.WithWatchReactors)-1-i] + c.client.PrependWatchReactor("*", watchReactor) + } c.apiReader = c.createClient(apiGivenObjects, c.StatusSubResourceTypes) c.recorder = &eventRecorder{ events: []Event{}, diff --git a/testing/reconciler.go b/testing/reconciler.go index 313e16b..eedb2a2 100644 --- a/testing/reconciler.go +++ b/testing/reconciler.go @@ -48,9 +48,10 @@ type ReconcilerTestCase struct { // Request identifies the object to be reconciled Request reconcilers.Request - // WithReactors installs each ReactionFunc into each fake clientset. ReactionFuncs intercept + // WithReactors and WithWatchReactors installs each ReactionFunc into each fake clientset. ReactionFuncs intercept // each call to the clientset providing the ability to mutate the resource or inject an error. - WithReactors []ReactionFunc + WithReactors []ReactionFunc + WithWatchReactors []WatchReactionFunc // WithClientBuilder allows a test to modify the fake client initialization. WithClientBuilder func(*fake.ClientBuilder) *fake.ClientBuilder // StatusSubResourceTypes is a set of object types that support the status sub-resource. For @@ -188,6 +189,7 @@ func (tc *ReconcilerTestCase) Run(t *testing.T, scheme *runtime.Scheme, factory APIGivenObjects: tc.APIGivenObjects, WithClientBuilder: tc.WithClientBuilder, WithReactors: tc.WithReactors, + WithWatchReactors: tc.WithWatchReactors, GivenTracks: tc.GivenTracks, ExpectTracks: tc.ExpectTracks, ExpectEvents: tc.ExpectEvents, diff --git a/testing/subreconciler.go b/testing/subreconciler.go index 2865380..d6502ba 100644 --- a/testing/subreconciler.go +++ b/testing/subreconciler.go @@ -56,9 +56,10 @@ type SubReconcilerTestCase[Type client.Object] struct { GivenStashedValues map[reconcilers.StashKey]interface{} // WithClientBuilder allows a test to modify the fake client initialization. WithClientBuilder func(*fake.ClientBuilder) *fake.ClientBuilder - // WithReactors installs each ReactionFunc into each fake clientset. ReactionFuncs intercept + // WithReactors and WithWatchReactors installs each ReactionFunc into each fake clientset. ReactionFuncs intercept // each call to the clientset providing the ability to mutate the resource or inject an error. - WithReactors []ReactionFunc + WithReactors []ReactionFunc + WithWatchReactors []WatchReactionFunc // StatusSubResourceTypes is a set of object types that support the status sub-resource. For // these types, the only way to modify the resource's status is update or patch the status // sub-resource. Patching or updating the main resource will not mutated the status field. @@ -216,6 +217,7 @@ func (tc *SubReconcilerTestCase[T]) Run(t *testing.T, scheme *runtime.Scheme, fa APIGivenObjects: append(tc.APIGivenObjects, givenResource), WithClientBuilder: tc.WithClientBuilder, WithReactors: tc.WithReactors, + WithWatchReactors: tc.WithWatchReactors, GivenTracks: tc.GivenTracks, ExpectTracks: tc.ExpectTracks, ExpectEvents: tc.ExpectEvents, diff --git a/testing/webhook.go b/testing/webhook.go index d127177..6403338 100644 --- a/testing/webhook.go +++ b/testing/webhook.go @@ -54,9 +54,10 @@ type AdmissionWebhookTestCase struct { HTTPRequest *http.Request // WithClientBuilder allows a test to modify the fake client initialization. WithClientBuilder func(*fake.ClientBuilder) *fake.ClientBuilder - // WithReactors installs each ReactionFunc into each fake clientset. ReactionFuncs intercept + // WithReactors and WithWatchReactors installs each ReactionFunc into each fake clientset. ReactionFuncs intercept // each call to the clientset providing the ability to mutate the resource or inject an error. - WithReactors []ReactionFunc + WithReactors []ReactionFunc + WithWatchReactors []WatchReactionFunc // StatusSubResourceTypes is a set of object types that support the status sub-resource. For // these types, the only way to modify the resource's status is update or patch the status // sub-resource. Patching or updating the main resource will not mutated the status field. @@ -181,6 +182,7 @@ func (tc *AdmissionWebhookTestCase) Run(t *testing.T, scheme *runtime.Scheme, fa APIGivenObjects: tc.APIGivenObjects, WithClientBuilder: tc.WithClientBuilder, WithReactors: tc.WithReactors, + WithWatchReactors: tc.WithWatchReactors, GivenTracks: tc.GivenTracks, ExpectTracks: tc.ExpectTracks, ExpectEvents: tc.ExpectEvents,