Skip to content

Commit e399d2b

Browse files
authored
Add integration test for sharder webhook (#489)
* Prefactor: make `WebhookConfigForControllerRing` reusable * Simplify existing integration tests * Add integration test for `sharder` webhook
1 parent 342a97a commit e399d2b

File tree

11 files changed

+508
-37
lines changed

11 files changed

+508
-37
lines changed

.golangci.yaml

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -75,3 +75,7 @@ issues:
7575
- linters:
7676
- nolintlint
7777
text: "should be written without leading space"
78+
79+
exclude:
80+
# ginkgolinter false positive: https://github.com/nunnatsa/ginkgolinter/issues/190
81+
- the MatchError matcher used to assert a non error type \(ctx\)

pkg/controller/controllerring/reconciler.go

Lines changed: 16 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,6 @@ import (
2020
"context"
2121
"fmt"
2222
"maps"
23-
"path"
2423
"strings"
2524

2625
"github.com/go-logr/logr"
@@ -150,7 +149,16 @@ func (r *Reconciler) reconcileWebhooks(ctx context.Context, controllerRing *shar
150149
}
151150

152151
func (r *Reconciler) WebhookConfigForControllerRing(controllerRing *shardingv1alpha1.ControllerRing) (*admissionregistrationv1.MutatingWebhookConfiguration, error) {
153-
webhookConfig := &admissionregistrationv1.MutatingWebhookConfiguration{
152+
webhookConfig := WebhookConfigForControllerRing(controllerRing, r.Config.Webhook.Config)
153+
if err := controllerutil.SetControllerReference(controllerRing, webhookConfig, r.Client.Scheme()); err != nil {
154+
return nil, fmt.Errorf("error setting controller reference: %w", err)
155+
}
156+
157+
return webhookConfig, nil
158+
}
159+
160+
func WebhookConfigForControllerRing(controllerRing *shardingv1alpha1.ControllerRing, config *configv1alpha1.WebhookConfig) *admissionregistrationv1.MutatingWebhookConfiguration {
161+
return &admissionregistrationv1.MutatingWebhookConfiguration{
154162
TypeMeta: metav1.TypeMeta{
155163
APIVersion: admissionregistrationv1.SchemeGroupVersion.String(),
156164
Kind: "MutatingWebhookConfiguration",
@@ -161,22 +169,17 @@ func (r *Reconciler) WebhookConfigForControllerRing(controllerRing *shardingv1al
161169
"app.kubernetes.io/name": shardingv1alpha1.AppControllerSharding,
162170
shardingv1alpha1.LabelControllerRing: controllerRing.Name,
163171
},
164-
Annotations: maps.Clone(r.Config.Webhook.Config.Annotations),
172+
Annotations: maps.Clone(config.Annotations),
165173
},
166-
Webhooks: []admissionregistrationv1.MutatingWebhook{r.WebhookForControllerRing(controllerRing)},
167-
}
168-
if err := controllerutil.SetControllerReference(controllerRing, webhookConfig, r.Client.Scheme()); err != nil {
169-
return nil, fmt.Errorf("error setting controller reference: %w", err)
174+
Webhooks: []admissionregistrationv1.MutatingWebhook{WebhookForControllerRing(controllerRing, config)},
170175
}
171-
172-
return webhookConfig, nil
173176
}
174177

175-
func (r *Reconciler) WebhookForControllerRing(controllerRing *shardingv1alpha1.ControllerRing) admissionregistrationv1.MutatingWebhook {
178+
func WebhookForControllerRing(controllerRing *shardingv1alpha1.ControllerRing, config *configv1alpha1.WebhookConfig) admissionregistrationv1.MutatingWebhook {
176179
webhook := admissionregistrationv1.MutatingWebhook{
177180
Name: "sharder.sharding.timebertt.dev",
178-
ClientConfig: *r.Config.Webhook.Config.ClientConfig.DeepCopy(),
179-
NamespaceSelector: r.Config.Webhook.Config.NamespaceSelector.DeepCopy(),
181+
ClientConfig: *config.ClientConfig.DeepCopy(),
182+
NamespaceSelector: config.NamespaceSelector.DeepCopy(),
180183

181184
// only process unassigned objects
182185
ObjectSelector: &metav1.LabelSelector{
@@ -203,7 +206,7 @@ func (r *Reconciler) WebhookForControllerRing(controllerRing *shardingv1alpha1.C
203206
webhookPath := sharder.WebhookPathForControllerRing(controllerRing)
204207

205208
if service := webhook.ClientConfig.Service; service != nil {
206-
service.Path = ptr.To(path.Join(ptr.Deref(service.Path, ""), webhookPath))
209+
service.Path = ptr.To(webhookPath)
207210
}
208211
if url := webhook.ClientConfig.URL; url != nil {
209212
// We can't use path.Join on URLs because it will drop one slash from the scheme.

pkg/controller/controllerring/reconciler_test.go

Lines changed: 11 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -180,23 +180,23 @@ var _ = Describe("Reconciler", func() {
180180

181181
Describe("#WebhookForControllerRing", func() {
182182
It("should have the correct settings", func() {
183-
Expect(r.WebhookForControllerRing(ring)).To(MatchFields(IgnoreExtras, Fields{
183+
Expect(WebhookForControllerRing(ring, config.Webhook.Config)).To(MatchFields(IgnoreExtras, Fields{
184184
"Name": Equal("sharder.sharding.timebertt.dev"),
185185
"SideEffects": PointTo(Equal(admissionregistrationv1.SideEffectClassNone)),
186186
"AdmissionReviewVersions": ConsistOf("v1"),
187187
}))
188188
})
189189

190190
It("should have non-problematic failure settings", func() {
191-
Expect(r.WebhookForControllerRing(ring)).To(MatchFields(IgnoreExtras, Fields{
191+
Expect(WebhookForControllerRing(ring, config.Webhook.Config)).To(MatchFields(IgnoreExtras, Fields{
192192
"FailurePolicy": PointTo(Equal(admissionregistrationv1.Ignore)),
193193
"TimeoutSeconds": PointTo(BeEquivalentTo(5)),
194194
}))
195195
})
196196

197197
Context("client config", func() {
198198
It("should use the config's default client config and add the path", func() {
199-
Expect(r.WebhookForControllerRing(ring).ClientConfig).To(Equal(admissionregistrationv1.WebhookClientConfig{
199+
Expect(WebhookForControllerRing(ring, config.Webhook.Config).ClientConfig).To(Equal(admissionregistrationv1.WebhookClientConfig{
200200
Service: &admissionregistrationv1.ServiceReference{
201201
Namespace: "sharding-system",
202202
Name: "sharder",
@@ -215,7 +215,7 @@ var _ = Describe("Reconciler", func() {
215215
}
216216
clientConfig := config.Webhook.Config.ClientConfig.DeepCopy()
217217

218-
Expect(r.WebhookForControllerRing(ring).ClientConfig).To(Equal(admissionregistrationv1.WebhookClientConfig{
218+
Expect(WebhookForControllerRing(ring, config.Webhook.Config).ClientConfig).To(Equal(admissionregistrationv1.WebhookClientConfig{
219219
Service: &admissionregistrationv1.ServiceReference{
220220
Namespace: clientConfig.Service.Namespace,
221221
Name: clientConfig.Service.Name,
@@ -230,7 +230,7 @@ var _ = Describe("Reconciler", func() {
230230
URL: ptr.To("https://example.com/webhook"),
231231
}
232232

233-
Expect(r.WebhookForControllerRing(ring).ClientConfig).To(Equal(admissionregistrationv1.WebhookClientConfig{
233+
Expect(WebhookForControllerRing(ring, config.Webhook.Config).ClientConfig).To(Equal(admissionregistrationv1.WebhookClientConfig{
234234
URL: ptr.To("https://example.com/webhook/webhooks/sharder/controllerring/foo"),
235235
}))
236236
})
@@ -240,15 +240,15 @@ var _ = Describe("Reconciler", func() {
240240
URL: ptr.To("https://example.com/"),
241241
}
242242

243-
Expect(r.WebhookForControllerRing(ring).ClientConfig).To(Equal(admissionregistrationv1.WebhookClientConfig{
243+
Expect(WebhookForControllerRing(ring, config.Webhook.Config).ClientConfig).To(Equal(admissionregistrationv1.WebhookClientConfig{
244244
URL: ptr.To("https://example.com/webhooks/sharder/controllerring/foo"),
245245
}))
246246
})
247247
})
248248

249249
Context("namespace selector", func() {
250250
It("should use the config's default namespace selector", func() {
251-
Expect(r.WebhookForControllerRing(ring).NamespaceSelector).To(Equal(&metav1.LabelSelector{
251+
Expect(WebhookForControllerRing(ring, config.Webhook.Config).NamespaceSelector).To(Equal(&metav1.LabelSelector{
252252
MatchExpressions: []metav1.LabelSelectorRequirement{{
253253
Key: corev1.LabelMetadataName,
254254
Operator: metav1.LabelSelectorOpNotIn,
@@ -263,7 +263,7 @@ var _ = Describe("Reconciler", func() {
263263
}
264264
namespaceSelector := config.Webhook.Config.NamespaceSelector.DeepCopy()
265265

266-
Expect(r.WebhookForControllerRing(ring).NamespaceSelector).To(Equal(namespaceSelector))
266+
Expect(WebhookForControllerRing(ring, config.Webhook.Config).NamespaceSelector).To(Equal(namespaceSelector))
267267
})
268268

269269
It("should use the ControllerRing's namespace selector", func() {
@@ -272,12 +272,12 @@ var _ = Describe("Reconciler", func() {
272272
}
273273
namespaceSelector := ring.Spec.NamespaceSelector.DeepCopy()
274274

275-
Expect(r.WebhookForControllerRing(ring).NamespaceSelector).To(Equal(namespaceSelector))
275+
Expect(WebhookForControllerRing(ring, config.Webhook.Config).NamespaceSelector).To(Equal(namespaceSelector))
276276
})
277277
})
278278

279279
It("should only select unassigned objects", func() {
280-
selector, err := metav1.LabelSelectorAsSelector(r.WebhookForControllerRing(ring).ObjectSelector)
280+
selector, err := metav1.LabelSelectorAsSelector(WebhookForControllerRing(ring, config.Webhook.Config).ObjectSelector)
281281
Expect(err).NotTo(HaveOccurred())
282282

283283
Expect(selector.Matches(labels.Set{})).To(BeTrue())
@@ -295,7 +295,7 @@ var _ = Describe("Reconciler", func() {
295295
},
296296
}
297297

298-
Expect(r.WebhookForControllerRing(ring).Rules).To(ConsistOf(
298+
Expect(WebhookForControllerRing(ring, config.Webhook.Config).Rules).To(ConsistOf(
299299
RuleForResource(ring.Spec.Resources[0].GroupResource),
300300
RuleForResource(ring.Spec.Resources[1].GroupResource),
301301
RuleForResource(ring.Spec.Resources[1].ControlledResources[0]),

pkg/sharding/key/key.go

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -117,8 +117,6 @@ func ForController(obj client.Object) (string, error) {
117117
return "", fmt.Errorf("invalid apiVersion of controller reference: %w", err)
118118
}
119119

120-
// Namespace can be empty for cluster-scoped resources. Only check the other fields as an optimistic check for
121-
// preventing wrong usage of the function.
122120
return forMetadata(gv.Group, ref.Kind, obj.GetNamespace(), ref.Name), nil
123121
}
124122

pkg/utils/test/envtest.go

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
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 test
18+
19+
import (
20+
"os"
21+
"strings"
22+
)
23+
24+
// UseExistingCluster reads the `USE_EXISTING_CLUSTER` env var similar to what envtest does, though exported.
25+
func UseExistingCluster() bool {
26+
return strings.ToLower(os.Getenv("USE_EXISTING_CLUSTER")) == "true"
27+
}

pkg/webhook/sharder/handler.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -72,7 +72,7 @@ func (h *Handler) Handle(ctx context.Context, req admission.Request) admission.R
7272
Resource: req.Resource.Resource,
7373
}, controllerRing)
7474
if err != nil {
75-
return admission.Errored(http.StatusBadRequest, fmt.Errorf("error deteriming hash key func for object: %w", err))
75+
return admission.Errored(http.StatusBadRequest, fmt.Errorf("error determining hash key func for object: %w", err))
7676
}
7777

7878
hashKey, err := keyFunc(obj)

test/integration/sharder/controller/controllerring/controllerring_test.go

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -51,7 +51,6 @@ var _ = Describe("ControllerRing controller", func() {
5151
GroupResource: metav1.GroupResource{Group: "apps", Resource: "deployments"},
5252
},
5353
},
54-
NamespaceSelector: nil,
5554
},
5655
}
5756

test/integration/sharder/controller/shardlease/shardlease_suite_test.go

Lines changed: 0 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -37,7 +37,6 @@ import (
3737
"sigs.k8s.io/controller-runtime/pkg/manager"
3838
metricsserver "sigs.k8s.io/controller-runtime/pkg/metrics/server"
3939

40-
configv1alpha1 "github.com/timebertt/kubernetes-controller-sharding/pkg/apis/config/v1alpha1"
4140
shardingv1alpha1 "github.com/timebertt/kubernetes-controller-sharding/pkg/apis/sharding/v1alpha1"
4241
"github.com/timebertt/kubernetes-controller-sharding/pkg/controller/shardlease"
4342
utilclient "github.com/timebertt/kubernetes-controller-sharding/pkg/utils/client"
@@ -127,9 +126,6 @@ var _ = BeforeSuite(func(ctx SpecContext) {
127126
Expect(err).NotTo(HaveOccurred())
128127

129128
By("Register controller")
130-
config := &configv1alpha1.SharderConfig{}
131-
mgr.GetScheme().Default(config)
132-
133129
clock = testclock.NewFakeClock(time.Now())
134130

135131
Expect((&shardlease.Reconciler{

test/integration/sharder/controller/shardlease/shardlease_test.go

Lines changed: 0 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -51,7 +51,6 @@ var _ = Describe("Shard Lease controller", func() {
5151
GroupResource: metav1.GroupResource{Group: "apps", Resource: "deployments"},
5252
},
5353
},
54-
NamespaceSelector: nil,
5554
},
5655
}
5756

@@ -81,10 +80,6 @@ var _ = Describe("Shard Lease controller", func() {
8180

8281
Expect(testClient.Create(ctx, lease)).To(Succeed())
8382
log.Info("Created Lease for test", "leaseName", lease.Name)
84-
85-
DeferCleanup(func(ctx SpecContext) {
86-
Expect(testClient.Delete(ctx, lease)).To(Or(Succeed(), BeNotFoundError()))
87-
}, NodeTimeout(time.Minute))
8883
}, NodeTimeout(time.Minute), OncePerOrdered)
8984

9085
Describe("should reflect the shard state in the label", Ordered, func() {

0 commit comments

Comments
 (0)