Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions testing/aliases.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
72 changes: 72 additions & 0 deletions testing/client.go
Original file line number Diff line number Diff line change
Expand Up @@ -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"
)

Expand All @@ -50,6 +53,7 @@ type clientWrapper struct {
StatusPatchActions []PatchAction
genCount int
reactionChain []Reactor
watchReactionChain []WatchReactor
}

var _ TestClient = (*clientWrapper)(nil)
Expand All @@ -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) {
Expand Down Expand Up @@ -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 {
Expand All @@ -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()
}
Expand Down Expand Up @@ -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
}
Comment on lines +346 to +361
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why are we trying to react to get/create/delete/update here? I would expect it to only react with watch actions. I'm not familiar enough with the mechanics of watch to know exactly what should be here.


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(),
Expand Down
8 changes: 8 additions & 0 deletions testing/config.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

May also need to define WithWatchReactors for (Sub)ReconcilerTestCase/AdmissionWebhookTestCase and pass it through when creating the ExpectConfig. Then again, I'm not sure watch makes sense in those particular contexts since reconcilers and webhooks are intended to execute at a moment in time while watches are long lived observations of change. What do you think?

// GivenTracks provide a set of tracked resources to seed the tracker with
GivenTracks []TrackRequest

Expand Down Expand Up @@ -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{},
Expand Down
6 changes: 4 additions & 2 deletions testing/reconciler.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

document each field separately, otherwise the info won't show up in tooling for WithWatchReactors.

x3

// 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
Expand Down Expand Up @@ -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,
Expand Down
6 changes: 4 additions & 2 deletions testing/subreconciler.go
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down Expand Up @@ -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,
Expand Down
6 changes: 4 additions & 2 deletions testing/webhook.go
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down Expand Up @@ -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,
Expand Down