diff --git a/PROJECT b/PROJECT index 9c35cde7..8ed70604 100644 --- a/PROJECT +++ b/PROJECT @@ -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 @@ -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 diff --git a/charts/network-operator/templates/webhook/webhooks.yaml b/charts/network-operator/templates/webhook/webhooks.yaml index 0691ab93..601c7cf4 100644 --- a/charts/network-operator/templates/webhook/webhooks.yaml +++ b/charts/network-operator/templates/webhook/webhooks.yaml @@ -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: diff --git a/cmd/main.go b/cmd/main.go index 3b0532b6..a1f787ab 100644 --- a/cmd/main.go +++ b/cmd/main.go @@ -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(), @@ -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 { diff --git a/config/webhook/manifests.yaml b/config/webhook/manifests.yaml index 72b88cd0..5453a7c7 100644 --- a/config/webhook/manifests.yaml +++ b/config/webhook/manifests.yaml @@ -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: diff --git a/internal/webhook/core/v1alpha1/bgp_webhook.go b/internal/webhook/core/v1alpha1/bgp_webhook.go new file mode 100644 index 00000000..69d8c96c --- /dev/null +++ b/internal/webhook/core/v1alpha1/bgp_webhook.go @@ -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 +} diff --git a/internal/webhook/core/v1alpha1/bgp_webhook_test.go b/internal/webhook/core/v1alpha1/bgp_webhook_test.go new file mode 100644 index 00000000..4b43ed9c --- /dev/null +++ b/internal/webhook/core/v1alpha1/bgp_webhook_test.go @@ -0,0 +1,178 @@ +// SPDX-FileCopyrightText: 2025 SAP SE or an SAP affiliate company and IronCore contributors +// SPDX-License-Identifier: Apache-2.0 + +package v1alpha1 + +import ( + "math" + + . "github.com/onsi/ginkgo/v2" + . "github.com/onsi/gomega" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/util/intstr" + + "github.com/ironcore-dev/network-operator/api/core/v1alpha1" +) + +var _ = Describe("BGP Webhook", func() { + var ( + obj *v1alpha1.BGP + oldObj *v1alpha1.BGP + validator BGPCustomValidator + ) + + BeforeEach(func() { + obj = &v1alpha1.BGP{ + ObjectMeta: metav1.ObjectMeta{ + Name: "test-bgp", + Namespace: "default", + }, + Spec: v1alpha1.BGPSpec{ + DeviceRef: v1alpha1.LocalObjectReference{ + Name: "test-device", + }, + RouterID: "10.0.0.1", + }, + } + oldObj = obj.DeepCopy() + validator = BGPCustomValidator{} + Expect(validator).NotTo(BeNil(), "Expected validator to be initialized") + Expect(oldObj).NotTo(BeNil(), "Expected oldObj to be initialized") + Expect(obj).NotTo(BeNil(), "Expected obj to be initialized") + }) + + Context("When creating BGP under Validating Webhook", func() { + It("Should allow creation with valid integer AS number", func() { + obj.Spec.ASNumber = intstr.FromInt32(65001) + _, err := validator.ValidateCreate(ctx, obj) + Expect(err).NotTo(HaveOccurred()) + }) + + It("Should allow creation with valid string AS number in plain format", func() { + obj.Spec.ASNumber = intstr.FromString("4294967295") + _, err := validator.ValidateCreate(ctx, obj) + Expect(err).NotTo(HaveOccurred()) + }) + + It("Should allow creation with valid string AS number in dotted notation", func() { + obj.Spec.ASNumber = intstr.FromString("65000.100") + _, err := validator.ValidateCreate(ctx, obj) + Expect(err).NotTo(HaveOccurred()) + }) + + It("Should allow creation with minimum valid integer AS number", func() { + obj.Spec.ASNumber = intstr.FromInt32(1) + _, err := validator.ValidateCreate(ctx, obj) + Expect(err).NotTo(HaveOccurred()) + }) + + It("Should allow creation with maximum valid integer AS number", func() { + obj.Spec.ASNumber = intstr.FromInt32(math.MaxInt32) + _, err := validator.ValidateCreate(ctx, obj) + Expect(err).NotTo(HaveOccurred()) + }) + + It("Should deny creation with zero AS number", func() { + obj.Spec.ASNumber = intstr.FromInt32(0) + _, err := validator.ValidateCreate(ctx, obj) + Expect(err).To(HaveOccurred()) + }) + + It("Should deny creation with negative AS number", func() { + obj.Spec.ASNumber = intstr.FromInt32(-1) + _, err := validator.ValidateCreate(ctx, obj) + Expect(err).To(HaveOccurred()) + }) + + It("Should deny creation with string AS number exceeding uint32 max", func() { + obj.Spec.ASNumber = intstr.FromString("4294967296") + _, err := validator.ValidateCreate(ctx, obj) + Expect(err).To(HaveOccurred()) + }) + + It("Should deny creation with invalid string AS number", func() { + obj.Spec.ASNumber = intstr.FromString("invalid") + _, err := validator.ValidateCreate(ctx, obj) + Expect(err).To(HaveOccurred()) + }) + + It("Should deny creation with invalid dotted notation - too many parts", func() { + obj.Spec.ASNumber = intstr.FromString("1.2.3") + _, err := validator.ValidateCreate(ctx, obj) + Expect(err).To(HaveOccurred()) + }) + + It("Should deny creation with invalid dotted notation - high part out of range", func() { + obj.Spec.ASNumber = intstr.FromString("65536.100") + _, err := validator.ValidateCreate(ctx, obj) + Expect(err).To(HaveOccurred()) + }) + + It("Should deny creation with invalid dotted notation - low part out of range", func() { + obj.Spec.ASNumber = intstr.FromString("100.65536") + _, err := validator.ValidateCreate(ctx, obj) + Expect(err).To(HaveOccurred()) + }) + + It("Should deny creation with invalid dotted notation - high part is zero", func() { + obj.Spec.ASNumber = intstr.FromString("0.100") + _, err := validator.ValidateCreate(ctx, obj) + Expect(err).To(HaveOccurred()) + }) + + It("Should allow creation with dotted notation - low part is zero", func() { + obj.Spec.ASNumber = intstr.FromString("100.0") + _, err := validator.ValidateCreate(ctx, obj) + Expect(err).NotTo(HaveOccurred()) + }) + + It("Should allow creation with dotted notation at boundary values", func() { + obj.Spec.ASNumber = intstr.FromString("65535.65535") + _, err := validator.ValidateCreate(ctx, obj) + Expect(err).NotTo(HaveOccurred()) + }) + + It("Should allow creation with dotted notation at minimum valid values", func() { + obj.Spec.ASNumber = intstr.FromString("1.0") + _, err := validator.ValidateCreate(ctx, obj) + Expect(err).NotTo(HaveOccurred()) + }) + }) + + Context("When updating BGP under Validating Webhook", func() { + It("Should allow update with valid AS number", func() { + oldObj.Spec.ASNumber = intstr.FromInt32(65001) + obj.Spec.ASNumber = intstr.FromInt32(65002) + _, err := validator.ValidateUpdate(ctx, oldObj, obj) + Expect(err).NotTo(HaveOccurred()) + }) + + It("Should deny update with invalid AS number", func() { + oldObj.Spec.ASNumber = intstr.FromInt32(65001) + obj.Spec.ASNumber = intstr.FromInt32(0) + _, err := validator.ValidateUpdate(ctx, oldObj, obj) + Expect(err).To(HaveOccurred()) + }) + + It("Should allow update from integer to string format", func() { + oldObj.Spec.ASNumber = intstr.FromInt32(65001) + obj.Spec.ASNumber = intstr.FromString("4000000000") + _, err := validator.ValidateUpdate(ctx, oldObj, obj) + Expect(err).NotTo(HaveOccurred()) + }) + + It("Should allow update from plain to dotted notation", func() { + oldObj.Spec.ASNumber = intstr.FromString("4000000000") + obj.Spec.ASNumber = intstr.FromString("61035.36928") + _, err := validator.ValidateUpdate(ctx, oldObj, obj) + Expect(err).NotTo(HaveOccurred()) + }) + }) + + Context("When deleting BGP under Validating Webhook", func() { + It("Should allow deletion", func() { + _, err := validator.ValidateDelete(ctx, obj) + Expect(err).NotTo(HaveOccurred()) + }) + }) +}) diff --git a/internal/webhook/core/v1alpha1/bgppeer_webhook.go b/internal/webhook/core/v1alpha1/bgppeer_webhook.go new file mode 100644 index 00000000..2363ffae --- /dev/null +++ b/internal/webhook/core/v1alpha1/bgppeer_webhook.go @@ -0,0 +1,64 @@ +// SPDX-FileCopyrightText: 2025 SAP SE or an SAP affiliate company and IronCore contributors +// SPDX-License-Identifier: Apache-2.0 + +package v1alpha1 + +import ( + "context" + "fmt" + + "k8s.io/apimachinery/pkg/runtime" + 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 bgppeerlog = logf.Log.WithName("bgppeer-resource") + +// SetupBGPPeerWebhookWithManager registers the webhook for BGPPeer in the manager. +func SetupBGPPeerWebhookWithManager(mgr ctrl.Manager) error { + return ctrl.NewWebhookManagedBy(mgr).For(&v1alpha1.BGPPeer{}). + WithValidator(&BGPPeerCustomValidator{}). + Complete() +} + +// +kubebuilder:webhook:path=/validate-networking-metal-ironcore-dev-v1alpha1-bgppeer,mutating=false,failurePolicy=Fail,sideEffects=None,groups=networking.metal.ironcore.dev,resources=bgppeers,verbs=create;update,versions=v1alpha1,name=bgppeer-v1alpha1.kb.io,admissionReviewVersions=v1 + +// BGPPeerCustomValidator struct is responsible for validating the BGPPeer resource +// when it is created, updated, or deleted. +type BGPPeerCustomValidator struct{} + +var _ webhook.CustomValidator = &BGPPeerCustomValidator{} + +// ValidateCreate implements webhook.CustomValidator so a webhook will be registered for the type BGPPeer. +func (v *BGPPeerCustomValidator) ValidateCreate(_ context.Context, obj runtime.Object) (admission.Warnings, error) { + bgppeer, ok := obj.(*v1alpha1.BGPPeer) + if !ok { + return nil, fmt.Errorf("expected a BGPPeer object but got %T", obj) + } + + bgppeerlog.Info("Validation for BGPPeer upon creation", "name", bgppeer.GetName()) + + return nil, validateASNumber(bgppeer.Spec.ASNumber) +} + +// ValidateUpdate implements webhook.CustomValidator so a webhook will be registered for the type BGPPeer. +func (v *BGPPeerCustomValidator) ValidateUpdate(_ context.Context, oldObj, newObj runtime.Object) (admission.Warnings, error) { + bgppeer, ok := newObj.(*v1alpha1.BGPPeer) + if !ok { + return nil, fmt.Errorf("expected a BGPPeer object for the newObj but got %T", newObj) + } + + bgppeerlog.Info("Validation for BGPPeer upon update", "name", bgppeer.GetName()) + + return nil, validateASNumber(bgppeer.Spec.ASNumber) +} + +// ValidateDelete implements webhook.CustomValidator so a webhook will be registered for the type BGPPeer. +func (v *BGPPeerCustomValidator) ValidateDelete(ctx context.Context, obj runtime.Object) (admission.Warnings, error) { + return nil, nil +} diff --git a/internal/webhook/core/v1alpha1/bgppeer_webhook_test.go b/internal/webhook/core/v1alpha1/bgppeer_webhook_test.go new file mode 100644 index 00000000..608877ba --- /dev/null +++ b/internal/webhook/core/v1alpha1/bgppeer_webhook_test.go @@ -0,0 +1,179 @@ +// SPDX-FileCopyrightText: 2025 SAP SE or an SAP affiliate company and IronCore contributors +// SPDX-License-Identifier: Apache-2.0 + +package v1alpha1 + +import ( + "context" + + . "github.com/onsi/ginkgo/v2" + . "github.com/onsi/gomega" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/util/intstr" + + "github.com/ironcore-dev/network-operator/api/core/v1alpha1" +) + +var _ = Describe("BGPPeer Webhook", func() { + var ( + obj *v1alpha1.BGPPeer + oldObj *v1alpha1.BGPPeer + validator BGPPeerCustomValidator + ) + + BeforeEach(func() { + ctx = context.Background() + obj = &v1alpha1.BGPPeer{ + ObjectMeta: metav1.ObjectMeta{ + Name: "test-bgppeer", + Namespace: "default", + }, + Spec: v1alpha1.BGPPeerSpec{ + DeviceRef: v1alpha1.LocalObjectReference{ + Name: "test-device", + }, + Address: "192.168.1.1", + }, + } + oldObj = obj.DeepCopy() + validator = BGPPeerCustomValidator{} + Expect(validator).NotTo(BeNil(), "Expected validator to be initialized") + Expect(oldObj).NotTo(BeNil(), "Expected oldObj to be initialized") + Expect(obj).NotTo(BeNil(), "Expected obj to be initialized") + }) + + Context("When creating BGPPeer under Validating Webhook", func() { + It("Should admit creation with valid integer AS number", func() { + obj.Spec.ASNumber = intstr.FromInt32(65001) + _, err := validator.ValidateCreate(ctx, obj) + Expect(err).NotTo(HaveOccurred()) + }) + + It("Should admit creation with valid string AS number in plain format", func() { + obj.Spec.ASNumber = intstr.FromString("4294967295") + _, err := validator.ValidateCreate(ctx, obj) + Expect(err).NotTo(HaveOccurred()) + }) + + It("Should admit creation with valid string AS number in dotted notation", func() { + obj.Spec.ASNumber = intstr.FromString("65000.100") + _, err := validator.ValidateCreate(ctx, obj) + Expect(err).NotTo(HaveOccurred()) + }) + + It("Should admit creation with minimum valid integer AS number", func() { + obj.Spec.ASNumber = intstr.FromInt32(1) + _, err := validator.ValidateCreate(ctx, obj) + Expect(err).NotTo(HaveOccurred()) + }) + + It("Should admit creation with maximum valid integer AS number", func() { + obj.Spec.ASNumber = intstr.FromInt32(2147483647) + _, err := validator.ValidateCreate(ctx, obj) + Expect(err).NotTo(HaveOccurred()) + }) + + It("Should deny creation with zero AS number", func() { + obj.Spec.ASNumber = intstr.FromInt32(0) + _, err := validator.ValidateCreate(ctx, obj) + Expect(err).To(HaveOccurred()) + }) + + It("Should deny creation with negative AS number", func() { + obj.Spec.ASNumber = intstr.FromInt32(-1) + _, err := validator.ValidateCreate(ctx, obj) + Expect(err).To(HaveOccurred()) + }) + + It("Should deny creation with string AS number exceeding uint32 max", func() { + obj.Spec.ASNumber = intstr.FromString("4294967296") + _, err := validator.ValidateCreate(ctx, obj) + Expect(err).To(HaveOccurred()) + }) + + It("Should deny creation with invalid string AS number", func() { + obj.Spec.ASNumber = intstr.FromString("invalid") + _, err := validator.ValidateCreate(ctx, obj) + Expect(err).To(HaveOccurred()) + }) + + It("Should deny creation with invalid dotted notation - too many parts", func() { + obj.Spec.ASNumber = intstr.FromString("1.2.3") + _, err := validator.ValidateCreate(ctx, obj) + Expect(err).To(HaveOccurred()) + }) + + It("Should deny creation with invalid dotted notation - high part out of range", func() { + obj.Spec.ASNumber = intstr.FromString("65536.100") + _, err := validator.ValidateCreate(ctx, obj) + Expect(err).To(HaveOccurred()) + }) + + It("Should deny creation with invalid dotted notation - low part out of range", func() { + obj.Spec.ASNumber = intstr.FromString("100.65536") + _, err := validator.ValidateCreate(ctx, obj) + Expect(err).To(HaveOccurred()) + }) + + It("Should deny creation with invalid dotted notation - high part is zero", func() { + obj.Spec.ASNumber = intstr.FromString("0.100") + _, err := validator.ValidateCreate(ctx, obj) + Expect(err).To(HaveOccurred()) + }) + + It("Should admit creation with dotted notation - low part is zero", func() { + obj.Spec.ASNumber = intstr.FromString("100.0") + _, err := validator.ValidateCreate(ctx, obj) + Expect(err).NotTo(HaveOccurred()) + }) + + It("Should admit creation with dotted notation at boundary values", func() { + obj.Spec.ASNumber = intstr.FromString("65535.65535") + _, err := validator.ValidateCreate(ctx, obj) + Expect(err).NotTo(HaveOccurred()) + }) + + It("Should admit creation with dotted notation at minimum valid values", func() { + obj.Spec.ASNumber = intstr.FromString("1.0") + _, err := validator.ValidateCreate(ctx, obj) + Expect(err).NotTo(HaveOccurred()) + }) + }) + + Context("When updating BGPPeer under Validating Webhook", func() { + It("Should admit update with valid AS number", func() { + oldObj.Spec.ASNumber = intstr.FromInt32(65001) + obj.Spec.ASNumber = intstr.FromInt32(65002) + _, err := validator.ValidateUpdate(ctx, oldObj, obj) + Expect(err).NotTo(HaveOccurred()) + }) + + It("Should deny update with invalid AS number", func() { + oldObj.Spec.ASNumber = intstr.FromInt32(65001) + obj.Spec.ASNumber = intstr.FromInt32(0) + _, err := validator.ValidateUpdate(ctx, oldObj, obj) + Expect(err).To(HaveOccurred()) + }) + + It("Should admit update from integer to string format", func() { + oldObj.Spec.ASNumber = intstr.FromInt32(65001) + obj.Spec.ASNumber = intstr.FromString("4000000000") + _, err := validator.ValidateUpdate(ctx, oldObj, obj) + Expect(err).NotTo(HaveOccurred()) + }) + + It("Should admit update from plain to dotted notation", func() { + oldObj.Spec.ASNumber = intstr.FromString("4000000000") + obj.Spec.ASNumber = intstr.FromString("61035.36928") + _, err := validator.ValidateUpdate(ctx, oldObj, obj) + Expect(err).NotTo(HaveOccurred()) + }) + }) + + Context("When deleting BGPPeer under Validating Webhook", func() { + It("Should admit deletion", func() { + _, err := validator.ValidateDelete(ctx, obj) + Expect(err).NotTo(HaveOccurred()) + }) + }) +}) diff --git a/internal/webhook/core/v1alpha1/prefixset_webhook_test.go b/internal/webhook/core/v1alpha1/prefixset_webhook_test.go index 9d79f614..2a8b299e 100644 --- a/internal/webhook/core/v1alpha1/prefixset_webhook_test.go +++ b/internal/webhook/core/v1alpha1/prefixset_webhook_test.go @@ -15,7 +15,6 @@ import ( var _ = Describe("PrefixSet Webhook", func() { var ( - ctx context.Context obj *v1alpha1.PrefixSet oldObj *v1alpha1.PrefixSet validator PrefixSetCustomValidator