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 ec819175 feat: add secret/service resource checker for webhook (#2583)
ec819175 is described below

commit ec819175cff42354a55525efc9cd28f1a9e52c18
Author: Ashing Zheng <[email protected]>
AuthorDate: Mon Sep 29 14:15:44 2025 +0800

    feat: add secret/service resource checker for webhook (#2583)
    
    Signed-off-by: Ashing Zheng <[email protected]>
---
 config/webhook/manifests.yaml                      |  60 ++++++++
 internal/controller/grpcroute_controller.go        |   1 -
 internal/manager/webhooks.go                       |   9 ++
 internal/webhook/v1/apisixconsumer_webhook.go      |   8 +
 internal/webhook/v1/apisixconsumer_webhook_test.go |  23 ++-
 internal/webhook/v1/apisixroute_webhook.go         |   7 +
 internal/webhook/v1/apisixroute_webhook_test.go    |  24 ++-
 internal/webhook/v1/apisixtls_webhook.go           |   7 +
 internal/webhook/v1/apisixtls_webhook_test.go      |  23 ++-
 internal/webhook/v1/consumer_webhook.go            |   7 +
 internal/webhook/v1/consumer_webhook_test.go       |  26 +++-
 internal/webhook/v1/gateway_webhook.go             |  98 +++++++++++--
 internal/webhook/v1/gateway_webhook_test.go        | 113 ++++++++++++++
 internal/webhook/v1/grpcroute_webhook.go           | 162 +++++++++++++++++++++
 internal/webhook/v1/grpcroute_webhook_test.go      | 116 +++++++++++++++
 internal/webhook/v1/httproute_webhook.go           | 162 +++++++++++++++++++++
 internal/webhook/v1/httproute_webhook_test.go      | 138 ++++++++++++++++++
 internal/webhook/v1/ingress_webhook.go             |  96 ++++++++++--
 internal/webhook/v1/ingress_webhook_test.go        | 114 +++++++++++++--
 internal/webhook/v1/ownership.go                   | 103 +++++++++++++
 internal/webhook/v1/tcproute_webhook.go            | 146 +++++++++++++++++++
 internal/webhook/v1/tcproute_webhook_test.go       | 122 ++++++++++++++++
 22 files changed, 1513 insertions(+), 52 deletions(-)

diff --git a/config/webhook/manifests.yaml b/config/webhook/manifests.yaml
index b5b33201..e2ad06a3 100644
--- a/config/webhook/manifests.yaml
+++ b/config/webhook/manifests.yaml
@@ -124,6 +124,46 @@ webhooks:
     resources:
     - gatewayproxies
   sideEffects: None
+- admissionReviewVersions:
+  - v1
+  clientConfig:
+    service:
+      name: webhook-service
+      namespace: system
+      path: /validate-gateway-networking-k8s-io-v1-grpcroute
+  failurePolicy: Fail
+  name: vgrpcroute-v1.kb.io
+  rules:
+  - apiGroups:
+    - gateway.networking.k8s.io
+    apiVersions:
+    - v1
+    operations:
+    - CREATE
+    - UPDATE
+    resources:
+    - grpcroutes
+  sideEffects: None
+- admissionReviewVersions:
+  - v1
+  clientConfig:
+    service:
+      name: webhook-service
+      namespace: system
+      path: /validate-gateway-networking-k8s-io-v1-httproute
+  failurePolicy: Fail
+  name: vhttproute-v1.kb.io
+  rules:
+  - apiGroups:
+    - gateway.networking.k8s.io
+    apiVersions:
+    - v1
+    operations:
+    - CREATE
+    - UPDATE
+    resources:
+    - httproutes
+  sideEffects: None
 - admissionReviewVersions:
   - v1
   clientConfig:
@@ -164,3 +204,23 @@ webhooks:
     resources:
     - ingressclasses
   sideEffects: None
+- 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
diff --git a/internal/controller/grpcroute_controller.go 
b/internal/controller/grpcroute_controller.go
index 138011a9..782f98f8 100644
--- a/internal/controller/grpcroute_controller.go
+++ b/internal/controller/grpcroute_controller.go
@@ -297,7 +297,6 @@ func (r *GRPCRouteReconciler) 
listGRPCRoutesForBackendTrafficPolicy(ctx context.
                r.Log.Error(fmt.Errorf("unexpected object type"), "failed to 
convert object to BackendTrafficPolicy")
                return nil
        }
-
        grpcRouteList := []gatewayv1.GRPCRoute{}
        for _, targetRef := range policy.Spec.TargetRefs {
                service := &corev1.Service{}
diff --git a/internal/manager/webhooks.go b/internal/manager/webhooks.go
index 6907d762..a1bc1dae 100644
--- a/internal/manager/webhooks.go
+++ b/internal/manager/webhooks.go
@@ -38,6 +38,15 @@ func setupWebhooks(_ context.Context, mgr manager.Manager) 
error {
        if err := webhookv1.SetupGatewayProxyWebhookWithManager(mgr); err != 
nil {
                return err
        }
+       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.SetupApisixConsumerWebhookWithManager(mgr); err != 
nil {
                return err
        }
diff --git a/internal/webhook/v1/apisixconsumer_webhook.go 
b/internal/webhook/v1/apisixconsumer_webhook.go
index 83ffb699..ad22d4b2 100644
--- a/internal/webhook/v1/apisixconsumer_webhook.go
+++ b/internal/webhook/v1/apisixconsumer_webhook.go
@@ -29,6 +29,7 @@ import (
        "sigs.k8s.io/controller-runtime/pkg/webhook/admission"
 
        apisixv2 "github.com/apache/apisix-ingress-controller/api/v2"
+       "github.com/apache/apisix-ingress-controller/internal/controller"
        
"github.com/apache/apisix-ingress-controller/internal/webhook/v1/reference"
 )
 
@@ -64,6 +65,10 @@ func (v *ApisixConsumerCustomValidator) ValidateCreate(ctx 
context.Context, obj
        }
        apisixConsumerLog.Info("Validation for ApisixConsumer upon creation", 
"name", consumer.GetName(), "namespace", consumer.GetNamespace())
 
+       if !controller.MatchesIngressClass(v.Client, apisixConsumerLog, 
consumer) {
+               return nil, nil
+       }
+
        return v.collectWarnings(ctx, consumer), nil
 }
 
@@ -73,6 +78,9 @@ func (v *ApisixConsumerCustomValidator) ValidateUpdate(ctx 
context.Context, oldO
                return nil, fmt.Errorf("expected an ApisixConsumer object for 
the newObj but got %T", newObj)
        }
        apisixConsumerLog.Info("Validation for ApisixConsumer upon update", 
"name", consumer.GetName(), "namespace", consumer.GetNamespace())
+       if !controller.MatchesIngressClass(v.Client, apisixConsumerLog, 
consumer) {
+               return nil, nil
+       }
 
        return v.collectWarnings(ctx, consumer), nil
 }
diff --git a/internal/webhook/v1/apisixconsumer_webhook_test.go 
b/internal/webhook/v1/apisixconsumer_webhook_test.go
index 1fe9d9fb..8c31768c 100644
--- a/internal/webhook/v1/apisixconsumer_webhook_test.go
+++ b/internal/webhook/v1/apisixconsumer_webhook_test.go
@@ -21,12 +21,14 @@ import (
 
        "github.com/stretchr/testify/require"
        corev1 "k8s.io/api/core/v1"
+       networkingv1 "k8s.io/api/networking/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"
 
        apisixv2 "github.com/apache/apisix-ingress-controller/api/v2"
+       "github.com/apache/apisix-ingress-controller/internal/controller/config"
 )
 
 func buildApisixConsumerValidator(t *testing.T, objects ...runtime.Object) 
*ApisixConsumerCustomValidator {
@@ -34,12 +36,24 @@ func buildApisixConsumerValidator(t *testing.T, objects 
...runtime.Object) *Apis
 
        scheme := runtime.NewScheme()
        require.NoError(t, clientgoscheme.AddToScheme(scheme))
+       require.NoError(t, networkingv1.AddToScheme(scheme))
        require.NoError(t, apisixv2.AddToScheme(scheme))
 
-       builder := fake.NewClientBuilder().WithScheme(scheme)
-       if len(objects) > 0 {
-               builder = builder.WithRuntimeObjects(objects...)
+       managed := []runtime.Object{
+               &networkingv1.IngressClass{
+                       ObjectMeta: metav1.ObjectMeta{
+                               Name: "apisix",
+                               Annotations: map[string]string{
+                                       
"ingressclass.kubernetes.io/is-default-class": "true",
+                               },
+                       },
+                       Spec: networkingv1.IngressClassSpec{
+                               Controller: 
config.ControllerConfig.ControllerName,
+                       },
+               },
        }
+       allObjects := append(managed, objects...)
+       builder := 
fake.NewClientBuilder().WithScheme(scheme).WithRuntimeObjects(allObjects...)
 
        return NewApisixConsumerCustomValidator(builder.Build())
 }
@@ -51,6 +65,7 @@ func TestApisixConsumerValidator_MissingBasicAuthSecret(t 
*testing.T) {
                        Namespace: "default",
                },
                Spec: apisixv2.ApisixConsumerSpec{
+                       IngressClassName: "apisix",
                        AuthParameter: apisixv2.ApisixConsumerAuthParameter{
                                BasicAuth: &apisixv2.ApisixConsumerBasicAuth{
                                        SecretRef: 
&corev1.LocalObjectReference{Name: "basic-auth"},
@@ -74,6 +89,7 @@ func TestApisixConsumerValidator_MultipleSecretWarnings(t 
*testing.T) {
                        Namespace: "default",
                },
                Spec: apisixv2.ApisixConsumerSpec{
+                       IngressClassName: "apisix",
                        AuthParameter: apisixv2.ApisixConsumerAuthParameter{
                                BasicAuth: &apisixv2.ApisixConsumerBasicAuth{
                                        SecretRef: 
&corev1.LocalObjectReference{Name: "basic-auth"},
@@ -113,6 +129,7 @@ func 
TestApisixConsumerValidator_NoWarningsWhenSecretsExist(t *testing.T) {
                        Namespace: "default",
                },
                Spec: apisixv2.ApisixConsumerSpec{
+                       IngressClassName: "apisix",
                        AuthParameter: apisixv2.ApisixConsumerAuthParameter{
                                KeyAuth: &apisixv2.ApisixConsumerKeyAuth{
                                        SecretRef: 
&corev1.LocalObjectReference{Name: "key-auth"},
diff --git a/internal/webhook/v1/apisixroute_webhook.go 
b/internal/webhook/v1/apisixroute_webhook.go
index 6ad83c6b..aeb801ec 100644
--- a/internal/webhook/v1/apisixroute_webhook.go
+++ b/internal/webhook/v1/apisixroute_webhook.go
@@ -28,6 +28,7 @@ import (
        "sigs.k8s.io/controller-runtime/pkg/webhook/admission"
 
        apisixv2 "github.com/apache/apisix-ingress-controller/api/v2"
+       "github.com/apache/apisix-ingress-controller/internal/controller"
        
"github.com/apache/apisix-ingress-controller/internal/webhook/v1/reference"
 )
 
@@ -62,6 +63,9 @@ func (v *ApisixRouteCustomValidator) ValidateCreate(ctx 
context.Context, obj run
                return nil, fmt.Errorf("expected an ApisixRoute object but got 
%T", obj)
        }
        apisixRouteLog.Info("Validation for ApisixRoute upon creation", "name", 
route.GetName(), "namespace", route.GetNamespace())
+       if !controller.MatchesIngressClass(v.Client, apisixRouteLog, route) {
+               return nil, nil
+       }
 
        return v.collectWarnings(ctx, route), nil
 }
@@ -72,6 +76,9 @@ func (v *ApisixRouteCustomValidator) ValidateUpdate(ctx 
context.Context, oldObj,
                return nil, fmt.Errorf("expected an ApisixRoute object for the 
newObj but got %T", newObj)
        }
        apisixRouteLog.Info("Validation for ApisixRoute upon update", "name", 
route.GetName(), "namespace", route.GetNamespace())
+       if !controller.MatchesIngressClass(v.Client, apisixRouteLog, route) {
+               return nil, nil
+       }
 
        return v.collectWarnings(ctx, route), nil
 }
diff --git a/internal/webhook/v1/apisixroute_webhook_test.go 
b/internal/webhook/v1/apisixroute_webhook_test.go
index cd60fb99..b8ca3aa2 100644
--- a/internal/webhook/v1/apisixroute_webhook_test.go
+++ b/internal/webhook/v1/apisixroute_webhook_test.go
@@ -21,12 +21,14 @@ import (
 
        "github.com/stretchr/testify/require"
        corev1 "k8s.io/api/core/v1"
+       networkingv1 "k8s.io/api/networking/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"
 
        apisixv2 "github.com/apache/apisix-ingress-controller/api/v2"
+       "github.com/apache/apisix-ingress-controller/internal/controller/config"
 )
 
 func buildApisixRouteValidator(t *testing.T, objects ...runtime.Object) 
*ApisixRouteCustomValidator {
@@ -34,12 +36,24 @@ func buildApisixRouteValidator(t *testing.T, objects 
...runtime.Object) *ApisixR
 
        scheme := runtime.NewScheme()
        require.NoError(t, clientgoscheme.AddToScheme(scheme))
+       require.NoError(t, networkingv1.AddToScheme(scheme))
        require.NoError(t, apisixv2.AddToScheme(scheme))
 
-       builder := fake.NewClientBuilder().WithScheme(scheme)
-       if len(objects) > 0 {
-               builder = builder.WithRuntimeObjects(objects...)
+       managed := []runtime.Object{
+               &networkingv1.IngressClass{
+                       ObjectMeta: metav1.ObjectMeta{
+                               Name: "apisix",
+                               Annotations: map[string]string{
+                                       
"ingressclass.kubernetes.io/is-default-class": "true",
+                               },
+                       },
+                       Spec: networkingv1.IngressClassSpec{
+                               Controller: 
config.ControllerConfig.ControllerName,
+                       },
+               },
        }
+       allObjects := append(managed, objects...)
+       builder := 
fake.NewClientBuilder().WithScheme(scheme).WithRuntimeObjects(allObjects...)
 
        return NewApisixRouteCustomValidator(builder.Build())
 }
@@ -51,6 +65,7 @@ func TestApisixRouteValidator_MissingHTTPService(t 
*testing.T) {
                        Namespace: "default",
                },
                Spec: apisixv2.ApisixRouteSpec{
+                       IngressClassName: "apisix",
                        HTTP: []apisixv2.ApisixRouteHTTP{{
                                Name: "rule",
                                Backends: []apisixv2.ApisixRouteHTTPBackend{{
@@ -75,6 +90,7 @@ func TestApisixRouteValidator_MissingPluginSecret(t 
*testing.T) {
                        Namespace: "default",
                },
                Spec: apisixv2.ApisixRouteSpec{
+                       IngressClassName: "apisix",
                        HTTP: []apisixv2.ApisixRouteHTTP{{
                                Name: "rule",
                                Backends: []apisixv2.ApisixRouteHTTPBackend{{
@@ -106,6 +122,7 @@ func TestApisixRouteValidator_MissingStreamService(t 
*testing.T) {
                        Namespace: "default",
                },
                Spec: apisixv2.ApisixRouteSpec{
+                       IngressClassName: "apisix",
                        Stream: []apisixv2.ApisixRouteStream{{
                                Name:     "stream",
                                Protocol: "TCP",
@@ -131,6 +148,7 @@ func TestApisixRouteValidator_NoWarnings(t *testing.T) {
                        Namespace: "default",
                },
                Spec: apisixv2.ApisixRouteSpec{
+                       IngressClassName: "apisix",
                        HTTP: []apisixv2.ApisixRouteHTTP{{
                                Name: "rule",
                                Backends: []apisixv2.ApisixRouteHTTPBackend{{
diff --git a/internal/webhook/v1/apisixtls_webhook.go 
b/internal/webhook/v1/apisixtls_webhook.go
index fe3faf7d..16bcf88f 100644
--- a/internal/webhook/v1/apisixtls_webhook.go
+++ b/internal/webhook/v1/apisixtls_webhook.go
@@ -28,6 +28,7 @@ import (
        "sigs.k8s.io/controller-runtime/pkg/webhook/admission"
 
        apisixv2 "github.com/apache/apisix-ingress-controller/api/v2"
+       "github.com/apache/apisix-ingress-controller/internal/controller"
        
"github.com/apache/apisix-ingress-controller/internal/webhook/v1/reference"
 )
 
@@ -62,6 +63,9 @@ func (v *ApisixTlsCustomValidator) ValidateCreate(ctx 
context.Context, obj runti
                return nil, fmt.Errorf("expected an ApisixTls object but got 
%T", obj)
        }
        apisixTlsLog.Info("Validation for ApisixTls upon creation", "name", 
tls.GetName(), "namespace", tls.GetNamespace())
+       if !controller.MatchesIngressClass(v.Client, apisixTlsLog, tls) {
+               return nil, nil
+       }
 
        return v.collectWarnings(ctx, tls), nil
 }
@@ -72,6 +76,9 @@ func (v *ApisixTlsCustomValidator) ValidateUpdate(ctx 
context.Context, oldObj, n
                return nil, fmt.Errorf("expected an ApisixTls object for the 
newObj but got %T", newObj)
        }
        apisixTlsLog.Info("Validation for ApisixTls upon update", "name", 
tls.GetName(), "namespace", tls.GetNamespace())
+       if !controller.MatchesIngressClass(v.Client, apisixTlsLog, tls) {
+               return nil, nil
+       }
 
        return v.collectWarnings(ctx, tls), nil
 }
diff --git a/internal/webhook/v1/apisixtls_webhook_test.go 
b/internal/webhook/v1/apisixtls_webhook_test.go
index 45a764a5..205236f6 100644
--- a/internal/webhook/v1/apisixtls_webhook_test.go
+++ b/internal/webhook/v1/apisixtls_webhook_test.go
@@ -21,12 +21,14 @@ import (
 
        "github.com/stretchr/testify/require"
        corev1 "k8s.io/api/core/v1"
+       networkingv1 "k8s.io/api/networking/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"
 
        apisixv2 "github.com/apache/apisix-ingress-controller/api/v2"
+       "github.com/apache/apisix-ingress-controller/internal/controller/config"
 )
 
 func buildApisixTlsValidator(t *testing.T, objects ...runtime.Object) 
*ApisixTlsCustomValidator {
@@ -34,12 +36,24 @@ func buildApisixTlsValidator(t *testing.T, objects 
...runtime.Object) *ApisixTls
 
        scheme := runtime.NewScheme()
        require.NoError(t, clientgoscheme.AddToScheme(scheme))
+       require.NoError(t, networkingv1.AddToScheme(scheme))
        require.NoError(t, apisixv2.AddToScheme(scheme))
 
-       builder := fake.NewClientBuilder().WithScheme(scheme)
-       if len(objects) > 0 {
-               builder = builder.WithRuntimeObjects(objects...)
+       managed := []runtime.Object{
+               &networkingv1.IngressClass{
+                       ObjectMeta: metav1.ObjectMeta{
+                               Name: "apisix",
+                               Annotations: map[string]string{
+                                       
"ingressclass.kubernetes.io/is-default-class": "true",
+                               },
+                       },
+                       Spec: networkingv1.IngressClassSpec{
+                               Controller: 
config.ControllerConfig.ControllerName,
+                       },
+               },
        }
+       allObjects := append(managed, objects...)
+       builder := 
fake.NewClientBuilder().WithScheme(scheme).WithRuntimeObjects(allObjects...)
 
        return NewApisixTlsCustomValidator(builder.Build())
 }
@@ -51,7 +65,8 @@ func newApisixTls() *apisixv2.ApisixTls {
                        Namespace: "default",
                },
                Spec: apisixv2.ApisixTlsSpec{
-                       Hosts: []apisixv2.HostType{"example.com"},
+                       IngressClassName: "apisix",
+                       Hosts:            []apisixv2.HostType{"example.com"},
                        Secret: apisixv2.ApisixSecret{
                                Name:      "server-cert",
                                Namespace: "default",
diff --git a/internal/webhook/v1/consumer_webhook.go 
b/internal/webhook/v1/consumer_webhook.go
index 401550ef..0b74c714 100644
--- a/internal/webhook/v1/consumer_webhook.go
+++ b/internal/webhook/v1/consumer_webhook.go
@@ -28,6 +28,7 @@ import (
        "sigs.k8s.io/controller-runtime/pkg/webhook/admission"
 
        apisixv1alpha1 
"github.com/apache/apisix-ingress-controller/api/v1alpha1"
+       "github.com/apache/apisix-ingress-controller/internal/controller"
        
"github.com/apache/apisix-ingress-controller/internal/webhook/v1/reference"
 )
 
@@ -62,6 +63,9 @@ func (v *ConsumerCustomValidator) ValidateCreate(ctx 
context.Context, obj runtim
                return nil, fmt.Errorf("expected a Consumer object but got %T", 
obj)
        }
        consumerLog.Info("Validation for Consumer upon creation", "name", 
consumer.GetName(), "namespace", consumer.GetNamespace())
+       if !controller.MatchConsumerGatewayRef(ctx, v.Client, consumerLog, 
consumer) {
+               return nil, nil
+       }
 
        return v.collectWarnings(ctx, consumer), nil
 }
@@ -72,6 +76,9 @@ func (v *ConsumerCustomValidator) ValidateUpdate(ctx 
context.Context, oldObj, ne
                return nil, fmt.Errorf("expected a Consumer object for the 
newObj but got %T", newObj)
        }
        consumerLog.Info("Validation for Consumer upon update", "name", 
consumer.GetName(), "namespace", consumer.GetNamespace())
+       if !controller.MatchConsumerGatewayRef(ctx, v.Client, consumerLog, 
consumer) {
+               return nil, nil
+       }
 
        return v.collectWarnings(ctx, consumer), nil
 }
diff --git a/internal/webhook/v1/consumer_webhook_test.go 
b/internal/webhook/v1/consumer_webhook_test.go
index ed6489ff..045bc12b 100644
--- a/internal/webhook/v1/consumer_webhook_test.go
+++ b/internal/webhook/v1/consumer_webhook_test.go
@@ -25,8 +25,10 @@ import (
        "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"
 
        apisixv1alpha1 
"github.com/apache/apisix-ingress-controller/api/v1alpha1"
+       "github.com/apache/apisix-ingress-controller/internal/controller/config"
 )
 
 func buildConsumerValidator(t *testing.T, objects ...runtime.Object) 
*ConsumerCustomValidator {
@@ -35,11 +37,24 @@ func buildConsumerValidator(t *testing.T, objects 
...runtime.Object) *ConsumerCu
        scheme := runtime.NewScheme()
        require.NoError(t, clientgoscheme.AddToScheme(scheme))
        require.NoError(t, apisixv1alpha1.AddToScheme(scheme))
-
-       builder := fake.NewClientBuilder().WithScheme(scheme)
-       if len(objects) > 0 {
-               builder = builder.WithRuntimeObjects(objects...)
+       require.NoError(t, gatewayv1.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 NewConsumerCustomValidator(builder.Build())
 }
@@ -51,6 +66,7 @@ func TestConsumerValidator_MissingSecretDefaultNamespace(t 
*testing.T) {
                        Namespace: "default",
                },
                Spec: apisixv1alpha1.ConsumerSpec{
+                       GatewayRef: apisixv1alpha1.GatewayRef{Name: 
"test-gateway"},
                        Credentials: []apisixv1alpha1.Credential{{
                                Type: "jwt-auth",
                                SecretRef: &apisixv1alpha1.SecretReference{
@@ -76,6 +92,7 @@ func TestConsumerValidator_MissingSecretCustomNamespace(t 
*testing.T) {
                        Namespace: "default",
                },
                Spec: apisixv1alpha1.ConsumerSpec{
+                       GatewayRef: apisixv1alpha1.GatewayRef{Name: 
"test-gateway"},
                        Credentials: []apisixv1alpha1.Credential{{
                                Type: "jwt-auth",
                                SecretRef: &apisixv1alpha1.SecretReference{
@@ -102,6 +119,7 @@ func TestConsumerValidator_NoWarnings(t *testing.T) {
                        Namespace: "default",
                },
                Spec: apisixv1alpha1.ConsumerSpec{
+                       GatewayRef: apisixv1alpha1.GatewayRef{Name: 
"test-gateway"},
                        Credentials: []apisixv1alpha1.Credential{{
                                Type: "jwt-auth",
                                SecretRef: &apisixv1alpha1.SecretReference{
diff --git a/internal/webhook/v1/gateway_webhook.go 
b/internal/webhook/v1/gateway_webhook.go
index e2c11ff4..bb21b236 100644
--- a/internal/webhook/v1/gateway_webhook.go
+++ b/internal/webhook/v1/gateway_webhook.go
@@ -19,18 +19,20 @@ import (
        "context"
        "fmt"
 
+       corev1 "k8s.io/api/core/v1"
        k8serrors "k8s.io/apimachinery/pkg/api/errors"
        "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"
-       gatewaynetworkingk8siov1 "sigs.k8s.io/gateway-api/apis/v1"
+       gatewayv1 "sigs.k8s.io/gateway-api/apis/v1"
 
        v1alpha1 "github.com/apache/apisix-ingress-controller/api/v1alpha1"
-       "github.com/apache/apisix-ingress-controller/internal/controller/config"
        internaltypes 
"github.com/apache/apisix-ingress-controller/internal/types"
+       
"github.com/apache/apisix-ingress-controller/internal/webhook/v1/reference"
 )
 
 // nolint:unused
@@ -39,8 +41,8 @@ var gatewaylog = logf.Log.WithName("gateway-resource")
 
 // SetupGatewayWebhookWithManager registers the webhook for Gateway in the 
manager.
 func SetupGatewayWebhookWithManager(mgr ctrl.Manager) error {
-       return 
ctrl.NewWebhookManagedBy(mgr).For(&gatewaynetworkingk8siov1.Gateway{}).
-               WithValidator(&GatewayCustomValidator{Client: mgr.GetClient()}).
+       return ctrl.NewWebhookManagedBy(mgr).For(&gatewayv1.Gateway{}).
+               WithValidator(NewGatewayCustomValidator(mgr.GetClient())).
                Complete()
 }
 
@@ -54,33 +56,61 @@ func SetupGatewayWebhookWithManager(mgr ctrl.Manager) error 
{
 // NOTE: The +kubebuilder:object:generate=false marker prevents controller-gen 
from generating DeepCopy methods,
 // as this struct is used only for temporary operations and does not need to 
be deeply copied.
 type GatewayCustomValidator struct {
-       Client client.Client
+       Client  client.Client
+       checker reference.Checker
 }
 
 var _ webhook.CustomValidator = &GatewayCustomValidator{}
 
+func NewGatewayCustomValidator(c client.Client) *GatewayCustomValidator {
+       return &GatewayCustomValidator{
+               Client:  c,
+               checker: reference.NewChecker(c, gatewaylog),
+       }
+}
+
 // ValidateCreate implements webhook.CustomValidator so a webhook will be 
registered for the type Gateway.
 func (v *GatewayCustomValidator) ValidateCreate(ctx context.Context, obj 
runtime.Object) (admission.Warnings, error) {
-       gateway, ok := obj.(*gatewaynetworkingk8siov1.Gateway)
+       gateway, ok := obj.(*gatewayv1.Gateway)
        if !ok {
                return nil, fmt.Errorf("expected a Gateway object but got %T", 
obj)
        }
        gatewaylog.Info("Validation for Gateway upon creation", "name", 
gateway.GetName())
 
+       managed, err := isGatewayManaged(ctx, v.Client, gateway)
+       if err != nil {
+               gatewaylog.Error(err, "failed to decide controller ownership", 
"name", gateway.GetName(), "namespace", gateway.GetNamespace())
+               return nil, nil
+       }
+       if !managed {
+               return nil, nil
+       }
+
        warnings := v.warnIfMissingGatewayProxyForGateway(ctx, gateway)
+       warnings = append(warnings, v.collectReferenceWarnings(ctx, gateway)...)
 
        return warnings, nil
 }
 
 // ValidateUpdate implements webhook.CustomValidator so a webhook will be 
registered for the type Gateway.
 func (v *GatewayCustomValidator) ValidateUpdate(ctx context.Context, oldObj, 
newObj runtime.Object) (admission.Warnings, error) {
-       gateway, ok := newObj.(*gatewaynetworkingk8siov1.Gateway)
+       gateway, ok := newObj.(*gatewayv1.Gateway)
        if !ok {
                return nil, fmt.Errorf("expected a Gateway object for the 
newObj but got %T", newObj)
        }
        gatewaylog.Info("Validation for Gateway upon update", "name", 
gateway.GetName())
 
+       managed, err := isGatewayManaged(ctx, v.Client, gateway)
+       if err != nil {
+               gatewaylog.Error(err, "failed to decide controller ownership", 
"name", gateway.GetName(), "namespace", gateway.GetNamespace())
+               return nil, nil
+       }
+       if !managed {
+               return nil, nil
+       }
+
        warnings := v.warnIfMissingGatewayProxyForGateway(ctx, gateway)
+       warnings = append(warnings, v.collectReferenceWarnings(ctx, gateway)...)
 
        return warnings, nil
 }
@@ -90,20 +120,56 @@ func (v *GatewayCustomValidator) ValidateDelete(_ 
context.Context, obj runtime.O
        return nil, nil
 }
 
-func (v *GatewayCustomValidator) warnIfMissingGatewayProxyForGateway(ctx 
context.Context, gateway *gatewaynetworkingk8siov1.Gateway) admission.Warnings {
+func (v *GatewayCustomValidator) collectReferenceWarnings(ctx context.Context, 
gateway *gatewayv1.Gateway) admission.Warnings {
+       if gateway == nil {
+               return nil
+       }
+
        var warnings admission.Warnings
+       secretVisited := make(map[types.NamespacedName]struct{})
 
-       // get gateway class
-       gatewayClass := &gatewaynetworkingk8siov1.GatewayClass{}
-       if err := v.Client.Get(ctx, client.ObjectKey{Name: 
string(gateway.Spec.GatewayClassName)}, gatewayClass); err != nil {
-               gatewaylog.Error(err, "failed to get gateway class", "gateway", 
gateway.GetName(), "gatewayclass", gateway.Spec.GatewayClassName)
-               return nil
+       addSecretWarning := func(nn types.NamespacedName) {
+               if nn.Name == "" || nn.Namespace == "" {
+                       return
+               }
+               if _, seen := secretVisited[nn]; seen {
+                       return
+               }
+               secretVisited[nn] = struct{}{}
+               warnings = append(warnings, v.checker.Secret(ctx, 
reference.SecretRef{
+                       Object:         gateway,
+                       NamespacedName: nn,
+               })...)
        }
-       // match controller
-       if string(gatewayClass.Spec.ControllerName) != 
config.ControllerConfig.ControllerName {
-               return nil
+
+       for _, listener := range gateway.Spec.Listeners {
+               if listener.TLS == nil {
+                       continue
+               }
+               for _, ref := range listener.TLS.CertificateRefs {
+                       if ref.Kind != nil && *ref.Kind != 
internaltypes.KindSecret {
+                               continue
+                       }
+                       if ref.Group != nil && string(*ref.Group) != 
corev1.GroupName {
+                               continue
+                       }
+                       nn := types.NamespacedName{
+                               Namespace: gateway.GetNamespace(),
+                               Name:      string(ref.Name),
+                       }
+                       if ref.Namespace != nil && *ref.Namespace != "" {
+                               nn.Namespace = string(*ref.Namespace)
+                       }
+                       addSecretWarning(nn)
+               }
        }
 
+       return warnings
+}
+
+func (v *GatewayCustomValidator) warnIfMissingGatewayProxyForGateway(ctx 
context.Context, gateway *gatewayv1.Gateway) admission.Warnings {
+       var warnings admission.Warnings
+
        infra := gateway.Spec.Infrastructure
        if infra == nil || infra.ParametersRef == nil {
                return nil
diff --git a/internal/webhook/v1/gateway_webhook_test.go 
b/internal/webhook/v1/gateway_webhook_test.go
new file mode 100644
index 00000000..c47be6bc
--- /dev/null
+++ b/internal/webhook/v1/gateway_webhook_test.go
@@ -0,0 +1,113 @@
+// 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"
+
+       "github.com/apache/apisix-ingress-controller/internal/controller/config"
+)
+
+func buildGatewayValidator(t *testing.T, objects ...runtime.Object) 
*GatewayCustomValidator {
+       t.Helper()
+
+       scheme := runtime.NewScheme()
+       require.NoError(t, clientgoscheme.AddToScheme(scheme))
+       require.NoError(t, gatewayv1.Install(scheme))
+
+       builder := fake.NewClientBuilder().WithScheme(scheme)
+       if len(objects) > 0 {
+               builder = builder.WithRuntimeObjects(objects...)
+       }
+
+       return NewGatewayCustomValidator(builder.Build())
+}
+
+func TestGatewayCustomValidator_WarnsWhenTLSSecretMissing(t *testing.T) {
+       className := gatewayv1.ObjectName("apisix")
+       gatewayClass := &gatewayv1.GatewayClass{
+               ObjectMeta: metav1.ObjectMeta{Name: string(className)},
+               Spec: gatewayv1.GatewayClassSpec{
+                       ControllerName: 
gatewayv1.GatewayController(config.ControllerConfig.ControllerName),
+               },
+       }
+       validator := buildGatewayValidator(t, gatewayClass)
+
+       gateway := &gatewayv1.Gateway{
+               ObjectMeta: metav1.ObjectMeta{Name: "example", Namespace: 
"default"},
+               Spec: gatewayv1.GatewaySpec{
+                       GatewayClassName: className,
+                       Listeners: []gatewayv1.Listener{{
+                               Name:     "https",
+                               Port:     443,
+                               Protocol: gatewayv1.HTTPSProtocolType,
+                               TLS: &gatewayv1.GatewayTLSConfig{
+                                       CertificateRefs: 
[]gatewayv1.SecretObjectReference{{
+                                               Name: "missing-cert",
+                                       }},
+                               },
+                       }},
+               },
+       }
+
+       warnings, err := validator.ValidateCreate(context.Background(), gateway)
+       require.NoError(t, err)
+       require.Len(t, warnings, 1)
+       assert.Equal(t, warnings[0], "Referenced Secret 'default/missing-cert' 
not found")
+}
+
+func TestGatewayCustomValidator_NoWarningsWhenSecretExists(t *testing.T) {
+       className := gatewayv1.ObjectName("apisix")
+       gatewayClass := &gatewayv1.GatewayClass{
+               ObjectMeta: metav1.ObjectMeta{Name: string(className)},
+               Spec: gatewayv1.GatewayClassSpec{
+                       ControllerName: 
gatewayv1.GatewayController(config.ControllerConfig.ControllerName),
+               },
+       }
+       secret := &corev1.Secret{ObjectMeta: metav1.ObjectMeta{Name: 
"tls-cert", Namespace: "default"}}
+       validator := buildGatewayValidator(t, gatewayClass, secret)
+
+       gateway := &gatewayv1.Gateway{
+               ObjectMeta: metav1.ObjectMeta{Name: "example", Namespace: 
"default"},
+               Spec: gatewayv1.GatewaySpec{
+                       GatewayClassName: className,
+                       Listeners: []gatewayv1.Listener{{
+                               Name:     "https",
+                               Port:     443,
+                               Protocol: gatewayv1.HTTPSProtocolType,
+                               TLS: &gatewayv1.GatewayTLSConfig{
+                                       CertificateRefs: 
[]gatewayv1.SecretObjectReference{{
+                                               Name: "tls-cert",
+                                       }},
+                               },
+                       }},
+               },
+       }
+
+       warnings, err := validator.ValidateCreate(context.Background(), gateway)
+       require.NoError(t, err)
+       assert.Empty(t, warnings)
+}
diff --git a/internal/webhook/v1/grpcroute_webhook.go 
b/internal/webhook/v1/grpcroute_webhook.go
new file mode 100644
index 00000000..26d0e9f3
--- /dev/null
+++ b/internal/webhook/v1/grpcroute_webhook.go
@@ -0,0 +1,162 @@
+// 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"
+       gatewayv1 "sigs.k8s.io/gateway-api/apis/v1"
+
+       internaltypes 
"github.com/apache/apisix-ingress-controller/internal/types"
+       
"github.com/apache/apisix-ingress-controller/internal/webhook/v1/reference"
+)
+
+var grpcRouteLog = logf.Log.WithName("grpcroute-resource")
+
+func SetupGRPCRouteWebhookWithManager(mgr ctrl.Manager) error {
+       return ctrl.NewWebhookManagedBy(mgr).
+               For(&gatewayv1.GRPCRoute{}).
+               WithValidator(NewGRPCRouteCustomValidator(mgr.GetClient())).
+               Complete()
+}
+
+// 
+kubebuilder:webhook:path=/validate-gateway-networking-k8s-io-v1-grpcroute,mutating=false,failurePolicy=fail,sideEffects=None,groups=gateway.networking.k8s.io,resources=grpcroutes,verbs=create;update,versions=v1,name=vgrpcroute-v1.kb.io,admissionReviewVersions=v1
+
+type GRPCRouteCustomValidator struct {
+       Client  client.Client
+       checker reference.Checker
+}
+
+var _ webhook.CustomValidator = &GRPCRouteCustomValidator{}
+
+func NewGRPCRouteCustomValidator(c client.Client) *GRPCRouteCustomValidator {
+       return &GRPCRouteCustomValidator{
+               Client:  c,
+               checker: reference.NewChecker(c, grpcRouteLog),
+       }
+}
+
+func (v *GRPCRouteCustomValidator) ValidateCreate(ctx context.Context, obj 
runtime.Object) (admission.Warnings, error) {
+       route, ok := obj.(*gatewayv1.GRPCRoute)
+       if !ok {
+               return nil, fmt.Errorf("expected a GRPCRoute object but got 
%T", obj)
+       }
+       grpcRouteLog.Info("Validation for GRPCRoute upon creation", "name", 
route.GetName(), "namespace", route.GetNamespace())
+       managed, err := isGRPCRouteManaged(ctx, v.Client, route)
+       if err != nil {
+               grpcRouteLog.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 *GRPCRouteCustomValidator) ValidateUpdate(ctx context.Context, oldObj, 
newObj runtime.Object) (admission.Warnings, error) {
+       route, ok := newObj.(*gatewayv1.GRPCRoute)
+       if !ok {
+               return nil, fmt.Errorf("expected a GRPCRoute object for the 
newObj but got %T", newObj)
+       }
+       grpcRouteLog.Info("Validation for GRPCRoute upon update", "name", 
route.GetName(), "namespace", route.GetNamespace())
+       managed, err := isGRPCRouteManaged(ctx, v.Client, route)
+       if err != nil {
+               grpcRouteLog.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 (*GRPCRouteCustomValidator) ValidateDelete(context.Context, 
runtime.Object) (admission.Warnings, error) {
+       return nil, nil
+}
+
+func (v *GRPCRouteCustomValidator) collectWarnings(ctx context.Context, route 
*gatewayv1.GRPCRoute) 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 string, name string, group *gatewayv1.Group, 
kind *gatewayv1.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)
+       }
+
+       processFilters := func(filters []gatewayv1.GRPCRouteFilter) {
+               for _, filter := range filters {
+                       if filter.RequestMirror != nil {
+                               targetNamespace := namespace
+                               if filter.RequestMirror.BackendRef.Namespace != 
nil && *filter.RequestMirror.BackendRef.Namespace != "" {
+                                       targetNamespace = 
string(*filter.RequestMirror.BackendRef.Namespace)
+                               }
+                               addBackendRef(targetNamespace, 
string(filter.RequestMirror.BackendRef.Name),
+                                       filter.RequestMirror.BackendRef.Group, 
filter.RequestMirror.BackendRef.Kind)
+                       }
+               }
+       }
+
+       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)
+                       processFilters(backend.Filters)
+               }
+
+               processFilters(rule.Filters)
+       }
+
+       return warnings
+}
diff --git a/internal/webhook/v1/grpcroute_webhook_test.go 
b/internal/webhook/v1/grpcroute_webhook_test.go
new file mode 100644
index 00000000..4bf3a686
--- /dev/null
+++ b/internal/webhook/v1/grpcroute_webhook_test.go
@@ -0,0 +1,116 @@
+// 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"
+
+       "github.com/apache/apisix-ingress-controller/internal/controller/config"
+)
+
+func buildGRPCRouteValidator(t *testing.T, objects ...runtime.Object) 
*GRPCRouteCustomValidator {
+       t.Helper()
+
+       scheme := runtime.NewScheme()
+       require.NoError(t, clientgoscheme.AddToScheme(scheme))
+       require.NoError(t, gatewayv1.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 NewGRPCRouteCustomValidator(builder.Build())
+}
+
+func TestGRPCRouteCustomValidator_WarnsForMissingService(t *testing.T) {
+       route := &gatewayv1.GRPCRoute{
+               ObjectMeta: metav1.ObjectMeta{Name: "demo", Namespace: 
"default"},
+               Spec: gatewayv1.GRPCRouteSpec{
+                       CommonRouteSpec: gatewayv1.CommonRouteSpec{
+                               ParentRefs: []gatewayv1.ParentReference{{
+                                       Name: 
gatewayv1.ObjectName("test-gateway"),
+                               }},
+                       },
+                       Rules: []gatewayv1.GRPCRouteRule{{
+                               BackendRefs: []gatewayv1.GRPCBackendRef{{
+                                       BackendRef: gatewayv1.BackendRef{
+                                               BackendObjectReference: 
gatewayv1.BackendObjectReference{
+                                                       Name: 
gatewayv1.ObjectName("missing"),
+                                               },
+                                       },
+                               }},
+                       }},
+               },
+       }
+
+       validator := buildGRPCRouteValidator(t)
+       warnings, err := validator.ValidateCreate(context.Background(), route)
+       require.NoError(t, err)
+       require.Len(t, warnings, 1)
+       assert.Equal(t, warnings[0], "Referenced Service 'default/missing' not 
found")
+}
+
+func TestGRPCRouteCustomValidator_NoWarningsWhenServiceExists(t *testing.T) {
+       service := &corev1.Service{ObjectMeta: metav1.ObjectMeta{Name: 
"backend", Namespace: "default"}}
+       validator := buildGRPCRouteValidator(t, service)
+
+       route := &gatewayv1.GRPCRoute{
+               ObjectMeta: metav1.ObjectMeta{Name: "demo", Namespace: 
"default"},
+               Spec: gatewayv1.GRPCRouteSpec{
+                       CommonRouteSpec: gatewayv1.CommonRouteSpec{
+                               ParentRefs: []gatewayv1.ParentReference{{
+                                       Name: 
gatewayv1.ObjectName("test-gateway"),
+                               }},
+                       },
+                       Rules: []gatewayv1.GRPCRouteRule{{
+                               BackendRefs: []gatewayv1.GRPCBackendRef{{
+                                       BackendRef: gatewayv1.BackendRef{
+                                               BackendObjectReference: 
gatewayv1.BackendObjectReference{
+                                                       Name: 
gatewayv1.ObjectName("backend"),
+                                               },
+                                       },
+                               }},
+                       }},
+               },
+       }
+
+       warnings, err := validator.ValidateCreate(context.Background(), route)
+       require.NoError(t, err)
+       assert.Empty(t, warnings)
+}
diff --git a/internal/webhook/v1/httproute_webhook.go 
b/internal/webhook/v1/httproute_webhook.go
new file mode 100644
index 00000000..191cd9b6
--- /dev/null
+++ b/internal/webhook/v1/httproute_webhook.go
@@ -0,0 +1,162 @@
+// 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"
+       gatewayv1 "sigs.k8s.io/gateway-api/apis/v1"
+
+       internaltypes 
"github.com/apache/apisix-ingress-controller/internal/types"
+       
"github.com/apache/apisix-ingress-controller/internal/webhook/v1/reference"
+)
+
+var httpRouteLog = logf.Log.WithName("httproute-resource")
+
+func SetupHTTPRouteWebhookWithManager(mgr ctrl.Manager) error {
+       return ctrl.NewWebhookManagedBy(mgr).
+               For(&gatewayv1.HTTPRoute{}).
+               WithValidator(NewHTTPRouteCustomValidator(mgr.GetClient())).
+               Complete()
+}
+
+// 
+kubebuilder:webhook:path=/validate-gateway-networking-k8s-io-v1-httproute,mutating=false,failurePolicy=fail,sideEffects=None,groups=gateway.networking.k8s.io,resources=httproutes,verbs=create;update,versions=v1,name=vhttproute-v1.kb.io,admissionReviewVersions=v1
+
+type HTTPRouteCustomValidator struct {
+       Client  client.Client
+       checker reference.Checker
+}
+
+var _ webhook.CustomValidator = &HTTPRouteCustomValidator{}
+
+func NewHTTPRouteCustomValidator(c client.Client) *HTTPRouteCustomValidator {
+       return &HTTPRouteCustomValidator{
+               Client:  c,
+               checker: reference.NewChecker(c, httpRouteLog),
+       }
+}
+
+func (v *HTTPRouteCustomValidator) ValidateCreate(ctx context.Context, obj 
runtime.Object) (admission.Warnings, error) {
+       route, ok := obj.(*gatewayv1.HTTPRoute)
+       if !ok {
+               return nil, fmt.Errorf("expected a HTTPRoute object but got 
%T", obj)
+       }
+       httpRouteLog.Info("Validation for HTTPRoute upon creation", "name", 
route.GetName(), "namespace", route.GetNamespace())
+       managed, err := isHTTPRouteManaged(ctx, v.Client, route)
+       if err != nil {
+               httpRouteLog.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 *HTTPRouteCustomValidator) ValidateUpdate(ctx context.Context, oldObj, 
newObj runtime.Object) (admission.Warnings, error) {
+       route, ok := newObj.(*gatewayv1.HTTPRoute)
+       if !ok {
+               return nil, fmt.Errorf("expected a HTTPRoute object for the 
newObj but got %T", newObj)
+       }
+       httpRouteLog.Info("Validation for HTTPRoute upon update", "name", 
route.GetName(), "namespace", route.GetNamespace())
+       managed, err := isHTTPRouteManaged(ctx, v.Client, route)
+       if err != nil {
+               httpRouteLog.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 (*HTTPRouteCustomValidator) ValidateDelete(context.Context, 
runtime.Object) (admission.Warnings, error) {
+       return nil, nil
+}
+
+func (v *HTTPRouteCustomValidator) collectWarnings(ctx context.Context, route 
*gatewayv1.HTTPRoute) 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 string, name string, group *gatewayv1.Group, 
kind *gatewayv1.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)
+       }
+
+       processFilters := func(filters []gatewayv1.HTTPRouteFilter) {
+               for _, filter := range filters {
+                       if filter.RequestMirror != nil {
+                               targetNamespace := namespace
+                               if filter.RequestMirror.BackendRef.Namespace != 
nil && *filter.RequestMirror.BackendRef.Namespace != "" {
+                                       targetNamespace = 
string(*filter.RequestMirror.BackendRef.Namespace)
+                               }
+                               addBackendRef(targetNamespace, 
string(filter.RequestMirror.BackendRef.Name),
+                                       filter.RequestMirror.BackendRef.Group, 
filter.RequestMirror.BackendRef.Kind)
+                       }
+               }
+       }
+
+       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)
+                       processFilters(backend.Filters)
+               }
+
+               processFilters(rule.Filters)
+       }
+
+       return warnings
+}
diff --git a/internal/webhook/v1/httproute_webhook_test.go 
b/internal/webhook/v1/httproute_webhook_test.go
new file mode 100644
index 00000000..1dd4fe0c
--- /dev/null
+++ b/internal/webhook/v1/httproute_webhook_test.go
@@ -0,0 +1,138 @@
+// 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"
+
+       "github.com/apache/apisix-ingress-controller/internal/controller/config"
+)
+
+func buildHTTPRouteValidator(t *testing.T, objects ...runtime.Object) 
*HTTPRouteCustomValidator {
+       t.Helper()
+
+       scheme := runtime.NewScheme()
+       require.NoError(t, clientgoscheme.AddToScheme(scheme))
+       require.NoError(t, gatewayv1.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 NewHTTPRouteCustomValidator(builder.Build())
+}
+
+func TestHTTPRouteCustomValidator_WarnsForMissingReferences(t *testing.T) {
+       route := &gatewayv1.HTTPRoute{
+               ObjectMeta: metav1.ObjectMeta{Name: "demo", Namespace: 
"default"},
+               Spec: gatewayv1.HTTPRouteSpec{
+                       CommonRouteSpec: gatewayv1.CommonRouteSpec{
+                               ParentRefs: []gatewayv1.ParentReference{{
+                                       Name: 
gatewayv1.ObjectName("test-gateway"),
+                               }},
+                       },
+                       Rules: []gatewayv1.HTTPRouteRule{{
+                               BackendRefs: []gatewayv1.HTTPBackendRef{{
+                                       BackendRef: gatewayv1.BackendRef{
+                                               BackendObjectReference: 
gatewayv1.BackendObjectReference{
+                                                       Name: 
gatewayv1.ObjectName("missing-svc"),
+                                               },
+                                       },
+                               }},
+                               Filters: []gatewayv1.HTTPRouteFilter{{
+                                       Type: 
gatewayv1.HTTPRouteFilterRequestMirror,
+                                       RequestMirror: 
&gatewayv1.HTTPRequestMirrorFilter{
+                                               BackendRef: 
gatewayv1.BackendObjectReference{
+                                                       Name: 
gatewayv1.ObjectName("mirror-svc"),
+                                               },
+                                       },
+                               }},
+                       }},
+               },
+       }
+
+       validator := buildHTTPRouteValidator(t)
+       warnings, err := validator.ValidateCreate(context.Background(), route)
+       require.NoError(t, err)
+       assert.ElementsMatch(t, []string{
+               "Referenced Service 'default/mirror-svc' not found",
+               "Referenced Service 'default/missing-svc' not found",
+       }, warnings)
+}
+
+func TestHTTPRouteCustomValidator_NoWarningsWhenResourcesExist(t *testing.T) {
+       objects := []runtime.Object{
+               &corev1.Service{ObjectMeta: metav1.ObjectMeta{Name: "primary", 
Namespace: "default"}},
+               &corev1.Service{ObjectMeta: metav1.ObjectMeta{Name: "mirror", 
Namespace: "default"}},
+       }
+
+       validator := buildHTTPRouteValidator(t, objects...)
+
+       route := &gatewayv1.HTTPRoute{
+               ObjectMeta: metav1.ObjectMeta{Name: "demo", Namespace: 
"default"},
+               Spec: gatewayv1.HTTPRouteSpec{
+                       CommonRouteSpec: gatewayv1.CommonRouteSpec{
+                               ParentRefs: []gatewayv1.ParentReference{{
+                                       Name: 
gatewayv1.ObjectName("test-gateway"),
+                               }},
+                       },
+                       Rules: []gatewayv1.HTTPRouteRule{{
+                               BackendRefs: []gatewayv1.HTTPBackendRef{{
+                                       BackendRef: gatewayv1.BackendRef{
+                                               BackendObjectReference: 
gatewayv1.BackendObjectReference{
+                                                       Name: 
gatewayv1.ObjectName("primary"),
+                                               },
+                                       },
+                               }},
+                               Filters: []gatewayv1.HTTPRouteFilter{{
+                                       Type: 
gatewayv1.HTTPRouteFilterRequestMirror,
+                                       RequestMirror: 
&gatewayv1.HTTPRequestMirrorFilter{
+                                               BackendRef: 
gatewayv1.BackendObjectReference{
+                                                       Name: 
gatewayv1.ObjectName("mirror"),
+                                               },
+                                       },
+                               }},
+                       }},
+               },
+       }
+
+       warnings, err := validator.ValidateCreate(context.Background(), route)
+       require.NoError(t, err)
+       assert.Empty(t, warnings)
+}
diff --git a/internal/webhook/v1/ingress_webhook.go 
b/internal/webhook/v1/ingress_webhook.go
index 777d1e1b..10e18ab4 100644
--- a/internal/webhook/v1/ingress_webhook.go
+++ b/internal/webhook/v1/ingress_webhook.go
@@ -20,12 +20,17 @@ import (
        "fmt"
        "slices"
 
-       networkingk8siov1 "k8s.io/api/networking/v1"
+       networkingv1 "k8s.io/api/networking/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"
+
+       "github.com/apache/apisix-ingress-controller/internal/controller"
+       
"github.com/apache/apisix-ingress-controller/internal/webhook/v1/reference"
 )
 
 var ingresslog = logf.Log.WithName("ingress-resource")
@@ -75,7 +80,7 @@ var unsupportedAnnotations = []string{
 
 // checkUnsupportedAnnotations checks if the Ingress contains any unsupported 
annotations
 // and returns appropriate warnings
-func checkUnsupportedAnnotations(ingress *networkingk8siov1.Ingress) 
admission.Warnings {
+func checkUnsupportedAnnotations(ingress *networkingv1.Ingress) 
admission.Warnings {
        var warnings admission.Warnings
 
        if len(ingress.Annotations) == 0 {
@@ -98,8 +103,8 @@ func checkUnsupportedAnnotations(ingress 
*networkingk8siov1.Ingress) admission.W
 
 // SetupIngressWebhookWithManager registers the webhook for Ingress in the 
manager.
 func SetupIngressWebhookWithManager(mgr ctrl.Manager) error {
-       return ctrl.NewWebhookManagedBy(mgr).For(&networkingk8siov1.Ingress{}).
-               WithValidator(&IngressCustomValidator{}).
+       return ctrl.NewWebhookManagedBy(mgr).For(&networkingv1.Ingress{}).
+               WithValidator(NewIngressCustomValidator(mgr.GetClient())).
                Complete()
 }
 
@@ -112,34 +117,52 @@ func SetupIngressWebhookWithManager(mgr ctrl.Manager) 
error {
 //
 // NOTE: The +kubebuilder:object:generate=false marker prevents controller-gen 
from generating DeepCopy methods,
 // as this struct is used only for temporary operations and does not need to 
be deeply copied.
-type IngressCustomValidator struct{}
+type IngressCustomValidator struct {
+       Client  client.Client
+       checker reference.Checker
+}
 
 var _ webhook.CustomValidator = &IngressCustomValidator{}
 
+func NewIngressCustomValidator(c client.Client) *IngressCustomValidator {
+       return &IngressCustomValidator{
+               Client:  c,
+               checker: reference.NewChecker(c, ingresslog),
+       }
+}
+
 // ValidateCreate implements webhook.CustomValidator so a webhook will be 
registered for the type Ingress.
-func (v *IngressCustomValidator) ValidateCreate(_ context.Context, obj 
runtime.Object) (admission.Warnings, error) {
-       ingress, ok := obj.(*networkingk8siov1.Ingress)
+func (v *IngressCustomValidator) ValidateCreate(ctx context.Context, obj 
runtime.Object) (admission.Warnings, error) {
+       ingress, ok := obj.(*networkingv1.Ingress)
        if !ok {
                return nil, fmt.Errorf("expected a Ingress object but got %T", 
obj)
        }
        ingresslog.Info("Validation for Ingress upon creation", "name", 
ingress.GetName(), "namespace", ingress.GetNamespace())
+       if !controller.MatchesIngressClass(v.Client, ingresslog, ingress) {
+               return nil, nil
+       }
 
        // Check for unsupported annotations and generate warnings
        warnings := checkUnsupportedAnnotations(ingress)
+       warnings = append(warnings, v.collectReferenceWarnings(ctx, ingress)...)
 
        return warnings, nil
 }
 
 // ValidateUpdate implements webhook.CustomValidator so a webhook will be 
registered for the type Ingress.
-func (v *IngressCustomValidator) ValidateUpdate(_ context.Context, oldObj, 
newObj runtime.Object) (admission.Warnings, error) {
-       ingress, ok := newObj.(*networkingk8siov1.Ingress)
+func (v *IngressCustomValidator) ValidateUpdate(ctx context.Context, oldObj, 
newObj runtime.Object) (admission.Warnings, error) {
+       ingress, ok := newObj.(*networkingv1.Ingress)
        if !ok {
                return nil, fmt.Errorf("expected a Ingress object for the 
newObj but got %T", newObj)
        }
        ingresslog.Info("Validation for Ingress upon update", "name", 
ingress.GetName(), "namespace", ingress.GetNamespace())
+       if !controller.MatchesIngressClass(v.Client, ingresslog, ingress) {
+               return nil, nil
+       }
 
        // Check for unsupported annotations and generate warnings
        warnings := checkUnsupportedAnnotations(ingress)
+       warnings = append(warnings, v.collectReferenceWarnings(ctx, ingress)...)
 
        return warnings, nil
 }
@@ -148,3 +171,58 @@ func (v *IngressCustomValidator) ValidateUpdate(_ 
context.Context, oldObj, newOb
 func (v *IngressCustomValidator) ValidateDelete(ctx context.Context, obj 
runtime.Object) (admission.Warnings, error) {
        return nil, nil
 }
+
+func (v *IngressCustomValidator) collectReferenceWarnings(ctx context.Context, 
ingress *networkingv1.Ingress) admission.Warnings {
+       serviceVisited := make(map[types.NamespacedName]struct{})
+       secretVisited := make(map[types.NamespacedName]struct{})
+       namespace := ingress.GetNamespace()
+
+       var warnings admission.Warnings
+
+       addServiceWarning := func(name string) {
+               if name == "" {
+                       return
+               }
+               nn := types.NamespacedName{Namespace: namespace, Name: name}
+               if _, seen := serviceVisited[nn]; seen {
+                       return
+               }
+               serviceVisited[nn] = struct{}{}
+               warnings = append(warnings, v.checker.Service(ctx, 
reference.ServiceRef{
+                       Object:         ingress,
+                       NamespacedName: nn,
+               })...)
+       }
+
+       addSecretWarning := func(name string) {
+               if name == "" {
+                       return
+               }
+               nn := types.NamespacedName{Namespace: namespace, Name: name}
+               if _, seen := secretVisited[nn]; seen {
+                       return
+               }
+               secretVisited[nn] = struct{}{}
+               warnings = append(warnings, v.checker.Secret(ctx, 
reference.SecretRef{
+                       Object:         ingress,
+                       NamespacedName: nn,
+               })...)
+       }
+
+       for _, rule := range ingress.Spec.Rules {
+               if rule.HTTP == nil {
+                       continue
+               }
+               for _, path := range rule.HTTP.Paths {
+                       if path.Backend.Service != nil {
+                               addServiceWarning(path.Backend.Service.Name)
+                       }
+               }
+       }
+
+       for _, tls := range ingress.Spec.TLS {
+               addSecretWarning(tls.SecretName)
+       }
+
+       return warnings
+}
diff --git a/internal/webhook/v1/ingress_webhook_test.go 
b/internal/webhook/v1/ingress_webhook_test.go
index 5ae9bd2a..89f3fa6d 100644
--- a/internal/webhook/v1/ingress_webhook_test.go
+++ b/internal/webhook/v1/ingress_webhook_test.go
@@ -21,13 +21,52 @@ import (
        "testing"
 
        "github.com/stretchr/testify/assert"
-       networkingk8siov1 "k8s.io/api/networking/v1"
+       "github.com/stretchr/testify/require"
+       corev1 "k8s.io/api/core/v1"
+       networkingv1 "k8s.io/api/networking/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"
+
+       apisixv2 "github.com/apache/apisix-ingress-controller/api/v2"
+       "github.com/apache/apisix-ingress-controller/internal/controller/config"
+       
"github.com/apache/apisix-ingress-controller/internal/controller/indexer"
 )
 
+func buildIngressValidator(t *testing.T, objects ...runtime.Object) 
*IngressCustomValidator {
+       t.Helper()
+
+       scheme := runtime.NewScheme()
+       require.NoError(t, clientgoscheme.AddToScheme(scheme))
+       require.NoError(t, networkingv1.AddToScheme(scheme))
+       require.NoError(t, apisixv2.AddToScheme(scheme))
+
+       managed := []runtime.Object{
+               &networkingv1.IngressClass{
+                       ObjectMeta: metav1.ObjectMeta{
+                               Name: "apisix",
+                               Annotations: map[string]string{
+                                       
"ingressclass.kubernetes.io/is-default-class": "true",
+                               },
+                       },
+                       Spec: networkingv1.IngressClassSpec{
+                               Controller: 
config.ControllerConfig.ControllerName,
+                       },
+               },
+       }
+       allObjects := append(managed, objects...)
+       builder := fake.NewClientBuilder().
+               WithScheme(scheme).
+               WithIndex(&networkingv1.IngressClass{}, indexer.IngressClass, 
indexer.IngressClassIndexFunc).
+               WithRuntimeObjects(allObjects...)
+
+       return NewIngressCustomValidator(builder.Build())
+}
+
 func TestIngressCustomValidator_ValidateCreate_UnsupportedAnnotations(t 
*testing.T) {
-       validator := IngressCustomValidator{}
-       obj := &networkingk8siov1.Ingress{
+       validator := buildIngressValidator(t)
+       obj := &networkingv1.Ingress{
                ObjectMeta: metav1.ObjectMeta{
                        Name:      "test-ingress",
                        Namespace: "default",
@@ -49,8 +88,8 @@ func 
TestIngressCustomValidator_ValidateCreate_UnsupportedAnnotations(t *testing
 }
 
 func TestIngressCustomValidator_ValidateCreate_SupportedAnnotations(t 
*testing.T) {
-       validator := IngressCustomValidator{}
-       obj := &networkingk8siov1.Ingress{
+       validator := buildIngressValidator(t)
+       obj := &networkingv1.Ingress{
                ObjectMeta: metav1.ObjectMeta{
                        Name:      "test-ingress",
                        Namespace: "default",
@@ -66,9 +105,9 @@ func 
TestIngressCustomValidator_ValidateCreate_SupportedAnnotations(t *testing.T
 }
 
 func TestIngressCustomValidator_ValidateUpdate_UnsupportedAnnotations(t 
*testing.T) {
-       validator := IngressCustomValidator{}
-       oldObj := &networkingk8siov1.Ingress{}
-       obj := &networkingk8siov1.Ingress{
+       validator := buildIngressValidator(t)
+       oldObj := &networkingv1.Ingress{}
+       obj := &networkingv1.Ingress{
                ObjectMeta: metav1.ObjectMeta{
                        Name:      "test-ingress",
                        Namespace: "default",
@@ -90,8 +129,8 @@ func 
TestIngressCustomValidator_ValidateUpdate_UnsupportedAnnotations(t *testing
 }
 
 func TestIngressCustomValidator_ValidateDelete_NoWarnings(t *testing.T) {
-       validator := IngressCustomValidator{}
-       obj := &networkingk8siov1.Ingress{
+       validator := buildIngressValidator(t)
+       obj := &networkingv1.Ingress{
                ObjectMeta: metav1.ObjectMeta{
                        Name:      "test-ingress",
                        Namespace: "default",
@@ -107,8 +146,8 @@ func TestIngressCustomValidator_ValidateDelete_NoWarnings(t 
*testing.T) {
 }
 
 func TestIngressCustomValidator_ValidateCreate_NoAnnotations(t *testing.T) {
-       validator := IngressCustomValidator{}
-       obj := &networkingk8siov1.Ingress{
+       validator := buildIngressValidator(t)
+       obj := &networkingv1.Ingress{
                ObjectMeta: metav1.ObjectMeta{
                        Name:      "test-ingress",
                        Namespace: "default",
@@ -119,3 +158,54 @@ func 
TestIngressCustomValidator_ValidateCreate_NoAnnotations(t *testing.T) {
        assert.NoError(t, err)
        assert.Empty(t, warnings)
 }
+
+func TestIngressCustomValidator_WarnsForMissingServiceAndSecret(t *testing.T) {
+       validator := buildIngressValidator(t)
+       obj := &networkingv1.Ingress{
+               ObjectMeta: metav1.ObjectMeta{Name: "test-ingress", Namespace: 
"default"},
+               Spec: networkingv1.IngressSpec{
+                       Rules: []networkingv1.IngressRule{{
+                               IngressRuleValue: 
networkingv1.IngressRuleValue{HTTP: &networkingv1.HTTPIngressRuleValue{
+                                       Paths: []networkingv1.HTTPIngressPath{{
+                                               Backend: 
networkingv1.IngressBackend{
+                                                       Service: 
&networkingv1.IngressServiceBackend{Name: "default-svc"},
+                                               },
+                                       }},
+                               }},
+                       }},
+                       TLS: []networkingv1.IngressTLS{{SecretName: 
"missing-cert"}},
+               },
+       }
+
+       warnings, err := validator.ValidateCreate(context.Background(), obj)
+       require.NoError(t, err)
+       require.Len(t, warnings, 2)
+       require.Contains(t, warnings, "Referenced Service 'default/default-svc' 
not found")
+       require.Contains(t, warnings, "Referenced Secret 'default/missing-cert' 
not found")
+}
+
+func TestIngressCustomValidator_NoWarningsWhenReferencesExist(t *testing.T) {
+       service := &corev1.Service{ObjectMeta: metav1.ObjectMeta{Name: 
"default-svc", Namespace: "default"}}
+       secret := &corev1.Secret{ObjectMeta: metav1.ObjectMeta{Name: 
"tls-cert", Namespace: "default"}}
+       validator := buildIngressValidator(t, service, secret)
+
+       obj := &networkingv1.Ingress{
+               ObjectMeta: metav1.ObjectMeta{Name: "test-ingress", Namespace: 
"default"},
+               Spec: networkingv1.IngressSpec{
+                       Rules: []networkingv1.IngressRule{{
+                               IngressRuleValue: 
networkingv1.IngressRuleValue{HTTP: &networkingv1.HTTPIngressRuleValue{
+                                       Paths: []networkingv1.HTTPIngressPath{{
+                                               Backend: 
networkingv1.IngressBackend{
+                                                       Service: 
&networkingv1.IngressServiceBackend{Name: "default-svc"},
+                                               },
+                                       }},
+                               }},
+                       }},
+                       TLS: []networkingv1.IngressTLS{{SecretName: 
"tls-cert"}},
+               },
+       }
+
+       warnings, err := validator.ValidateCreate(context.Background(), obj)
+       require.NoError(t, err)
+       assert.Empty(t, warnings)
+}
diff --git a/internal/webhook/v1/ownership.go b/internal/webhook/v1/ownership.go
new file mode 100644
index 00000000..d2a72eae
--- /dev/null
+++ b/internal/webhook/v1/ownership.go
@@ -0,0 +1,103 @@
+// 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 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/tcproute_webhook.go 
b/internal/webhook/v1/tcproute_webhook.go
new file mode 100644
index 00000000..7edc6a0c
--- /dev/null
+++ b/internal/webhook/v1/tcproute_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 tcpRouteLog = logf.Log.WithName("tcproute-resource")
+
+func SetupTCPRouteWebhookWithManager(mgr ctrl.Manager) error {
+       return ctrl.NewWebhookManagedBy(mgr).
+               For(&gatewayv1alpha2.TCPRoute{}).
+               WithValidator(NewTCPRouteCustomValidator(mgr.GetClient())).
+               Complete()
+}
+
+// 
+kubebuilder:webhook:path=/validate-gateway-networking-k8s-io-v1alpha2-tcproute,mutating=false,failurePolicy=fail,sideEffects=None,groups=gateway.networking.k8s.io,resources=tcproutes,verbs=create;update,versions=v1alpha2,name=vtcproute-v1alpha2.kb.io,admissionReviewVersions=v1
+
+type TCPRouteCustomValidator struct {
+       Client  client.Client
+       checker reference.Checker
+}
+
+var _ webhook.CustomValidator = &TCPRouteCustomValidator{}
+
+func NewTCPRouteCustomValidator(c client.Client) *TCPRouteCustomValidator {
+       return &TCPRouteCustomValidator{
+               Client:  c,
+               checker: reference.NewChecker(c, tcpRouteLog),
+       }
+}
+
+func (v *TCPRouteCustomValidator) ValidateCreate(ctx context.Context, obj 
runtime.Object) (admission.Warnings, error) {
+       route, ok := obj.(*gatewayv1alpha2.TCPRoute)
+       if !ok {
+               return nil, fmt.Errorf("expected a TCPRoute object but got %T", 
obj)
+       }
+       tcpRouteLog.Info("Validation for TCPRoute upon creation", "name", 
route.GetName(), "namespace", route.GetNamespace())
+       managed, err := isTCPRouteManaged(ctx, v.Client, route)
+       if err != nil {
+               tcpRouteLog.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 *TCPRouteCustomValidator) ValidateUpdate(ctx context.Context, oldObj, 
newObj runtime.Object) (admission.Warnings, error) {
+       route, ok := newObj.(*gatewayv1alpha2.TCPRoute)
+       if !ok {
+               return nil, fmt.Errorf("expected a TCPRoute object for the 
newObj but got %T", newObj)
+       }
+       tcpRouteLog.Info("Validation for TCPRoute upon update", "name", 
route.GetName(), "namespace", route.GetNamespace())
+       managed, err := isTCPRouteManaged(ctx, v.Client, route)
+       if err != nil {
+               tcpRouteLog.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 (*TCPRouteCustomValidator) ValidateDelete(context.Context, 
runtime.Object) (admission.Warnings, error) {
+       return nil, nil
+}
+
+func (v *TCPRouteCustomValidator) collectWarnings(ctx context.Context, route 
*gatewayv1alpha2.TCPRoute) 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/tcproute_webhook_test.go 
b/internal/webhook/v1/tcproute_webhook_test.go
new file mode 100644
index 00000000..6381ea0a
--- /dev/null
+++ b/internal/webhook/v1/tcproute_webhook_test.go
@@ -0,0 +1,122 @@
+// 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 buildTCPRouteValidator(t *testing.T, objects ...runtime.Object) 
*TCPRouteCustomValidator {
+       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 NewTCPRouteCustomValidator(builder.Build())
+}
+
+func TestTCPRouteCustomValidator_WarnsForMissingReferences(t *testing.T) {
+       route := &gatewayv1alpha2.TCPRoute{
+               ObjectMeta: metav1.ObjectMeta{Name: "demo", Namespace: 
"default"},
+               Spec: gatewayv1alpha2.TCPRouteSpec{
+                       CommonRouteSpec: gatewayv1alpha2.CommonRouteSpec{
+                               ParentRefs: []gatewayv1alpha2.ParentReference{{
+                                       Name: 
gatewayv1alpha2.ObjectName("test-gateway"),
+                               }},
+                       },
+                       Rules: []gatewayv1alpha2.TCPRouteRule{{
+                               BackendRefs: []gatewayv1alpha2.BackendRef{
+                                       {
+                                               BackendObjectReference: 
gatewayv1alpha2.BackendObjectReference{
+                                                       Name: 
gatewayv1alpha2.ObjectName("missing-svc"),
+                                               },
+                                       },
+                               },
+                       }},
+               },
+       }
+
+       validator := buildTCPRouteValidator(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 TestTCPRouteCustomValidator_NoWarningsWhenResourcesExist(t *testing.T) {
+       objs := []runtime.Object{
+               &corev1.Service{ObjectMeta: metav1.ObjectMeta{Name: "backend", 
Namespace: "default"}},
+       }
+
+       validator := buildTCPRouteValidator(t, objs...)
+
+       route := &gatewayv1alpha2.TCPRoute{
+               ObjectMeta: metav1.ObjectMeta{Name: "demo", Namespace: 
"default"},
+               Spec: gatewayv1alpha2.TCPRouteSpec{
+                       CommonRouteSpec: gatewayv1alpha2.CommonRouteSpec{
+                               ParentRefs: []gatewayv1alpha2.ParentReference{{
+                                       Name: 
gatewayv1alpha2.ObjectName("test-gateway"),
+                               }},
+                       },
+                       Rules: []gatewayv1alpha2.TCPRouteRule{{
+                               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)
+}

Reply via email to