You are viewing a plain text version of this content. The canonical link for it is here.
Posted to notifications@apisix.apache.org by ro...@apache.org on 2025/09/30 06:24:08 UTC

(apisix-ingress-controller) branch master updated: test: add e2e test case for webhook (#2585)

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 501b4e89 test: add e2e test case for webhook (#2585)
501b4e89 is described below

commit 501b4e89f2567a8166660131d36511824cf13375
Author: Ashing Zheng <ax...@gmail.com>
AuthorDate: Tue Sep 30 14:24:03 2025 +0800

    test: add e2e test case for webhook (#2585)
    
    Signed-off-by: Ashing Zheng <ax...@gmail.com>
---
 test/e2e/apisix/e2e_test.go                        |   1 +
 test/e2e/framework/manifests/webhook.yaml          | 194 +++++++++++++++++++--
 test/e2e/webhook/apisixconsumer.go                 |  88 ++++++++++
 test/e2e/webhook/apisixroute.go                    | 117 +++++++++++++
 test/e2e/webhook/apisixtls.go                      | 111 ++++++++++++
 test/e2e/webhook/consumer.go                       |  93 ++++++++++
 .../{gatewayapi/webhook.go => webhook/gateway.go}  |   2 +-
 test/e2e/webhook/gatewayproxy.go                   | 129 ++++++++++++++
 .../{apisix/e2e_test.go => webhook/grpcroute.go}   |  40 ++---
 test/e2e/webhook/helpers.go                        | 116 ++++++++++++
 .../{apisix/e2e_test.go => webhook/httproute.go}   |  40 ++---
 .../e2e/{ingress/webhook.go => webhook/ingress.go} |   2 +-
 .../ingressclass.go}                               |   2 +-
 test/e2e/webhook/tcproute.go                       | 121 +++++++++++++
 14 files changed, 1000 insertions(+), 56 deletions(-)

diff --git a/test/e2e/apisix/e2e_test.go b/test/e2e/apisix/e2e_test.go
index 03fe0ca6..fde91636 100644
--- a/test/e2e/apisix/e2e_test.go
+++ b/test/e2e/apisix/e2e_test.go
@@ -30,6 +30,7 @@ import (
 	_ "github.com/apache/apisix-ingress-controller/test/e2e/gatewayapi"
 	_ "github.com/apache/apisix-ingress-controller/test/e2e/ingress"
 	"github.com/apache/apisix-ingress-controller/test/e2e/scaffold"
+	_ "github.com/apache/apisix-ingress-controller/test/e2e/webhook"
 )
 
 // TestAPISIXE2E runs e2e tests using the APISIX standalone mode
diff --git a/test/e2e/framework/manifests/webhook.yaml b/test/e2e/framework/manifests/webhook.yaml
index 432873c9..954ce72d 100644
--- a/test/e2e/framework/manifests/webhook.yaml
+++ b/test/e2e/framework/manifests/webhook.yaml
@@ -20,6 +20,174 @@ kind: ValidatingWebhookConfiguration
 metadata:
   name: test-webhook-{{ .Namespace }}
 webhooks:
+- name: vapisixconsumer-v2.kb.io
+  clientConfig:
+    service:
+      name: webhook-service
+      namespace: {{ .Namespace }}
+      path: /validate-apisix-apache-org-v2-apisixconsumer
+    caBundle: {{ .CABundle }}
+  admissionReviewVersions:
+    - v1
+  rules:
+    - operations:
+        - CREATE
+        - UPDATE
+      apiGroups:
+        - apisix.apache.org
+      apiVersions:
+        - v2
+      resources:
+        - apisixconsumers
+  failurePolicy: Fail
+  sideEffects: None
+- name: vapisixroute-v2.kb.io
+  clientConfig:
+    service:
+      name: webhook-service
+      namespace: {{ .Namespace }}
+      path: /validate-apisix-apache-org-v2-apisixroute
+    caBundle: {{ .CABundle }}
+  admissionReviewVersions:
+    - v1
+  rules:
+    - operations:
+        - CREATE
+        - UPDATE
+      apiGroups:
+        - apisix.apache.org
+      apiVersions:
+        - v2
+      resources:
+        - apisixroutes
+  failurePolicy: Fail
+  sideEffects: None
+- name: vapisixtls-v2.kb.io
+  clientConfig:
+    service:
+      name: webhook-service
+      namespace: {{ .Namespace }}
+      path: /validate-apisix-apache-org-v2-apisixtls
+    caBundle: {{ .CABundle }}
+  admissionReviewVersions:
+    - v1
+  rules:
+    - operations:
+        - CREATE
+        - UPDATE
+      apiGroups:
+        - apisix.apache.org
+      apiVersions:
+        - v2
+      resources:
+        - apisixtlses
+  failurePolicy: Fail
+  sideEffects: None
+- name: vconsumer-v1alpha1.kb.io
+  clientConfig:
+    service:
+      name: webhook-service
+      namespace: {{ .Namespace }}
+      path: /validate-apisix-apache-org-v1alpha1-consumer
+    caBundle: {{ .CABundle }}
+  admissionReviewVersions:
+    - v1
+  rules:
+    - operations:
+        - CREATE
+        - UPDATE
+      apiGroups:
+        - apisix.apache.org
+      apiVersions:
+        - v1alpha1
+      resources:
+        - consumers
+  failurePolicy: Fail
+  sideEffects: None
+- name: vgateway-v1.kb.io
+  clientConfig:
+    service:
+      name: webhook-service
+      namespace: {{ .Namespace }}
+      path: /validate-gateway-networking-k8s-io-v1-gateway
+    caBundle: {{ .CABundle }}
+  admissionReviewVersions:
+    - v1
+  rules:
+    - operations:
+        - CREATE
+        - UPDATE
+      apiGroups:
+        - gateway.networking.k8s.io
+      apiVersions:
+        - v1
+      resources:
+        - gateways
+  failurePolicy: Fail
+  sideEffects: None
+- name: vgatewayproxy-v1alpha1.kb.io
+  clientConfig:
+    service:
+      name: webhook-service
+      namespace: {{ .Namespace }}
+      path: /validate-apisix-apache-org-v1alpha1-gatewayproxy
+    caBundle: {{ .CABundle }}
+  admissionReviewVersions:
+    - v1
+  rules:
+    - operations:
+        - CREATE
+        - UPDATE
+      apiGroups:
+        - apisix.apache.org
+      apiVersions:
+        - v1alpha1
+      resources:
+        - gatewayproxies
+  failurePolicy: Fail
+  sideEffects: None
+- name: vgrpcroute-v1.kb.io
+  clientConfig:
+    service:
+      name: webhook-service
+      namespace: {{ .Namespace }}
+      path: /validate-gateway-networking-k8s-io-v1-grpcroute
+    caBundle: {{ .CABundle }}
+  admissionReviewVersions:
+    - v1
+  rules:
+    - operations:
+        - CREATE
+        - UPDATE
+      apiGroups:
+        - gateway.networking.k8s.io
+      apiVersions:
+        - v1
+      resources:
+        - grpcroutes
+  failurePolicy: Fail
+  sideEffects: None
+- name: vhttproute-v1.kb.io
+  clientConfig:
+    service:
+      name: webhook-service
+      namespace: {{ .Namespace }}
+      path: /validate-gateway-networking-k8s-io-v1-httproute
+    caBundle: {{ .CABundle }}
+  admissionReviewVersions:
+    - v1
+  rules:
+    - operations:
+        - CREATE
+        - UPDATE
+      apiGroups:
+        - gateway.networking.k8s.io
+      apiVersions:
+        - v1
+      resources:
+        - httproutes
+  failurePolicy: Fail
+  sideEffects: None
 - name: vingress-v1.kb.io
   clientConfig:
     service:
@@ -30,15 +198,15 @@ webhooks:
   admissionReviewVersions:
     - v1
   rules:
-  - operations:
-      - CREATE
-      - UPDATE
-    apiGroups:
-      - networking.k8s.io
-    apiVersions:
-      - v1
-    resources:
-      - ingresses
+    - operations:
+        - CREATE
+        - UPDATE
+      apiGroups:
+        - networking.k8s.io
+      apiVersions:
+        - v1
+      resources:
+        - ingresses
   failurePolicy: Fail
   sideEffects: None
 - name: vingressclass-v1.kb.io
@@ -62,12 +230,12 @@ webhooks:
         - ingressclasses
   failurePolicy: Fail
   sideEffects: None
-- name: vgateway-v1.kb.io
+- name: vtcproute-v1alpha2.kb.io
   clientConfig:
     service:
       name: webhook-service
       namespace: {{ .Namespace }}
-      path: /validate-gateway-networking-k8s-io-v1-gateway
+      path: /validate-gateway-networking-k8s-io-v1alpha2-tcproute
     caBundle: {{ .CABundle }}
   admissionReviewVersions:
     - v1
@@ -78,8 +246,8 @@ webhooks:
       apiGroups:
         - gateway.networking.k8s.io
       apiVersions:
-        - v1
+        - v1alpha2
       resources:
-        - gateways
+        - tcproutes
   failurePolicy: Fail
   sideEffects: None
diff --git a/test/e2e/webhook/apisixconsumer.go b/test/e2e/webhook/apisixconsumer.go
new file mode 100644
index 00000000..7aa1a256
--- /dev/null
+++ b/test/e2e/webhook/apisixconsumer.go
@@ -0,0 +1,88 @@
+// Licensed to the Apache Software Foundation (ASF) under one
+// or more contributor license agreements.  See the NOTICE file
+// distributed with this work for additional information
+// regarding copyright ownership.  The ASF licenses this file
+// to you under the Apache License, Version 2.0 (the
+// "License"); you may not use this file except in compliance
+// with the License.  You may obtain a copy of the License at
+//
+//   http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing,
+// software distributed under the License is distributed on an
+// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+// KIND, either express or implied.  See the License for the
+// specific language governing permissions and limitations
+// under the License.
+
+package webhook
+
+import (
+	"fmt"
+	"time"
+
+	. "github.com/onsi/ginkgo/v2"
+	. "github.com/onsi/gomega"
+
+	"github.com/apache/apisix-ingress-controller/test/e2e/scaffold"
+)
+
+var _ = Describe("Test ApisixConsumer Webhook", Label("webhook"), func() {
+	s := scaffold.NewScaffold(scaffold.Options{
+		Name:          "apisixconsumer-webhook-test",
+		EnableWebhook: true,
+	})
+
+	BeforeEach(func() {
+		By("creating GatewayProxy")
+		err := s.CreateResourceFromString(s.GetGatewayProxySpec())
+		Expect(err).NotTo(HaveOccurred(), "creating GatewayProxy")
+		time.Sleep(5 * time.Second)
+
+		By("creating IngressClass")
+		err = s.CreateResourceFromStringWithNamespace(s.GetIngressClassYaml(), "")
+		Expect(err).NotTo(HaveOccurred(), "creating IngressClass")
+		time.Sleep(5 * time.Second)
+	})
+
+	It("should warn on missing authentication secrets", func() {
+		missingSecret := "missing-basic-secret"
+		consumerName := "webhook-apisixconsumer"
+		consumerYAML := `
+apiVersion: apisix.apache.org/v2
+kind: ApisixConsumer
+metadata:
+  name: %s
+  namespace: %s
+spec:
+  ingressClassName: %s
+  authParameter:
+    basicAuth:
+      secretRef:
+        name: %s
+`
+
+		output, err := s.CreateResourceFromStringAndGetOutput(fmt.Sprintf(consumerYAML, consumerName, s.Namespace(), s.Namespace(), missingSecret))
+		Expect(err).ShouldNot(HaveOccurred())
+		Expect(output).To(ContainSubstring(fmt.Sprintf("Warning: Referenced Secret '%s/%s' not found", s.Namespace(), missingSecret)))
+
+		By("creating referenced secret")
+		secretYAML := fmt.Sprintf(`
+apiVersion: v1
+kind: Secret
+metadata:
+  name: %s
+stringData:
+  username: demo
+  password: demo
+`, missingSecret)
+		err = s.CreateResourceFromString(secretYAML)
+		Expect(err).NotTo(HaveOccurred(), "creating basic auth secret")
+
+		time.Sleep(2 * time.Second)
+
+		output, err = s.CreateResourceFromStringAndGetOutput(fmt.Sprintf(consumerYAML, consumerName, s.Namespace(), s.Namespace(), missingSecret))
+		Expect(err).ShouldNot(HaveOccurred())
+		Expect(output).NotTo(ContainSubstring(fmt.Sprintf("Warning: Referenced Secret '%s/%s' not found", s.Namespace(), missingSecret)))
+	})
+})
diff --git a/test/e2e/webhook/apisixroute.go b/test/e2e/webhook/apisixroute.go
new file mode 100644
index 00000000..51904f43
--- /dev/null
+++ b/test/e2e/webhook/apisixroute.go
@@ -0,0 +1,117 @@
+// Licensed to the Apache Software Foundation (ASF) under one
+// or more contributor license agreements.  See the NOTICE file
+// distributed with this work for additional information
+// regarding copyright ownership.  The ASF licenses this file
+// to you under the Apache License, Version 2.0 (the
+// "License"); you may not use this file except in compliance
+// with the License.  You may obtain a copy of the License at
+//
+//   http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing,
+// software distributed under the License is distributed on an
+// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+// KIND, either express or implied.  See the License for the
+// specific language governing permissions and limitations
+// under the License.
+
+package webhook
+
+import (
+	"fmt"
+	"time"
+
+	. "github.com/onsi/ginkgo/v2"
+	. "github.com/onsi/gomega"
+
+	"github.com/apache/apisix-ingress-controller/test/e2e/scaffold"
+)
+
+var _ = Describe("Test ApisixRoute Webhook", Label("webhook"), func() {
+	s := scaffold.NewScaffold(scaffold.Options{
+		Name:          "apisixroute-webhook-test",
+		EnableWebhook: true,
+	})
+
+	BeforeEach(func() {
+		By("creating GatewayProxy")
+		err := s.CreateResourceFromString(s.GetGatewayProxySpec())
+		Expect(err).NotTo(HaveOccurred(), "creating GatewayProxy")
+		time.Sleep(5 * time.Second)
+
+		By("creating IngressClass")
+		err = s.CreateResourceFromStringWithNamespace(s.GetIngressClassYaml(), "")
+		Expect(err).NotTo(HaveOccurred(), "creating IngressClass")
+		time.Sleep(5 * time.Second)
+	})
+
+	It("should warn on missing service or secret references", func() {
+		missingService := "missing-backend"
+		missingSecret := "missing-plugin-secret"
+		routeName := "webhook-apisixroute"
+		routeYAML := `
+apiVersion: apisix.apache.org/v2
+kind: ApisixRoute
+metadata:
+  name: %s
+  namespace: %s
+spec:
+  ingressClassName: %s
+  http:
+  - name: rule-webhook
+    match:
+      hosts:
+      - webhook.example.com
+      paths:
+      - /webhook
+    backends:
+    - serviceName: %s
+      servicePort: 80
+    plugins:
+    - name: echo
+      enable: true
+      secretRef: %s
+`
+
+		output, err := s.CreateResourceFromStringAndGetOutput(fmt.Sprintf(routeYAML, routeName, s.Namespace(), s.Namespace(), missingService, missingSecret))
+		Expect(err).ShouldNot(HaveOccurred())
+		Expect(output).To(ContainSubstring(fmt.Sprintf("Warning: Referenced Service '%s/%s' not found", s.Namespace(), missingService)))
+		Expect(output).To(ContainSubstring(fmt.Sprintf("Warning: Referenced Secret '%s/%s' not found", s.Namespace(), missingSecret)))
+
+		By("creating referenced Service and Secret")
+		serviceYAML := fmt.Sprintf(`
+apiVersion: v1
+kind: Service
+metadata:
+  name: %s
+spec:
+  selector:
+    app: placeholder
+  ports:
+  - name: http
+    port: 80
+    targetPort: 80
+  type: ClusterIP
+`, missingService)
+		err = s.CreateResourceFromString(serviceYAML)
+		Expect(err).NotTo(HaveOccurred(), "creating backend service placeholder")
+
+		secretYAML := fmt.Sprintf(`
+apiVersion: v1
+kind: Secret
+metadata:
+  name: %s
+stringData:
+  config: enabled
+`, missingSecret)
+		err = s.CreateResourceFromString(secretYAML)
+		Expect(err).NotTo(HaveOccurred(), "creating plugin secret placeholder")
+
+		time.Sleep(2 * time.Second)
+
+		output, err = s.CreateResourceFromStringAndGetOutput(fmt.Sprintf(routeYAML, routeName, s.Namespace(), s.Namespace(), missingService, missingSecret))
+		Expect(err).ShouldNot(HaveOccurred())
+		Expect(output).NotTo(ContainSubstring(fmt.Sprintf("Warning: Referenced Service '%s/%s' not found", s.Namespace(), missingService)))
+		Expect(output).NotTo(ContainSubstring(fmt.Sprintf("Warning: Referenced Secret '%s/%s' not found", s.Namespace(), missingSecret)))
+	})
+})
diff --git a/test/e2e/webhook/apisixtls.go b/test/e2e/webhook/apisixtls.go
new file mode 100644
index 00000000..08defed9
--- /dev/null
+++ b/test/e2e/webhook/apisixtls.go
@@ -0,0 +1,111 @@
+// Licensed to the Apache Software Foundation (ASF) under one
+// or more contributor license agreements.  See the NOTICE file
+// distributed with this work for additional information
+// regarding copyright ownership.  The ASF licenses this file
+// to you under the Apache License, Version 2.0 (the
+// "License"); you may not use this file except in compliance
+// with the License.  You may obtain a copy of the License at
+//
+//   http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing,
+// software distributed under the License is distributed on an
+// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+// KIND, either express or implied.  See the License for the
+// specific language governing permissions and limitations
+// under the License.
+
+package webhook
+
+import (
+	"fmt"
+	"time"
+
+	. "github.com/onsi/ginkgo/v2"
+	. "github.com/onsi/gomega"
+
+	"github.com/apache/apisix-ingress-controller/test/e2e/scaffold"
+)
+
+var _ = Describe("Test ApisixTls Webhook", Label("webhook"), func() {
+	s := scaffold.NewScaffold(scaffold.Options{
+		Name:          "apisixtls-webhook-test",
+		EnableWebhook: true,
+	})
+
+	BeforeEach(func() {
+		By("creating GatewayProxy")
+		err := s.CreateResourceFromString(s.GetGatewayProxySpec())
+		Expect(err).NotTo(HaveOccurred(), "creating GatewayProxy")
+		time.Sleep(5 * time.Second)
+
+		By("creating IngressClass")
+		err = s.CreateResourceFromStringWithNamespace(s.GetIngressClassYaml(), "")
+		Expect(err).NotTo(HaveOccurred(), "creating IngressClass")
+		time.Sleep(5 * time.Second)
+	})
+
+	It("should warn on missing TLS secrets", func() {
+		serverSecret := "missing-server-tls"
+		clientSecret := "missing-client-ca"
+		tlsName := "webhook-apisixtls"
+		tlsYAML := `
+apiVersion: apisix.apache.org/v2
+kind: ApisixTls
+metadata:
+  name: %s
+  namespace: %s
+spec:
+  ingressClassName: %s
+  hosts:
+  - webhook.example.com
+  secret:
+    name: %s
+    namespace: %s
+  client:
+    caSecret:
+      name: %s
+      namespace: %s
+`
+
+		output, err := s.CreateResourceFromStringAndGetOutput(fmt.Sprintf(tlsYAML, tlsName, s.Namespace(), s.Namespace(), serverSecret, s.Namespace(), clientSecret, s.Namespace()))
+		Expect(err).ShouldNot(HaveOccurred())
+		Expect(output).To(ContainSubstring(fmt.Sprintf("Warning: Referenced Secret '%s/%s' not found", s.Namespace(), serverSecret)))
+		Expect(output).To(ContainSubstring(fmt.Sprintf("Warning: Referenced Secret '%s/%s' not found", s.Namespace(), clientSecret)))
+
+		By("creating referenced TLS secrets")
+		serverSecretYAML := fmt.Sprintf(`
+apiVersion: v1
+kind: Secret
+metadata:
+  name: %s
+  namespace: %s
+type: kubernetes.io/tls
+stringData:
+  tls.crt: dummy-cert
+  tls.key: dummy-key
+`, serverSecret, s.Namespace())
+		err = s.CreateResourceFromString(serverSecretYAML)
+		Expect(err).NotTo(HaveOccurred(), "creating server TLS secret")
+
+		clientSecretYAML := fmt.Sprintf(`
+apiVersion: v1
+kind: Secret
+metadata:
+  name: %s
+  namespace: %s
+type: Opaque
+stringData:
+  ca.crt: dummy-ca
+`, clientSecret, s.Namespace())
+		err = s.CreateResourceFromString(clientSecretYAML)
+		Expect(err).NotTo(HaveOccurred(), "creating client CA secret")
+
+		time.Sleep(2 * time.Second)
+
+		output, err = s.CreateResourceFromStringAndGetOutput(fmt.Sprintf(tlsYAML, tlsName, s.Namespace(), s.Namespace(), serverSecret, s.Namespace(), clientSecret, s.Namespace()))
+		Expect(err).ShouldNot(HaveOccurred())
+		Expect(output).NotTo(ContainSubstring(fmt.Sprintf("Warning: Referenced Secret '%s/%s' not found", s.Namespace(), serverSecret)))
+		Expect(output).NotTo(ContainSubstring(fmt.Sprintf("Warning: Referenced Secret '%s/%s' not found", s.Namespace(), clientSecret)))
+	})
+})
diff --git a/test/e2e/webhook/consumer.go b/test/e2e/webhook/consumer.go
new file mode 100644
index 00000000..676adbb8
--- /dev/null
+++ b/test/e2e/webhook/consumer.go
@@ -0,0 +1,93 @@
+// Licensed to the Apache Software Foundation (ASF) under one
+// or more contributor license agreements.  See the NOTICE file
+// distributed with this work for additional information
+// regarding copyright ownership.  The ASF licenses this file
+// to you under the Apache License, Version 2.0 (the
+// "License"); you may not use this file except in compliance
+// with the License.  You may obtain a copy of the License at
+//
+//   http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing,
+// software distributed under the License is distributed on an
+// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+// KIND, either express or implied.  See the License for the
+// specific language governing permissions and limitations
+// under the License.
+
+package webhook
+
+import (
+	"fmt"
+	"time"
+
+	. "github.com/onsi/ginkgo/v2"
+	. "github.com/onsi/gomega"
+
+	"github.com/apache/apisix-ingress-controller/test/e2e/scaffold"
+)
+
+var _ = Describe("Test Consumer Webhook", Label("webhook"), func() {
+	s := scaffold.NewScaffold(scaffold.Options{
+		Name:          "consumer-webhook-test",
+		EnableWebhook: true,
+	})
+
+	BeforeEach(func() {
+		By("creating GatewayProxy")
+		err := s.CreateResourceFromString(s.GetGatewayProxySpec())
+		Expect(err).NotTo(HaveOccurred(), "creating GatewayProxy")
+		time.Sleep(5 * time.Second)
+
+		By("creating GatewayClass")
+		err = s.CreateResourceFromString(s.GetGatewayClassYaml())
+		Expect(err).NotTo(HaveOccurred(), "creating GatewayClass")
+		time.Sleep(2 * time.Second)
+
+		By("creating Gateway")
+		err = s.CreateResourceFromString(s.GetGatewayYaml())
+		Expect(err).NotTo(HaveOccurred(), "creating Gateway")
+		time.Sleep(5 * time.Second)
+	})
+
+	It("should warn on missing secret references", func() {
+		missingSecret := "missing-consumer-secret"
+		consumerName := "webhook-consumer"
+		gatewayName := s.Namespace()
+		consumerYAML := `
+apiVersion: apisix.apache.org/v1alpha1
+kind: Consumer
+metadata:
+  name: %s
+spec:
+  gatewayRef:
+    name: %s
+  credentials:
+  - type: jwt-auth
+    secretRef:
+      name: %s
+`
+
+		output, err := s.CreateResourceFromStringAndGetOutput(fmt.Sprintf(consumerYAML, consumerName, gatewayName, missingSecret))
+		Expect(err).ShouldNot(HaveOccurred())
+		Expect(output).To(ContainSubstring(fmt.Sprintf("Warning: Referenced Secret '%s/%s' not found", s.Namespace(), missingSecret)))
+
+		By("creating referenced secret")
+		secretYAML := fmt.Sprintf(`
+apiVersion: v1
+kind: Secret
+metadata:
+  name: %s
+stringData:
+  token: %s
+`, missingSecret, s.AdminKey())
+		err = s.CreateResourceFromString(secretYAML)
+		Expect(err).NotTo(HaveOccurred(), "creating consumer secret")
+
+		time.Sleep(2 * time.Second)
+
+		output, err = s.CreateResourceFromStringAndGetOutput(fmt.Sprintf(consumerYAML, consumerName, gatewayName, missingSecret))
+		Expect(err).ShouldNot(HaveOccurred())
+		Expect(output).NotTo(ContainSubstring(fmt.Sprintf("Warning: Referenced Secret '%s/%s' not found", s.Namespace(), missingSecret)))
+	})
+})
diff --git a/test/e2e/gatewayapi/webhook.go b/test/e2e/webhook/gateway.go
similarity index 99%
rename from test/e2e/gatewayapi/webhook.go
rename to test/e2e/webhook/gateway.go
index 92f65f93..09bfc3ce 100644
--- a/test/e2e/gatewayapi/webhook.go
+++ b/test/e2e/webhook/gateway.go
@@ -15,7 +15,7 @@
 // specific language governing permissions and limitations
 // under the License.
 
-package gatewayapi
+package webhook
 
 import (
 	"fmt"
diff --git a/test/e2e/webhook/gatewayproxy.go b/test/e2e/webhook/gatewayproxy.go
new file mode 100644
index 00000000..6b1f6189
--- /dev/null
+++ b/test/e2e/webhook/gatewayproxy.go
@@ -0,0 +1,129 @@
+// Licensed to the Apache Software Foundation (ASF) under one
+// or more contributor license agreements.  See the NOTICE file
+// distributed with this work for additional information
+// regarding copyright ownership.  The ASF licenses this file
+// to you under the Apache License, Version 2.0 (the
+// "License"); you may not use this file except in compliance
+// with the License.  You may obtain a copy of the License at
+//
+//   http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing,
+// software distributed under the License is distributed on an
+// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+// KIND, either express or implied.  See the License for the
+// specific language governing permissions and limitations
+// under the License.
+
+package webhook
+
+import (
+	"fmt"
+	"time"
+
+	. "github.com/onsi/ginkgo/v2"
+	. "github.com/onsi/gomega"
+
+	"github.com/apache/apisix-ingress-controller/test/e2e/scaffold"
+)
+
+var _ = Describe("Test GatewayProxy Webhook", Label("webhook"), func() {
+	s := scaffold.NewScaffold(scaffold.Options{
+		Name:          "gatewayproxy-webhook-test",
+		EnableWebhook: true,
+	})
+
+	It("should warn on missing service or secret references", func() {
+		missingService := "missing-control-plane"
+		missingSecret := "missing-admin-secret"
+		gpName := "webhook-gateway-proxy"
+		gpWithSecrets := `
+apiVersion: apisix.apache.org/v1alpha1
+kind: GatewayProxy
+metadata:
+  name: %s
+spec:
+  provider:
+    type: ControlPlane
+    controlPlane:
+      service:
+        name: %s
+        port: 9180
+      auth:
+        type: AdminKey
+        adminKey:
+          valueFrom:
+            secretKeyRef:
+              name: %s
+              key: token
+`
+
+		output, err := s.CreateResourceFromStringAndGetOutput(fmt.Sprintf(gpWithSecrets, gpName, missingService, missingSecret))
+		Expect(err).ShouldNot(HaveOccurred())
+		Expect(output).To(ContainSubstring(fmt.Sprintf("Warning: Referenced Service '%s/%s' not found", s.Namespace(), missingService)))
+		Expect(output).To(ContainSubstring(fmt.Sprintf("Warning: Referenced Secret '%s/%s' not found", s.Namespace(), missingSecret)))
+
+		By("creating the referenced Service and Secret without the required key")
+		serviceYAML := fmt.Sprintf(`
+apiVersion: v1
+kind: Service
+metadata:
+  name: %s
+spec:
+  selector:
+    app: placeholder
+  ports:
+  - name: admin
+    port: 9180
+    targetPort: 9180
+  type: ClusterIP
+`, missingService)
+		err = s.CreateResourceFromString(serviceYAML)
+		Expect(err).NotTo(HaveOccurred(), "creating placeholder service")
+
+		secretWithoutKey := fmt.Sprintf(`
+apiVersion: v1
+kind: Secret
+metadata:
+  name: %s
+stringData:
+  another: value
+`, missingSecret)
+		err = s.CreateResourceFromString(secretWithoutKey)
+		Expect(err).NotTo(HaveOccurred(), "creating placeholder secret without token key")
+
+		time.Sleep(2 * time.Second)
+
+		By("delete and reapply the GatewayProxy, because gatewayproxy has no change")
+		err = s.DeleteResource("GatewayProxy", gpName)
+		Expect(err).ShouldNot(HaveOccurred())
+
+		output, err = s.CreateResourceFromStringAndGetOutput(fmt.Sprintf(gpWithSecrets, gpName, missingService, missingSecret))
+		Expect(err).ShouldNot(HaveOccurred())
+		Expect(output).NotTo(ContainSubstring(fmt.Sprintf("Warning: Referenced Service '%s/%s' not found", s.Namespace(), missingService)))
+		Expect(output).To(ContainSubstring(fmt.Sprintf("Warning: Secret key 'token' not found in Secret '%s/%s'", s.Namespace(), missingSecret)))
+
+		By("updating the Secret to include the expected key")
+		secretWithKey := fmt.Sprintf(`
+apiVersion: v1
+kind: Secret
+metadata:
+  name: %s
+stringData:
+  token: %s
+`, missingSecret, s.AdminKey())
+		err = s.CreateResourceFromString(secretWithKey)
+		Expect(err).NotTo(HaveOccurred(), "adding token key to secret")
+
+		time.Sleep(2 * time.Second)
+
+		By("delete and reapply the GatewayProxy, because gatewayproxy has no change")
+		err = s.DeleteResource("GatewayProxy", gpName)
+		Expect(err).ShouldNot(HaveOccurred())
+
+		output, err = s.CreateResourceFromStringAndGetOutput(fmt.Sprintf(gpWithSecrets, gpName, missingService, missingSecret))
+		Expect(err).ShouldNot(HaveOccurred())
+		Expect(output).NotTo(ContainSubstring(fmt.Sprintf("Warning: Referenced Service '%s/%s' not found", s.Namespace(), missingService)))
+		Expect(output).NotTo(ContainSubstring(fmt.Sprintf("Warning: Secret key 'token' not found in Secret '%s/%s'", s.Namespace(), missingSecret)))
+	})
+})
diff --git a/test/e2e/apisix/e2e_test.go b/test/e2e/webhook/grpcroute.go
similarity index 53%
copy from test/e2e/apisix/e2e_test.go
copy to test/e2e/webhook/grpcroute.go
index 03fe0ca6..0954c969 100644
--- a/test/e2e/apisix/e2e_test.go
+++ b/test/e2e/webhook/grpcroute.go
@@ -15,32 +15,32 @@
 // specific language governing permissions and limitations
 // under the License.
 
-package apisix
+package webhook
 
 import (
-	"fmt"
-	"testing"
-
 	. "github.com/onsi/ginkgo/v2"
-	. "github.com/onsi/gomega"
 
-	_ "github.com/apache/apisix-ingress-controller/test/e2e/crds/v1alpha1"
-	_ "github.com/apache/apisix-ingress-controller/test/e2e/crds/v2"
-	"github.com/apache/apisix-ingress-controller/test/e2e/framework"
-	_ "github.com/apache/apisix-ingress-controller/test/e2e/gatewayapi"
-	_ "github.com/apache/apisix-ingress-controller/test/e2e/ingress"
 	"github.com/apache/apisix-ingress-controller/test/e2e/scaffold"
 )
 
-// TestAPISIXE2E runs e2e tests using the APISIX standalone mode
-func TestAPISIXE2E(t *testing.T) {
-	RegisterFailHandler(Fail)
-	// init framework
-	_ = framework.NewFramework()
+var _ = Describe("Test GRPCRoute Webhook", Label("webhook"), func() {
+	s := scaffold.NewScaffold(scaffold.Options{
+		Name:          "grpcroute-webhook-test",
+		EnableWebhook: true,
+	})
 
-	// init newDeployer function
-	scaffold.NewDeployer = scaffold.NewAPISIXDeployer
+	BeforeEach(func() {
+		setupGatewayResources(s)
+	})
 
-	_, _ = fmt.Fprintf(GinkgoWriter, "Starting APISIX standalone e2e suite\n")
-	RunSpecs(t, "apisix standalone e2e suite")
-}
+	It("should warn on missing backend services", func() {
+		verifyMissingBackendWarnings(s, routeWebhookTestCase{
+			routeKind:       "GRPCRoute",
+			routeName:       "webhook-grpcroute",
+			missingService:  "missing-grpc-backend",
+			mirrorService:   "missing-grpc-mirror",
+			servicePortName: "grpc",
+			servicePort:     8080,
+		})
+	})
+})
diff --git a/test/e2e/webhook/helpers.go b/test/e2e/webhook/helpers.go
new file mode 100644
index 00000000..696e81df
--- /dev/null
+++ b/test/e2e/webhook/helpers.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 webhook
+
+import (
+	"fmt"
+	"time"
+
+	. "github.com/onsi/ginkgo/v2"
+	. "github.com/onsi/gomega"
+
+	"github.com/apache/apisix-ingress-controller/test/e2e/scaffold"
+)
+
+type routeWebhookTestCase struct {
+	routeKind       string
+	routeName       string
+	missingService  string
+	mirrorService   string
+	servicePortName string
+	servicePort     int
+}
+
+func setupGatewayResources(s *scaffold.Scaffold) {
+	By("creating GatewayProxy")
+	err := s.CreateResourceFromString(s.GetGatewayProxySpec())
+	Expect(err).NotTo(HaveOccurred(), "creating GatewayProxy")
+	time.Sleep(5 * time.Second)
+
+	By("creating GatewayClass")
+	err = s.CreateResourceFromString(s.GetGatewayClassYaml())
+	Expect(err).NotTo(HaveOccurred(), "creating GatewayClass")
+	time.Sleep(2 * time.Second)
+
+	By("creating Gateway")
+	err = s.CreateResourceFromString(s.GetGatewayYaml())
+	Expect(err).NotTo(HaveOccurred(), "creating Gateway")
+	time.Sleep(5 * time.Second)
+}
+
+func verifyMissingBackendWarnings(s *scaffold.Scaffold, tc routeWebhookTestCase) {
+	gatewayName := s.Namespace()
+	routeYAML := fmt.Sprintf(`
+apiVersion: gateway.networking.k8s.io/v1
+kind: %s
+metadata:
+  name: %s
+spec:
+  parentRefs:
+  - name: %s
+  rules:
+  - backendRefs:
+    - name: %s
+      port: %d
+    filters:
+    - type: RequestMirror
+      requestMirror:
+        backendRef:
+          name: %s
+          port: %d
+`, tc.routeKind, tc.routeName, gatewayName, tc.missingService, tc.servicePort, tc.mirrorService, tc.servicePort)
+
+	missingBackendWarning := fmt.Sprintf("Warning: Referenced Service '%s/%s' not found", gatewayName, tc.missingService)
+	mirrorBackendWarning := fmt.Sprintf("Warning: Referenced Service '%s/%s' not found", gatewayName, tc.mirrorService)
+
+	output, err := s.CreateResourceFromStringAndGetOutput(routeYAML)
+	Expect(err).ShouldNot(HaveOccurred())
+	Expect(output).To(ContainSubstring(missingBackendWarning))
+	Expect(output).To(ContainSubstring(mirrorBackendWarning))
+
+	By(fmt.Sprintf("creating referenced backend services for %s", tc.routeKind))
+	serviceYAML := `
+apiVersion: v1
+kind: Service
+metadata:
+  name: %s
+spec:
+  selector:
+    app: placeholder
+  ports:
+  - name: %s
+    port: %d
+    targetPort: %d
+  type: ClusterIP
+`
+
+	backendService := fmt.Sprintf(serviceYAML, tc.missingService, tc.servicePortName, tc.servicePort, tc.servicePort)
+	err = s.CreateResourceFromString(backendService)
+	Expect(err).NotTo(HaveOccurred(), "creating primary backend service")
+
+	mirrorService := fmt.Sprintf(serviceYAML, tc.mirrorService, tc.servicePortName, tc.servicePort, tc.servicePort)
+	err = s.CreateResourceFromString(mirrorService)
+	Expect(err).NotTo(HaveOccurred(), "creating mirror backend service")
+
+	time.Sleep(2 * time.Second)
+
+	output, err = s.CreateResourceFromStringAndGetOutput(routeYAML)
+	Expect(err).ShouldNot(HaveOccurred())
+	Expect(output).NotTo(ContainSubstring(missingBackendWarning))
+	Expect(output).NotTo(ContainSubstring(mirrorBackendWarning))
+}
diff --git a/test/e2e/apisix/e2e_test.go b/test/e2e/webhook/httproute.go
similarity index 53%
copy from test/e2e/apisix/e2e_test.go
copy to test/e2e/webhook/httproute.go
index 03fe0ca6..240d7686 100644
--- a/test/e2e/apisix/e2e_test.go
+++ b/test/e2e/webhook/httproute.go
@@ -15,32 +15,32 @@
 // specific language governing permissions and limitations
 // under the License.
 
-package apisix
+package webhook
 
 import (
-	"fmt"
-	"testing"
-
 	. "github.com/onsi/ginkgo/v2"
-	. "github.com/onsi/gomega"
 
-	_ "github.com/apache/apisix-ingress-controller/test/e2e/crds/v1alpha1"
-	_ "github.com/apache/apisix-ingress-controller/test/e2e/crds/v2"
-	"github.com/apache/apisix-ingress-controller/test/e2e/framework"
-	_ "github.com/apache/apisix-ingress-controller/test/e2e/gatewayapi"
-	_ "github.com/apache/apisix-ingress-controller/test/e2e/ingress"
 	"github.com/apache/apisix-ingress-controller/test/e2e/scaffold"
 )
 
-// TestAPISIXE2E runs e2e tests using the APISIX standalone mode
-func TestAPISIXE2E(t *testing.T) {
-	RegisterFailHandler(Fail)
-	// init framework
-	_ = framework.NewFramework()
+var _ = Describe("Test HTTPRoute Webhook", Label("webhook"), func() {
+	s := scaffold.NewScaffold(scaffold.Options{
+		Name:          "httproute-webhook-test",
+		EnableWebhook: true,
+	})
 
-	// init newDeployer function
-	scaffold.NewDeployer = scaffold.NewAPISIXDeployer
+	BeforeEach(func() {
+		setupGatewayResources(s)
+	})
 
-	_, _ = fmt.Fprintf(GinkgoWriter, "Starting APISIX standalone e2e suite\n")
-	RunSpecs(t, "apisix standalone e2e suite")
-}
+	It("should warn on missing backend services", func() {
+		verifyMissingBackendWarnings(s, routeWebhookTestCase{
+			routeKind:       "HTTPRoute",
+			routeName:       "webhook-httproute",
+			missingService:  "missing-http-backend",
+			mirrorService:   "missing-http-mirror",
+			servicePortName: "http",
+			servicePort:     80,
+		})
+	})
+})
diff --git a/test/e2e/ingress/webhook.go b/test/e2e/webhook/ingress.go
similarity index 99%
rename from test/e2e/ingress/webhook.go
rename to test/e2e/webhook/ingress.go
index 310b6629..37608fb2 100644
--- a/test/e2e/ingress/webhook.go
+++ b/test/e2e/webhook/ingress.go
@@ -15,7 +15,7 @@
 // specific language governing permissions and limitations
 // under the License.
 
-package ingress
+package webhook
 
 import (
 	"fmt"
diff --git a/test/e2e/ingress/ingressclass_webhook.go b/test/e2e/webhook/ingressclass.go
similarity index 99%
rename from test/e2e/ingress/ingressclass_webhook.go
rename to test/e2e/webhook/ingressclass.go
index 9c1f3174..8192a51b 100644
--- a/test/e2e/ingress/ingressclass_webhook.go
+++ b/test/e2e/webhook/ingressclass.go
@@ -15,7 +15,7 @@
 // specific language governing permissions and limitations
 // under the License.
 
-package ingress
+package webhook
 
 import (
 	"fmt"
diff --git a/test/e2e/webhook/tcproute.go b/test/e2e/webhook/tcproute.go
new file mode 100644
index 00000000..de226a33
--- /dev/null
+++ b/test/e2e/webhook/tcproute.go
@@ -0,0 +1,121 @@
+// Licensed to the Apache Software Foundation (ASF) under one
+// or more contributor license agreements.  See the NOTICE file
+// distributed with this work for additional information
+// regarding copyright ownership.  The ASF licenses this file
+// to you under the Apache License, Version 2.0 (the
+// "License"); you may not use this file except in compliance
+// with the License.  You may obtain a copy of the License at
+//
+//   http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing,
+// software distributed under the License is distributed on an
+// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+// KIND, either express or implied.  See the License for the
+// specific language governing permissions and limitations
+// under the License.
+
+package webhook
+
+import (
+	"fmt"
+	"time"
+
+	. "github.com/onsi/ginkgo/v2"
+	. "github.com/onsi/gomega"
+
+	"github.com/apache/apisix-ingress-controller/test/e2e/scaffold"
+)
+
+var _ = Describe("Test TCPRoute Webhook", Label("webhook"), func() {
+	s := scaffold.NewScaffold(scaffold.Options{
+		Name:          "tcproute-webhook-test",
+		EnableWebhook: true,
+	})
+
+	const tcpGateway = `
+apiVersion: gateway.networking.k8s.io/v1
+kind: Gateway
+metadata:
+  name: %s
+spec:
+  gatewayClassName: %s
+  listeners:
+  - name: tcp
+    protocol: TCP
+    port: 9000
+    allowedRoutes:
+      kinds:
+      - kind: TCPRoute
+  infrastructure:
+    parametersRef:
+      group: apisix.apache.org
+      kind: GatewayProxy
+      name: apisix-proxy-config
+`
+
+	BeforeEach(func() {
+		By("creating GatewayProxy")
+		err := s.CreateResourceFromString(s.GetGatewayProxySpec())
+		Expect(err).NotTo(HaveOccurred(), "creating GatewayProxy")
+		time.Sleep(5 * time.Second)
+
+		By("creating GatewayClass")
+		err = s.CreateResourceFromString(s.GetGatewayClassYaml())
+		Expect(err).NotTo(HaveOccurred(), "creating GatewayClass")
+		time.Sleep(2 * time.Second)
+
+		By("creating Gateway with TCP listener")
+		err = s.CreateResourceFromString(fmt.Sprintf(tcpGateway, s.Namespace(), s.Namespace()))
+		Expect(err).NotTo(HaveOccurred(), "creating TCP-capable Gateway")
+		time.Sleep(5 * time.Second)
+	})
+
+	It("should warn on missing backend services", func() {
+		missingService := "missing-tcp-backend"
+		routeName := "webhook-tcproute"
+		gatewayName := s.Namespace()
+		routeYAML := `
+apiVersion: gateway.networking.k8s.io/v1alpha2
+kind: TCPRoute
+metadata:
+  name: %s
+spec:
+  parentRefs:
+  - name: %s
+    sectionName: tcp
+  rules:
+  - backendRefs:
+    - name: %s
+      port: 80
+`
+
+		output, err := s.CreateResourceFromStringAndGetOutput(fmt.Sprintf(routeYAML, routeName, gatewayName, missingService))
+		Expect(err).ShouldNot(HaveOccurred())
+		Expect(output).To(ContainSubstring(fmt.Sprintf("Warning: Referenced Service '%s/%s' not found", s.Namespace(), missingService)))
+
+		By("creating referenced backend service")
+		backendService := fmt.Sprintf(`
+apiVersion: v1
+kind: Service
+metadata:
+  name: %s
+spec:
+  selector:
+    app: placeholder
+  ports:
+  - name: tcp
+    port: 80
+    targetPort: 80
+  type: ClusterIP
+`, missingService)
+		err = s.CreateResourceFromString(backendService)
+		Expect(err).NotTo(HaveOccurred(), "creating tcp backend service")
+
+		time.Sleep(2 * time.Second)
+
+		output, err = s.CreateResourceFromStringAndGetOutput(fmt.Sprintf(routeYAML, routeName, gatewayName, missingService))
+		Expect(err).ShouldNot(HaveOccurred())
+		Expect(output).NotTo(ContainSubstring(fmt.Sprintf("Warning: Referenced Service '%s/%s' not found", s.Namespace(), missingService)))
+	})
+})