diff --git a/internal/controller/logindiscovery/publisher.go b/internal/controller/logindiscovery/publisher.go index d1ba4456..0d6d04b8 100644 --- a/internal/controller/logindiscovery/publisher.go +++ b/internal/controller/logindiscovery/publisher.go @@ -30,6 +30,25 @@ const ( configName = "placeholder-name-config" ) +func nameAndNamespaceExactMatchFilterFactory(name, namespace string) controller.FilterFuncs { + objMatchesFunc := func(obj metav1.Object) bool { + return obj.GetName() == name && obj.GetNamespace() == namespace + } + return controller.FilterFuncs{ + AddFunc: objMatchesFunc, + UpdateFunc: func(oldObj, newObj metav1.Object) bool { + return objMatchesFunc(oldObj) || objMatchesFunc(newObj) + }, + DeleteFunc: objMatchesFunc, + } +} + +// Same signature as controller.WithInformer(). +type withInformerOptionFunc func( + getter controller.InformerGetter, + filter controller.Filter, + opt controller.InformerOption) controller.Option + type publisherController struct { namespace string placeholderClient placeholderclientset.Interface @@ -42,6 +61,7 @@ func NewPublisherController( placeholderClient placeholderclientset.Interface, configMapInformer corev1informers.ConfigMapInformer, loginDiscoveryConfigInformer placeholderv1alpha1informers.LoginDiscoveryConfigInformer, + withInformer withInformerOptionFunc, ) controller.Controller { return controller.New( controller.Config{ @@ -53,14 +73,14 @@ func NewPublisherController( loginDiscoveryConfigInformer: loginDiscoveryConfigInformer, }, }, - controller.WithInformer( + withInformer( configMapInformer, - controller.FilterFuncs{}, // TODO fix this and write tests + nameAndNamespaceExactMatchFilterFactory(clusterInfoName, clusterInfoNamespace), controller.InformerOption{}, ), - controller.WithInformer( + withInformer( loginDiscoveryConfigInformer, - controller.FilterFuncs{}, // TODO fix this and write tests + nameAndNamespaceExactMatchFilterFactory(configName, namespace), controller.InformerOption{}, ), ) diff --git a/internal/controller/logindiscovery/publisher_test.go b/internal/controller/logindiscovery/publisher_test.go index 2099c722..8d90354c 100644 --- a/internal/controller/logindiscovery/publisher_test.go +++ b/internal/controller/logindiscovery/publisher_test.go @@ -29,8 +29,157 @@ import ( placeholderinformers "github.com/suzerain-io/placeholder-name-client-go/pkg/generated/informers/externalversions" ) -func TestRun(t *testing.T) { - spec.Run(t, "publisher", func(t *testing.T, when spec.G, it spec.S) { +type ObservableWithInformerOption struct { + InformerToFilterMap map[controller.InformerGetter]controller.Filter +} + +func NewObservableWithInformerOption() *ObservableWithInformerOption { + return &ObservableWithInformerOption{ + InformerToFilterMap: make(map[controller.InformerGetter]controller.Filter), + } +} + +func (owi *ObservableWithInformerOption) WithInformer( + getter controller.InformerGetter, + filter controller.Filter, + opt controller.InformerOption) controller.Option { + owi.InformerToFilterMap[getter] = filter + return controller.WithInformer(getter, filter, opt) +} + +func TestInformerFilters(t *testing.T) { + spec.Run(t, "informer filters", func(t *testing.T, when spec.G, it spec.S) { + const installedInNamespace = "some-namespace" + + var r *require.Assertions + var observableWithInformerOption *ObservableWithInformerOption + var configMapInformerFilter controller.Filter + var loginDiscoveryConfigInformerFilter controller.Filter + + it.Before(func() { + r = require.New(t) + observableWithInformerOption = NewObservableWithInformerOption() + configMapInformer := kubeinformers.NewSharedInformerFactory(nil, 0).Core().V1().ConfigMaps() + loginDiscoveryConfigInformer := placeholderinformers.NewSharedInformerFactory(nil, 0).Placeholder().V1alpha1().LoginDiscoveryConfigs() + _ = NewPublisherController( + installedInNamespace, + nil, + configMapInformer, + loginDiscoveryConfigInformer, + observableWithInformerOption.WithInformer, // make it possible to observe the behavior of the Filters + ) + configMapInformerFilter = observableWithInformerOption.InformerToFilterMap[configMapInformer] + loginDiscoveryConfigInformerFilter = observableWithInformerOption.InformerToFilterMap[loginDiscoveryConfigInformer] + }) + + when("watching ConfigMap objects", func() { + var subject controller.Filter + var target, wrongNamespace, wrongName, unrelated *corev1.ConfigMap + + it.Before(func() { + subject = configMapInformerFilter + target = &corev1.ConfigMap{ObjectMeta: metav1.ObjectMeta{Name: "cluster-info", Namespace: "kube-public"}} + wrongNamespace = &corev1.ConfigMap{ObjectMeta: metav1.ObjectMeta{Name: "cluster-info", Namespace: "wrong-namespace"}} + wrongName = &corev1.ConfigMap{ObjectMeta: metav1.ObjectMeta{Name: "wrong-name", Namespace: "kube-public"}} + unrelated = &corev1.ConfigMap{ObjectMeta: metav1.ObjectMeta{Name: "wrong-name", Namespace: "wrong-namespace"}} + }) + + when("the target ConfigMap changes", func() { + it("returns true to trigger the sync method", func() { + r.True(subject.Add(target)) + r.True(subject.Update(target, unrelated)) + r.True(subject.Update(unrelated, target)) + r.True(subject.Delete(target)) + }) + }) + + when("a ConfigMap from another namespace changes", func() { + it("returns false to avoid triggering the sync method", func() { + r.False(subject.Add(wrongNamespace)) + r.False(subject.Update(wrongNamespace, unrelated)) + r.False(subject.Update(unrelated, wrongNamespace)) + r.False(subject.Delete(wrongNamespace)) + }) + }) + + when("a ConfigMap with a different name changes", func() { + it("returns false to avoid triggering the sync method", func() { + r.False(subject.Add(wrongName)) + r.False(subject.Update(wrongName, unrelated)) + r.False(subject.Update(unrelated, wrongName)) + r.False(subject.Delete(wrongName)) + }) + }) + + when("a ConfigMap with a different name and a different namespace changes", func() { + it("returns false to avoid triggering the sync method", func() { + r.False(subject.Add(unrelated)) + r.False(subject.Update(unrelated, unrelated)) + r.False(subject.Delete(unrelated)) + }) + }) + }) + + when("watching LoginDiscoveryConfig objects", func() { + var subject controller.Filter + var target, wrongNamespace, wrongName, unrelated *placeholderv1alpha1.LoginDiscoveryConfig + + it.Before(func() { + subject = loginDiscoveryConfigInformerFilter + target = &placeholderv1alpha1.LoginDiscoveryConfig{ + ObjectMeta: metav1.ObjectMeta{Name: "placeholder-name-config", Namespace: installedInNamespace}, + } + wrongNamespace = &placeholderv1alpha1.LoginDiscoveryConfig{ + ObjectMeta: metav1.ObjectMeta{Name: "placeholder-name-config", Namespace: "wrong-namespace"}, + } + wrongName = &placeholderv1alpha1.LoginDiscoveryConfig{ + ObjectMeta: metav1.ObjectMeta{Name: "wrong-name", Namespace: installedInNamespace}, + } + unrelated = &placeholderv1alpha1.LoginDiscoveryConfig{ + ObjectMeta: metav1.ObjectMeta{Name: "wrong-name", Namespace: "wrong-namespace"}, + } + }) + + when("the target LoginDiscoveryConfig changes", func() { + it("returns true to trigger the sync method", func() { + r.True(subject.Add(target)) + r.True(subject.Update(target, unrelated)) + r.True(subject.Update(unrelated, target)) + r.True(subject.Delete(target)) + }) + }) + + when("a LoginDiscoveryConfig from another namespace changes", func() { + it("returns false to avoid triggering the sync method", func() { + r.False(subject.Add(wrongNamespace)) + r.False(subject.Update(wrongNamespace, unrelated)) + r.False(subject.Update(unrelated, wrongNamespace)) + r.False(subject.Delete(wrongNamespace)) + }) + }) + + when("a LoginDiscoveryConfig with a different name changes", func() { + it("returns false to avoid triggering the sync method", func() { + r.False(subject.Add(wrongName)) + r.False(subject.Update(wrongName, unrelated)) + r.False(subject.Update(unrelated, wrongName)) + r.False(subject.Delete(wrongName)) + }) + }) + + when("a LoginDiscoveryConfig with a different name and a different namespace changes", func() { + it("returns false to avoid triggering the sync method", func() { + r.False(subject.Add(unrelated)) + r.False(subject.Update(unrelated, unrelated)) + r.False(subject.Delete(unrelated)) + }) + }) + }) + }, spec.Parallel(), spec.Report(report.Terminal{})) +} + +func TestSync(t *testing.T) { + spec.Run(t, "Sync", func(t *testing.T, when spec.G, it spec.S) { const installedInNamespace = "some-namespace" var r *require.Assertions @@ -89,6 +238,7 @@ func TestRun(t *testing.T) { placeholderAPIClient, kubeInformers.Core().V1().ConfigMaps(), placeholderInformers.Placeholder().V1alpha1().LoginDiscoveryConfigs(), + controller.WithInformer, ) syncContext = &controller.Context{ @@ -131,7 +281,7 @@ func TestRun(t *testing.T) { }) when("the LoginDiscoveryConfig does not already exist", func() { - it.Focus("creates a LoginDiscoveryConfig", func() { + it("creates a LoginDiscoveryConfig", func() { startInformersAndController() err := controller.TestSync(t, subject, *syncContext) r.NoError(err) @@ -287,23 +437,5 @@ func TestRun(t *testing.T) { r.Empty(placeholderAPIClient.Actions()) }) }) - - when("getting the cluster-info ConfigMap in the kube-public namespace fails", func() { - it.Before(func() { - kubeInformerClient.PrependReactor( - "get", - "configmaps", - func(_ coretesting.Action) (bool, runtime.Object, error) { - return true, nil, errors.New("get failed") - }, - ) - }) - - it("returns an error", func() { - startInformersAndController() - err := controller.TestSync(t, subject, *syncContext) - r.EqualError(err, "failed to get cluster-info configmap: get failed") - }) - }) }, spec.Parallel(), spec.Report(report.Terminal{})) }