Skip to content

Commit 11f34ed

Browse files
authored
Add unit tests for controllerring and shardlease controllers (#477)
* Add unit tests for `controllerring` controller * Add unit tests for `shardlease` controller
1 parent 59bcb66 commit 11f34ed

File tree

6 files changed

+629
-7
lines changed

6 files changed

+629
-7
lines changed
Lines changed: 105 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,105 @@
1+
/*
2+
Copyright 2025 Tim Ebert.
3+
4+
Licensed under the Apache License, Version 2.0 (the "License");
5+
you may not use this file except in compliance with the License.
6+
You may obtain a copy of the License at
7+
8+
http://www.apache.org/licenses/LICENSE-2.0
9+
10+
Unless required by applicable law or agreed to in writing, software
11+
distributed under the License is distributed on an "AS IS" BASIS,
12+
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
See the License for the specific language governing permissions and
14+
limitations under the License.
15+
*/
16+
17+
package controllerring_test
18+
19+
import (
20+
"time"
21+
22+
. "github.com/onsi/ginkgo/v2"
23+
. "github.com/onsi/gomega"
24+
coordinationv1 "k8s.io/api/coordination/v1"
25+
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
26+
"k8s.io/utils/clock/testing"
27+
"k8s.io/utils/ptr"
28+
"sigs.k8s.io/controller-runtime/pkg/event"
29+
"sigs.k8s.io/controller-runtime/pkg/predicate"
30+
31+
. "github.com/timebertt/kubernetes-controller-sharding/pkg/controller/controllerring"
32+
)
33+
34+
var _ = Describe("Reconciler", func() {
35+
var r *Reconciler
36+
37+
BeforeEach(func() {
38+
r = &Reconciler{}
39+
})
40+
41+
Describe("#LeasePredicate", func() {
42+
var (
43+
p predicate.Predicate
44+
obj, objOld *coordinationv1.Lease
45+
46+
fakeClock *testing.FakePassiveClock
47+
)
48+
49+
BeforeEach(func() {
50+
fakeClock = testing.NewFakePassiveClock(time.Now())
51+
r.Clock = fakeClock
52+
53+
p = r.LeasePredicate()
54+
55+
obj = &coordinationv1.Lease{
56+
ObjectMeta: metav1.ObjectMeta{
57+
Name: "foo-0",
58+
},
59+
Spec: coordinationv1.LeaseSpec{
60+
HolderIdentity: ptr.To("foo-0"),
61+
LeaseDurationSeconds: ptr.To[int32](10),
62+
AcquireTime: ptr.To(metav1.NewMicroTime(fakeClock.Now().Add(-5 * time.Minute))),
63+
RenewTime: ptr.To(metav1.NewMicroTime(fakeClock.Now().Add(-2 * time.Second))),
64+
},
65+
}
66+
metav1.SetMetaDataLabel(&obj.ObjectMeta, "alpha.sharding.timebertt.dev/controllerring", "foo")
67+
objOld = obj.DeepCopy()
68+
})
69+
70+
It("should ignore leases with empty label", func() {
71+
metav1.SetMetaDataLabel(&obj.ObjectMeta, "alpha.sharding.timebertt.dev/controllerring", "")
72+
objOld = obj.DeepCopy()
73+
74+
Expect(p.Create(event.CreateEvent{Object: obj})).To(BeFalse())
75+
Expect(p.Update(event.UpdateEvent{ObjectOld: objOld, ObjectNew: obj})).To(BeFalse())
76+
Expect(p.Delete(event.DeleteEvent{Object: obj})).To(BeFalse())
77+
})
78+
79+
It("should react on create events", func() {
80+
Expect(p.Create(event.CreateEvent{Object: obj})).To(BeTrue())
81+
})
82+
83+
It("should react on delete events", func() {
84+
Expect(p.Delete(event.DeleteEvent{Object: obj})).To(BeTrue())
85+
})
86+
87+
It("should react when shard state changed to available", func() {
88+
objOld.Spec.HolderIdentity = nil
89+
Expect(p.Update(event.UpdateEvent{ObjectOld: objOld, ObjectNew: obj})).To(BeTrue())
90+
})
91+
92+
It("should react when shard state changed to unavailable", func() {
93+
obj.Spec.HolderIdentity = nil
94+
Expect(p.Update(event.UpdateEvent{ObjectOld: objOld, ObjectNew: obj})).To(BeTrue())
95+
})
96+
97+
It("should ignore when shard state hasn't changed", func() {
98+
Expect(p.Update(event.UpdateEvent{ObjectOld: objOld, ObjectNew: obj})).To(BeFalse())
99+
100+
obj.Spec.HolderIdentity = nil
101+
objOld.Spec.HolderIdentity = nil
102+
Expect(p.Update(event.UpdateEvent{ObjectOld: objOld, ObjectNew: obj})).To(BeFalse())
103+
})
104+
})
105+
})
Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
/*
2+
Copyright 2025 Tim Ebert.
3+
4+
Licensed under the Apache License, Version 2.0 (the "License");
5+
you may not use this file except in compliance with the License.
6+
You may obtain a copy of the License at
7+
8+
http://www.apache.org/licenses/LICENSE-2.0
9+
10+
Unless required by applicable law or agreed to in writing, software
11+
distributed under the License is distributed on an "AS IS" BASIS,
12+
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
See the License for the specific language governing permissions and
14+
limitations under the License.
15+
*/
16+
17+
package controllerring_test
18+
19+
import (
20+
"testing"
21+
22+
. "github.com/onsi/ginkgo/v2"
23+
. "github.com/onsi/gomega"
24+
)
25+
26+
func TestControllerRing(t *testing.T) {
27+
RegisterFailHandler(Fail)
28+
RunSpecs(t, "ControllerRing Controller Suite")
29+
}

pkg/controller/controllerring/reconciler.go

Lines changed: 19 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -94,7 +94,7 @@ func (r *Reconciler) Reconcile(ctx context.Context, req reconcile.Request) (reco
9494
}
9595

9696
func (r *Reconciler) updateStatusSuccess(ctx context.Context, controllerRing, before *shardingv1alpha1.ControllerRing) error {
97-
if err := r.optionallyUpdateStatus(ctx, controllerRing, before, func(ready *metav1.Condition) {
97+
if err := r.OptionallyUpdateStatus(ctx, controllerRing, before, func(ready *metav1.Condition) {
9898
ready.Status = metav1.ConditionTrue
9999
ready.Reason = "ReconciliationSucceeded"
100100
ready.Message = "ControllerRing was successfully reconciled"
@@ -109,7 +109,7 @@ func (r *Reconciler) updateStatusError(ctx context.Context, log logr.Logger, rec
109109

110110
r.Recorder.Event(controllerRing, corev1.EventTypeWarning, "ReconciliationFailed", message)
111111

112-
if err := r.optionallyUpdateStatus(ctx, controllerRing, before, func(ready *metav1.Condition) {
112+
if err := r.OptionallyUpdateStatus(ctx, controllerRing, before, func(ready *metav1.Condition) {
113113
ready.Status = metav1.ConditionFalse
114114
ready.Reason = "ReconciliationFailed"
115115
ready.Message = message
@@ -122,7 +122,7 @@ func (r *Reconciler) updateStatusError(ctx context.Context, log logr.Logger, rec
122122
return reconcileError
123123
}
124124

125-
func (r *Reconciler) optionallyUpdateStatus(ctx context.Context, controllerRing, before *shardingv1alpha1.ControllerRing, mutate func(ready *metav1.Condition)) error {
125+
func (r *Reconciler) OptionallyUpdateStatus(ctx context.Context, controllerRing, before *shardingv1alpha1.ControllerRing, mutate func(ready *metav1.Condition)) error {
126126
// always update status with the latest observed generation, no matter if reconciliation succeeded or not
127127
controllerRing.Status.ObservedGeneration = controllerRing.Generation
128128
readyCondition := metav1.Condition{
@@ -141,6 +141,15 @@ func (r *Reconciler) optionallyUpdateStatus(ctx context.Context, controllerRing,
141141
}
142142

143143
func (r *Reconciler) reconcileWebhooks(ctx context.Context, controllerRing *shardingv1alpha1.ControllerRing) error {
144+
webhookConfig, err := r.WebhookConfigForControllerRing(controllerRing)
145+
if err != nil {
146+
return err
147+
}
148+
149+
return r.Client.Patch(ctx, webhookConfig, client.Apply)
150+
}
151+
152+
func (r *Reconciler) WebhookConfigForControllerRing(controllerRing *shardingv1alpha1.ControllerRing) (*admissionregistrationv1.MutatingWebhookConfiguration, error) {
144153
webhookConfig := &admissionregistrationv1.MutatingWebhookConfiguration{
145154
TypeMeta: metav1.TypeMeta{
146155
APIVersion: admissionregistrationv1.SchemeGroupVersion.String(),
@@ -154,11 +163,16 @@ func (r *Reconciler) reconcileWebhooks(ctx context.Context, controllerRing *shar
154163
},
155164
Annotations: maps.Clone(r.Config.Webhook.Config.Annotations),
156165
},
166+
Webhooks: []admissionregistrationv1.MutatingWebhook{r.WebhookForControllerRing(controllerRing)},
157167
}
158168
if err := controllerutil.SetControllerReference(controllerRing, webhookConfig, r.Client.Scheme()); err != nil {
159-
return fmt.Errorf("error setting controller reference: %w", err)
169+
return nil, fmt.Errorf("error setting controller reference: %w", err)
160170
}
161171

172+
return webhookConfig, nil
173+
}
174+
175+
func (r *Reconciler) WebhookForControllerRing(controllerRing *shardingv1alpha1.ControllerRing) admissionregistrationv1.MutatingWebhook {
162176
webhook := admissionregistrationv1.MutatingWebhook{
163177
Name: "sharder.sharding.timebertt.dev",
164178
ClientConfig: *r.Config.Webhook.Config.ClientConfig.DeepCopy(),
@@ -207,9 +221,7 @@ func (r *Reconciler) reconcileWebhooks(ctx context.Context, controllerRing *shar
207221
}
208222
}
209223

210-
webhookConfig.Webhooks = []admissionregistrationv1.MutatingWebhook{webhook}
211-
212-
return r.Client.Patch(ctx, webhookConfig, client.Apply)
224+
return webhook
213225
}
214226

215227
// RuleForResource returns the sharder's webhook rule for the given resource.

0 commit comments

Comments
 (0)