Skip to content

Commit e3d9a19

Browse files
committed
Add unit tests for sharder controller
1 parent f350825 commit e3d9a19

File tree

2 files changed

+227
-0
lines changed

2 files changed

+227
-0
lines changed
Lines changed: 198 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,198 @@
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 sharder_test
18+
19+
import (
20+
"context"
21+
"time"
22+
23+
. "github.com/onsi/ginkgo/v2"
24+
. "github.com/onsi/gomega"
25+
coordinationv1 "k8s.io/api/coordination/v1"
26+
corev1 "k8s.io/api/core/v1"
27+
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
28+
"k8s.io/utils/clock/testing"
29+
"k8s.io/utils/ptr"
30+
"sigs.k8s.io/controller-runtime/pkg/client"
31+
"sigs.k8s.io/controller-runtime/pkg/client/fake"
32+
. "sigs.k8s.io/controller-runtime/pkg/envtest/komega"
33+
"sigs.k8s.io/controller-runtime/pkg/reconcile"
34+
35+
configv1alpha1 "github.com/timebertt/kubernetes-controller-sharding/pkg/apis/config/v1alpha1"
36+
shardingv1alpha1 "github.com/timebertt/kubernetes-controller-sharding/pkg/apis/sharding/v1alpha1"
37+
. "github.com/timebertt/kubernetes-controller-sharding/pkg/controller/sharder"
38+
utilclient "github.com/timebertt/kubernetes-controller-sharding/pkg/utils/client"
39+
"github.com/timebertt/kubernetes-controller-sharding/pkg/utils/test"
40+
)
41+
42+
var (
43+
ctx context.Context
44+
clock *testing.FakePassiveClock
45+
fakeClient client.Client
46+
r *Reconciler
47+
48+
controllerRing *shardingv1alpha1.ControllerRing
49+
config *configv1alpha1.SharderConfig
50+
51+
namespace *corev1.Namespace
52+
53+
availableShard *coordinationv1.Lease
54+
)
55+
56+
var _ = BeforeEach(func() {
57+
ctx = context.Background()
58+
clock = testing.NewFakePassiveClock(time.Now())
59+
60+
namespace = newNamespace("test")
61+
62+
controllerRing = &shardingv1alpha1.ControllerRing{
63+
ObjectMeta: metav1.ObjectMeta{
64+
Name: "foo",
65+
},
66+
Spec: shardingv1alpha1.ControllerRingSpec{
67+
Resources: []shardingv1alpha1.RingResource{{
68+
GroupResource: metav1.GroupResource{
69+
Resource: "secrets",
70+
},
71+
ControlledResources: []metav1.GroupResource{{
72+
Resource: "configmaps",
73+
}},
74+
}},
75+
NamespaceSelector: &metav1.LabelSelector{
76+
MatchExpressions: []metav1.LabelSelectorRequirement{{
77+
Key: corev1.LabelMetadataName,
78+
Operator: metav1.LabelSelectorOpIn,
79+
Values: []string{namespace.Name},
80+
}},
81+
},
82+
},
83+
}
84+
85+
availableShard = newLease()
86+
87+
fakeClient = fake.NewClientBuilder().
88+
WithScheme(utilclient.SharderScheme).
89+
WithObjects(controllerRing, namespace, availableShard).
90+
Build()
91+
SetClient(fakeClient)
92+
93+
config = &configv1alpha1.SharderConfig{}
94+
utilclient.SharderScheme.Default(config)
95+
96+
r = &Reconciler{
97+
Client: fakeClient,
98+
Reader: fakeClient,
99+
Clock: clock,
100+
Config: config,
101+
}
102+
})
103+
104+
var _ = Describe("#GetSelectedNamespaces", func() {
105+
BeforeEach(func() {
106+
Expect(fakeClient.Create(ctx, newNamespace("other"))).To(Succeed())
107+
Expect(fakeClient.Create(ctx, newNamespace(metav1.NamespaceSystem))).To(Succeed())
108+
Expect(fakeClient.Create(ctx, newNamespace(shardingv1alpha1.NamespaceSystem))).To(Succeed())
109+
})
110+
111+
It("should return all matching namespaces", func() {
112+
namespaces, err := r.GetSelectedNamespaces(ctx, controllerRing)
113+
Expect(err).NotTo(HaveOccurred())
114+
Expect(namespaces.UnsortedList()).To(ConsistOf("test"))
115+
})
116+
117+
When("the ControllerRing's namespace selector is unset", func() {
118+
BeforeEach(func() {
119+
controllerRing.Spec.NamespaceSelector = nil
120+
})
121+
122+
It("should use the config's namespace selector", func() {
123+
namespaces, err := r.GetSelectedNamespaces(ctx, controllerRing)
124+
Expect(err).NotTo(HaveOccurred())
125+
Expect(namespaces.UnsortedList()).To(ConsistOf("test", "other"))
126+
})
127+
})
128+
129+
When("the ControllerRing's namespace selector is invalid", func() {
130+
BeforeEach(func() {
131+
controllerRing.Spec.NamespaceSelector.MatchExpressions[0].Operator = "invalid"
132+
})
133+
134+
It("should return a terminal error", func() {
135+
Expect(r.GetSelectedNamespaces(ctx, controllerRing)).Error().To(MatchError(reconcile.TerminalError(nil)))
136+
})
137+
})
138+
})
139+
140+
var _ = Describe("#NewOperation", func() {
141+
var deadShard *coordinationv1.Lease
142+
143+
BeforeEach(func() {
144+
deadShard = newLease()
145+
deadShard.Spec.HolderIdentity = nil
146+
Expect(fakeClient.Create(ctx, deadShard)).To(Succeed())
147+
})
148+
149+
It("should collect the shard leases", func() {
150+
o, err := r.NewOperation(ctx, controllerRing)
151+
Expect(err).NotTo(HaveOccurred())
152+
153+
Expect(o.Shards.IDs()).To(ConsistOf(availableShard.Name, deadShard.Name))
154+
Expect(o.Shards.ByID(availableShard.Name).State.IsAvailable()).To(BeTrue())
155+
Expect(o.Shards.ByID(deadShard.Name).State.IsAvailable()).To(BeFalse())
156+
})
157+
158+
It("should construct a hash ring", func() {
159+
o, err := r.NewOperation(ctx, controllerRing)
160+
Expect(err).NotTo(HaveOccurred())
161+
162+
Expect(o.HashRing.Hash("foo")).To(Equal(availableShard.Name))
163+
})
164+
165+
It("should collect all matching namespaces", func() {
166+
o, err := r.NewOperation(ctx, controllerRing)
167+
Expect(err).NotTo(HaveOccurred())
168+
169+
Expect(o.Namespaces.UnsortedList()).To(ConsistOf("test"))
170+
})
171+
})
172+
173+
func newNamespace(name string) *corev1.Namespace {
174+
return &corev1.Namespace{ObjectMeta: metav1.ObjectMeta{
175+
Name: name,
176+
Labels: map[string]string{corev1.LabelMetadataName: name},
177+
}}
178+
}
179+
180+
func newLease() *coordinationv1.Lease {
181+
name := "shard-" + test.RandomSuffix()
182+
183+
return &coordinationv1.Lease{
184+
ObjectMeta: metav1.ObjectMeta{
185+
Name: name,
186+
Namespace: namespace.Namespace,
187+
Labels: map[string]string{
188+
shardingv1alpha1.LabelControllerRing: controllerRing.Name,
189+
},
190+
},
191+
Spec: coordinationv1.LeaseSpec{
192+
HolderIdentity: ptr.To(name),
193+
LeaseDurationSeconds: ptr.To[int32](10),
194+
AcquireTime: ptr.To(metav1.NewMicroTime(clock.Now().Add(-5 * time.Minute))),
195+
RenewTime: ptr.To(metav1.NewMicroTime(clock.Now().Add(-2 * time.Second))),
196+
},
197+
}
198+
}
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 sharder_test
18+
19+
import (
20+
"testing"
21+
22+
. "github.com/onsi/ginkgo/v2"
23+
. "github.com/onsi/gomega"
24+
)
25+
26+
func TestSharder(t *testing.T) {
27+
RegisterFailHandler(Fail)
28+
RunSpecs(t, "Sharder Controller Suite")
29+
}

0 commit comments

Comments
 (0)