Skip to content

Commit 3fbf214

Browse files
authored
Add unit tests for remaining pkg/sharding packages (#485)
* Add unit tests for `pkg/sharding/ring` * Extract `pkg/sharding/key` package * Don't include API version in hash key * Add unit tests for `pkg/sharding/key`
1 parent 299ced7 commit 3fbf214

File tree

9 files changed

+376
-42
lines changed

9 files changed

+376
-42
lines changed

pkg/apis/sharding/v1alpha1/types_controllerring.go

Lines changed: 0 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -130,8 +130,3 @@ func (c *ControllerRing) LabelShard() string {
130130
func (c *ControllerRing) LabelDrain() string {
131131
return LabelDrain(c.Name)
132132
}
133-
134-
// RingResources returns the list of resources that are distributed across shards in this ControllerRing.
135-
func (c *ControllerRing) RingResources() []RingResource {
136-
return c.Spec.Resources
137-
}

pkg/apis/sharding/v1alpha1/types_controllerring_test.go

Lines changed: 0 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -64,10 +64,4 @@ var _ = Describe("ControllerRing", func() {
6464
Expect(ring.LabelDrain()).To(Equal("drain.alpha.sharding.timebertt.dev/operator"))
6565
})
6666
})
67-
68-
Describe("#RingResources", func() {
69-
It("should return the specified resources", func() {
70-
Expect(ring.RingResources()).To(Equal(ring.Spec.Resources))
71-
})
72-
})
7367
})

pkg/controller/sharder/reconciler.go

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -35,8 +35,8 @@ import (
3535

3636
configv1alpha1 "github.com/timebertt/kubernetes-controller-sharding/pkg/apis/config/v1alpha1"
3737
shardingv1alpha1 "github.com/timebertt/kubernetes-controller-sharding/pkg/apis/sharding/v1alpha1"
38-
"github.com/timebertt/kubernetes-controller-sharding/pkg/sharding"
3938
"github.com/timebertt/kubernetes-controller-sharding/pkg/sharding/consistenthash"
39+
"github.com/timebertt/kubernetes-controller-sharding/pkg/sharding/key"
4040
"github.com/timebertt/kubernetes-controller-sharding/pkg/sharding/leases"
4141
shardingmetrics "github.com/timebertt/kubernetes-controller-sharding/pkg/sharding/metrics"
4242
"github.com/timebertt/kubernetes-controller-sharding/pkg/sharding/ring"
@@ -201,22 +201,22 @@ func (r *Reconciler) resyncObject(
201201
) error {
202202
log = log.WithValues("object", client.ObjectKeyFromObject(obj))
203203

204-
keyFunc := sharding.KeyForObject
204+
keyFunc := key.ForObject
205205
if controlled {
206-
keyFunc = sharding.KeyForController
206+
keyFunc = key.ForController
207207
}
208208

209-
key, err := keyFunc(obj)
209+
hashKey, err := keyFunc(obj)
210210
if err != nil {
211211
return err
212212
}
213-
if key == "" {
213+
if hashKey == "" {
214214
// object should not be assigned
215215
return nil
216216
}
217217

218218
var (
219-
desiredShard = hashRing.Hash(key)
219+
desiredShard = hashRing.Hash(hashKey)
220220
currentShard = obj.Labels[ring.LabelShard()]
221221
)
222222

pkg/sharding/key.go renamed to pkg/sharding/key/key.go

Lines changed: 26 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -14,25 +14,26 @@ See the License for the specific language governing permissions and
1414
limitations under the License.
1515
*/
1616

17-
package sharding
17+
package key
1818

1919
import (
2020
"fmt"
2121

2222
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
23+
"k8s.io/apimachinery/pkg/runtime/schema"
2324
"k8s.io/apimachinery/pkg/util/sets"
2425
"sigs.k8s.io/controller-runtime/pkg/client"
2526

2627
shardingv1alpha1 "github.com/timebertt/kubernetes-controller-sharding/pkg/apis/sharding/v1alpha1"
2728
)
2829

29-
// KeyFuncForResource returns the key function that maps the given resource or its controller depending on whether
30+
// FuncForResource returns the key function that maps the given resource or its controller depending on whether
3031
// the resource is listed as a resource or controlled resource in the given ring.
31-
func KeyFuncForResource(gr metav1.GroupResource, ring *shardingv1alpha1.ControllerRing) (KeyFunc, error) {
32+
func FuncForResource(gr metav1.GroupResource, ring *shardingv1alpha1.ControllerRing) (Func, error) {
3233
ringResources := sets.New[metav1.GroupResource]()
3334
controlledResources := sets.New[metav1.GroupResource]()
3435

35-
for _, ringResource := range ring.RingResources() {
36+
for _, ringResource := range ring.Spec.Resources {
3637
ringResources.Insert(ringResource.GroupResource)
3738

3839
for _, controlledResource := range ringResource.ControlledResources {
@@ -42,24 +43,24 @@ func KeyFuncForResource(gr metav1.GroupResource, ring *shardingv1alpha1.Controll
4243

4344
switch {
4445
case ringResources.Has(gr):
45-
return KeyForObject, nil
46+
return ForObject, nil
4647
case controlledResources.Has(gr):
47-
return KeyForController, nil
48+
return ForController, nil
4849
}
4950

50-
return nil, fmt.Errorf("object's resource %q was not found in Ring", gr.String())
51+
return nil, fmt.Errorf("object's resource %q was not found in ControllerRing", gr.String())
5152
}
5253

53-
// KeyFunc maps objects to hash keys.
54-
// It returns an error if the prequisities for sharding the given object are not fulfilled.
54+
// Func maps objects to hash keys.
55+
// It returns an error if the prerequisites for sharding the given object are not fulfilled.
5556
// If the returned key is empty, the object should not be assigned.
56-
type KeyFunc func(client.Object) (string, error)
57+
type Func func(client.Object) (string, error)
5758

58-
// KeyForObject returns a ring key for the given object itself.
59+
// ForObject returns a ring key for the given object itself.
5960
// It needs the TypeMeta (GVK) to be set, which is not set on objects after decoding by default.
60-
func KeyForObject(obj client.Object) (string, error) {
61+
func ForObject(obj client.Object) (string, error) {
6162
// We can't use the object's UID, as it is unset during admission for CREATE requests.
62-
// Instead, we need to calculate a unique ID ourselves. The ID has this pattern (see keyForMetadata):
63+
// Instead, we need to calculate a unique ID ourselves. The ID has this pattern (see forMetadata):
6364
// group/version/kind/namespace/name
6465
// With this, different object instances with the same name will use the same hash key, which sounds acceptable.
6566
// We can only use fields that are also present in owner references as we need to assign owners and ownees to the same
@@ -77,7 +78,7 @@ func KeyForObject(obj client.Object) (string, error) {
7778
// object ID that we can also reconstruct later on for owned objects just by looking at the object itself.
7879
// We could use a cache lookup though, but this would restrict scalability of the sharding solution again.
7980
// Generally, this tradeoff seems acceptable, as generateName is mostly used on owned objects, but rarely the
80-
// owner itself. In such case, KeyForController will be used instead, which doesn't care about the object's own
81+
// owner itself. In such case, ForController will be used instead, which doesn't care about the object's own
8182
// name but only that of the owner.
8283
// If generateName is used nevertheless, respond with a proper error.
8384
// We could assign the object after creation, however we can't use a watch cache because of the mentioned
@@ -90,12 +91,12 @@ func KeyForObject(obj client.Object) (string, error) {
9091

9192
// Namespace can be empty for cluster-scoped resources. Only check the name field as an optimistic check for
9293
// preventing wrong usage of the function.
93-
return keyForMetadata(gvk.GroupVersion().String(), gvk.Kind, obj.GetNamespace(), obj.GetName()), nil
94+
return forMetadata(gvk.Group, gvk.Kind, obj.GetNamespace(), obj.GetName()), nil
9495
}
9596

96-
// KeyForController returns a ring key for the controller of the given object.
97+
// ForController returns a ring key for the controller of the given object.
9798
// It returns an empty key if the object doesn't have an ownerReference with controller=true".
98-
func KeyForController(obj client.Object) (string, error) {
99+
func ForController(obj client.Object) (string, error) {
99100
ref := metav1.GetControllerOf(obj)
100101
if ref == nil {
101102
return "", nil
@@ -111,11 +112,16 @@ func KeyForController(obj client.Object) (string, error) {
111112
return "", fmt.Errorf("name of controller reference must not be empty")
112113
}
113114

115+
gv, err := schema.ParseGroupVersion(ref.APIVersion)
116+
if err != nil {
117+
return "", fmt.Errorf("invalid apiVersion of controller reference: %w", err)
118+
}
119+
114120
// Namespace can be empty for cluster-scoped resources. Only check the other fields as an optimistic check for
115121
// preventing wrong usage of the function.
116-
return keyForMetadata(ref.APIVersion, ref.Kind, obj.GetNamespace(), ref.Name), nil
122+
return forMetadata(gv.Group, ref.Kind, obj.GetNamespace(), ref.Name), nil
117123
}
118124

119-
func keyForMetadata(apiVersion, kind, namespace, name string) string {
120-
return apiVersion + "/" + kind + "/" + namespace + "/" + name
125+
func forMetadata(group, kind, namespace, name string) string {
126+
return group + "/" + kind + "/" + namespace + "/" + name
121127
}

pkg/sharding/key/key_suite_test.go

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 key_test
18+
19+
import (
20+
"testing"
21+
22+
. "github.com/onsi/ginkgo/v2"
23+
. "github.com/onsi/gomega"
24+
)
25+
26+
func TestKey(t *testing.T) {
27+
RegisterFailHandler(Fail)
28+
RunSpecs(t, "Sharding Key Suite")
29+
}

pkg/sharding/key/key_test.go

Lines changed: 185 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,185 @@
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 key_test
18+
19+
import (
20+
"reflect"
21+
22+
. "github.com/onsi/ginkgo/v2"
23+
. "github.com/onsi/gomega"
24+
"github.com/onsi/gomega/gcustom"
25+
gomegatypes "github.com/onsi/gomega/types"
26+
appsv1 "k8s.io/api/apps/v1"
27+
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
28+
"k8s.io/utils/ptr"
29+
30+
shardingv1alpha1 "github.com/timebertt/kubernetes-controller-sharding/pkg/apis/sharding/v1alpha1"
31+
. "github.com/timebertt/kubernetes-controller-sharding/pkg/sharding/key"
32+
)
33+
34+
var _ = Describe("#FuncForResource", func() {
35+
var controllerRing *shardingv1alpha1.ControllerRing
36+
37+
BeforeEach(func() {
38+
controllerRing = &shardingv1alpha1.ControllerRing{
39+
Spec: shardingv1alpha1.ControllerRingSpec{
40+
Resources: []shardingv1alpha1.RingResource{
41+
{
42+
GroupResource: metav1.GroupResource{
43+
Group: "operator",
44+
Resource: "foo",
45+
},
46+
ControlledResources: []metav1.GroupResource{
47+
{
48+
Group: "operator",
49+
Resource: "controlled",
50+
},
51+
{
52+
Resource: "foo",
53+
},
54+
},
55+
},
56+
{
57+
GroupResource: metav1.GroupResource{
58+
Resource: "foo",
59+
},
60+
},
61+
},
62+
},
63+
}
64+
})
65+
66+
It("should return an error if the resource is not part of the ring", func() {
67+
Expect(FuncForResource(metav1.GroupResource{
68+
Resource: "bar",
69+
}, controllerRing)).Error().To(
70+
MatchError(ContainSubstring("not found")),
71+
)
72+
})
73+
74+
It("should return ForObject if the resource is a main resource of the ring", func() {
75+
Expect(FuncForResource(metav1.GroupResource{
76+
Group: "operator",
77+
Resource: "foo",
78+
}, controllerRing)).To(
79+
beFunc(ForObject),
80+
)
81+
})
82+
83+
It("should return ForController if the resource is a controlled resource of the ring", func() {
84+
Expect(FuncForResource(metav1.GroupResource{
85+
Group: "operator",
86+
Resource: "controlled",
87+
}, controllerRing)).To(
88+
beFunc(ForController),
89+
)
90+
})
91+
92+
It("should return ForObject if the resource is a main and controlled resource of the ring", func() {
93+
Expect(FuncForResource(metav1.GroupResource{
94+
Resource: "foo",
95+
}, controllerRing)).To(
96+
beFunc(ForObject),
97+
)
98+
})
99+
})
100+
101+
var _ = Describe("#ForObject", func() {
102+
var obj *appsv1.Deployment
103+
104+
BeforeEach(func() {
105+
obj = &appsv1.Deployment{}
106+
obj.GetObjectKind().SetGroupVersionKind(appsv1.SchemeGroupVersion.WithKind("Deployment"))
107+
obj.Name = "foo"
108+
obj.Namespace = "bar"
109+
})
110+
111+
It("should return an error if the object has no TypeMeta", func() {
112+
Expect(ForObject(&appsv1.Deployment{})).Error().To(MatchError("apiVersion and kind must not be empty"))
113+
})
114+
115+
It("should return an error if the object has no name", func() {
116+
obj.Name = ""
117+
Expect(ForObject(obj)).Error().To(MatchError("name must not be empty"))
118+
119+
obj.GenerateName = "foo-"
120+
Expect(ForObject(obj)).Error().To(MatchError(ContainSubstring("generateName is not supported")))
121+
})
122+
123+
It("should return the object's hash key", func() {
124+
Expect(ForObject(obj)).To(Equal("apps/Deployment/bar/foo"))
125+
})
126+
})
127+
128+
var _ = Describe("#ForController", func() {
129+
var obj *appsv1.Deployment
130+
131+
BeforeEach(func() {
132+
obj = &appsv1.Deployment{}
133+
obj.SetOwnerReferences([]metav1.OwnerReference{
134+
{
135+
APIVersion: "other/v1",
136+
Kind: "Bar",
137+
Name: "owner-but-not-controller",
138+
},
139+
{
140+
APIVersion: "operator/v1",
141+
Kind: "Foo",
142+
Name: "foo",
143+
Controller: ptr.To(true),
144+
},
145+
})
146+
obj.Namespace = "bar"
147+
})
148+
149+
It("should return an empty key if the object has no controller ref", func() {
150+
Expect(ForController(&appsv1.Deployment{})).To(BeEmpty())
151+
152+
obj.OwnerReferences[1].Controller = nil
153+
Expect(ForController(obj)).To(BeEmpty())
154+
})
155+
156+
It("should return an error if the controller ref has no apiVersion", func() {
157+
obj.OwnerReferences[1].APIVersion = ""
158+
Expect(ForController(obj)).Error().To(MatchError("apiVersion of controller reference must not be empty"))
159+
})
160+
161+
It("should return an error if the controller ref has no kind", func() {
162+
obj.OwnerReferences[1].Kind = ""
163+
Expect(ForController(obj)).Error().To(MatchError("kind of controller reference must not be empty"))
164+
})
165+
166+
It("should return an error if the controller ref has no name", func() {
167+
obj.OwnerReferences[1].Name = ""
168+
Expect(ForController(obj)).Error().To(MatchError("name of controller reference must not be empty"))
169+
})
170+
171+
It("should return an error if the controller ref has an invalid apiVersion", func() {
172+
obj.OwnerReferences[1].APIVersion = "foo/bar/v1"
173+
Expect(ForController(obj)).Error().To(MatchError(ContainSubstring("invalid apiVersion of controller reference")))
174+
})
175+
176+
It("should return the controller's hash key", func() {
177+
Expect(ForController(obj)).To(Equal("operator/Foo/bar/foo"))
178+
})
179+
})
180+
181+
func beFunc(expected Func) gomegatypes.GomegaMatcher {
182+
return gcustom.MakeMatcher(func(actual Func) (bool, error) {
183+
return reflect.ValueOf(expected).Pointer() == reflect.ValueOf(actual).Pointer(), nil
184+
})
185+
}

0 commit comments

Comments
 (0)