Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions PROJECT
Original file line number Diff line number Diff line change
Expand Up @@ -138,6 +138,9 @@ resources:
kind: BGP
path: github.com/ironcore-dev/network-operator/api/core/v1alpha1
version: v1alpha1
webhooks:
validation: true
webhookVersion: v1
- api:
crdVersion: v1
namespaced: true
Expand All @@ -146,6 +149,9 @@ resources:
kind: BGPPeer
path: github.com/ironcore-dev/network-operator/api/core/v1alpha1
version: v1alpha1
webhooks:
validation: true
webhookVersion: v1
- api:
crdVersion: v1
namespaced: true
Expand Down
40 changes: 40 additions & 0 deletions charts/network-operator/templates/webhook/webhooks.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,46 @@ metadata:
labels:
{{- include "chart.labels" . | nindent 4 }}
webhooks:
- name: bgp-v1alpha1.kb.io
clientConfig:
service:
name: network-operator-webhook-service
namespace: {{ .Release.Namespace }}
path: /validate-networking-metal-ironcore-dev-v1alpha1-bgp
failurePolicy: Fail
sideEffects: None
admissionReviewVersions:
- v1
rules:
- operations:
- CREATE
- UPDATE
apiGroups:
- networking.metal.ironcore.dev
apiVersions:
- v1alpha1
resources:
- bgp
- name: bgppeer-v1alpha1.kb.io
clientConfig:
service:
name: network-operator-webhook-service
namespace: {{ .Release.Namespace }}
path: /validate-networking-metal-ironcore-dev-v1alpha1-bgppeer
failurePolicy: Fail
sideEffects: None
admissionReviewVersions:
- v1
rules:
- operations:
- CREATE
- UPDATE
apiGroups:
- networking.metal.ironcore.dev
apiVersions:
- v1alpha1
resources:
- bgppeers
- name: interface-v1alpha1.kb.io
clientConfig:
service:
Expand Down
47 changes: 27 additions & 20 deletions cmd/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -460,26 +460,6 @@ func main() {
os.Exit(1)
}

if os.Getenv("ENABLE_WEBHOOKS") != "false" {
if err := webhookv1alpha1.SetupVRFWebhookWithManager(mgr); err != nil {
setupLog.Error(err, "unable to create webhook", "webhook", "VRF")
os.Exit(1)
}
}

if os.Getenv("ENABLE_WEBHOOKS") != "false" {
if err := webhookv1alpha1.SetupInterfaceWebhookWithManager(mgr); err != nil {
setupLog.Error(err, "unable to create webhook", "webhook", "Interface")
os.Exit(1)
}
}

if os.Getenv("ENABLE_WEBHOOKS") != "false" {
if err := webhookv1alpha1.SetupPrefixSetWebhookWithManager(mgr); err != nil {
setupLog.Error(err, "unable to create webhook", "webhook", "PrefixSet")
os.Exit(1)
}
}
if err := (&corecontroller.RoutingPolicyReconciler{
Client: mgr.GetClient(),
Scheme: mgr.GetScheme(),
Expand All @@ -502,6 +482,33 @@ func main() {
os.Exit(1)
}

if os.Getenv("ENABLE_WEBHOOKS") != "false" {
if err := webhookv1alpha1.SetupVRFWebhookWithManager(mgr); err != nil {
setupLog.Error(err, "unable to create webhook", "webhook", "VRF")
os.Exit(1)
}

if err := webhookv1alpha1.SetupInterfaceWebhookWithManager(mgr); err != nil {
setupLog.Error(err, "unable to create webhook", "webhook", "Interface")
os.Exit(1)
}

if err := webhookv1alpha1.SetupPrefixSetWebhookWithManager(mgr); err != nil {
setupLog.Error(err, "unable to create webhook", "webhook", "PrefixSet")
os.Exit(1)
}

if err := webhookv1alpha1.SetupBGPWebhookWithManager(mgr); err != nil {
setupLog.Error(err, "unable to create webhook", "webhook", "BGP")
os.Exit(1)
}

if err := webhookv1alpha1.SetupBGPPeerWebhookWithManager(mgr); err != nil {
setupLog.Error(err, "unable to create webhook", "webhook", "BGPPeer")
os.Exit(1)
}
}

// +kubebuilder:scaffold:builder

if metricsCertWatcher != nil {
Expand Down
40 changes: 40 additions & 0 deletions config/webhook/manifests.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,46 @@ kind: ValidatingWebhookConfiguration
metadata:
name: validating-webhook-configuration
webhooks:
- admissionReviewVersions:
- v1
clientConfig:
service:
name: webhook-service
namespace: system
path: /validate-networking-metal-ironcore-dev-v1alpha1-bgp
failurePolicy: Fail
name: bgp-v1alpha1.kb.io
rules:
- apiGroups:
- networking.metal.ironcore.dev
apiVersions:
- v1alpha1
operations:
- CREATE
- UPDATE
resources:
- bgp
sideEffects: None
- admissionReviewVersions:
- v1
clientConfig:
service:
name: webhook-service
namespace: system
path: /validate-networking-metal-ironcore-dev-v1alpha1-bgppeer
failurePolicy: Fail
name: bgppeer-v1alpha1.kb.io
rules:
- apiGroups:
- networking.metal.ironcore.dev
apiVersions:
- v1alpha1
operations:
- CREATE
- UPDATE
resources:
- bgppeers
sideEffects: None
- admissionReviewVersions:
- v1
clientConfig:
Expand Down
121 changes: 121 additions & 0 deletions internal/webhook/core/v1alpha1/bgp_webhook.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,121 @@
// SPDX-FileCopyrightText: 2025 SAP SE or an SAP affiliate company and IronCore contributors
// SPDX-License-Identifier: Apache-2.0

package v1alpha1

import (
"context"
"fmt"
"math"
"strconv"
"strings"

"k8s.io/apimachinery/pkg/runtime"
"k8s.io/apimachinery/pkg/util/intstr"
ctrl "sigs.k8s.io/controller-runtime"
logf "sigs.k8s.io/controller-runtime/pkg/log"
"sigs.k8s.io/controller-runtime/pkg/webhook"
"sigs.k8s.io/controller-runtime/pkg/webhook/admission"

"github.com/ironcore-dev/network-operator/api/core/v1alpha1"
)

// log is for logging in this package.
var bgplog = logf.Log.WithName("bgp-resource")

// SetupBGPWebhookWithManager registers the webhook for BGP in the manager.
func SetupBGPWebhookWithManager(mgr ctrl.Manager) error {
return ctrl.NewWebhookManagedBy(mgr).For(&v1alpha1.BGP{}).
WithValidator(&BGPCustomValidator{}).
Complete()
}

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

// BGPCustomValidator struct is responsible for validating the BGP resource
// when it is created, updated, or deleted.
type BGPCustomValidator struct{}

var _ webhook.CustomValidator = &BGPCustomValidator{}

// ValidateCreate implements webhook.CustomValidator so a webhook will be registered for the type BGP.
func (v *BGPCustomValidator) ValidateCreate(_ context.Context, obj runtime.Object) (admission.Warnings, error) {
bgp, ok := obj.(*v1alpha1.BGP)
if !ok {
return nil, fmt.Errorf("expected a BGP object but got %T", obj)
}

bgplog.Info("Validation for BGP upon creation", "name", bgp.GetName())

return nil, validateASNumber(bgp.Spec.ASNumber)
}

// ValidateUpdate implements webhook.CustomValidator so a webhook will be registered for the type BGP.
func (v *BGPCustomValidator) ValidateUpdate(_ context.Context, oldObj, newObj runtime.Object) (admission.Warnings, error) {
bgp, ok := newObj.(*v1alpha1.BGP)
if !ok {
return nil, fmt.Errorf("expected a BGP object for the newObj but got %T", newObj)
}

bgplog.Info("Validation for BGP upon update", "name", bgp.GetName())

return nil, validateASNumber(bgp.Spec.ASNumber)
}

// ValidateDelete implements webhook.CustomValidator so a webhook will be registered for the type BGP.
func (v *BGPCustomValidator) ValidateDelete(ctx context.Context, obj runtime.Object) (admission.Warnings, error) {
return nil, nil
}

// validateASNumber performs validation on the autonomous system number (ASN).
// It ensures the ASN is within valid ranges for both plain and dotted notation as per [RFC 5396].
// [RFC 5396](https://datatracker.ietf.org/doc/html/rfc5396)
func validateASNumber(asn intstr.IntOrString) error {
// If the value is an integer, validate plain format only
if asn.Type == intstr.Int {
asnValue := asn.IntVal
if asnValue < 1 {
return fmt.Errorf("AS number %d must be >=1 (for larger values use string format to support full range 1-4294967295)", asnValue)
}
return nil
}

// If the value is a string, it can be either plain or dotted notation
asnStr := asn.StrVal

// Try to parse as plain format first
if !strings.Contains(asnStr, ".") {
asnValue, err := strconv.ParseInt(asnStr, 10, 64)
if err != nil {
return fmt.Errorf("invalid AS number format %q: %w", asnStr, err)
}
if asnValue < 1 || asnValue > math.MaxUint32 {
return fmt.Errorf("AS number %d is out of valid range (1-4294967295)", asnValue)
}
return nil
}

// Parse as dotted notation (high.low)
parts := strings.Split(asnStr, ".")
if len(parts) != 2 {
return fmt.Errorf("invalid AS number dotted notation %q: must be in format high.low", asnStr)
}

high, err := strconv.ParseInt(parts[0], 10, 64)
if err != nil {
return fmt.Errorf("invalid AS number dotted notation %q: high part is not a valid number: %w", asnStr, err)
}
if high < 1 || high > math.MaxUint16 {
return fmt.Errorf("invalid AS number dotted notation %q: high part %d is out of valid range (1-65535)", asnStr, high)
}

low, err := strconv.ParseInt(parts[1], 10, 64)
if err != nil {
return fmt.Errorf("invalid AS number dotted notation %q: low part is not a valid number: %w", asnStr, err)
}
if low < 0 || low > math.MaxUint16 {
return fmt.Errorf("invalid AS number dotted notation %q: low part %d is out of valid range (0-65535)", asnStr, low)
}

return nil
}
Loading
Loading