This is an automated email from the ASF dual-hosted git repository. ronething pushed a commit to branch feat/add_udproute in repository https://gitbox.apache.org/repos/asf/apisix-ingress-controller.git
commit f1f94e78ffeb710dc9ca85cccba7dfda0227ec21 Author: Ashing Zheng <[email protected]> AuthorDate: Tue Sep 30 12:29:44 2025 +0800 feat: support udproute webhook 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 +++++++++++++++++++++++++++ 5 files changed, 449 insertions(+) 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) +}
