This is an automated email from the ASF dual-hosted git repository.

ronething pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/apisix-ingress-controller.git


The following commit(s) were added to refs/heads/master by this push:
     new 63c7d111 feat: support udproute webhook (#2588)
63c7d111 is described below

commit 63c7d111a4fc4e557e570f34b2a452c9d4941bb2
Author: Ashing Zheng <[email protected]>
AuthorDate: Tue Sep 30 17:03:40 2025 +0800

    feat: support udproute webhook (#2588)
    
    Signed-off-by: Ashing Zheng <[email protected]>
---
 config/webhook/manifests.yaml                |  20 ++
 internal/manager/webhooks.go                 |   3 +
 internal/webhook/v1/ownership.go             |   7 +
 internal/webhook/v1/udproute_webhook.go      | 146 ++++++++++++++
 internal/webhook/v1/udproute_webhook_test.go | 273 +++++++++++++++++++++++++++
 test/e2e/framework/manifests/webhook.yaml    |  21 +++
 test/e2e/testdata/ldap/docker-compose.yaml   |   2 +-
 test/e2e/webhook/helpers.go                  | 121 ++++++++++++
 test/e2e/webhook/tcproute.go                 |  95 ++--------
 test/e2e/webhook/udproute.go                 |  48 +++++
 10 files changed, 651 insertions(+), 85 deletions(-)

diff --git a/config/webhook/manifests.yaml b/config/webhook/manifests.yaml
index e2ad06a3..da3ad2fc 100644
--- a/config/webhook/manifests.yaml
+++ b/config/webhook/manifests.yaml
@@ -224,3 +224,23 @@ webhooks:
     resources:
     - tcproutes
   sideEffects: None
+- admissionReviewVersions:
+  - v1
+  clientConfig:
+    service:
+      name: webhook-service
+      namespace: system
+      path: /validate-gateway-networking-k8s-io-v1alpha2-udproute
+  failurePolicy: Fail
+  name: vudproute-v1alpha2.kb.io
+  rules:
+  - apiGroups:
+    - gateway.networking.k8s.io
+    apiVersions:
+    - v1alpha2
+    operations:
+    - CREATE
+    - UPDATE
+    resources:
+    - udproutes
+  sideEffects: None
diff --git a/internal/manager/webhooks.go b/internal/manager/webhooks.go
index a1bc1dae..33731c49 100644
--- a/internal/manager/webhooks.go
+++ b/internal/manager/webhooks.go
@@ -47,6 +47,9 @@ func setupWebhooks(_ context.Context, mgr manager.Manager) 
error {
        if err := webhookv1.SetupTCPRouteWebhookWithManager(mgr); err != nil {
                return err
        }
+       if err := webhookv1.SetupUDPRouteWebhookWithManager(mgr); err != nil {
+               return err
+       }
        if err := webhookv1.SetupApisixConsumerWebhookWithManager(mgr); err != 
nil {
                return err
        }
diff --git a/internal/webhook/v1/ownership.go b/internal/webhook/v1/ownership.go
index d2a72eae..f5d5d60f 100644
--- a/internal/webhook/v1/ownership.go
+++ b/internal/webhook/v1/ownership.go
@@ -68,6 +68,13 @@ func isTCPRouteManaged(ctx context.Context, c client.Client, 
route *gatewayv1alp
        return routeReferencesManagedGateway(ctx, c, route.Spec.ParentRefs, 
route.Namespace)
 }
 
+func isUDPRouteManaged(ctx context.Context, c client.Client, route 
*gatewayv1alpha2.UDPRoute) (bool, error) {
+       if route == nil {
+               return false, nil
+       }
+       return routeReferencesManagedGateway(ctx, c, route.Spec.ParentRefs, 
route.Namespace)
+}
+
 func routeReferencesManagedGateway(ctx context.Context, c client.Client, 
parents []gatewayv1.ParentReference, defaultNamespace string) (bool, error) {
        for _, parent := range parents {
                if parent.Name == "" {
diff --git a/internal/webhook/v1/udproute_webhook.go 
b/internal/webhook/v1/udproute_webhook.go
new file mode 100644
index 00000000..23cfd81a
--- /dev/null
+++ b/internal/webhook/v1/udproute_webhook.go
@@ -0,0 +1,146 @@
+// Licensed to the Apache Software Foundation (ASF) under one or more
+// contributor license agreements.  See the NOTICE file distributed with
+// this work for additional information regarding copyright ownership.
+// The ASF licenses this file to You under the Apache License, Version 2.0
+// (the "License"); you may not use this file except in compliance with
+// the License.  You may obtain a copy of the License at
+//
+//     http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package v1
+
+import (
+       "context"
+       "fmt"
+
+       corev1 "k8s.io/api/core/v1"
+       "k8s.io/apimachinery/pkg/runtime"
+       "k8s.io/apimachinery/pkg/types"
+       ctrl "sigs.k8s.io/controller-runtime"
+       "sigs.k8s.io/controller-runtime/pkg/client"
+       logf "sigs.k8s.io/controller-runtime/pkg/log"
+       "sigs.k8s.io/controller-runtime/pkg/webhook"
+       "sigs.k8s.io/controller-runtime/pkg/webhook/admission"
+       gatewayv1alpha2 "sigs.k8s.io/gateway-api/apis/v1alpha2"
+
+       internaltypes 
"github.com/apache/apisix-ingress-controller/internal/types"
+       
"github.com/apache/apisix-ingress-controller/internal/webhook/v1/reference"
+)
+
+var udpRouteLog = logf.Log.WithName("udproute-resource")
+
+func SetupUDPRouteWebhookWithManager(mgr ctrl.Manager) error {
+       return ctrl.NewWebhookManagedBy(mgr).
+               For(&gatewayv1alpha2.UDPRoute{}).
+               WithValidator(NewUDPRouteCustomValidator(mgr.GetClient())).
+               Complete()
+}
+
+// 
+kubebuilder:webhook:path=/validate-gateway-networking-k8s-io-v1alpha2-udproute,mutating=false,failurePolicy=fail,sideEffects=None,groups=gateway.networking.k8s.io,resources=udproutes,verbs=create;update,versions=v1alpha2,name=vudproute-v1alpha2.kb.io,admissionReviewVersions=v1
+
+type UDPRouteCustomValidator struct {
+       Client  client.Client
+       checker reference.Checker
+}
+
+var _ webhook.CustomValidator = &UDPRouteCustomValidator{}
+
+func NewUDPRouteCustomValidator(c client.Client) *UDPRouteCustomValidator {
+       return &UDPRouteCustomValidator{
+               Client:  c,
+               checker: reference.NewChecker(c, udpRouteLog),
+       }
+}
+
+func (v *UDPRouteCustomValidator) ValidateCreate(ctx context.Context, obj 
runtime.Object) (admission.Warnings, error) {
+       route, ok := obj.(*gatewayv1alpha2.UDPRoute)
+       if !ok {
+               return nil, fmt.Errorf("expected a UDPRoute object but got %T", 
obj)
+       }
+       udpRouteLog.Info("Validation for UDPRoute upon creation", "name", 
route.GetName(), "namespace", route.GetNamespace())
+       managed, err := isUDPRouteManaged(ctx, v.Client, route)
+       if err != nil {
+               udpRouteLog.Error(err, "failed to decide controller ownership", 
"name", route.GetName(), "namespace", route.GetNamespace())
+               return nil, nil
+       }
+       if !managed {
+               return nil, nil
+       }
+
+       return v.collectWarnings(ctx, route), nil
+}
+
+func (v *UDPRouteCustomValidator) ValidateUpdate(ctx context.Context, oldObj, 
newObj runtime.Object) (admission.Warnings, error) {
+       route, ok := newObj.(*gatewayv1alpha2.UDPRoute)
+       if !ok {
+               return nil, fmt.Errorf("expected a UDPRoute object for the 
newObj but got %T", newObj)
+       }
+       udpRouteLog.Info("Validation for UDPRoute upon update", "name", 
route.GetName(), "namespace", route.GetNamespace())
+       managed, err := isUDPRouteManaged(ctx, v.Client, route)
+       if err != nil {
+               udpRouteLog.Error(err, "failed to decide controller ownership", 
"name", route.GetName(), "namespace", route.GetNamespace())
+               return nil, nil
+       }
+       if !managed {
+               return nil, nil
+       }
+
+       return v.collectWarnings(ctx, route), nil
+}
+
+func (*UDPRouteCustomValidator) ValidateDelete(context.Context, 
runtime.Object) (admission.Warnings, error) {
+       return nil, nil
+}
+
+func (v *UDPRouteCustomValidator) collectWarnings(ctx context.Context, route 
*gatewayv1alpha2.UDPRoute) admission.Warnings {
+       serviceVisited := make(map[types.NamespacedName]struct{})
+       namespace := route.GetNamespace()
+
+       var warnings admission.Warnings
+
+       addServiceWarning := func(nn types.NamespacedName) {
+               if nn.Name == "" || nn.Namespace == "" {
+                       return
+               }
+               if _, seen := serviceVisited[nn]; seen {
+                       return
+               }
+               serviceVisited[nn] = struct{}{}
+               warnings = append(warnings, v.checker.Service(ctx, 
reference.ServiceRef{
+                       Object:         route,
+                       NamespacedName: nn,
+               })...)
+       }
+
+       addBackendRef := func(ns, name string, group *gatewayv1alpha2.Group, 
kind *gatewayv1alpha2.Kind) {
+               if name == "" {
+                       return
+               }
+               if group != nil && string(*group) != corev1.GroupName {
+                       return
+               }
+               if kind != nil && *kind != internaltypes.KindService {
+                       return
+               }
+               nn := types.NamespacedName{Namespace: ns, Name: name}
+               addServiceWarning(nn)
+       }
+
+       for _, rule := range route.Spec.Rules {
+               for _, backend := range rule.BackendRefs {
+                       targetNamespace := namespace
+                       if backend.Namespace != nil && *backend.Namespace != "" 
{
+                               targetNamespace = string(*backend.Namespace)
+                       }
+                       addBackendRef(targetNamespace, string(backend.Name), 
backend.Group, backend.Kind)
+               }
+       }
+
+       return warnings
+}
diff --git a/internal/webhook/v1/udproute_webhook_test.go 
b/internal/webhook/v1/udproute_webhook_test.go
new file mode 100644
index 00000000..82b87920
--- /dev/null
+++ b/internal/webhook/v1/udproute_webhook_test.go
@@ -0,0 +1,273 @@
+// Licensed to the Apache Software Foundation (ASF) under one or more
+// contributor license agreements.  See the NOTICE file distributed with
+// this work for additional information regarding copyright ownership.
+// The ASF licenses this file to You under the Apache License, Version 2.0
+// (the "License"); you may not use this file except in compliance with
+// the License.  You may obtain a copy of the License at
+//
+//     http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package v1
+
+import (
+       "context"
+       "testing"
+
+       "github.com/stretchr/testify/assert"
+       "github.com/stretchr/testify/require"
+       corev1 "k8s.io/api/core/v1"
+       metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
+       "k8s.io/apimachinery/pkg/runtime"
+       clientgoscheme "k8s.io/client-go/kubernetes/scheme"
+       "sigs.k8s.io/controller-runtime/pkg/client/fake"
+       gatewayv1 "sigs.k8s.io/gateway-api/apis/v1"
+       gatewayv1alpha2 "sigs.k8s.io/gateway-api/apis/v1alpha2"
+
+       "github.com/apache/apisix-ingress-controller/internal/controller/config"
+)
+
+func buildUDPRouteValidator(t *testing.T, objects ...runtime.Object) 
*UDPRouteCustomValidator {
+       t.Helper()
+
+       scheme := runtime.NewScheme()
+       require.NoError(t, clientgoscheme.AddToScheme(scheme))
+       require.NoError(t, gatewayv1.Install(scheme))
+       require.NoError(t, gatewayv1alpha2.Install(scheme))
+
+       managed := []runtime.Object{
+               &gatewayv1.GatewayClass{
+                       ObjectMeta: metav1.ObjectMeta{Name: 
"apisix-gateway-class"},
+                       Spec: gatewayv1.GatewayClassSpec{
+                               ControllerName: 
gatewayv1.GatewayController(config.ControllerConfig.ControllerName),
+                       },
+               },
+               &gatewayv1.Gateway{
+                       ObjectMeta: metav1.ObjectMeta{Name: "test-gateway", 
Namespace: "default"},
+                       Spec: gatewayv1.GatewaySpec{
+                               GatewayClassName: 
gatewayv1.ObjectName("apisix-gateway-class"),
+                       },
+               },
+       }
+       allObjects := append(managed, objects...)
+       builder := 
fake.NewClientBuilder().WithScheme(scheme).WithRuntimeObjects(allObjects...)
+
+       return NewUDPRouteCustomValidator(builder.Build())
+}
+
+func TestUDPRouteCustomValidator_WarnsForMissingReferences(t *testing.T) {
+       route := &gatewayv1alpha2.UDPRoute{
+               ObjectMeta: metav1.ObjectMeta{Name: "demo", Namespace: 
"default"},
+               Spec: gatewayv1alpha2.UDPRouteSpec{
+                       CommonRouteSpec: gatewayv1alpha2.CommonRouteSpec{
+                               ParentRefs: []gatewayv1alpha2.ParentReference{{
+                                       Name: 
gatewayv1alpha2.ObjectName("test-gateway"),
+                               }},
+                       },
+                       Rules: []gatewayv1alpha2.UDPRouteRule{{
+                               BackendRefs: []gatewayv1alpha2.BackendRef{
+                                       {
+                                               BackendObjectReference: 
gatewayv1alpha2.BackendObjectReference{
+                                                       Name: 
gatewayv1alpha2.ObjectName("missing-svc"),
+                                               },
+                                       },
+                               },
+                       }},
+               },
+       }
+
+       validator := buildUDPRouteValidator(t)
+       warnings, err := validator.ValidateCreate(context.Background(), route)
+       require.NoError(t, err)
+       assert.ElementsMatch(t, []string{
+               "Referenced Service 'default/missing-svc' not found",
+       }, warnings)
+}
+
+func TestUDPRouteCustomValidator_NoWarningsWhenResourcesExist(t *testing.T) {
+       objs := []runtime.Object{
+               &corev1.Service{ObjectMeta: metav1.ObjectMeta{Name: "backend", 
Namespace: "default"}},
+       }
+
+       validator := buildUDPRouteValidator(t, objs...)
+
+       route := &gatewayv1alpha2.UDPRoute{
+               ObjectMeta: metav1.ObjectMeta{Name: "demo", Namespace: 
"default"},
+               Spec: gatewayv1alpha2.UDPRouteSpec{
+                       CommonRouteSpec: gatewayv1alpha2.CommonRouteSpec{
+                               ParentRefs: []gatewayv1alpha2.ParentReference{{
+                                       Name: 
gatewayv1alpha2.ObjectName("test-gateway"),
+                               }},
+                       },
+                       Rules: []gatewayv1alpha2.UDPRouteRule{{
+                               BackendRefs: []gatewayv1alpha2.BackendRef{
+                                       {
+                                               BackendObjectReference: 
gatewayv1alpha2.BackendObjectReference{
+                                                       Name: 
gatewayv1alpha2.ObjectName("backend"),
+                                               },
+                                       },
+                               },
+                       }},
+               },
+       }
+
+       warnings, err := validator.ValidateCreate(context.Background(), route)
+       require.NoError(t, err)
+       assert.Empty(t, warnings)
+}
+
+func TestUDPRouteCustomValidator_ValidateUpdate(t *testing.T) {
+       objs := []runtime.Object{
+               &corev1.Service{ObjectMeta: metav1.ObjectMeta{Name: "backend", 
Namespace: "default"}},
+       }
+
+       validator := buildUDPRouteValidator(t, objs...)
+
+       oldRoute := &gatewayv1alpha2.UDPRoute{
+               ObjectMeta: metav1.ObjectMeta{Name: "demo", Namespace: 
"default"},
+               Spec: gatewayv1alpha2.UDPRouteSpec{
+                       CommonRouteSpec: gatewayv1alpha2.CommonRouteSpec{
+                               ParentRefs: []gatewayv1alpha2.ParentReference{{
+                                       Name: 
gatewayv1alpha2.ObjectName("test-gateway"),
+                               }},
+                       },
+                       Rules: []gatewayv1alpha2.UDPRouteRule{{
+                               BackendRefs: []gatewayv1alpha2.BackendRef{
+                                       {
+                                               BackendObjectReference: 
gatewayv1alpha2.BackendObjectReference{
+                                                       Name: 
gatewayv1alpha2.ObjectName("backend"),
+                                               },
+                                       },
+                               },
+                       }},
+               },
+       }
+
+       newRoute := &gatewayv1alpha2.UDPRoute{
+               ObjectMeta: metav1.ObjectMeta{Name: "demo", Namespace: 
"default"},
+               Spec: gatewayv1alpha2.UDPRouteSpec{
+                       CommonRouteSpec: gatewayv1alpha2.CommonRouteSpec{
+                               ParentRefs: []gatewayv1alpha2.ParentReference{{
+                                       Name: 
gatewayv1alpha2.ObjectName("test-gateway"),
+                               }},
+                       },
+                       Rules: []gatewayv1alpha2.UDPRouteRule{{
+                               BackendRefs: []gatewayv1alpha2.BackendRef{
+                                       {
+                                               BackendObjectReference: 
gatewayv1alpha2.BackendObjectReference{
+                                                       Name: 
gatewayv1alpha2.ObjectName("backend"),
+                                               },
+                                       },
+                               },
+                       }},
+               },
+       }
+
+       warnings, err := validator.ValidateUpdate(context.Background(), 
oldRoute, newRoute)
+       require.NoError(t, err)
+       assert.Empty(t, warnings)
+}
+
+func TestUDPRouteCustomValidator_ValidateDelete(t *testing.T) {
+       validator := buildUDPRouteValidator(t)
+
+       route := &gatewayv1alpha2.UDPRoute{
+               ObjectMeta: metav1.ObjectMeta{Name: "demo", Namespace: 
"default"},
+               Spec: gatewayv1alpha2.UDPRouteSpec{
+                       CommonRouteSpec: gatewayv1alpha2.CommonRouteSpec{
+                               ParentRefs: []gatewayv1alpha2.ParentReference{{
+                                       Name: 
gatewayv1alpha2.ObjectName("test-gateway"),
+                               }},
+                       },
+               },
+       }
+
+       warnings, err := validator.ValidateDelete(context.Background(), route)
+       require.NoError(t, err)
+       assert.Empty(t, warnings)
+}
+
+func TestUDPRouteCustomValidator_CrossNamespaceBackendRefs(t *testing.T) {
+       otherNamespace := gatewayv1alpha2.Namespace("other")
+       objs := []runtime.Object{
+               &corev1.Service{ObjectMeta: metav1.ObjectMeta{Name: "backend", 
Namespace: "other"}},
+       }
+
+       validator := buildUDPRouteValidator(t, objs...)
+
+       route := &gatewayv1alpha2.UDPRoute{
+               ObjectMeta: metav1.ObjectMeta{Name: "demo", Namespace: 
"default"},
+               Spec: gatewayv1alpha2.UDPRouteSpec{
+                       CommonRouteSpec: gatewayv1alpha2.CommonRouteSpec{
+                               ParentRefs: []gatewayv1alpha2.ParentReference{{
+                                       Name: 
gatewayv1alpha2.ObjectName("test-gateway"),
+                               }},
+                       },
+                       Rules: []gatewayv1alpha2.UDPRouteRule{{
+                               BackendRefs: []gatewayv1alpha2.BackendRef{
+                                       {
+                                               BackendObjectReference: 
gatewayv1alpha2.BackendObjectReference{
+                                                       Name:      
gatewayv1alpha2.ObjectName("backend"),
+                                                       Namespace: 
&otherNamespace,
+                                               },
+                                       },
+                               },
+                       }},
+               },
+       }
+
+       warnings, err := validator.ValidateCreate(context.Background(), route)
+       require.NoError(t, err)
+       // Cross-namespace Service references should have no warnings since the 
Service exists
+       assert.Empty(t, warnings)
+}
+
+func TestUDPRouteCustomValidator_MultipleBackendRefs(t *testing.T) {
+       objs := []runtime.Object{
+               &corev1.Service{ObjectMeta: metav1.ObjectMeta{Name: 
"backend-1", Namespace: "default"}},
+               &corev1.Service{ObjectMeta: metav1.ObjectMeta{Name: 
"backend-2", Namespace: "default"}},
+       }
+
+       validator := buildUDPRouteValidator(t, objs...)
+
+       route := &gatewayv1alpha2.UDPRoute{
+               ObjectMeta: metav1.ObjectMeta{Name: "demo", Namespace: 
"default"},
+               Spec: gatewayv1alpha2.UDPRouteSpec{
+                       CommonRouteSpec: gatewayv1alpha2.CommonRouteSpec{
+                               ParentRefs: []gatewayv1alpha2.ParentReference{{
+                                       Name: 
gatewayv1alpha2.ObjectName("test-gateway"),
+                               }},
+                       },
+                       Rules: []gatewayv1alpha2.UDPRouteRule{{
+                               BackendRefs: []gatewayv1alpha2.BackendRef{
+                                       {
+                                               BackendObjectReference: 
gatewayv1alpha2.BackendObjectReference{
+                                                       Name: 
gatewayv1alpha2.ObjectName("backend-1"),
+                                               },
+                                       },
+                                       {
+                                               BackendObjectReference: 
gatewayv1alpha2.BackendObjectReference{
+                                                       Name: 
gatewayv1alpha2.ObjectName("backend-2"),
+                                               },
+                                       },
+                                       {
+                                               BackendObjectReference: 
gatewayv1alpha2.BackendObjectReference{
+                                                       Name: 
gatewayv1alpha2.ObjectName("missing-backend"),
+                                               },
+                                       },
+                               },
+                       }},
+               },
+       }
+
+       warnings, err := validator.ValidateCreate(context.Background(), route)
+       require.NoError(t, err)
+       assert.ElementsMatch(t, []string{
+               "Referenced Service 'default/missing-backend' not found",
+       }, warnings)
+}
diff --git a/test/e2e/framework/manifests/webhook.yaml 
b/test/e2e/framework/manifests/webhook.yaml
index 954ce72d..5d9198a0 100644
--- a/test/e2e/framework/manifests/webhook.yaml
+++ b/test/e2e/framework/manifests/webhook.yaml
@@ -251,3 +251,24 @@ webhooks:
         - tcproutes
   failurePolicy: Fail
   sideEffects: None
+- name: vudproute-v1alpha2.kb.io
+  clientConfig:
+    service:
+      name: webhook-service
+      namespace: {{ .Namespace }}
+      path: /validate-gateway-networking-k8s-io-v1alpha2-udproute
+    caBundle: {{ .CABundle }}
+  admissionReviewVersions:
+    - v1
+  rules:
+    - operations:
+        - CREATE
+        - UPDATE
+      apiGroups:
+        - gateway.networking.k8s.io
+      apiVersions:
+        - v1alpha2
+      resources:
+        - udproutes
+  failurePolicy: Fail
+  sideEffects: None
diff --git a/test/e2e/testdata/ldap/docker-compose.yaml 
b/test/e2e/testdata/ldap/docker-compose.yaml
index 364aef39..d36e5e7e 100644
--- a/test/e2e/testdata/ldap/docker-compose.yaml
+++ b/test/e2e/testdata/ldap/docker-compose.yaml
@@ -20,7 +20,7 @@ version: '3'
 services:
   openldap:
     container_name: openldap
-    image: docker.io/bitnami/openldap:2.6
+    image: docker.io/bitnamilegacy/openldap:2.6
     ports:
       - '1389:1389'
     environment:
diff --git a/test/e2e/webhook/helpers.go b/test/e2e/webhook/helpers.go
index 696e81df..1b21c8b7 100644
--- a/test/e2e/webhook/helpers.go
+++ b/test/e2e/webhook/helpers.go
@@ -36,6 +36,16 @@ type routeWebhookTestCase struct {
        servicePort     int
 }
 
+type simpleRouteWebhookTestCase struct {
+       routeKind       string
+       routeName       string
+       sectionName     string
+       missingService  string
+       servicePortName string
+       servicePort     int
+       serviceProtocol string
+}
+
 func setupGatewayResources(s *scaffold.Scaffold) {
        By("creating GatewayProxy")
        err := s.CreateResourceFromString(s.GetGatewayProxySpec())
@@ -83,6 +93,11 @@ spec:
        Expect(output).To(ContainSubstring(missingBackendWarning))
        Expect(output).To(ContainSubstring(mirrorBackendWarning))
 
+       By("delete the " + tc.routeKind)
+       err = s.DeleteResource(tc.routeKind, tc.routeName)
+       Expect(err).NotTo(HaveOccurred())
+       time.Sleep(2 * time.Second)
+
        By(fmt.Sprintf("creating referenced backend services for %s", 
tc.routeKind))
        serviceYAML := `
 apiVersion: v1
@@ -114,3 +129,109 @@ spec:
        Expect(output).NotTo(ContainSubstring(missingBackendWarning))
        Expect(output).NotTo(ContainSubstring(mirrorBackendWarning))
 }
+
+func setupSimpleGatewayWithProtocol(s *scaffold.Scaffold, protocol, 
listenerName string, port int) {
+       By("creating GatewayProxy")
+       err := s.CreateResourceFromString(s.GetGatewayProxySpec())
+       Expect(err).NotTo(HaveOccurred(), "creating GatewayProxy")
+       time.Sleep(5 * time.Second)
+
+       By("creating GatewayClass")
+       err = s.CreateResourceFromString(s.GetGatewayClassYaml())
+       Expect(err).NotTo(HaveOccurred(), "creating GatewayClass")
+       time.Sleep(2 * time.Second)
+
+       gatewayYAML := fmt.Sprintf(`
+apiVersion: gateway.networking.k8s.io/v1
+kind: Gateway
+metadata:
+  name: %s
+spec:
+  gatewayClassName: %s
+  listeners:
+  - name: %s
+    protocol: %s
+    port: %d
+    allowedRoutes:
+      kinds:
+      - kind: %sRoute
+  infrastructure:
+    parametersRef:
+      group: apisix.apache.org
+      kind: GatewayProxy
+      name: apisix-proxy-config
+`, s.Namespace(), s.Namespace(), listenerName, protocol, port, protocol)
+
+       By(fmt.Sprintf("creating Gateway with %s listener", protocol))
+       err = s.CreateResourceFromString(gatewayYAML)
+       Expect(err).NotTo(HaveOccurred(), fmt.Sprintf("creating %s-capable 
Gateway", protocol))
+       time.Sleep(5 * time.Second)
+}
+
+func verifySimpleRouteMissingBackendWarnings(s *scaffold.Scaffold, tc 
simpleRouteWebhookTestCase) {
+       gatewayName := s.Namespace()
+       routeYAML := fmt.Sprintf(`
+apiVersion: gateway.networking.k8s.io/v1alpha2
+kind: %s
+metadata:
+  name: %s
+spec:
+  parentRefs:
+  - name: %s
+    sectionName: %s
+  rules:
+  - backendRefs:
+    - name: %s
+      port: %d
+`, tc.routeKind, tc.routeName, gatewayName, tc.sectionName, tc.missingService, 
tc.servicePort)
+
+       missingBackendWarning := fmt.Sprintf("Warning: Referenced Service 
'%s/%s' not found", gatewayName, tc.missingService)
+
+       output, err := s.CreateResourceFromStringAndGetOutput(routeYAML)
+       Expect(err).ShouldNot(HaveOccurred())
+       Expect(output).To(ContainSubstring(missingBackendWarning))
+
+       By("delete the " + tc.routeKind)
+       err = s.DeleteResource(tc.routeKind, tc.routeName)
+       Expect(err).NotTo(HaveOccurred())
+       time.Sleep(2 * time.Second)
+
+       By("creating referenced backend service")
+       serviceYAML := `
+apiVersion: v1
+kind: Service
+metadata:
+  name: %s
+spec:
+  selector:
+    app: placeholder
+  ports:
+  - name: %s
+    port: %d
+    targetPort: %d`
+
+       if tc.serviceProtocol != "" {
+               serviceYAML += `
+    protocol: %s`
+       }
+
+       serviceYAML += `
+  type: ClusterIP
+`
+
+       var backendService string
+       if tc.serviceProtocol != "" {
+               backendService = fmt.Sprintf(serviceYAML, tc.missingService, 
tc.servicePortName, tc.servicePort, tc.servicePort, tc.serviceProtocol)
+       } else {
+               backendService = fmt.Sprintf(serviceYAML, tc.missingService, 
tc.servicePortName, tc.servicePort, tc.servicePort)
+       }
+
+       err = s.CreateResourceFromString(backendService)
+       Expect(err).NotTo(HaveOccurred(), fmt.Sprintf("creating %s backend 
service", tc.servicePortName))
+
+       time.Sleep(2 * time.Second)
+
+       output, err = s.CreateResourceFromStringAndGetOutput(routeYAML)
+       Expect(err).ShouldNot(HaveOccurred())
+       Expect(output).NotTo(ContainSubstring(missingBackendWarning))
+}
diff --git a/test/e2e/webhook/tcproute.go b/test/e2e/webhook/tcproute.go
index de226a33..b07c4c97 100644
--- a/test/e2e/webhook/tcproute.go
+++ b/test/e2e/webhook/tcproute.go
@@ -18,11 +18,7 @@
 package webhook
 
 import (
-       "fmt"
-       "time"
-
        . "github.com/onsi/ginkgo/v2"
-       . "github.com/onsi/gomega"
 
        "github.com/apache/apisix-ingress-controller/test/e2e/scaffold"
 )
@@ -33,89 +29,20 @@ var _ = Describe("Test TCPRoute Webhook", Label("webhook"), 
func() {
                EnableWebhook: true,
        })
 
-       const tcpGateway = `
-apiVersion: gateway.networking.k8s.io/v1
-kind: Gateway
-metadata:
-  name: %s
-spec:
-  gatewayClassName: %s
-  listeners:
-  - name: tcp
-    protocol: TCP
-    port: 9000
-    allowedRoutes:
-      kinds:
-      - kind: TCPRoute
-  infrastructure:
-    parametersRef:
-      group: apisix.apache.org
-      kind: GatewayProxy
-      name: apisix-proxy-config
-`
-
        BeforeEach(func() {
-               By("creating GatewayProxy")
-               err := s.CreateResourceFromString(s.GetGatewayProxySpec())
-               Expect(err).NotTo(HaveOccurred(), "creating GatewayProxy")
-               time.Sleep(5 * time.Second)
-
-               By("creating GatewayClass")
-               err = s.CreateResourceFromString(s.GetGatewayClassYaml())
-               Expect(err).NotTo(HaveOccurred(), "creating GatewayClass")
-               time.Sleep(2 * time.Second)
-
-               By("creating Gateway with TCP listener")
-               err = s.CreateResourceFromString(fmt.Sprintf(tcpGateway, 
s.Namespace(), s.Namespace()))
-               Expect(err).NotTo(HaveOccurred(), "creating TCP-capable 
Gateway")
-               time.Sleep(5 * time.Second)
+               setupSimpleGatewayWithProtocol(s, "TCP", "tcp", 9000)
        })
 
        It("should warn on missing backend services", func() {
-               missingService := "missing-tcp-backend"
-               routeName := "webhook-tcproute"
-               gatewayName := s.Namespace()
-               routeYAML := `
-apiVersion: gateway.networking.k8s.io/v1alpha2
-kind: TCPRoute
-metadata:
-  name: %s
-spec:
-  parentRefs:
-  - name: %s
-    sectionName: tcp
-  rules:
-  - backendRefs:
-    - name: %s
-      port: 80
-`
-
-               output, err := 
s.CreateResourceFromStringAndGetOutput(fmt.Sprintf(routeYAML, routeName, 
gatewayName, missingService))
-               Expect(err).ShouldNot(HaveOccurred())
-               Expect(output).To(ContainSubstring(fmt.Sprintf("Warning: 
Referenced Service '%s/%s' not found", s.Namespace(), missingService)))
-
-               By("creating referenced backend service")
-               backendService := fmt.Sprintf(`
-apiVersion: v1
-kind: Service
-metadata:
-  name: %s
-spec:
-  selector:
-    app: placeholder
-  ports:
-  - name: tcp
-    port: 80
-    targetPort: 80
-  type: ClusterIP
-`, missingService)
-               err = s.CreateResourceFromString(backendService)
-               Expect(err).NotTo(HaveOccurred(), "creating tcp backend 
service")
-
-               time.Sleep(2 * time.Second)
-
-               output, err = 
s.CreateResourceFromStringAndGetOutput(fmt.Sprintf(routeYAML, routeName, 
gatewayName, missingService))
-               Expect(err).ShouldNot(HaveOccurred())
-               Expect(output).NotTo(ContainSubstring(fmt.Sprintf("Warning: 
Referenced Service '%s/%s' not found", s.Namespace(), missingService)))
+               tc := simpleRouteWebhookTestCase{
+                       routeKind:       "TCPRoute",
+                       routeName:       "webhook-tcproute",
+                       sectionName:     "tcp",
+                       missingService:  "missing-tcp-backend",
+                       servicePortName: "tcp",
+                       servicePort:     80,
+                       serviceProtocol: "",
+               }
+               verifySimpleRouteMissingBackendWarnings(s, tc)
        })
 })
diff --git a/test/e2e/webhook/udproute.go b/test/e2e/webhook/udproute.go
new file mode 100644
index 00000000..8cdcd8a2
--- /dev/null
+++ b/test/e2e/webhook/udproute.go
@@ -0,0 +1,48 @@
+// Licensed to the Apache Software Foundation (ASF) under one
+// or more contributor license agreements.  See the NOTICE file
+// distributed with this work for additional information
+// regarding copyright ownership.  The ASF licenses this file
+// to you under the Apache License, Version 2.0 (the
+// "License"); you may not use this file except in compliance
+// with the License.  You may obtain a copy of the License at
+//
+//   http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing,
+// software distributed under the License is distributed on an
+// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+// KIND, either express or implied.  See the License for the
+// specific language governing permissions and limitations
+// under the License.
+
+package webhook
+
+import (
+       . "github.com/onsi/ginkgo/v2"
+
+       "github.com/apache/apisix-ingress-controller/test/e2e/scaffold"
+)
+
+var _ = Describe("Test UDPRoute Webhook", Label("webhook"), func() {
+       s := scaffold.NewScaffold(scaffold.Options{
+               Name:          "udproute-webhook-test",
+               EnableWebhook: true,
+       })
+
+       BeforeEach(func() {
+               setupSimpleGatewayWithProtocol(s, "UDP", "udp", 9000)
+       })
+
+       It("should warn on missing backend services", func() {
+               tc := simpleRouteWebhookTestCase{
+                       routeKind:       "UDPRoute",
+                       routeName:       "webhook-udproute",
+                       sectionName:     "udp",
+                       missingService:  "missing-udp-backend",
+                       servicePortName: "udp",
+                       servicePort:     53,
+                       serviceProtocol: "UDP",
+               }
+               verifySimpleRouteMissingBackendWarnings(s, tc)
+       })
+})

Reply via email to