Skip to content

Commit

Permalink
Allow test cases to interact with client builder (#328)
Browse files Browse the repository at this point in the history
Controller runtime now supports list queries using field indexes with
the fake client. If a fake client makes a request using a field index
that is not registered, it will return an error.

The fake client was previously created with a scheme and an initial set
of objects. It now can be created with a func that can interact with the
client builder which is where a field index can be registered. The
`WithClientBuilder` field is optional on each test case struct and
ExpectConfig, allowing the fake client to be further configured.

Signed-off-by: Scott Andrews <andrewssc@vmware.com>
  • Loading branch information
scothis committed Jan 4, 2023
1 parent 3fe9f80 commit 084f021
Show file tree
Hide file tree
Showing 5 changed files with 57 additions and 17 deletions.
43 changes: 28 additions & 15 deletions testing/client.go
Expand Up @@ -35,18 +35,18 @@ type clientWrapper struct {

var _ client.Client = &clientWrapper{}

// Deprecated NewFakeClient use NewFakeClientWrapper
func NewFakeClient(scheme *runtime.Scheme, objs ...client.Object) *clientWrapper {
o := make([]runtime.Object, len(objs))
for i := range objs {
obj := objs[i].DeepCopyObject().(client.Object)
// default to a non-zero creation timestamp
if obj.GetCreationTimestamp().Time.IsZero() {
obj.SetCreationTimestamp(metav1.NewTime(time.UnixMilli(1000)))
}
o[i] = obj
}
client := &clientWrapper{
client: fakeclient.NewFakeClientWithScheme(scheme, o...),
builder := fakeclient.NewClientBuilder()
builder = builder.WithScheme(scheme)
builder = builder.WithObjects(prepareObjects(objs)...)

return NewFakeClientWrapper(builder.Build())
}

func NewFakeClientWrapper(client client.Client) *clientWrapper {
c := &clientWrapper{
client: client,
CreateActions: []objectAction{},
UpdateActions: []objectAction{},
PatchActions: []PatchAction{},
Expand All @@ -58,22 +58,35 @@ func NewFakeClient(scheme *runtime.Scheme, objs ...client.Object) *clientWrapper
reactionChain: []Reactor{},
}
// generate names on create
client.AddReactor("create", "*", func(action Action) (bool, runtime.Object, error) {
c.AddReactor("create", "*", func(action Action) (bool, runtime.Object, error) {
if createAction, ok := action.(CreateAction); ok && action.GetSubresource() == "" {
obj := createAction.GetObject()
if accessor, ok := obj.(metav1.ObjectMetaAccessor); ok {
objmeta := accessor.GetObjectMeta()
if objmeta.GetName() == "" && objmeta.GetGenerateName() != "" {
client.genCount++
c.genCount++
// mutate the existing obj
objmeta.SetName(fmt.Sprintf("%s%03d", objmeta.GetGenerateName(), client.genCount))
objmeta.SetName(fmt.Sprintf("%s%03d", objmeta.GetGenerateName(), c.genCount))
}
}
}
// never handle the action
return false, nil, nil
})
return client
return c
}

func prepareObjects(objs []client.Object) []client.Object {
o := make([]client.Object, len(objs))
for i := range objs {
obj := objs[i].DeepCopyObject().(client.Object)
// default to a non-zero creation timestamp
if obj.GetCreationTimestamp().Time.IsZero() {
obj.SetCreationTimestamp(metav1.NewTime(time.UnixMilli(1000)))
}
o[i] = obj
}
return o
}

func (w *clientWrapper) AddReactor(verb, kind string, reaction ReactionFunc) {
Expand Down
19 changes: 17 additions & 2 deletions testing/config.go
Expand Up @@ -21,6 +21,7 @@ import (
"k8s.io/apimachinery/pkg/runtime"
"k8s.io/apimachinery/pkg/types"
"sigs.k8s.io/controller-runtime/pkg/client"
"sigs.k8s.io/controller-runtime/pkg/client/fake"
)

// ExpectConfig encompasses the creation of a config object using given state, captures observed
Expand All @@ -41,6 +42,8 @@ type ExpectConfig struct {
GivenObjects []client.Object
// APIGivenObjects contains objects that are only available via an API reader instead of the normal cache
APIGivenObjects []client.Object
// 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
// each call to the clientset providing the ability to mutate the resource or inject an error.
WithReactors []ReactionFunc
Expand Down Expand Up @@ -88,13 +91,13 @@ func (c *ExpectConfig) init() {
apiGivenObjects[i] = c.APIGivenObjects[i].DeepCopyObject().(client.Object)
}

c.client = NewFakeClient(c.Scheme, givenObjects...)
c.client = c.createClient(givenObjects)
for i := range c.WithReactors {
// in reverse order since we prepend
reactor := c.WithReactors[len(c.WithReactors)-1-i]
c.client.PrependReactor("*", "*", reactor)
}
c.apiReader = NewFakeClient(c.Scheme, apiGivenObjects...)
c.apiReader = c.createClient(apiGivenObjects)
c.recorder = &eventRecorder{
events: []Event{},
scheme: c.Scheme,
Expand All @@ -104,6 +107,18 @@ func (c *ExpectConfig) init() {
})
}

func (c *ExpectConfig) createClient(objs []client.Object) *clientWrapper {
builder := fake.NewClientBuilder()

builder.WithScheme(c.Scheme)
builder.WithObjects(prepareObjects(objs)...)
if c.WithClientBuilder != nil {
builder = c.WithClientBuilder(builder)
}

return NewFakeClientWrapper(builder.Build())
}

// Config returns the Config object. This method should only be called once. Subsequent calls are
// ignored returning the Config from the first call.
func (c *ExpectConfig) Config() reconcilers.Config {
Expand Down
4 changes: 4 additions & 0 deletions testing/reconciler.go
Expand Up @@ -17,6 +17,7 @@ import (
"k8s.io/apimachinery/pkg/types"
controllerruntime "sigs.k8s.io/controller-runtime"
"sigs.k8s.io/controller-runtime/pkg/client"
"sigs.k8s.io/controller-runtime/pkg/client/fake"
"sigs.k8s.io/controller-runtime/pkg/reconcile"
)

Expand All @@ -41,6 +42,8 @@ type ReconcilerTestCase 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
// WithClientBuilder allows a test to modify the fake client initialization.
WithClientBuilder func(*fake.ClientBuilder) *fake.ClientBuilder
// GivenObjects build the kubernetes objects which are present at the onset of reconciliation
GivenObjects []client.Object
// APIGivenObjects contains objects that are only available via an API reader instead of the normal cache
Expand Down Expand Up @@ -157,6 +160,7 @@ func (tc *ReconcilerTestCase) Run(t *testing.T, scheme *runtime.Scheme, factory
Scheme: scheme,
GivenObjects: tc.GivenObjects,
APIGivenObjects: tc.APIGivenObjects,
WithClientBuilder: tc.WithClientBuilder,
WithReactors: tc.WithReactors,
GivenTracks: tc.GivenTracks,
ExpectTracks: tc.ExpectTracks,
Expand Down
4 changes: 4 additions & 0 deletions testing/subreconciler.go
Expand Up @@ -18,6 +18,7 @@ import (
"k8s.io/apimachinery/pkg/types"
controllerruntime "sigs.k8s.io/controller-runtime"
"sigs.k8s.io/controller-runtime/pkg/client"
"sigs.k8s.io/controller-runtime/pkg/client/fake"
"sigs.k8s.io/controller-runtime/pkg/reconcile"
)

Expand All @@ -41,6 +42,8 @@ type SubReconcilerTestCase struct {
Resource client.Object
// GivenStashedValues adds these items to the stash passed into the reconciler. Factories are resolved to their object.
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
// each call to the clientset providing the ability to mutate the resource or inject an error.
WithReactors []ReactionFunc
Expand Down Expand Up @@ -181,6 +184,7 @@ func (tc *SubReconcilerTestCase) Run(t *testing.T, scheme *runtime.Scheme, facto
Scheme: scheme,
GivenObjects: append(tc.GivenObjects, tc.Resource),
APIGivenObjects: append(tc.APIGivenObjects, tc.Resource),
WithClientBuilder: tc.WithClientBuilder,
WithReactors: tc.WithReactors,
GivenTracks: tc.GivenTracks,
ExpectTracks: tc.ExpectTracks,
Expand Down
4 changes: 4 additions & 0 deletions testing/webhook.go
Expand Up @@ -17,6 +17,7 @@ import (
"github.com/vmware-labs/reconciler-runtime/reconcilers"
"k8s.io/apimachinery/pkg/runtime"
"sigs.k8s.io/controller-runtime/pkg/client"
"sigs.k8s.io/controller-runtime/pkg/client/fake"
"sigs.k8s.io/controller-runtime/pkg/webhook/admission"
)

Expand All @@ -38,6 +39,8 @@ type AdmissionWebhookTestCase struct {
Request *admission.Request
// HTTPRequest is the http request used to create the admission request object. If not defined, a minimal request is provided.
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
// each call to the clientset providing the ability to mutate the resource or inject an error.
WithReactors []ReactionFunc
Expand Down Expand Up @@ -146,6 +149,7 @@ func (tc *AdmissionWebhookTestCase) Run(t *testing.T, scheme *runtime.Scheme, fa
Scheme: scheme,
GivenObjects: tc.GivenObjects,
APIGivenObjects: tc.APIGivenObjects,
WithClientBuilder: tc.WithClientBuilder,
WithReactors: tc.WithReactors,
GivenTracks: tc.GivenTracks,
ExpectTracks: tc.ExpectTracks,
Expand Down

0 comments on commit 084f021

Please sign in to comment.