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)
+ })
+})