From 34391a86da8eb8db36d05abca5237ab58f7fff8e Mon Sep 17 00:00:00 2001 From: Ashing Zheng Date: Tue, 30 Sep 2025 17:03:40 +0800 Subject: [PATCH] feat: support udproute webhook (#2588) Signed-off-by: Ashing Zheng (cherry picked from commit 63c7d111a4fc4e557e570f34b2a452c9d4941bb2) --- config/webhook/manifests.yaml | 43 +++ internal/manager/webhooks.go | 15 + internal/webhook/v1/ownership.go | 110 ++++++++ internal/webhook/v1/udproute_webhook.go | 146 ++++++++++ internal/webhook/v1/udproute_webhook_test.go | 273 +++++++++++++++++++ test/e2e/framework/manifests/webhook.yaml | 45 +++ test/e2e/ingress/udproute.go | 48 ++++ test/e2e/testdata/ldap/docker-compose.yaml | 33 +++ test/e2e/webhook/helpers.go | 237 ++++++++++++++++ test/e2e/webhook/tcproute.go | 48 ++++ 10 files changed, 998 insertions(+) create mode 100644 internal/webhook/v1/ownership.go create mode 100644 internal/webhook/v1/udproute_webhook.go create mode 100644 internal/webhook/v1/udproute_webhook_test.go create mode 100644 test/e2e/ingress/udproute.go create mode 100644 test/e2e/testdata/ldap/docker-compose.yaml create mode 100644 test/e2e/webhook/helpers.go create mode 100644 test/e2e/webhook/tcproute.go diff --git a/config/webhook/manifests.yaml b/config/webhook/manifests.yaml index b5b33201..e3ad8331 100644 --- a/config/webhook/manifests.yaml +++ b/config/webhook/manifests.yaml @@ -164,3 +164,46 @@ webhooks: resources: - ingressclasses sideEffects: None +<<<<<<< HEAD +======= +- admissionReviewVersions: + - v1 + clientConfig: + service: + name: webhook-service + namespace: system + path: /validate-gateway-networking-k8s-io-v1alpha2-tcproute + failurePolicy: Fail + name: vtcproute-v1alpha2.kb.io + rules: + - apiGroups: + - gateway.networking.k8s.io + apiVersions: + - v1alpha2 + operations: + - CREATE + - UPDATE + 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 +>>>>>>> 63c7d111 (feat: support udproute webhook (#2588)) diff --git a/internal/manager/webhooks.go b/internal/manager/webhooks.go index 6907d762..e9250798 100644 --- a/internal/manager/webhooks.go +++ b/internal/manager/webhooks.go @@ -38,6 +38,21 @@ func setupWebhooks(_ context.Context, mgr manager.Manager) error { if err := webhookv1.SetupGatewayProxyWebhookWithManager(mgr); err != nil { return err } +<<<<<<< HEAD +======= + if err := webhookv1.SetupHTTPRouteWebhookWithManager(mgr); err != nil { + return err + } + if err := webhookv1.SetupGRPCRouteWebhookWithManager(mgr); err != nil { + return err + } + if err := webhookv1.SetupTCPRouteWebhookWithManager(mgr); err != nil { + return err + } + if err := webhookv1.SetupUDPRouteWebhookWithManager(mgr); err != nil { + return err + } +>>>>>>> 63c7d111 (feat: support udproute webhook (#2588)) if err := webhookv1.SetupApisixConsumerWebhookWithManager(mgr); err != nil { return err } diff --git a/internal/webhook/v1/ownership.go b/internal/webhook/v1/ownership.go new file mode 100644 index 00000000..f5d5d60f --- /dev/null +++ b/internal/webhook/v1/ownership.go @@ -0,0 +1,110 @@ +// 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" + + "sigs.k8s.io/controller-runtime/pkg/client" + 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" + internaltypes "github.com/apache/apisix-ingress-controller/internal/types" +) + +func isGatewayManaged(ctx context.Context, c client.Client, gateway *gatewayv1.Gateway) (bool, error) { + if gateway == nil { + return false, nil + } + + className := string(gateway.Spec.GatewayClassName) + if className == "" { + return false, nil + } + + var gatewayClass gatewayv1.GatewayClass + if err := c.Get(ctx, client.ObjectKey{Name: className}, &gatewayClass); err != nil { + if client.IgnoreNotFound(err) == nil { + return false, nil + } + return false, err + } + + return string(gatewayClass.Spec.ControllerName) == config.ControllerConfig.ControllerName, nil +} + +func isHTTPRouteManaged(ctx context.Context, c client.Client, route *gatewayv1.HTTPRoute) (bool, error) { + if route == nil { + return false, nil + } + return routeReferencesManagedGateway(ctx, c, route.Spec.ParentRefs, route.Namespace) +} + +func isGRPCRouteManaged(ctx context.Context, c client.Client, route *gatewayv1.GRPCRoute) (bool, error) { + if route == nil { + return false, nil + } + return routeReferencesManagedGateway(ctx, c, route.Spec.ParentRefs, route.Namespace) +} + +func isTCPRouteManaged(ctx context.Context, c client.Client, route *gatewayv1alpha2.TCPRoute) (bool, error) { + if route == nil { + return false, nil + } + 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 == "" { + continue + } + if parent.Kind != nil && string(*parent.Kind) != internaltypes.KindGateway { + continue + } + + namespace := defaultNamespace + if parent.Namespace != nil && *parent.Namespace != "" { + namespace = string(*parent.Namespace) + } + + var gateway gatewayv1.Gateway + if err := c.Get(ctx, client.ObjectKey{Namespace: namespace, Name: string(parent.Name)}, &gateway); err != nil { + if client.IgnoreNotFound(err) == nil { + continue + } + return false, err + } + + managed, err := isGatewayManaged(ctx, c, &gateway) + if err != nil { + return false, err + } + if managed { + return true, nil + } + } + + return false, nil +} 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 5e6b7517..a4c40405 100644 --- a/test/e2e/framework/manifests/webhook.yaml +++ b/test/e2e/framework/manifests/webhook.yaml @@ -188,3 +188,48 @@ webhooks: - ingressclasses failurePolicy: Fail sideEffects: None +<<<<<<< HEAD +======= +- name: vtcproute-v1alpha2.kb.io + clientConfig: + service: + name: webhook-service + namespace: {{ .Namespace }} + path: /validate-gateway-networking-k8s-io-v1alpha2-tcproute + caBundle: {{ .CABundle }} + admissionReviewVersions: + - v1 + rules: + - operations: + - CREATE + - UPDATE + apiGroups: + - gateway.networking.k8s.io + apiVersions: + - v1alpha2 + resources: + - 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 +>>>>>>> 63c7d111 (feat: support udproute webhook (#2588)) diff --git a/test/e2e/ingress/udproute.go b/test/e2e/ingress/udproute.go new file mode 100644 index 00000000..8cdcd8a2 --- /dev/null +++ b/test/e2e/ingress/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) + }) +}) diff --git a/test/e2e/testdata/ldap/docker-compose.yaml b/test/e2e/testdata/ldap/docker-compose.yaml new file mode 100644 index 00000000..d36e5e7e --- /dev/null +++ b/test/e2e/testdata/ldap/docker-compose.yaml @@ -0,0 +1,33 @@ +# +# 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. +# + +version: '3' + +services: + openldap: + container_name: openldap + image: docker.io/bitnamilegacy/openldap:2.6 + ports: + - '1389:1389' + environment: + - LDAP_PORT_NUMBER=1389 + - LDAP_ENABLE_TLS=no + - LDAP_ADMIN_USERNAME=admin + - LDAP_ADMIN_PASSWORD=admin + - LDAP_ROOT=dc=ldap,dc=example,dc=org + - LDAP_USERS=jack + - LDAP_PASSWORDS=jackPassword diff --git a/test/e2e/webhook/helpers.go b/test/e2e/webhook/helpers.go new file mode 100644 index 00000000..1b21c8b7 --- /dev/null +++ b/test/e2e/webhook/helpers.go @@ -0,0 +1,237 @@ +// 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 ( + "fmt" + "time" + + . "github.com/onsi/ginkgo/v2" + . "github.com/onsi/gomega" + + "github.com/apache/apisix-ingress-controller/test/e2e/scaffold" +) + +type routeWebhookTestCase struct { + routeKind string + routeName string + missingService string + mirrorService string + servicePortName string + 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()) + 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") + err = s.CreateResourceFromString(s.GetGatewayYaml()) + Expect(err).NotTo(HaveOccurred(), "creating Gateway") + time.Sleep(5 * time.Second) +} + +func verifyMissingBackendWarnings(s *scaffold.Scaffold, tc routeWebhookTestCase) { + gatewayName := s.Namespace() + routeYAML := fmt.Sprintf(` +apiVersion: gateway.networking.k8s.io/v1 +kind: %s +metadata: + name: %s +spec: + parentRefs: + - name: %s + rules: + - backendRefs: + - name: %s + port: %d + filters: + - type: RequestMirror + requestMirror: + backendRef: + name: %s + port: %d +`, tc.routeKind, tc.routeName, gatewayName, tc.missingService, tc.servicePort, tc.mirrorService, tc.servicePort) + + missingBackendWarning := fmt.Sprintf("Warning: Referenced Service '%s/%s' not found", gatewayName, tc.missingService) + mirrorBackendWarning := fmt.Sprintf("Warning: Referenced Service '%s/%s' not found", gatewayName, tc.mirrorService) + + output, err := s.CreateResourceFromStringAndGetOutput(routeYAML) + Expect(err).ShouldNot(HaveOccurred()) + 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 +kind: Service +metadata: + name: %s +spec: + selector: + app: placeholder + ports: + - name: %s + port: %d + targetPort: %d + type: ClusterIP +` + + backendService := fmt.Sprintf(serviceYAML, tc.missingService, tc.servicePortName, tc.servicePort, tc.servicePort) + err = s.CreateResourceFromString(backendService) + Expect(err).NotTo(HaveOccurred(), "creating primary backend service") + + mirrorService := fmt.Sprintf(serviceYAML, tc.mirrorService, tc.servicePortName, tc.servicePort, tc.servicePort) + err = s.CreateResourceFromString(mirrorService) + Expect(err).NotTo(HaveOccurred(), "creating mirror backend service") + + time.Sleep(2 * time.Second) + + output, err = s.CreateResourceFromStringAndGetOutput(routeYAML) + Expect(err).ShouldNot(HaveOccurred()) + 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 new file mode 100644 index 00000000..b07c4c97 --- /dev/null +++ b/test/e2e/webhook/tcproute.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 TCPRoute Webhook", Label("webhook"), func() { + s := scaffold.NewScaffold(scaffold.Options{ + Name: "tcproute-webhook-test", + EnableWebhook: true, + }) + + BeforeEach(func() { + setupSimpleGatewayWithProtocol(s, "TCP", "tcp", 9000) + }) + + It("should warn on missing backend services", func() { + tc := simpleRouteWebhookTestCase{ + routeKind: "TCPRoute", + routeName: "webhook-tcproute", + sectionName: "tcp", + missingService: "missing-tcp-backend", + servicePortName: "tcp", + servicePort: 80, + serviceProtocol: "", + } + verifySimpleRouteMissingBackendWarnings(s, tc) + }) +})