Skip to content

Commit bb30d1b

Browse files
Add validation webhook for the IPv4 spec of Interface resource
1 parent 2103346 commit bb30d1b

File tree

10 files changed

+250
-42
lines changed

10 files changed

+250
-42
lines changed

PROJECT

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,9 @@ resources:
1919
kind: Interface
2020
path: github.com/ironcore-dev/network-operator/api/core/v1alpha1
2121
version: v1alpha1
22+
webhooks:
23+
validation: true
24+
webhookVersion: v1
2225
- api:
2326
crdVersion: v1
2427
namespaced: true

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

Lines changed: 21 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,12 +11,32 @@ metadata:
1111
labels:
1212
{{- include "chart.labels" . | nindent 4 }}
1313
webhooks:
14+
- name: interface-v1alpha1.kb.io
15+
clientConfig:
16+
service:
17+
name: network-operator-webhook-service
18+
namespace: {{ .Release.Namespace }}
19+
path: /validate-networking-metal-ironcore-dev-v1alpha1-interface
20+
failurePolicy: Fail
21+
sideEffects: None
22+
admissionReviewVersions:
23+
- v1
24+
rules:
25+
- operations:
26+
- CREATE
27+
- UPDATE
28+
apiGroups:
29+
- networking.metal.ironcore.dev
30+
apiVersions:
31+
- v1alpha1
32+
resources:
33+
- interfaces
1434
- name: vrf-v1alpha1.kb.io
1535
clientConfig:
1636
service:
1737
name: network-operator-webhook-service
1838
namespace: {{ .Release.Namespace }}
19-
path: /validate-networking.metal.ironcore.dev-v1alpha1-vrf
39+
path: /validate-networking-metal-ironcore-dev-v1alpha1-vrf
2040
failurePolicy: Fail
2141
sideEffects: None
2242
admissionReviewVersions:

cmd/main.go

Lines changed: 13 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -427,13 +427,6 @@ func main() {
427427
os.Exit(1)
428428
}
429429

430-
if os.Getenv("ENABLE_WEBHOOKS") != "false" {
431-
if err := webhookv1alpha1.SetupVRFWebhookWithManager(mgr); err != nil {
432-
setupLog.Error(err, "unable to create webhook", "webhook", "VRF")
433-
os.Exit(1)
434-
}
435-
}
436-
437430
if err := (&nxcontroller.SystemReconciler{
438431
Client: mgr.GetClient(),
439432
Scheme: mgr.GetScheme(),
@@ -445,6 +438,19 @@ func main() {
445438
os.Exit(1)
446439
}
447440

441+
if os.Getenv("ENABLE_WEBHOOKS") != "false" {
442+
if err := webhookv1alpha1.SetupVRFWebhookWithManager(mgr); err != nil {
443+
setupLog.Error(err, "unable to create webhook", "webhook", "VRF")
444+
os.Exit(1)
445+
}
446+
}
447+
448+
if os.Getenv("ENABLE_WEBHOOKS") != "false" {
449+
if err := webhookv1alpha1.SetupInterfaceWebhookWithManager(mgr); err != nil {
450+
setupLog.Error(err, "unable to create webhook", "webhook", "Interface")
451+
os.Exit(1)
452+
}
453+
}
448454
// +kubebuilder:scaffold:builder
449455

450456
if metricsCertWatcher != nil {

config/webhook/manifests.yaml

Lines changed: 21 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,27 @@ webhooks:
1010
service:
1111
name: webhook-service
1212
namespace: system
13-
path: /validate-networking.metal.ironcore.dev-v1alpha1-vrf
13+
path: /validate-networking-metal-ironcore-dev-v1alpha1-interface
14+
failurePolicy: Fail
15+
name: interface-v1alpha1.kb.io
16+
rules:
17+
- apiGroups:
18+
- networking.metal.ironcore.dev
19+
apiVersions:
20+
- v1alpha1
21+
operations:
22+
- CREATE
23+
- UPDATE
24+
resources:
25+
- interfaces
26+
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-vrf
1434
failurePolicy: Fail
1535
name: vrf-v1alpha1.kb.io
1636
rules:

internal/provider/cisco/nxos/provider.go

Lines changed: 0 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -427,7 +427,6 @@ func (p *Provider) EnsureInterface(ctx context.Context, req *provider.EnsureInte
427427
}
428428

429429
var addr *AddrItem
430-
var prefixes []netip.Prefix
431430
if req.IPv4 != nil {
432431
addr = new(AddrItem)
433432
addr.ID = name
@@ -436,7 +435,6 @@ func (p *Provider) EnsureInterface(ctx context.Context, req *provider.EnsureInte
436435
switch v := req.IPv4.(type) {
437436
case provider.IPv4AddressList:
438437
for i, p := range v {
439-
prefixes = append(prefixes, p)
440438
nth := IntfAddrTypePrimary
441439
if i > 0 {
442440
nth = IntfAddrTypeSecondary
@@ -445,9 +443,6 @@ func (p *Provider) EnsureInterface(ctx context.Context, req *provider.EnsureInte
445443
Addr: p.String(),
446444
Type: nth,
447445
}
448-
if p.Addr().Is6() {
449-
return fmt.Errorf("invalid ipv4 address %q: not an ipv4 address", p.String())
450-
}
451446
addr.AddrItems.AddrList.Set(ip)
452447
}
453448

@@ -459,14 +454,6 @@ func (p *Provider) EnsureInterface(ctx context.Context, req *provider.EnsureInte
459454
}
460455
}
461456

462-
for i, p := range prefixes {
463-
for j := i + 1; j < len(prefixes); j++ {
464-
if p.Overlaps(prefixes[j]) {
465-
return fmt.Errorf("overlapping IP prefixes: %s and %s", p, prefixes[j])
466-
}
467-
}
468-
}
469-
470457
if req.Interface.Spec.Type != v1alpha1.InterfaceTypeAggregate {
471458
del := make([]gnmiext.Configurable, 0, 2)
472459
addrs := new(AddrList)
Lines changed: 92 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,92 @@
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 interfacelog = logf.Log.WithName("interface-resource")
22+
23+
// SetupInterfaceWebhookWithManager registers the webhook for Interfaces in the manager.
24+
func SetupInterfaceWebhookWithManager(mgr ctrl.Manager) error {
25+
return ctrl.NewWebhookManagedBy(mgr).
26+
For(&v1alpha1.Interface{}).
27+
WithValidator(&InterfaceCustomValidator{}).
28+
Complete()
29+
}
30+
31+
// +kubebuilder:webhook:path=/validate-networking-metal-ironcore-dev-v1alpha1-interface,mutating=false,failurePolicy=Fail,sideEffects=None,groups=networking.metal.ironcore.dev,resources=interfaces,verbs=create;update,versions=v1alpha1,name=interface-v1alpha1.kb.io,admissionReviewVersions=v1
32+
33+
// InterfaceCustomValidator struct is responsible for validating the Interface resource
34+
// when it is created, updated, or deleted.
35+
type InterfaceCustomValidator struct{}
36+
37+
var _ webhook.CustomValidator = &InterfaceCustomValidator{}
38+
39+
// ValidateCreate implements webhook.CustomValidator so a webhook will be registered for the type Interface.
40+
func (v *InterfaceCustomValidator) ValidateCreate(_ context.Context, obj runtime.Object) (admission.Warnings, error) {
41+
intf, ok := obj.(*v1alpha1.Interface)
42+
if !ok {
43+
return nil, fmt.Errorf("expected a Interfaces object but got %T", obj)
44+
}
45+
46+
interfacelog.Info("Validation for Interfaces upon creation", "name", intf.GetName())
47+
48+
return nil, validateInterfaceSpec(intf)
49+
}
50+
51+
// ValidateUpdate implements webhook.CustomValidator so a webhook will be registered for the type Interface.
52+
func (v *InterfaceCustomValidator) ValidateUpdate(_ context.Context, oldObj, newObj runtime.Object) (admission.Warnings, error) {
53+
intf, ok := newObj.(*v1alpha1.Interface)
54+
if !ok {
55+
return nil, fmt.Errorf("expected a Interfaces object for the newObj but got %T", newObj)
56+
}
57+
58+
interfacelog.Info("Validation for Interfaces upon update", "name", intf.GetName())
59+
60+
return nil, validateInterfaceSpec(intf)
61+
}
62+
63+
// ValidateDelete implements webhook.CustomValidator so a webhook will be registered for the type Interface.
64+
func (v *InterfaceCustomValidator) ValidateDelete(ctx context.Context, obj runtime.Object) (admission.Warnings, error) {
65+
return nil, nil
66+
}
67+
68+
// validateInterfaceSpec performs validation on the Interface spec.
69+
func validateInterfaceSpec(intf *v1alpha1.Interface) error {
70+
if intf.Spec.IPv4 == nil {
71+
return nil
72+
}
73+
74+
return validateInterfaceIPv4(intf.Spec.IPv4)
75+
}
76+
77+
// validateInterfaceIPv4 performs validation on the InterfaceIPv4 spec.
78+
func validateInterfaceIPv4(ip *v1alpha1.InterfaceIPv4) error {
79+
var errAgg []error
80+
for i, cidr := range ip.Addresses {
81+
if cidr.Prefix.Addr().Is6() {
82+
errAgg = append(errAgg, fmt.Errorf("invalid IPv4 address %q: address is IPv6", cidr.String()))
83+
continue
84+
}
85+
for j := i + 1; j < len(ip.Addresses); j++ {
86+
if p := ip.Addresses[j].Prefix; cidr.Overlaps(p) {
87+
errAgg = append(errAgg, fmt.Errorf("invalid IPv4 address %q: overlaps with %q", cidr.String(), p.String()))
88+
}
89+
}
90+
}
91+
return errors.Join(errAgg...)
92+
}
Lines changed: 94 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,94 @@
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+
"net/netip"
9+
10+
. "github.com/onsi/ginkgo/v2"
11+
. "github.com/onsi/gomega"
12+
13+
"github.com/ironcore-dev/network-operator/api/core/v1alpha1"
14+
)
15+
16+
var _ = Describe("Interface Webhook", func() {
17+
var (
18+
obj *v1alpha1.Interface
19+
oldObj *v1alpha1.Interface
20+
validator InterfaceCustomValidator
21+
)
22+
23+
BeforeEach(func() {
24+
obj = &v1alpha1.Interface{
25+
Spec: v1alpha1.InterfaceSpec{
26+
DeviceRef: v1alpha1.LocalObjectReference{Name: "test-device"},
27+
Name: "test-interface",
28+
AdminState: v1alpha1.AdminStateUp,
29+
Type: v1alpha1.InterfaceTypeLoopback,
30+
},
31+
}
32+
oldObj = &v1alpha1.Interface{
33+
Spec: v1alpha1.InterfaceSpec{
34+
DeviceRef: v1alpha1.LocalObjectReference{Name: "test-device"},
35+
Name: "test-interface",
36+
AdminState: v1alpha1.AdminStateUp,
37+
Type: v1alpha1.InterfaceTypeLoopback,
38+
},
39+
}
40+
validator = InterfaceCustomValidator{}
41+
Expect(validator).NotTo(BeNil(), "Expected validator to be initialized")
42+
Expect(oldObj).NotTo(BeNil(), "Expected oldObj to be initialized")
43+
Expect(obj).NotTo(BeNil(), "Expected obj to be initialized")
44+
})
45+
46+
Context("When creating or updating Interfaces under Validating Webhook", func() {
47+
It("Should allow valid IPv4 addresses", func() {
48+
obj.Spec.IPv4 = &v1alpha1.InterfaceIPv4{
49+
Addresses: []v1alpha1.IPPrefix{
50+
{Prefix: netip.MustParsePrefix("10.0.0.1/32")},
51+
{Prefix: netip.MustParsePrefix("10.0.1.1/32")},
52+
},
53+
}
54+
_, err := validator.ValidateCreate(context.Background(), obj)
55+
Expect(err).NotTo(HaveOccurred())
56+
})
57+
58+
It("Should reject IPv6 addresses in IPv4 field", func() {
59+
obj.Spec.IPv4 = &v1alpha1.InterfaceIPv4{
60+
Addresses: []v1alpha1.IPPrefix{
61+
{Prefix: netip.MustParsePrefix("2001:db8::1/128")},
62+
},
63+
}
64+
_, err := validator.ValidateCreate(context.Background(), obj)
65+
Expect(err).To(HaveOccurred())
66+
Expect(err.Error()).To(ContainSubstring("invalid IPv4 address"))
67+
Expect(err.Error()).To(ContainSubstring("address is IPv6"))
68+
})
69+
70+
It("Should reject overlapping IPv4 addresses", func() {
71+
obj.Spec.IPv4 = &v1alpha1.InterfaceIPv4{
72+
Addresses: []v1alpha1.IPPrefix{
73+
{Prefix: netip.MustParsePrefix("10.0.0.0/24")},
74+
{Prefix: netip.MustParsePrefix("10.0.0.128/25")},
75+
},
76+
}
77+
_, err := validator.ValidateCreate(context.Background(), obj)
78+
Expect(err).To(HaveOccurred())
79+
Expect(err.Error()).To(ContainSubstring("overlaps with"))
80+
})
81+
82+
It("Should reject updates with invalid IPv4 addresses", func() {
83+
obj.Spec.IPv4 = &v1alpha1.InterfaceIPv4{
84+
Addresses: []v1alpha1.IPPrefix{
85+
{Prefix: netip.MustParsePrefix("10.0.0.0/24")},
86+
{Prefix: netip.MustParsePrefix("10.0.0.128/25")},
87+
},
88+
}
89+
_, err := validator.ValidateUpdate(context.Background(), oldObj, obj)
90+
Expect(err).To(HaveOccurred())
91+
Expect(err.Error()).To(ContainSubstring("overlaps with"))
92+
})
93+
})
94+
})

internal/webhook/core/v1alpha1/vrf_webhook.go

Lines changed: 3 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -25,12 +25,13 @@ var vrflog = logf.Log.WithName("vrf-resource")
2525

2626
// SetupVRFWebhookWithManager registers the webhook for VRF in the manager.
2727
func SetupVRFWebhookWithManager(mgr ctrl.Manager) error {
28-
return ctrl.NewWebhookManagedBy(mgr).For(&v1alpha1.VRF{}).
28+
return ctrl.NewWebhookManagedBy(mgr).
29+
For(&v1alpha1.VRF{}).
2930
WithValidator(&VRFCustomValidator{}).
3031
Complete()
3132
}
3233

33-
// +kubebuilder:webhook:path=/validate-networking.metal.ironcore.dev-v1alpha1-vrf,mutating=false,failurePolicy=Fail,sideEffects=None,groups=networking.metal.ironcore.dev,resources=vrfs,verbs=create;update,versions=v1alpha1,name=vrf-v1alpha1.kb.io,admissionReviewVersions=v1
34+
// +kubebuilder:webhook:path=/validate-networking-metal-ironcore-dev-v1alpha1-vrf,mutating=false,failurePolicy=Fail,sideEffects=None,groups=networking.metal.ironcore.dev,resources=vrfs,verbs=create;update,versions=v1alpha1,name=vrf-v1alpha1.kb.io,admissionReviewVersions=v1
3435

3536
// VRFCustomValidator struct is responsible for validating the VRF resource
3637
// when it is created, updated, or deleted.
@@ -62,11 +63,6 @@ func (v *VRFCustomValidator) ValidateUpdate(_ context.Context, oldObj, newObj ru
6263

6364
// ValidateDelete implements webhook.CustomValidator so a webhook will be registered for the type VRF.
6465
func (v *VRFCustomValidator) ValidateDelete(ctx context.Context, obj runtime.Object) (admission.Warnings, error) {
65-
_, ok := obj.(*v1alpha1.VRF)
66-
if !ok {
67-
return nil, fmt.Errorf("expected a VRF object but got %T", obj)
68-
}
69-
7066
return nil, nil
7167
}
7268

internal/webhook/core/v1alpha1/vrf_webhook_test.go

Lines changed: 0 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -130,19 +130,6 @@ var _ = Describe("VRF Webhook", func() {
130130
})
131131
})
132132

133-
Context("ValidateDelete", func() {
134-
It("allows delete on VRF object", func() {
135-
_, err := validator.ValidateDelete(ctx, obj)
136-
Expect(err).ToNot(HaveOccurred())
137-
})
138-
139-
It("rejects delete when object type is wrong", func() {
140-
// Passing a different type (nil interface would panic so use something else)
141-
_, err := validator.ValidateDelete(ctx, &v1alpha1.VRFList{})
142-
Expect(err).To(HaveOccurred())
143-
})
144-
})
145-
146133
Context("ValidateCreate RouteTargets", func() {
147134
It("accepts valid type-0 route target", func() {
148135
obj.Spec.RouteTargets = []v1alpha1.RouteTarget{

internal/webhook/core/v1alpha1/webhook_suite_test.go

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -98,6 +98,9 @@ var _ = BeforeSuite(func() {
9898
err = SetupVRFWebhookWithManager(mgr)
9999
Expect(err).NotTo(HaveOccurred())
100100

101+
err = SetupInterfaceWebhookWithManager(mgr)
102+
Expect(err).NotTo(HaveOccurred())
103+
101104
// +kubebuilder:scaffold:webhook
102105

103106
go func() {

0 commit comments

Comments
 (0)