From 2d373a28cc0df3d23fe566ef425751ded9454535 Mon Sep 17 00:00:00 2001 From: Tim Ebert Date: Wed, 26 Feb 2025 23:12:17 +0100 Subject: [PATCH 1/3] Prefactor: extract `matchers.BeFunc` --- pkg/sharding/key/key_test.go | 17 ++++------------- pkg/utils/test/matchers/matchers.go | 23 +++++++++++++++++++++++ 2 files changed, 27 insertions(+), 13 deletions(-) diff --git a/pkg/sharding/key/key_test.go b/pkg/sharding/key/key_test.go index 8cc8d89e..f2096ebb 100644 --- a/pkg/sharding/key/key_test.go +++ b/pkg/sharding/key/key_test.go @@ -17,18 +17,15 @@ limitations under the License. package key_test import ( - "reflect" - . "github.com/onsi/ginkgo/v2" . "github.com/onsi/gomega" - "github.com/onsi/gomega/gcustom" - gomegatypes "github.com/onsi/gomega/types" appsv1 "k8s.io/api/apps/v1" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/utils/ptr" shardingv1alpha1 "github.com/timebertt/kubernetes-controller-sharding/pkg/apis/sharding/v1alpha1" . "github.com/timebertt/kubernetes-controller-sharding/pkg/sharding/key" + . "github.com/timebertt/kubernetes-controller-sharding/pkg/utils/test/matchers" ) var _ = Describe("#FuncForResource", func() { @@ -76,7 +73,7 @@ var _ = Describe("#FuncForResource", func() { Group: "operator", Resource: "foo", }, controllerRing)).To( - beFunc(ForObject), + BeFunc(ForObject), ) }) @@ -85,7 +82,7 @@ var _ = Describe("#FuncForResource", func() { Group: "operator", Resource: "controlled", }, controllerRing)).To( - beFunc(ForController), + BeFunc(ForController), ) }) @@ -93,7 +90,7 @@ var _ = Describe("#FuncForResource", func() { Expect(FuncForResource(metav1.GroupResource{ Resource: "foo", }, controllerRing)).To( - beFunc(ForObject), + BeFunc(ForObject), ) }) }) @@ -177,9 +174,3 @@ var _ = Describe("#ForController", func() { Expect(ForController(obj)).To(Equal("operator/Foo/bar/foo")) }) }) - -func beFunc(expected Func) gomegatypes.GomegaMatcher { - return gcustom.MakeMatcher(func(actual Func) (bool, error) { - return reflect.ValueOf(expected).Pointer() == reflect.ValueOf(actual).Pointer(), nil - }) -} diff --git a/pkg/utils/test/matchers/matchers.go b/pkg/utils/test/matchers/matchers.go index 7d2de382..29f00940 100644 --- a/pkg/utils/test/matchers/matchers.go +++ b/pkg/utils/test/matchers/matchers.go @@ -17,6 +17,10 @@ limitations under the License. package matchers import ( + "fmt" + "reflect" + + "github.com/onsi/gomega/gcustom" gomegatypes "github.com/onsi/gomega/types" apierrors "k8s.io/apimachinery/pkg/api/errors" ) @@ -28,3 +32,22 @@ func BeNotFoundError() gomegatypes.GomegaMatcher { message: "NotFound", } } + +// BeFunc is a matcher that returns true if expected and actual are the same func. +func BeFunc(expected any) gomegatypes.GomegaMatcher { + return gcustom.MakeMatcher(func(actual any) (bool, error) { + var ( + valueExpected = reflect.ValueOf(expected) + valueActual = reflect.ValueOf(actual) + ) + + if valueExpected.Kind() != reflect.Func { + return false, fmt.Errorf("expected should be a func, got %v", valueExpected.Kind()) + } + if valueActual.Kind() != reflect.Func { + return false, fmt.Errorf("actual should be a func, got %v", valueActual.Kind()) + } + + return valueExpected.Pointer() == valueActual.Pointer(), nil + }) +} From 7364019e0fe57633cfc18ea7ff444db09baf9523 Mon Sep 17 00:00:00 2001 From: Tim Ebert Date: Wed, 26 Feb 2025 22:34:38 +0100 Subject: [PATCH 2/3] Prefactor: simplify instantiating `shardcontroller.Reconciler` --- pkg/shard/controller/builder.go | 13 +++++-------- pkg/shard/controller/reconciler.go | 22 +++++++++++++--------- 2 files changed, 18 insertions(+), 17 deletions(-) diff --git a/pkg/shard/controller/builder.go b/pkg/shard/controller/builder.go index c4925b22..997cd7b9 100644 --- a/pkg/shard/controller/builder.go +++ b/pkg/shard/controller/builder.go @@ -23,8 +23,6 @@ import ( "sigs.k8s.io/controller-runtime/pkg/client" "sigs.k8s.io/controller-runtime/pkg/manager" "sigs.k8s.io/controller-runtime/pkg/reconcile" - - shardingv1alpha1 "github.com/timebertt/kubernetes-controller-sharding/pkg/apis/sharding/v1alpha1" ) // Builder can build a sharded reconciler. @@ -105,11 +103,10 @@ func (b *Builder) Build(r reconcile.Reconciler) (reconcile.Reconciler, error) { } return &Reconciler{ - Object: b.object, - Client: b.client, - ShardName: b.shardName, - LabelShard: shardingv1alpha1.LabelShard(b.controllerRingName), - LabelDrain: shardingv1alpha1.LabelDrain(b.controllerRingName), - Do: r, + Object: b.object, + Client: b.client, + ControllerRingName: b.controllerRingName, + ShardName: b.shardName, + Do: r, }, nil } diff --git a/pkg/shard/controller/reconciler.go b/pkg/shard/controller/reconciler.go index 98a7f477..abc6374b 100644 --- a/pkg/shard/controller/reconciler.go +++ b/pkg/shard/controller/reconciler.go @@ -24,6 +24,8 @@ import ( "sigs.k8s.io/controller-runtime/pkg/client" logf "sigs.k8s.io/controller-runtime/pkg/log" "sigs.k8s.io/controller-runtime/pkg/reconcile" + + shardingv1alpha1 "github.com/timebertt/kubernetes-controller-sharding/pkg/apis/sharding/v1alpha1" ) // Reconciler wraps another reconciler to ensure that the controller correctly handles the shard and drain labels. @@ -35,12 +37,10 @@ type Reconciler struct { Object client.Object // Client is used to read and patch the controller's objects. Client client.Client + // ControllerRingName is the name of the manager's ControllerRing. + ControllerRingName string // ShardName is the shard ID of the manager. ShardName string - // LabelShard is the shard label specific to the manager's ControllerRing. - LabelShard string - // LabelDrain is the drain label specific to the manager's ControllerRing. - LabelDrain string // Do is the actual Reconciler. Do reconcile.Reconciler } @@ -62,24 +62,28 @@ func (r *Reconciler) Reconcile(ctx context.Context, request reconcile.Request) ( return reconcile.Result{}, fmt.Errorf("error retrieving object from store for determining responsibility: %w", err) } - labels := obj.GetLabels() + var ( + labels = obj.GetLabels() + labelShard = shardingv1alpha1.LabelShard(r.ControllerRingName) + labelDrain = shardingv1alpha1.LabelDrain(r.ControllerRingName) + ) // check if we are responsible for this object // Note that objects should already be filtered by the cache and the predicate for being assigned to this shard. // However, we still need to do a final check before reconciling here. The controller might requeue the object with // a delay or exponential. This might trigger another reconciliation even after observing a label change. - if shard, ok := labels[r.LabelShard]; !ok || shard != r.ShardName { + if shard, ok := labels[labelShard]; !ok || shard != r.ShardName { log.V(1).Info("Ignoring object as it is assigned to different shard", "shard", shard) return reconcile.Result{}, nil } - if _, drain := labels[r.LabelDrain]; drain { + if _, drain := labels[labelDrain]; drain { log.V(1).Info("Draining object") // acknowledge drain operation patch := client.MergeFromWithOptions(obj.DeepCopyObject().(client.Object), client.MergeFromWithOptimisticLock{}) - delete(labels, r.LabelShard) - delete(labels, r.LabelDrain) + delete(labels, labelShard) + delete(labels, labelDrain) if err := r.Client.Patch(ctx, obj, patch); err != nil { return reconcile.Result{}, fmt.Errorf("error draining object: %w", err) From 865a86c586df2170ba3af279b2f0fd7ce74a3814 Mon Sep 17 00:00:00 2001 From: Tim Ebert Date: Wed, 26 Feb 2025 22:35:27 +0100 Subject: [PATCH 3/3] Add unit tests for `pkg/shard/controller` --- pkg/shard/controller/builder.go | 4 +- pkg/shard/controller/builder_test.go | 154 +++++++++++++++++ pkg/shard/controller/controller_suite_test.go | 29 ++++ pkg/shard/controller/predicate_test.go | 129 ++++++++++++++ pkg/shard/controller/reconciler_test.go | 159 ++++++++++++++++++ 5 files changed, 472 insertions(+), 3 deletions(-) create mode 100644 pkg/shard/controller/builder_test.go create mode 100644 pkg/shard/controller/controller_suite_test.go create mode 100644 pkg/shard/controller/predicate_test.go create mode 100644 pkg/shard/controller/reconciler_test.go diff --git a/pkg/shard/controller/builder.go b/pkg/shard/controller/builder.go index 997cd7b9..89152d56 100644 --- a/pkg/shard/controller/builder.go +++ b/pkg/shard/controller/builder.go @@ -40,9 +40,7 @@ type Builder struct { // reconciler that takes care of the sharding-related logic and calls the delegate reconciler whenever the shard is // responsible for reconciling an object. func NewShardedReconciler(mgr manager.Manager) *Builder { - return &Builder{ - client: mgr.GetClient(), - } + return (&Builder{}).WithClient(mgr.GetClient()) } // For sets the object kind being reconciled by the reconciler. diff --git a/pkg/shard/controller/builder_test.go b/pkg/shard/controller/builder_test.go new file mode 100644 index 00000000..a52a7d8a --- /dev/null +++ b/pkg/shard/controller/builder_test.go @@ -0,0 +1,154 @@ +/* +Copyright 2025 Tim Ebert. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package controller_test + +import ( + "context" + + . "github.com/onsi/ginkgo/v2" + . "github.com/onsi/gomega" + corev1 "k8s.io/api/core/v1" + "sigs.k8s.io/controller-runtime/pkg/client" + fakeclient "sigs.k8s.io/controller-runtime/pkg/client/fake" + "sigs.k8s.io/controller-runtime/pkg/manager" + "sigs.k8s.io/controller-runtime/pkg/reconcile" + + . "github.com/timebertt/kubernetes-controller-sharding/pkg/shard/controller" + . "github.com/timebertt/kubernetes-controller-sharding/pkg/utils/test/matchers" +) + +var _ = Describe("#Builder", func() { + var ( + mgr fakeManager + b *Builder + + obj *corev1.Pod + controllerRingName string + shardName string + client client.Client + r reconcile.Reconciler + ) + + BeforeEach(func() { + obj = &corev1.Pod{} + controllerRingName = "operator" + shardName = "operator-0" + client = fakeclient.NewFakeClient() + r = reconcile.Func(func(context.Context, reconcile.Request) (reconcile.Result, error) { + return reconcile.Result{}, nil + }) + }) + + JustBeforeEach(func() { + mgr = fakeManager{Client: client} + b = NewShardedReconciler(mgr). + For(obj). + InControllerRing(controllerRingName). + WithShardName(shardName) + }) + + Describe("#For", func() { + It("should complain about calling For twice", func() { + Expect(b.For(obj).Build(r)).Error().To(MatchError("must not call For() more than once")) + }) + + It("should complain about not calling For", func() { + b = NewShardedReconciler(mgr). + InControllerRing(controllerRingName). + WithShardName(shardName) + Expect(b.Build(r)).Error().To(MatchError("missing object kind, must call to For()")) + }) + }) + + Describe("#WithClient", func() { + It("should use the custom client instead of the manager's client", func() { + customClient := fakeclient.NewFakeClient() + Expect(b.WithClient(customClient).Build(r)).To( + HaveField("Client", BeIdenticalTo(customClient)), + ) + }) + + It("should complain about missing client", func() { + Expect(b.WithClient(nil).Build(r)).Error().To(MatchError("missing client")) + }) + }) + + Describe("#InControllerRing", func() { + It("should complain about missing ControllerRing name", func() { + Expect(b.InControllerRing("").Build(r)).Error().To(MatchError("missing ControllerRing name")) + }) + }) + + Describe("#WithShardName", func() { + It("should complain about missing shard name", func() { + Expect(b.WithShardName("").Build(r)).Error().To(MatchError("missing shard name")) + }) + }) + + Describe("#Build", func() { + It("should complain about nil reconciler", func() { + Expect(b.Build(nil)).Error().To(MatchError("must provide a non-nil Reconciler")) + }) + + It("should correctly set up the Reconciler", func() { + shardReconciler, err := b.Build(r) + Expect(err).NotTo(HaveOccurred()) + + reconciler, ok := shardReconciler.(*Reconciler) + Expect(ok).To(BeTrue()) + + Expect(reconciler.Object).To(BeAssignableToTypeOf(obj)) + Expect(reconciler.Client).To(BeIdenticalTo(client)) + Expect(reconciler.ControllerRingName).To(Equal(controllerRingName)) + Expect(reconciler.ShardName).To(Equal(shardName)) + Expect(reconciler.Do).To(BeFunc(r)) + }) + }) + + Describe("#MustBuild", func() { + It("should panic for nil reconciler", func() { + Expect(func() { + b.MustBuild(nil) + }).To(Panic()) + }) + + It("should correctly set up the Reconciler", func() { + var shardReconciler reconcile.Reconciler + Expect(func() { + shardReconciler = b.MustBuild(r) + }).NotTo(Panic()) + + reconciler, ok := shardReconciler.(*Reconciler) + Expect(ok).To(BeTrue()) + + Expect(reconciler.Object).To(BeAssignableToTypeOf(obj)) + Expect(reconciler.Client).To(BeIdenticalTo(client)) + Expect(reconciler.ControllerRingName).To(Equal(controllerRingName)) + Expect(reconciler.ShardName).To(Equal(shardName)) + Expect(reconciler.Do).To(BeFunc(r)) + }) + }) +}) + +type fakeManager struct { + manager.Manager + Client client.Client +} + +func (f fakeManager) GetClient() client.Client { + return f.Client +} diff --git a/pkg/shard/controller/controller_suite_test.go b/pkg/shard/controller/controller_suite_test.go new file mode 100644 index 00000000..497c475e --- /dev/null +++ b/pkg/shard/controller/controller_suite_test.go @@ -0,0 +1,29 @@ +/* +Copyright 2025 Tim Ebert. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package controller_test + +import ( + "testing" + + . "github.com/onsi/ginkgo/v2" + . "github.com/onsi/gomega" +) + +func TestController(t *testing.T) { + RegisterFailHandler(Fail) + RunSpecs(t, "Shard Controller Suite") +} diff --git a/pkg/shard/controller/predicate_test.go b/pkg/shard/controller/predicate_test.go new file mode 100644 index 00000000..fd16e0dd --- /dev/null +++ b/pkg/shard/controller/predicate_test.go @@ -0,0 +1,129 @@ +/* +Copyright 2025 Tim Ebert. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package controller_test + +import ( + . "github.com/onsi/ginkgo/v2" + . "github.com/onsi/gomega" + corev1 "k8s.io/api/core/v1" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "sigs.k8s.io/controller-runtime/pkg/client" + "sigs.k8s.io/controller-runtime/pkg/event" + "sigs.k8s.io/controller-runtime/pkg/predicate" + + . "github.com/timebertt/kubernetes-controller-sharding/pkg/shard/controller" +) + +var _ = Describe("#Predicate", func() { + var ( + controllerRingName string + shardName string + + mainPredicate, p predicate.Predicate + + obj, objOld *corev1.Pod + ) + + BeforeEach(func() { + controllerRingName = "operator" + shardName = "operator-0" + + obj = &corev1.Pod{} + }) + + JustBeforeEach(func() { + p = Predicate(controllerRingName, shardName, mainPredicate) + }) + + When("main predicate returns false", func() { + BeforeEach(func() { + mainPredicate = predicate.NewPredicateFuncs(func(client.Object) bool { + return false + }) + }) + + It("should handle drained objects", func() { + metav1.SetMetaDataLabel(&obj.ObjectMeta, "drain.alpha.sharding.timebertt.dev/"+controllerRingName, "true") + objOld = obj.DeepCopy() + + Expect(p.Create(event.CreateEvent{Object: obj})).To(BeTrue()) + Expect(p.Update(event.UpdateEvent{ObjectOld: objOld, ObjectNew: obj})).To(BeTrue()) + Expect(p.Delete(event.DeleteEvent{Object: obj})).To(BeTrue()) + Expect(p.Generic(event.GenericEvent{Object: obj})).To(BeTrue()) + }) + + It("should handle assigned and drained objects", func() { + metav1.SetMetaDataLabel(&obj.ObjectMeta, "shard.alpha.sharding.timebertt.dev/"+controllerRingName, shardName) + metav1.SetMetaDataLabel(&obj.ObjectMeta, "drain.alpha.sharding.timebertt.dev/"+controllerRingName, "true") + objOld = obj.DeepCopy() + + Expect(p.Create(event.CreateEvent{Object: obj})).To(BeTrue()) + Expect(p.Update(event.UpdateEvent{ObjectOld: objOld, ObjectNew: obj})).To(BeTrue()) + Expect(p.Delete(event.DeleteEvent{Object: obj})).To(BeTrue()) + Expect(p.Generic(event.GenericEvent{Object: obj})).To(BeTrue()) + }) + + It("should not handle assigned objects", func() { + metav1.SetMetaDataLabel(&obj.ObjectMeta, "shard.alpha.sharding.timebertt.dev/"+controllerRingName, shardName) + objOld = obj.DeepCopy() + + Expect(p.Create(event.CreateEvent{Object: obj})).To(BeFalse()) + Expect(p.Update(event.UpdateEvent{ObjectOld: objOld, ObjectNew: obj})).To(BeFalse()) + Expect(p.Delete(event.DeleteEvent{Object: obj})).To(BeFalse()) + Expect(p.Generic(event.GenericEvent{Object: obj})).To(BeFalse()) + }) + }) + + When("main predicate returns true", func() { + BeforeEach(func() { + mainPredicate = predicate.NewPredicateFuncs(func(client.Object) bool { + return true + }) + }) + + It("should handle drained objects", func() { + metav1.SetMetaDataLabel(&obj.ObjectMeta, "drain.alpha.sharding.timebertt.dev/"+controllerRingName, "true") + objOld = obj.DeepCopy() + + Expect(p.Create(event.CreateEvent{Object: obj})).To(BeTrue()) + Expect(p.Update(event.UpdateEvent{ObjectOld: objOld, ObjectNew: obj})).To(BeTrue()) + Expect(p.Delete(event.DeleteEvent{Object: obj})).To(BeTrue()) + Expect(p.Generic(event.GenericEvent{Object: obj})).To(BeTrue()) + }) + + It("should handle assigned objects", func() { + metav1.SetMetaDataLabel(&obj.ObjectMeta, "shard.alpha.sharding.timebertt.dev/"+controllerRingName, shardName) + objOld = obj.DeepCopy() + + Expect(p.Create(event.CreateEvent{Object: obj})).To(BeTrue()) + Expect(p.Update(event.UpdateEvent{ObjectOld: objOld, ObjectNew: obj})).To(BeTrue()) + Expect(p.Delete(event.DeleteEvent{Object: obj})).To(BeTrue()) + Expect(p.Generic(event.GenericEvent{Object: obj})).To(BeTrue()) + }) + + It("should handle assigned and drained objects", func() { + metav1.SetMetaDataLabel(&obj.ObjectMeta, "shard.alpha.sharding.timebertt.dev/"+controllerRingName, shardName) + metav1.SetMetaDataLabel(&obj.ObjectMeta, "drain.alpha.sharding.timebertt.dev/"+controllerRingName, "true") + objOld = obj.DeepCopy() + + Expect(p.Create(event.CreateEvent{Object: obj})).To(BeTrue()) + Expect(p.Update(event.UpdateEvent{ObjectOld: objOld, ObjectNew: obj})).To(BeTrue()) + Expect(p.Delete(event.DeleteEvent{Object: obj})).To(BeTrue()) + Expect(p.Generic(event.GenericEvent{Object: obj})).To(BeTrue()) + }) + }) +}) diff --git a/pkg/shard/controller/reconciler_test.go b/pkg/shard/controller/reconciler_test.go new file mode 100644 index 00000000..91cd2b8a --- /dev/null +++ b/pkg/shard/controller/reconciler_test.go @@ -0,0 +1,159 @@ +/* +Copyright 2025 Tim Ebert. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package controller_test + +import ( + "context" + "fmt" + "io" + + . "github.com/onsi/ginkgo/v2" + . "github.com/onsi/gomega" + . "github.com/onsi/gomega/gbytes" + corev1 "k8s.io/api/core/v1" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "sigs.k8s.io/controller-runtime/pkg/client" + fakeclient "sigs.k8s.io/controller-runtime/pkg/client/fake" + logf "sigs.k8s.io/controller-runtime/pkg/log" + "sigs.k8s.io/controller-runtime/pkg/log/zap" + "sigs.k8s.io/controller-runtime/pkg/reconcile" + + . "github.com/timebertt/kubernetes-controller-sharding/pkg/shard/controller" +) + +var _ = Describe("#Reconciler", func() { + var ( + ctx context.Context + + logBuffer *Buffer + + controllerRingName string + shardName string + + fakeClient client.Client + + r *Reconciler + + obj *corev1.Pod + req reconcile.Request + ) + + BeforeEach(func() { + logBuffer = NewBuffer() + ctx = logf.IntoContext(context.Background(), zap.New(zap.UseDevMode(true), zap.WriteTo(io.MultiWriter(logBuffer, GinkgoWriter)))) + + controllerRingName = "operator" + shardName = "operator-0" + + fakeClient = fakeclient.NewFakeClient() + + r = &Reconciler{ + Object: &corev1.Pod{}, + Client: fakeClient, + ControllerRingName: controllerRingName, + ShardName: shardName, + } + + obj = &corev1.Pod{ + ObjectMeta: metav1.ObjectMeta{ + Name: "foo", + Namespace: "bar", + }, + } + req = reconcile.Request{NamespacedName: client.ObjectKeyFromObject(obj)} + }) + + JustBeforeEach(func() { + if obj != nil { + Expect(fakeClient.Create(ctx, obj)).To(Succeed()) + } + }) + + When("the object does not exist", func() { + BeforeEach(func() { + obj = nil + }) + + It("should ignore the request", func() { + Expect(r.Reconcile(ctx, req)).To(BeZero()) + Eventually(logBuffer).Should(Say("Object is gone")) + }) + }) + + When("the object is assigned to another shard", func() { + BeforeEach(func() { + metav1.SetMetaDataLabel(&obj.ObjectMeta, "shard.alpha.sharding.timebertt.dev/"+controllerRingName, "other") + }) + + It("should ignore the request", func() { + Expect(r.Reconcile(ctx, req)).To(BeZero()) + Eventually(logBuffer).Should(Say("Ignoring object as it is assigned to different shard")) + }) + }) + + When("the object is assigned to this shard", func() { + BeforeEach(func() { + metav1.SetMetaDataLabel(&obj.ObjectMeta, "shard.alpha.sharding.timebertt.dev/"+controllerRingName, shardName) + }) + + When("the object is drained", func() { + BeforeEach(func() { + metav1.SetMetaDataLabel(&obj.ObjectMeta, "drain.alpha.sharding.timebertt.dev/"+controllerRingName, "true") + }) + + It("should remove the shard and drain label", func() { + Expect(r.Reconcile(ctx, req)).To(BeZero()) + Eventually(logBuffer).Should(Say("Draining object")) + + Expect(fakeClient.Get(ctx, client.ObjectKeyFromObject(obj), obj)).To(Succeed()) + Expect(obj.GetLabels()).Should(BeEmpty()) + }) + }) + + When("the object is not drained", func() { + var ( + result reconcile.Result + err error + called int + ) + + BeforeEach(func() { + result, err = reconcile.Result{}, nil + called = 0 + + r.Do = reconcile.Func(func(context.Context, reconcile.Request) (reconcile.Result, error) { + called++ + return result, err + }) + }) + + It("should call the reconciler and return its result", func() { + Expect(r.Reconcile(ctx, req)).To(BeZero()) + Expect(called).To(Equal(1)) + + result = reconcile.Result{Requeue: true} + Expect(r.Reconcile(ctx, req)).To(Equal(reconcile.Result{Requeue: true})) + Expect(called).To(Equal(2)) + + result = reconcile.Result{} + err = fmt.Errorf("foo") + Expect(r.Reconcile(ctx, req)).Error().To(MatchError("foo")) + Expect(called).To(Equal(3)) + }) + }) + }) +})