From 9f2041d0cd7658ecc14d86d01234504501fbc1bc Mon Sep 17 00:00:00 2001 From: "Dr. Stefan Schimanski" Date: Thu, 22 Feb 2024 17:31:06 +0100 Subject: [PATCH 1/2] controller/engine: add queueing index method to NamedController Signed-off-by: Dr. Stefan Schimanski --- pkg/controller/engine.go | 35 +++++++++++++++++++++++++++++++++++ 1 file changed, 35 insertions(+) diff --git a/pkg/controller/engine.go b/pkg/controller/engine.go index fe9dfa000..87aa03138 100644 --- a/pkg/controller/engine.go +++ b/pkg/controller/engine.go @@ -187,8 +187,18 @@ func (e *Engine) Start(name string, o controller.Options, w ...Watch) error { // NamedController is a controller that's not yet started. It gives access to // the underlying cache, which may be used e.g. to add indexes. type NamedController interface { + // Start the named controller. Start does not block. Start(ctx context.Context) error + // GetCache returns the cache used by the named controller. If the + // controller hasn't been started, neither has the cache. + // + // Warning: Be careful when calling methods with a context before the + // controller has been started. The pass context will be used to e.g. start + // informers, and the life-cycle will differ from that of the controller. In + // particular, use IndexField instead to add indexes to the cache before the + // controller is started. GetCache() cache.Cache + IndexField(obj client.Object, field string, extractValue client.IndexerFunc) } type namedController struct { @@ -196,6 +206,14 @@ type namedController struct { e *Engine ca cache.Cache ctrl controller.Controller + + indexes []index +} + +type index struct { + obj client.Object + field string + extractValue client.IndexerFunc } // Create the named controller. Each controller gets its own cache @@ -265,6 +283,12 @@ func (c *namedController) Start(ctx context.Context) error { c.e.errors[c.name] = nil c.e.mx.Unlock() + for _, idx := range c.indexes { + if err := c.ca.IndexField(ctx, idx.obj, idx.field, idx.extractValue); err != nil { + return errors.Wrap(err, "cannot add index") + } + } + go func() { <-c.e.mgr.Elected() c.e.done(c.name, errors.Wrap(c.ca.Start(ctx), errCrashCache)) @@ -281,7 +305,18 @@ func (c *namedController) Start(ctx context.Context) error { return nil } +// IndexField queues an index for addition to the cache on start. +func (c *namedController) IndexField(obj client.Object, field string, extractValue client.IndexerFunc) { + c.indexes = append(c.indexes, index{obj: obj, field: field, extractValue: extractValue}) +} + // GetCache returns the cache used by the named controller. +// +// Warning: Be careful when calling methods with a context before the controller +// has been started. The pass context will be used to e.g. start informers, and +// the life-cycle will differ from that of the controller. In particular, use +// IndexField instead to add indexes to the cache before the controller is +// started. func (c *namedController) GetCache() cache.Cache { return c.ca } From 956937e26772e28357c2a823eb7732445c4601e7 Mon Sep 17 00:00:00 2001 From: "Dr. Stefan Schimanski" Date: Fri, 23 Feb 2024 09:44:16 +0100 Subject: [PATCH 2/2] testing: add reader and cache mock Signed-off-by: Dr. Stefan Schimanski --- pkg/test/fake.go | 93 ++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 93 insertions(+) diff --git a/pkg/test/fake.go b/pkg/test/fake.go index c9f4b1c1b..80cbf7d48 100644 --- a/pkg/test/fake.go +++ b/pkg/test/fake.go @@ -22,6 +22,7 @@ import ( "k8s.io/apimachinery/pkg/api/meta" "k8s.io/apimachinery/pkg/runtime" "k8s.io/apimachinery/pkg/runtime/schema" + "sigs.k8s.io/controller-runtime/pkg/cache" "sigs.k8s.io/controller-runtime/pkg/client" ) @@ -232,6 +233,23 @@ func NewMockIsObjectNamespacedFn(err error, isNamespaced bool, rofn ...RuntimeOb } } +// MockClientReader implements controller-runtime's client.Reader interface, +// allowing each method to be overridden for testing. +type MockClientReader struct { + MockGet MockGetFn + MockList MockListFn +} + +// Get calls MockClientReader's MockGet function. +func (c *MockClientReader) Get(ctx context.Context, key client.ObjectKey, obj client.Object, _ ...client.GetOption) error { + return c.MockGet(ctx, key, obj) +} + +// List calls MockClientReader's MockList function. +func (c *MockClientReader) List(ctx context.Context, list client.ObjectList, opts ...client.ListOption) error { + return c.MockList(ctx, list, opts...) +} + // MockClient implements controller-runtime's Client interface, allowing each // method to be overridden for testing. The controller-runtime provides a fake // client, but it is has surprising side effects (e.g. silently calling @@ -381,3 +399,78 @@ func (m *MockSubResourceClient) Update(ctx context.Context, obj client.Object, o func (m *MockSubResourceClient) Patch(ctx context.Context, obj client.Object, patch client.Patch, opts ...client.SubResourcePatchOption) error { return m.MockPatch(ctx, obj, patch, opts...) } + +var _ cache.Cache = &MockCache{} + +// MockCache implements controller-runtime's cache.Cache interface, allowing each +// method to be overridden for testing. +type MockCache struct { + MockClientReader + MockInformers +} + +// MockGetInformerFn is used to mock cache.Informers' GetInformer implementation. +type MockGetInformerFn func(ctx context.Context, obj client.Object, opts ...cache.InformerGetOption) (cache.Informer, error) + +// MockGetInformerForKindFn is used to mock cache.Informers' GetInformerForKind implementation. +type MockGetInformerForKindFn func(ctx context.Context, gvk schema.GroupVersionKind, opts ...cache.InformerGetOption) (cache.Informer, error) + +// MockRemoveInformersFn is used to mock cache.Informers' RemoveInformer implementation. +type MockRemoveInformersFn func(ctx context.Context, obj client.Object) error + +// MockStartFn is used to mock cache.Informers' Start implementation. +type MockStartFn func(ctx context.Context) error + +// MockWaitForCacheSyncFn is used to mock cache.Informers' WaitForCacheSync implementation. +type MockWaitForCacheSyncFn func(ctx context.Context) bool + +// MockInformers implements controller-runtime's cache.Informers interface, allowing each +// method to be overridden for testing. +type MockInformers struct { + MockFieldIndexer + + MockGetInformer MockGetInformerFn + MockGetInformerForKind MockGetInformerForKindFn + MockRemoveInformer MockRemoveInformersFn + MockStart MockStartFn + MockWaitForCacheSync MockWaitForCacheSyncFn +} + +// GetInformer calls MockInformers' MockGetInformer function. +func (m MockCache) GetInformer(ctx context.Context, obj client.Object, opts ...cache.InformerGetOption) (cache.Informer, error) { + return m.MockGetInformer(ctx, obj, opts...) +} + +// GetInformerForKind calls MockInformers' MockGetInformerForKind function. +func (m MockCache) GetInformerForKind(ctx context.Context, gvk schema.GroupVersionKind, opts ...cache.InformerGetOption) (cache.Informer, error) { + return m.MockGetInformerForKind(ctx, gvk, opts...) +} + +// RemoveInformer calls MockInformers' MockRemoveInformer function. +func (m MockCache) RemoveInformer(ctx context.Context, obj client.Object) error { + return m.MockRemoveInformer(ctx, obj) +} + +// Start calls MockInformers' MockStart function. +func (m MockCache) Start(ctx context.Context) error { + return m.MockStart(ctx) +} + +// WaitForCacheSync calls MockInformers' MockWaitForCacheSync function. +func (m MockCache) WaitForCacheSync(ctx context.Context) bool { + return m.MockWaitForCacheSync(ctx) +} + +// MockIndexFieldFn is used to mock cache.Cache's IndexField implementation. +type MockIndexFieldFn func(ctx context.Context, obj client.Object, field string, extractValue client.IndexerFunc) error + +// MockFieldIndexer implements controller-runtime's cache.FieldIndexer interface, allowing each +// method to be overridden for testing. +type MockFieldIndexer struct { + MockIndexField MockIndexFieldFn +} + +// IndexField calls MockFieldIndexer's MockIndexField function. +func (m MockCache) IndexField(ctx context.Context, obj client.Object, field string, extractValue client.IndexerFunc) error { + return m.MockIndexField(ctx, obj, field, extractValue) +}