Skip to content

Commit ac4578a

Browse files
Add webhook to validate PrefixSet resource
1 parent efddda4 commit ac4578a

File tree

7 files changed

+447
-9
lines changed

7 files changed

+447
-9
lines changed

PROJECT

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -195,4 +195,7 @@ resources:
195195
kind: PrefixSet
196196
path: github.com/ironcore-dev/network-operator/api/core/v1alpha1
197197
version: v1alpha1
198+
webhooks:
199+
validation: true
200+
webhookVersion: v1
198201
version: "3"

charts/network-operator/templates/webhook/webhooks.yaml

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,26 @@ webhooks:
3131
- v1alpha1
3232
resources:
3333
- interfaces
34+
- name: prefixset-v1alpha1.kb.io
35+
clientConfig:
36+
service:
37+
name: network-operator-webhook-service
38+
namespace: {{ .Release.Namespace }}
39+
path: /validate-networking-metal-ironcore-dev-v1alpha1-prefixset
40+
failurePolicy: Fail
41+
sideEffects: None
42+
admissionReviewVersions:
43+
- v1
44+
rules:
45+
- operations:
46+
- CREATE
47+
- UPDATE
48+
apiGroups:
49+
- networking.metal.ironcore.dev
50+
apiVersions:
51+
- v1alpha1
52+
resources:
53+
- prefixsets
3454
- name: vrf-v1alpha1.kb.io
3555
clientConfig:
3656
service:

cmd/main.go

Lines changed: 17 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -438,6 +438,17 @@ func main() {
438438
os.Exit(1)
439439
}
440440

441+
if err := (&corecontroller.EVPNInstanceReconciler{
442+
Client: mgr.GetClient(),
443+
Scheme: mgr.GetScheme(),
444+
Recorder: mgr.GetEventRecorderFor("evpn-instance-controller"),
445+
WatchFilterValue: watchFilterValue,
446+
Provider: prov,
447+
}).SetupWithManager(ctx, mgr); err != nil {
448+
setupLog.Error(err, "unable to create controller", "controller", "EVPNInstance")
449+
os.Exit(1)
450+
}
451+
441452
if err := (&corecontroller.PrefixSetReconciler{
442453
Client: mgr.GetClient(),
443454
Scheme: mgr.GetScheme(),
@@ -462,15 +473,12 @@ func main() {
462473
os.Exit(1)
463474
}
464475
}
465-
if err := (&corecontroller.EVPNInstanceReconciler{
466-
Client: mgr.GetClient(),
467-
Scheme: mgr.GetScheme(),
468-
Recorder: mgr.GetEventRecorderFor("evpn-instance-controller"),
469-
WatchFilterValue: watchFilterValue,
470-
Provider: prov,
471-
}).SetupWithManager(ctx, mgr); err != nil {
472-
setupLog.Error(err, "unable to create controller", "controller", "EVPNInstance")
473-
os.Exit(1)
476+
477+
if os.Getenv("ENABLE_WEBHOOKS") != "false" {
478+
if err := webhookv1alpha1.SetupPrefixSetWebhookWithManager(mgr); err != nil {
479+
setupLog.Error(err, "unable to create webhook", "webhook", "PrefixSet")
480+
os.Exit(1)
481+
}
474482
}
475483
// +kubebuilder:scaffold:builder
476484

config/webhook/manifests.yaml

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,26 @@ webhooks:
2424
resources:
2525
- interfaces
2626
sideEffects: None
27+
- admissionReviewVersions:
28+
- v1
29+
clientConfig:
30+
service:
31+
name: webhook-service
32+
namespace: system
33+
path: /validate-networking-metal-ironcore-dev-v1alpha1-prefixset
34+
failurePolicy: Fail
35+
name: prefixset-v1alpha1.kb.io
36+
rules:
37+
- apiGroups:
38+
- networking.metal.ironcore.dev
39+
apiVersions:
40+
- v1alpha1
41+
operations:
42+
- CREATE
43+
- UPDATE
44+
resources:
45+
- prefixsets
46+
sideEffects: None
2747
- admissionReviewVersions:
2848
- v1
2949
clientConfig:
Lines changed: 114 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,114 @@
1+
// SPDX-FileCopyrightText: 2025 SAP SE or an SAP affiliate company and IronCore contributors
2+
// SPDX-License-Identifier: Apache-2.0
3+
4+
package v1alpha1
5+
6+
import (
7+
"context"
8+
"errors"
9+
"fmt"
10+
11+
"k8s.io/apimachinery/pkg/runtime"
12+
ctrl "sigs.k8s.io/controller-runtime"
13+
logf "sigs.k8s.io/controller-runtime/pkg/log"
14+
"sigs.k8s.io/controller-runtime/pkg/webhook"
15+
"sigs.k8s.io/controller-runtime/pkg/webhook/admission"
16+
17+
"github.com/ironcore-dev/network-operator/api/core/v1alpha1"
18+
)
19+
20+
// log is for logging in this package.
21+
var prefixsetlog = logf.Log.WithName("prefixset-resource")
22+
23+
// SetupPrefixSetWebhookWithManager registers the webhook for PrefixSets in the manager.
24+
func SetupPrefixSetWebhookWithManager(mgr ctrl.Manager) error {
25+
return ctrl.NewWebhookManagedBy(mgr).
26+
For(&v1alpha1.PrefixSet{}).
27+
WithValidator(&PrefixSetCustomValidator{}).
28+
Complete()
29+
}
30+
31+
// +kubebuilder:webhook:path=/validate-networking-metal-ironcore-dev-v1alpha1-prefixset,mutating=false,failurePolicy=Fail,sideEffects=None,groups=networking.metal.ironcore.dev,resources=prefixsets,verbs=create;update,versions=v1alpha1,name=prefixset-v1alpha1.kb.io,admissionReviewVersions=v1
32+
33+
// PrefixSetCustomValidator struct is responsible for validating the PrefixSet resource
34+
// when it is created, updated, or deleted.
35+
type PrefixSetCustomValidator struct{}
36+
37+
var _ webhook.CustomValidator = &PrefixSetCustomValidator{}
38+
39+
// ValidateCreate implements webhook.CustomValidator so a webhook will be registered for the type PrefixSet.
40+
func (v *PrefixSetCustomValidator) ValidateCreate(_ context.Context, obj runtime.Object) (admission.Warnings, error) {
41+
ps, ok := obj.(*v1alpha1.PrefixSet)
42+
if !ok {
43+
return nil, fmt.Errorf("expected a PrefixSets object but got %T", obj)
44+
}
45+
46+
prefixsetlog.Info("Validation for PrefixSets upon creation", "name", ps.GetName())
47+
48+
return nil, validatePrefixSetSpec(ps)
49+
}
50+
51+
// ValidateUpdate implements webhook.CustomValidator so a webhook will be registered for the type PrefixSet.
52+
func (v *PrefixSetCustomValidator) ValidateUpdate(_ context.Context, oldObj, newObj runtime.Object) (admission.Warnings, error) {
53+
prev, ok := oldObj.(*v1alpha1.PrefixSet)
54+
if !ok {
55+
return nil, fmt.Errorf("expected a PrefixSets object for the oldObj but got %T", oldObj)
56+
}
57+
58+
curr, ok := newObj.(*v1alpha1.PrefixSet)
59+
if !ok {
60+
return nil, fmt.Errorf("expected a PrefixSets object for the newObj but got %T", newObj)
61+
}
62+
63+
prefixsetlog.Info("Validation for PrefixSets upon update", "name", curr.GetName())
64+
65+
if err := validatePrefixSetSpec(curr); err != nil {
66+
return nil, err
67+
}
68+
69+
if len(prev.Spec.Entries) > 0 && prev.Is6() != curr.Is6() {
70+
return nil, errors.New("cannot change IP family of a PrefixSet once created")
71+
}
72+
73+
return nil, nil
74+
}
75+
76+
// ValidateDelete implements webhook.CustomValidator so a webhook will be registered for the type PrefixSet.
77+
func (v *PrefixSetCustomValidator) ValidateDelete(ctx context.Context, obj runtime.Object) (admission.Warnings, error) {
78+
return nil, nil
79+
}
80+
81+
// validatePrefixSetSpec performs validation on the PrefixSet spec.
82+
func validatePrefixSetSpec(ps *v1alpha1.PrefixSet) error {
83+
var is6 bool
84+
var errAgg []error
85+
for i, ent := range ps.Spec.Entries {
86+
if i > 0 && ent.Prefix.Addr().Is6() != is6 {
87+
errAgg = append(errAgg, errors.New("all prefixes in a PrefixSet must be of the same IP family"))
88+
}
89+
is6 = ent.Prefix.Addr().Is6()
90+
if ent.MaskLengthRange != nil {
91+
bits := ent.Prefix.Bits()
92+
rmin := int(ent.MaskLengthRange.Min)
93+
rmax := int(ent.MaskLengthRange.Max)
94+
95+
if rmin < bits {
96+
errAgg = append(errAgg, fmt.Errorf("entry %d: mask length range min %d is invalid for prefix %s", i, rmin, ent.Prefix.String()))
97+
}
98+
if rmax < bits {
99+
errAgg = append(errAgg, fmt.Errorf("entry %d: mask length range max %d is invalid for prefix %s", i, rmax, ent.Prefix.String()))
100+
}
101+
if rmin > rmax {
102+
errAgg = append(errAgg, fmt.Errorf("entry %d: mask length range min %d cannot be greater than max %d", i, rmin, rmax))
103+
}
104+
const maxBits = 32
105+
if !is6 && rmin > maxBits {
106+
errAgg = append(errAgg, fmt.Errorf("entry %d: mask length range min %d exceeds maximum %d bits for IPv4", i, rmin, maxBits))
107+
}
108+
if !is6 && rmax > maxBits {
109+
errAgg = append(errAgg, fmt.Errorf("entry %d: mask length range max %d exceeds maximum %d bits for IPv4", i, rmax, maxBits))
110+
}
111+
}
112+
}
113+
return errors.Join(errAgg...)
114+
}

0 commit comments

Comments
 (0)