This is an automated email from the ASF dual-hosted git repository.

AlinsRan 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 5c236d07 feat: add ADC-backed admission validation for APISIX CRDs 
(#2758)
5c236d07 is described below

commit 5c236d07a9c99741f72ce33e944f84f3a0577dda
Author: AlinsRan <[email protected]>
AuthorDate: Mon May 11 08:45:42 2026 +0800

    feat: add ADC-backed admission validation for APISIX CRDs (#2758)
---
 internal/adc/client/client.go                      |  37 ++++
 internal/adc/client/executor.go                    | 143 ++++++++++++-
 internal/controller/webhook_validation.go          | 113 ++++++++++
 internal/types/error.go                            |  67 ++++++
 internal/webhook/v1/adc_validation.go              | 233 +++++++++++++++++++++
 internal/webhook/v1/adc_validation_test.go         |  91 ++++++++
 internal/webhook/v1/apisixconsumer_webhook.go      |  35 +++-
 internal/webhook/v1/apisixconsumer_webhook_test.go |  91 +++++++-
 internal/webhook/v1/apisixroute_webhook.go         |  29 ++-
 internal/webhook/v1/apisixroute_webhook_test.go    | 108 +++++++++-
 internal/webhook/v1/apisixtls_webhook.go           |  32 ++-
 internal/webhook/v1/apisixtls_webhook_test.go      |  45 +++-
 internal/webhook/v1/consumer_webhook.go            | 136 +++++++++++-
 internal/webhook/v1/consumer_webhook_test.go       |  48 ++++-
 test/e2e/webhook/apisixconsumer.go                 | 113 +++++++++-
 test/e2e/webhook/apisixroute.go                    | 184 ++++++++++++++--
 test/e2e/webhook/apisixtls.go                      | 139 ++++++++++--
 test/e2e/webhook/consumer.go                       | 125 +++++++++++
 test/e2e/webhook/helpers.go                        |  17 ++
 19 files changed, 1704 insertions(+), 82 deletions(-)

diff --git a/internal/adc/client/client.go b/internal/adc/client/client.go
index 2419dde9..203d9e14 100644
--- a/internal/adc/client/client.go
+++ b/internal/adc/client/client.go
@@ -171,6 +171,43 @@ func (c *Client) DeleteConfig(ctx context.Context, args 
Task) error {
        return err
 }
 
+func (c *Client) Validate(ctx context.Context, task Task) error {
+       if len(task.Configs) == 0 || task.Resources == nil {
+               return nil
+       }
+
+       fileIOStart := time.Now()
+       syncFilePath, cleanup, err := prepareSyncFile(task.Resources)
+       if err != nil {
+               pkgmetrics.RecordFileIODuration("prepare_sync_file", "failure", 
time.Since(fileIOStart).Seconds())
+               return err
+       }
+       pkgmetrics.RecordFileIODuration("prepare_sync_file", 
adctypes.StatusSuccess, time.Since(fileIOStart).Seconds())
+       defer cleanup()
+
+       args2 := BuildADCExecuteArgs(syncFilePath, task.Labels, 
task.ResourceTypes)
+
+       var errs types.ADCValidationErrors
+       for _, config := range task.Configs {
+               if config.BackendType == "" {
+                       config.BackendType = c.defaultMode
+               }
+               if err := c.executor.Validate(ctx, config, args2); err != nil {
+                       var validationErr types.ADCValidationError
+                       if errors.As(err, &validationErr) {
+                               errs.Errors = append(errs.Errors, validationErr)
+                               continue
+                       }
+                       return err
+               }
+       }
+
+       if len(errs.Errors) > 0 {
+               return errs
+       }
+       return nil
+}
+
 func (c *Client) Sync(ctx context.Context) 
(map[string]types.ADCExecutionErrors, error) {
        c.syncMu.Lock()
        defer c.syncMu.Unlock()
diff --git a/internal/adc/client/executor.go b/internal/adc/client/executor.go
index 08608611..5664b4f2 100644
--- a/internal/adc/client/executor.go
+++ b/internal/adc/client/executor.go
@@ -43,6 +43,7 @@ const (
 
 type ADCExecutor interface {
        Execute(ctx context.Context, config adctypes.Config, args []string) 
error
+       Validate(ctx context.Context, config adctypes.Config, args []string) 
error
 }
 
 func BuildADCExecuteArgs(filePath string, labels map[string]string, types 
[]string) []string {
@@ -81,6 +82,12 @@ type ADCServerOpts struct {
        CacheKey            string            `json:"cacheKey"`
 }
 
+type ADCValidateResult struct {
+       Success      *bool                       `json:"success,omitempty"`
+       ErrorMessage string                      `json:"message,omitempty"`
+       Errors       []types.ADCValidationDetail `json:"errors,omitempty"`
+}
+
 // HTTPADCExecutor implements ADCExecutor interface using HTTP calls to ADC 
Server
 type HTTPADCExecutor struct {
        httpClient *http.Client
@@ -123,6 +130,10 @@ func (e *HTTPADCExecutor) Execute(ctx context.Context, 
config adctypes.Config, a
        return e.runHTTPSync(ctx, config, args)
 }
 
+func (e *HTTPADCExecutor) Validate(ctx context.Context, config 
adctypes.Config, args []string) error {
+       return e.runHTTPValidate(ctx, config, args)
+}
+
 // runHTTPSync performs HTTP sync to ADC Server for each server address
 func (e *HTTPADCExecutor) runHTTPSync(ctx context.Context, config 
adctypes.Config, args []string) error {
        var execErrs = types.ADCExecutionError{
@@ -157,6 +168,38 @@ func (e *HTTPADCExecutor) runHTTPSync(ctx context.Context, 
config adctypes.Confi
        return nil
 }
 
+func (e *HTTPADCExecutor) runHTTPValidate(ctx context.Context, config 
adctypes.Config, args []string) error {
+       var validationErr = types.ADCValidationError{
+               Name: config.Name,
+       }
+       var infraErrs []error
+
+       serverAddrs := func() []string {
+               return config.ServerAddrs
+       }()
+       e.log.V(1).Info("running http validate", "serverAddrs", serverAddrs)
+
+       for _, addr := range serverAddrs {
+               if err := e.runHTTPValidateForSingleServer(ctx, addr, config, 
args); err != nil {
+                       e.log.Error(err, "failed to run http validate for 
server", "server", addr)
+                       var validationServerErr 
types.ADCValidationServerAddrError
+                       if errors.As(err, &validationServerErr) {
+                               validationErr.FailedErrors = 
append(validationErr.FailedErrors, validationServerErr)
+                               continue
+                       }
+                       infraErrs = append(infraErrs, err)
+               }
+       }
+
+       if len(validationErr.FailedErrors) > 0 {
+               return validationErr
+       }
+       if len(infraErrs) > 0 {
+               return errors.Join(infraErrs...)
+       }
+       return nil
+}
+
 // runHTTPSyncForSingleServer performs HTTP sync to a single ADC Server
 func (e *HTTPADCExecutor) runHTTPSyncForSingleServer(ctx context.Context, 
serverAddr string, config adctypes.Config, args []string) error {
        ctx, cancel := context.WithTimeout(ctx, e.httpClient.Timeout)
@@ -175,7 +218,7 @@ func (e *HTTPADCExecutor) runHTTPSyncForSingleServer(ctx 
context.Context, server
        }
 
        // Build HTTP request
-       req, err := e.buildHTTPRequest(ctx, serverAddr, config, labels, types, 
resources)
+       req, err := e.buildHTTPRequest(ctx, serverAddr, config, labels, types, 
resources, http.MethodPut, "/sync")
        if err != nil {
                return fmt.Errorf("failed to build HTTP request: %w", err)
        }
@@ -195,6 +238,38 @@ func (e *HTTPADCExecutor) runHTTPSyncForSingleServer(ctx 
context.Context, server
        return e.handleHTTPResponse(resp, serverAddr)
 }
 
+func (e *HTTPADCExecutor) runHTTPValidateForSingleServer(ctx context.Context, 
serverAddr string, config adctypes.Config, args []string) error {
+       ctx, cancel := context.WithTimeout(ctx, e.httpClient.Timeout)
+       defer cancel()
+
+       labels, types, filePath, err := e.parseArgs(args)
+       if err != nil {
+               return fmt.Errorf("failed to parse args: %w", err)
+       }
+
+       resources, err := e.loadResourcesFromFile(filePath)
+       if err != nil {
+               return fmt.Errorf("failed to load resources from file %s: %w", 
filePath, err)
+       }
+
+       req, err := e.buildHTTPRequest(ctx, serverAddr, config, labels, types, 
resources, http.MethodPut, "/validate")
+       if err != nil {
+               return fmt.Errorf("failed to build validate request: %w", err)
+       }
+
+       resp, err := e.httpClient.Do(req)
+       if err != nil {
+               return fmt.Errorf("failed to send HTTP request: %w", err)
+       }
+       defer func() {
+               if closeErr := resp.Body.Close(); closeErr != nil {
+                       e.log.Error(closeErr, "failed to close response body")
+               }
+       }()
+
+       return e.handleHTTPValidateResponse(resp, serverAddr)
+}
+
 // parseArgs parses the command line arguments to extract labels, types, and 
file path
 func (e *HTTPADCExecutor) parseArgs(args []string) (map[string]string, 
[]string, string, error) {
        labels := make(map[string]string)
@@ -248,7 +323,7 @@ func (e *HTTPADCExecutor) loadResourcesFromFile(filePath 
string) (*adctypes.Reso
 }
 
 // buildHTTPRequest builds the HTTP request for ADC Server
-func (e *HTTPADCExecutor) buildHTTPRequest(ctx context.Context, serverAddr 
string, config adctypes.Config, labels map[string]string, types []string, 
resources *adctypes.Resources) (*http.Request, error) {
+func (e *HTTPADCExecutor) buildHTTPRequest(ctx context.Context, serverAddr 
string, config adctypes.Config, labels map[string]string, types []string, 
resources *adctypes.Resources, method string, path string) (*http.Request, 
error) {
        // Prepare request body
        tlsVerify := config.TlsVerify
        reqBody := ADCServerRequest{
@@ -274,7 +349,7 @@ func (e *HTTPADCExecutor) buildHTTPRequest(ctx 
context.Context, serverAddr strin
        }
 
        e.log.V(1).Info("sending HTTP request to ADC Server",
-               "url", e.serverURL+"/sync",
+               "url", e.serverURL+path,
                "server", serverAddr,
                "mode", config.BackendType,
                "cacheKey", config.Name,
@@ -284,7 +359,7 @@ func (e *HTTPADCExecutor) buildHTTPRequest(ctx 
context.Context, serverAddr strin
        )
 
        // Create HTTP request
-       req, err := http.NewRequestWithContext(ctx, "PUT", e.serverURL+"/sync", 
bytes.NewBuffer(jsonData))
+       req, err := http.NewRequestWithContext(ctx, method, e.serverURL+path, 
bytes.NewBuffer(jsonData))
        if err != nil {
                return nil, fmt.Errorf("failed to create HTTP request: %w", err)
        }
@@ -357,3 +432,63 @@ func (e *HTTPADCExecutor) handleHTTPResponse(resp 
*http.Response, serverAddr str
        e.log.V(1).Info("ADC Server sync success", "result", result)
        return nil
 }
+
+func (e *HTTPADCExecutor) handleHTTPValidateResponse(resp *http.Response, 
serverAddr string) error {
+       body, err := io.ReadAll(resp.Body)
+       if err != nil {
+               return fmt.Errorf("failed to read response body: %w", err)
+       }
+
+       e.log.V(1).Info("received HTTP validate response from ADC Server",
+               "server", serverAddr,
+               "status", resp.StatusCode,
+               "response", string(body),
+       )
+
+       parseValidationResult := func() *ADCValidateResult {
+               if len(body) == 0 {
+                       return nil
+               }
+               var result ADCValidateResult
+               if err := json.Unmarshal(body, &result); err != nil {
+                       return nil
+               }
+               return &result
+       }
+
+       if resp.StatusCode == http.StatusBadRequest {
+               result := parseValidationResult()
+               errMsg := string(body)
+               if result != nil && result.ErrorMessage != "" {
+                       errMsg = result.ErrorMessage
+               }
+               return types.ADCValidationServerAddrError{
+                       ServerAddr: serverAddr,
+                       Err:        errMsg,
+                       ValidationErrors: func() []types.ADCValidationDetail {
+                               if result == nil {
+                                       return nil
+                               }
+                               return result.Errors
+                       }(),
+               }
+       }
+
+       if resp.StatusCode/100 != 2 {
+               return fmt.Errorf("HTTP %d: %s", resp.StatusCode, string(body))
+       }
+
+       if result := parseValidationResult(); result != nil && result.Success 
!= nil && !*result.Success {
+               errMsg := result.ErrorMessage
+               if errMsg == "" {
+                       errMsg = "ADC validation failed"
+               }
+               return types.ADCValidationServerAddrError{
+                       ServerAddr:       serverAddr,
+                       Err:              errMsg,
+                       ValidationErrors: result.Errors,
+               }
+       }
+
+       return nil
+}
diff --git a/internal/controller/webhook_validation.go 
b/internal/controller/webhook_validation.go
new file mode 100644
index 00000000..877550af
--- /dev/null
+++ b/internal/controller/webhook_validation.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 controller
+
+import (
+       "context"
+
+       "github.com/go-logr/logr"
+       "sigs.k8s.io/controller-runtime/pkg/client"
+
+       v1alpha1 "github.com/apache/apisix-ingress-controller/api/v1alpha1"
+       apiv2 "github.com/apache/apisix-ingress-controller/api/v2"
+       "github.com/apache/apisix-ingress-controller/internal/provider"
+       "github.com/apache/apisix-ingress-controller/internal/utils"
+)
+
+func PrepareApisixRouteForValidation(ctx context.Context, c client.Client, log 
logr.Logger, route *apiv2.ApisixRoute) (*provider.TranslateContext, error) {
+       tctx := provider.NewDefaultTranslateContext(ctx)
+
+       ingressClass, err := FindMatchingIngressClass(tctx, c, log, route)
+       if err != nil {
+               return nil, err
+       }
+       if err := ProcessIngressClassParameters(tctx, c, log, route, 
ingressClass); err != nil {
+               return nil, err
+       }
+
+       reconciler := &ApisixRouteReconciler{
+               Client: c,
+               Log:    log,
+       }
+       if err := reconciler.processApisixRoute(tctx, route); err != nil {
+               return nil, err
+       }
+       return tctx, nil
+}
+
+func PrepareApisixConsumerForValidation(ctx context.Context, c client.Client, 
log logr.Logger, consumer *apiv2.ApisixConsumer) (*provider.TranslateContext, 
error) {
+       tctx := provider.NewDefaultTranslateContext(ctx)
+
+       ingressClass, err := FindMatchingIngressClass(tctx, c, log, consumer)
+       if err != nil {
+               return nil, err
+       }
+       if err := ProcessIngressClassParameters(tctx, c, log, consumer, 
ingressClass); err != nil {
+               return nil, err
+       }
+
+       reconciler := &ApisixConsumerReconciler{
+               Client: c,
+               Log:    log,
+       }
+       if err := reconciler.processSpec(ctx, tctx, consumer); err != nil {
+               return nil, err
+       }
+       return tctx, nil
+}
+
+func PrepareConsumerForValidation(ctx context.Context, c client.Client, log 
logr.Logger, consumer *v1alpha1.Consumer) (*provider.TranslateContext, error) {
+       tctx := provider.NewDefaultTranslateContext(ctx)
+
+       reconciler := &ConsumerReconciler{
+               Client: c,
+               Log:    log,
+       }
+       gateway, err := reconciler.getGateway(ctx, consumer)
+       if err != nil {
+               return nil, err
+       }
+       if err := ProcessGatewayProxy(c, log, tctx, gateway, 
utils.NamespacedNameKind(consumer)); err != nil {
+               return nil, err
+       }
+       if err := reconciler.processSpec(ctx, tctx, consumer); err != nil {
+               return nil, err
+       }
+       return tctx, nil
+}
+
+func PrepareApisixTlsForValidation(ctx context.Context, c client.Client, log 
logr.Logger, tls *apiv2.ApisixTls) (*provider.TranslateContext, error) {
+       tctx := provider.NewDefaultTranslateContext(ctx)
+
+       ingressClass, err := FindMatchingIngressClass(tctx, c, log, tls)
+       if err != nil {
+               return nil, err
+       }
+       if err := ProcessIngressClassParameters(tctx, c, log, tls, 
ingressClass); err != nil {
+               return nil, err
+       }
+
+       reconciler := &ApisixTlsReconciler{
+               Client: c,
+               Log:    log,
+       }
+       if err := reconciler.processApisixTls(ctx, tctx, tls); err != nil {
+               return nil, err
+       }
+       return tctx, nil
+}
diff --git a/internal/types/error.go b/internal/types/error.go
index 80dbf568..1388637d 100644
--- a/internal/types/error.go
+++ b/internal/types/error.go
@@ -92,3 +92,70 @@ type ADCExecutionServerAddrError struct {
 func (e ADCExecutionServerAddrError) Error() string {
        return fmt.Sprintf("ServerAddr: %s, Err: %s", e.ServerAddr, e.Err)
 }
+
+type ADCValidationErrors struct {
+       Errors []ADCValidationError
+}
+
+func (e ADCValidationErrors) Error() string {
+       messages := make([]string, 0, len(e.Errors))
+       for _, err := range e.Errors {
+               messages = append(messages, err.Error())
+       }
+       return fmt.Sprintf("ADC validation errors: [%s]", 
strings.Join(messages, "; "))
+}
+
+type ADCValidationError struct {
+       Name         string
+       FailedErrors []ADCValidationServerAddrError
+}
+
+func (e ADCValidationError) Error() string {
+       messages := make([]string, 0, len(e.FailedErrors))
+       for _, failed := range e.FailedErrors {
+               messages = append(messages, failed.Error())
+       }
+       return fmt.Sprintf("ADC validation error for %s: [%s]", e.Name, 
strings.Join(messages, "; "))
+}
+
+type ADCValidationServerAddrError struct {
+       Err              string
+       ServerAddr       string
+       ValidationErrors []ADCValidationDetail
+}
+
+func (e ADCValidationServerAddrError) Error() string {
+       if len(e.ValidationErrors) == 0 {
+               return fmt.Sprintf("ServerAddr: %s, Err: %s", e.ServerAddr, 
e.Err)
+       }
+
+       messages := make([]string, 0, len(e.ValidationErrors))
+       for _, detail := range e.ValidationErrors {
+               messages = append(messages, detail.Error())
+       }
+       return fmt.Sprintf("ServerAddr: %s, Err: %s (%s)", e.ServerAddr, e.Err, 
strings.Join(messages, "; "))
+}
+
+type ADCValidationDetail struct {
+       ResourceType string `json:"resource_type,omitempty"`
+       ResourceName string `json:"resource_name,omitempty"`
+       Message      string `json:"message,omitempty"`
+       Index        int    `json:"index,omitempty"`
+}
+
+func (e ADCValidationDetail) Error() string {
+       var parts []string
+       if e.ResourceType != "" {
+               parts = append(parts, fmt.Sprintf("type=%s", e.ResourceType))
+       }
+       if e.ResourceName != "" {
+               parts = append(parts, fmt.Sprintf("name=%s", e.ResourceName))
+       }
+       if e.Message != "" {
+               parts = append(parts, e.Message)
+       }
+       if len(parts) == 0 {
+               return fmt.Sprintf("index=%d", e.Index)
+       }
+       return strings.Join(parts, ", ")
+}
diff --git a/internal/webhook/v1/adc_validation.go 
b/internal/webhook/v1/adc_validation.go
new file mode 100644
index 00000000..668a65ab
--- /dev/null
+++ b/internal/webhook/v1/adc_validation.go
@@ -0,0 +1,233 @@
+// 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"
+       "errors"
+
+       "github.com/go-logr/logr"
+       "sigs.k8s.io/controller-runtime/pkg/client"
+
+       adctypes "github.com/apache/apisix-ingress-controller/api/adc"
+       v1alpha1 "github.com/apache/apisix-ingress-controller/api/v1alpha1"
+       apiv2 "github.com/apache/apisix-ingress-controller/api/v2"
+       adcclient 
"github.com/apache/apisix-ingress-controller/internal/adc/client"
+       adctranslator 
"github.com/apache/apisix-ingress-controller/internal/adc/translator"
+       "github.com/apache/apisix-ingress-controller/internal/controller"
+       "github.com/apache/apisix-ingress-controller/internal/controller/config"
+       "github.com/apache/apisix-ingress-controller/internal/controller/label"
+       "github.com/apache/apisix-ingress-controller/internal/provider"
+       internaltypes 
"github.com/apache/apisix-ingress-controller/internal/types"
+       "github.com/apache/apisix-ingress-controller/internal/utils"
+)
+
+type adcAdmissionValidator struct {
+       kubeClient             client.Client
+       client                 *adcclient.Client
+       translator             *adctranslator.Translator
+       log                    logr.Logger
+       defaultResolveEndpoint bool
+}
+
+func newADCAdmissionValidator(kubeClient client.Client, log logr.Logger) 
(*adcAdmissionValidator, error) {
+       defaultMode := string(config.ControllerConfig.ProviderConfig.Type)
+       cli, err := adcclient.New(log, defaultMode, 
config.ControllerConfig.ExecADCTimeout.Duration)
+       if err != nil {
+               return nil, err
+       }
+
+       return &adcAdmissionValidator{
+               kubeClient:             kubeClient,
+               client:                 cli,
+               translator:             adctranslator.NewTranslator(log),
+               log:                    log.WithName("adc-validation"),
+               defaultResolveEndpoint: 
config.ControllerConfig.ProviderConfig.Type == config.ProviderTypeStandalone,
+       }, nil
+}
+
+func (v *adcAdmissionValidator) Validate(ctx context.Context, obj 
client.Object) error {
+       if v == nil {
+               return nil
+       }
+
+       task, err := v.buildTask(ctx, obj)
+       if err != nil {
+               return err
+       }
+       if task == nil {
+               return nil
+       }
+
+       if err := v.client.Validate(ctx, *task); err != nil {
+               var validationErrs internaltypes.ADCValidationErrors
+               if errors.As(err, &validationErrs) {
+                       return err
+               }
+
+               v.log.Error(err, "ADC validation unavailable, allowing 
admission", "resource", utils.NamespacedNameKind(obj))
+               return nil
+       }
+
+       return nil
+}
+
+func (v *adcAdmissionValidator) buildTask(ctx context.Context, obj 
client.Object) (*adcclient.Task, error) {
+       var (
+               tctx          *provider.TranslateContext
+               result        *adctranslator.TranslateResult
+               resourceTypes []string
+               err           error
+       )
+
+       switch resource := obj.(type) {
+       case *apiv2.ApisixRoute:
+               configs, err := v.buildIngressClassConfigs(ctx, 
resource.DeepCopy())
+               if err != nil {
+                       return nil, err
+               }
+               if len(configs) == 0 {
+                       return nil, nil
+               }
+               tctx, err = controller.PrepareApisixRouteForValidation(ctx, 
v.kubeClient, v.log, resource.DeepCopy())
+               if err != nil {
+                       return nil, err
+               }
+               result, err = v.translator.TranslateApisixRoute(tctx, 
resource.DeepCopy())
+               resourceTypes = append(resourceTypes, adctypes.TypeService)
+               if err != nil {
+                       return nil, err
+               }
+               if result == nil {
+                       return nil, nil
+               }
+               return v.newTask(obj, configs, resourceTypes, result), nil
+       case *apiv2.ApisixConsumer:
+               configs, err := v.buildIngressClassConfigs(ctx, 
resource.DeepCopy())
+               if err != nil {
+                       return nil, err
+               }
+               if len(configs) == 0 {
+                       return nil, nil
+               }
+               tctx, err = controller.PrepareApisixConsumerForValidation(ctx, 
v.kubeClient, v.log, resource.DeepCopy())
+               if err != nil {
+                       return nil, err
+               }
+               result, err = v.translator.TranslateApisixConsumer(tctx, 
resource.DeepCopy())
+               resourceTypes = append(resourceTypes, adctypes.TypeConsumer)
+               if err != nil {
+                       return nil, err
+               }
+               if result == nil {
+                       return nil, nil
+               }
+               return v.newTask(obj, configs, resourceTypes, result), nil
+       case *v1alpha1.Consumer:
+               tctx, err = controller.PrepareConsumerForValidation(ctx, 
v.kubeClient, v.log, resource.DeepCopy())
+               if err != nil {
+                       return nil, err
+               }
+               result, err = v.translator.TranslateConsumerV1alpha1(tctx, 
resource.DeepCopy())
+               resourceTypes = append(resourceTypes, adctypes.TypeConsumer)
+       case *apiv2.ApisixTls:
+               configs, err := v.buildIngressClassConfigs(ctx, 
resource.DeepCopy())
+               if err != nil {
+                       return nil, err
+               }
+               if len(configs) == 0 {
+                       return nil, nil
+               }
+               tctx, err = controller.PrepareApisixTlsForValidation(ctx, 
v.kubeClient, v.log, resource.DeepCopy())
+               if err != nil {
+                       return nil, err
+               }
+               result, err = v.translator.TranslateApisixTls(tctx, 
resource.DeepCopy())
+               resourceTypes = append(resourceTypes, adctypes.TypeSSL)
+               if err != nil {
+                       return nil, err
+               }
+               if result == nil {
+                       return nil, nil
+               }
+               return v.newTask(obj, configs, resourceTypes, result), nil
+       default:
+               return nil, nil
+       }
+       if err != nil {
+               return nil, err
+       }
+       if result == nil {
+               return nil, nil
+       }
+
+       configs, err := v.buildConfigs(tctx)
+       if err != nil {
+               return nil, err
+       }
+       if len(configs) == 0 {
+               return nil, nil
+       }
+
+       return v.newTask(obj, configs, resourceTypes, result), nil
+}
+
+func (v *adcAdmissionValidator) buildConfigs(tctx *provider.TranslateContext) 
(map[internaltypes.NamespacedNameKind]adctypes.Config, error) {
+       configs := make(map[internaltypes.NamespacedNameKind]adctypes.Config, 
len(tctx.GatewayProxies))
+       for key, gp := range tctx.GatewayProxies {
+               cfg, err := v.translator.TranslateGatewayProxyToConfig(tctx, 
&gp, v.defaultResolveEndpoint)
+               if err != nil {
+                       return nil, err
+               }
+               if cfg == nil {
+                       continue
+               }
+               configs[key] = *cfg
+       }
+       return configs, nil
+}
+
+func (v *adcAdmissionValidator) buildIngressClassConfigs(ctx context.Context, 
obj client.Object) (map[internaltypes.NamespacedNameKind]adctypes.Config, 
error) {
+       tctx := provider.NewDefaultTranslateContext(ctx)
+
+       ingressClass, err := controller.FindMatchingIngressClass(tctx, 
v.kubeClient, v.log, obj)
+       if err != nil {
+               return nil, err
+       }
+       if err := controller.ProcessIngressClassParameters(tctx, v.kubeClient, 
v.log, obj, ingressClass); err != nil {
+               return nil, err
+       }
+       return v.buildConfigs(tctx)
+}
+
+func (v *adcAdmissionValidator) newTask(obj client.Object, configs 
map[internaltypes.NamespacedNameKind]adctypes.Config, resourceTypes []string, 
result *adctranslator.TranslateResult) *adcclient.Task {
+       return &adcclient.Task{
+               Key:           utils.NamespacedNameKind(obj),
+               Name:          utils.NamespacedNameKind(obj).String(),
+               Labels:        label.GenLabel(obj),
+               Configs:       configs,
+               ResourceTypes: resourceTypes,
+               Resources: &adctypes.Resources{
+                       GlobalRules:    result.GlobalRules,
+                       PluginMetadata: result.PluginMetadata,
+                       Services:       result.Services,
+                       SSLs:           result.SSL,
+                       Consumers:      result.Consumers,
+               },
+       }
+}
diff --git a/internal/webhook/v1/adc_validation_test.go 
b/internal/webhook/v1/adc_validation_test.go
new file mode 100644
index 00000000..535051d6
--- /dev/null
+++ b/internal/webhook/v1/adc_validation_test.go
@@ -0,0 +1,91 @@
+// 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 (
+       "net/http"
+       "net/http/httptest"
+       "testing"
+
+       "github.com/stretchr/testify/require"
+       networkingv1 "k8s.io/api/networking/v1"
+       metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
+       "k8s.io/apimachinery/pkg/runtime"
+       "k8s.io/utils/ptr"
+
+       apisixv1alpha1 
"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"
+)
+
+func withMockADCServer(t *testing.T, handler http.HandlerFunc) string {
+       t.Helper()
+
+       server := httptest.NewServer(handler)
+       t.Setenv("ADC_SERVER_URL", server.URL)
+       t.Cleanup(server.Close)
+       return server.URL
+}
+
+func managedIngressClassWithGatewayProxy(endpoint string) []runtime.Object {
+       return managedIngressClassWithGatewayProxyMode(endpoint, 
"apisix-standalone")
+}
+
+func managedIngressClassWithGatewayProxyMode(endpoint, mode string) 
[]runtime.Object {
+       namespace := "default"
+
+       return []runtime.Object{
+               &networkingv1.IngressClass{
+                       ObjectMeta: metav1.ObjectMeta{Name: "apisix"},
+                       Spec: networkingv1.IngressClassSpec{
+                               Controller: 
config.ControllerConfig.ControllerName,
+                               Parameters: 
&networkingv1.IngressClassParametersReference{
+                                       APIGroup:  
ptr.To(apisixv1alpha1.GroupVersion.Group),
+                                       Kind:      
internaltypes.KindGatewayProxy,
+                                       Name:      "gateway-proxy",
+                                       Namespace: ptr.To(namespace),
+                               },
+                       },
+               },
+               &apisixv1alpha1.GatewayProxy{
+                       ObjectMeta: metav1.ObjectMeta{
+                               Name:      "gateway-proxy",
+                               Namespace: namespace,
+                       },
+                       Spec: apisixv1alpha1.GatewayProxySpec{
+                               Provider: &apisixv1alpha1.GatewayProxyProvider{
+                                       Type: 
apisixv1alpha1.ProviderTypeControlPlane,
+                                       ControlPlane: 
&apisixv1alpha1.ControlPlaneProvider{
+                                               Mode:      mode,
+                                               Endpoints: []string{endpoint},
+                                               Auth: 
apisixv1alpha1.ControlPlaneAuth{
+                                                       Type: 
apisixv1alpha1.AuthTypeAdminKey,
+                                                       AdminKey: 
&apisixv1alpha1.AdminKeyAuth{
+                                                               Value: "token",
+                                                       },
+                                               },
+                                       },
+                               },
+                       },
+               },
+       }
+}
+
+func requireValidateRequest(t *testing.T, r *http.Request) {
+       t.Helper()
+       require.Equal(t, http.MethodPut, r.Method)
+       require.Equal(t, "/validate", r.URL.Path)
+}
diff --git a/internal/webhook/v1/apisixconsumer_webhook.go 
b/internal/webhook/v1/apisixconsumer_webhook.go
index b0561259..796491f5 100644
--- a/internal/webhook/v1/apisixconsumer_webhook.go
+++ b/internal/webhook/v1/apisixconsumer_webhook.go
@@ -42,19 +42,24 @@ func SetupApisixConsumerWebhookWithManager(mgr 
ctrl.Manager) error {
                Complete()
 }
 
-// 
+kubebuilder:webhook:path=/validate-apisix-apache-org-v2-apisixconsumer,mutating=false,failurePolicy=fail,sideEffects=None,groups=apisix.apache.org,resources=apisixconsumers,verbs=create;update,versions=v2,name=vapisixconsumer-v2.kb.io,admissionReviewVersions=v1,failurePolicy=Ignore
+// 
+kubebuilder:webhook:path=/validate-apisix-apache-org-v2-apisixconsumer,mutating=false,failurePolicy=Ignore,sideEffects=None,groups=apisix.apache.org,resources=apisixconsumers,verbs=create;update,versions=v2,name=vapisixconsumer-v2.kb.io,admissionReviewVersions=v1
 
 type ApisixConsumerCustomValidator struct {
-       Client  client.Client
-       checker reference.Checker
+       Client       client.Client
+       checker      reference.Checker
+       adcValidator *adcAdmissionValidator
+       initErr      error
 }
 
 var _ webhook.CustomValidator = &ApisixConsumerCustomValidator{}
 
 func NewApisixConsumerCustomValidator(c client.Client) 
*ApisixConsumerCustomValidator {
+       adcValidator, err := newADCAdmissionValidator(c, apisixConsumerLog)
        return &ApisixConsumerCustomValidator{
-               Client:  c,
-               checker: reference.NewChecker(c, apisixConsumerLog),
+               Client:       c,
+               checker:      reference.NewChecker(c, apisixConsumerLog),
+               adcValidator: adcValidator,
+               initErr:      err,
        }
 }
 
@@ -69,7 +74,15 @@ func (v *ApisixConsumerCustomValidator) ValidateCreate(ctx 
context.Context, obj
                return nil, nil
        }
 
-       return v.collectWarnings(ctx, consumer), nil
+       warnings := v.collectWarnings(ctx, consumer)
+       if v.initErr != nil {
+               apisixConsumerLog.Error(v.initErr, "ADC validator init failed, 
skipping ADC validation")
+               return warnings, nil
+       }
+       if len(warnings) > 0 {
+               return warnings, nil
+       }
+       return warnings, v.adcValidator.Validate(ctx, consumer)
 }
 
 func (v *ApisixConsumerCustomValidator) ValidateUpdate(ctx context.Context, 
oldObj, newObj runtime.Object) (admission.Warnings, error) {
@@ -82,7 +95,15 @@ func (v *ApisixConsumerCustomValidator) ValidateUpdate(ctx 
context.Context, oldO
                return nil, nil
        }
 
-       return v.collectWarnings(ctx, consumer), nil
+       warnings := v.collectWarnings(ctx, consumer)
+       if v.initErr != nil {
+               apisixConsumerLog.Error(v.initErr, "ADC validator init failed, 
skipping ADC validation")
+               return warnings, nil
+       }
+       if len(warnings) > 0 {
+               return warnings, nil
+       }
+       return warnings, v.adcValidator.Validate(ctx, consumer)
 }
 
 func (*ApisixConsumerCustomValidator) ValidateDelete(context.Context, 
runtime.Object) (admission.Warnings, error) {
diff --git a/internal/webhook/v1/apisixconsumer_webhook_test.go 
b/internal/webhook/v1/apisixconsumer_webhook_test.go
index 8c31768c..e1be420d 100644
--- a/internal/webhook/v1/apisixconsumer_webhook_test.go
+++ b/internal/webhook/v1/apisixconsumer_webhook_test.go
@@ -17,6 +17,7 @@ package v1
 
 import (
        "context"
+       "net/http"
        "testing"
 
        "github.com/stretchr/testify/require"
@@ -27,22 +28,35 @@ import (
        clientgoscheme "k8s.io/client-go/kubernetes/scheme"
        "sigs.k8s.io/controller-runtime/pkg/client/fake"
 
+       apisixv1alpha1 
"github.com/apache/apisix-ingress-controller/api/v1alpha1"
        apisixv2 "github.com/apache/apisix-ingress-controller/api/v2"
        "github.com/apache/apisix-ingress-controller/internal/controller/config"
 )
 
+const managedIngressClassName = "apisix"
+
 func buildApisixConsumerValidator(t *testing.T, objects ...runtime.Object) 
*ApisixConsumerCustomValidator {
        t.Helper()
 
        scheme := runtime.NewScheme()
        require.NoError(t, clientgoscheme.AddToScheme(scheme))
        require.NoError(t, networkingv1.AddToScheme(scheme))
+       require.NoError(t, apisixv1alpha1.AddToScheme(scheme))
        require.NoError(t, apisixv2.AddToScheme(scheme))
 
-       managed := []runtime.Object{
-               &networkingv1.IngressClass{
+       managed := []runtime.Object{}
+       hasManagedIngressClass := false
+       for _, obj := range objects {
+               ingressClass, ok := obj.(*networkingv1.IngressClass)
+               if ok && ingressClass.Name == managedIngressClassName {
+                       hasManagedIngressClass = true
+                       break
+               }
+       }
+       if !hasManagedIngressClass {
+               managed = append(managed, &networkingv1.IngressClass{
                        ObjectMeta: metav1.ObjectMeta{
-                               Name: "apisix",
+                               Name: managedIngressClassName,
                                Annotations: map[string]string{
                                        
"ingressclass.kubernetes.io/is-default-class": "true",
                                },
@@ -50,7 +64,7 @@ func buildApisixConsumerValidator(t *testing.T, objects 
...runtime.Object) *Apis
                        Spec: networkingv1.IngressClassSpec{
                                Controller: 
config.ControllerConfig.ControllerName,
                        },
-               },
+               })
        }
        allObjects := append(managed, objects...)
        builder := 
fake.NewClientBuilder().WithScheme(scheme).WithRuntimeObjects(allObjects...)
@@ -152,3 +166,72 @@ func 
TestApisixConsumerValidator_NoWarningsWhenSecretsExist(t *testing.T) {
        require.NoError(t, err)
        require.Empty(t, warnings)
 }
+
+func TestApisixConsumerValidator_DeniesOnADCValidationFailure(t *testing.T) {
+       serverURL := withMockADCServer(t, func(w http.ResponseWriter, r 
*http.Request) {
+               requireValidateRequest(t, r)
+               w.WriteHeader(http.StatusBadRequest)
+               _, _ = w.Write([]byte(`{"message":"consumer 
rejected","errors":[{"resource_type":"consumers","resource_name":"demo","message":"duplicate
 credential"}]}`))
+       })
+
+       consumer := &apisixv2.ApisixConsumer{
+               ObjectMeta: metav1.ObjectMeta{
+                       Name:      "demo",
+                       Namespace: "default",
+               },
+               Spec: apisixv2.ApisixConsumerSpec{
+                       IngressClassName: "apisix",
+                       AuthParameter: apisixv2.ApisixConsumerAuthParameter{
+                               KeyAuth: &apisixv2.ApisixConsumerKeyAuth{
+                                       SecretRef: 
&corev1.LocalObjectReference{Name: "key-auth"},
+                               },
+                       },
+               },
+       }
+
+       objects := append(managedIngressClassWithGatewayProxy(serverURL),
+               &corev1.Secret{
+                       ObjectMeta: metav1.ObjectMeta{Name: "key-auth", 
Namespace: "default"},
+                       Data: map[string][]byte{
+                               "key": []byte("secret-key"),
+                       },
+               },
+       )
+
+       validator := buildApisixConsumerValidator(t, objects...)
+
+       warnings, err := validator.ValidateCreate(context.Background(), 
consumer)
+       require.Error(t, err)
+       require.Contains(t, err.Error(), "consumer rejected")
+       require.Empty(t, warnings)
+}
+
+func TestApisixConsumerValidator_UsesADCValidateEndpointForControlPlane(t 
*testing.T) {
+       serverURL := withMockADCServer(t, func(w http.ResponseWriter, r 
*http.Request) {
+               requireValidateRequest(t, r)
+               w.WriteHeader(http.StatusBadRequest)
+               _, _ = w.Write([]byte(`{"message":"consumer 
rejected","errors":[{"resource_type":"consumers","resource_name":"demo","message":"duplicate
 credential"}]}`))
+       })
+
+       consumer := &apisixv2.ApisixConsumer{
+               ObjectMeta: metav1.ObjectMeta{
+                       Name:      "demo",
+                       Namespace: "default",
+               },
+               Spec: apisixv2.ApisixConsumerSpec{
+                       IngressClassName: managedIngressClassName,
+                       AuthParameter: apisixv2.ApisixConsumerAuthParameter{
+                               KeyAuth: &apisixv2.ApisixConsumerKeyAuth{
+                                       Value: 
&apisixv2.ApisixConsumerKeyAuthValue{Key: "shared-key"},
+                               },
+                       },
+               },
+       }
+
+       validator := buildApisixConsumerValidator(t, 
managedIngressClassWithGatewayProxyMode(serverURL, "apisix")...)
+
+       warnings, err := validator.ValidateCreate(context.Background(), 
consumer)
+       require.Error(t, err)
+       require.Contains(t, err.Error(), "consumer rejected")
+       require.Empty(t, warnings)
+}
diff --git a/internal/webhook/v1/apisixroute_webhook.go 
b/internal/webhook/v1/apisixroute_webhook.go
index 31ec796e..27028321 100644
--- a/internal/webhook/v1/apisixroute_webhook.go
+++ b/internal/webhook/v1/apisixroute_webhook.go
@@ -41,19 +41,24 @@ func SetupApisixRouteWebhookWithManager(mgr ctrl.Manager) 
error {
                Complete()
 }
 
-// 
+kubebuilder:webhook:path=/validate-apisix-apache-org-v2-apisixroute,mutating=false,failurePolicy=fail,sideEffects=None,groups=apisix.apache.org,resources=apisixroutes,verbs=create;update,versions=v2,name=vapisixroute-v2.kb.io,admissionReviewVersions=v1,failurePolicy=Ignore
+// 
+kubebuilder:webhook:path=/validate-apisix-apache-org-v2-apisixroute,mutating=false,failurePolicy=Ignore,sideEffects=None,groups=apisix.apache.org,resources=apisixroutes,verbs=create;update,versions=v2,name=vapisixroute-v2.kb.io,admissionReviewVersions=v1
 
 type ApisixRouteCustomValidator struct {
-       Client  client.Client
-       checker reference.Checker
+       Client       client.Client
+       checker      reference.Checker
+       adcValidator *adcAdmissionValidator
+       initErr      error
 }
 
 var _ webhook.CustomValidator = &ApisixRouteCustomValidator{}
 
 func NewApisixRouteCustomValidator(c client.Client) 
*ApisixRouteCustomValidator {
+       adcValidator, err := newADCAdmissionValidator(c, apisixRouteLog)
        return &ApisixRouteCustomValidator{
-               Client:  c,
-               checker: reference.NewChecker(c, apisixRouteLog),
+               Client:       c,
+               checker:      reference.NewChecker(c, apisixRouteLog),
+               adcValidator: adcValidator,
+               initErr:      err,
        }
 }
 
@@ -67,7 +72,12 @@ func (v *ApisixRouteCustomValidator) ValidateCreate(ctx 
context.Context, obj run
                return nil, nil
        }
 
-       return v.collectWarnings(ctx, route), nil
+       warnings := v.collectWarnings(ctx, route)
+       if v.initErr != nil {
+               apisixRouteLog.Error(v.initErr, "ADC validator init failed, 
skipping ADC validation")
+               return warnings, nil
+       }
+       return warnings, v.adcValidator.Validate(ctx, route)
 }
 
 func (v *ApisixRouteCustomValidator) ValidateUpdate(ctx context.Context, 
oldObj, newObj runtime.Object) (admission.Warnings, error) {
@@ -80,7 +90,12 @@ func (v *ApisixRouteCustomValidator) ValidateUpdate(ctx 
context.Context, oldObj,
                return nil, nil
        }
 
-       return v.collectWarnings(ctx, route), nil
+       warnings := v.collectWarnings(ctx, route)
+       if v.initErr != nil {
+               apisixRouteLog.Error(v.initErr, "ADC validator init failed, 
skipping ADC validation")
+               return warnings, nil
+       }
+       return warnings, v.adcValidator.Validate(ctx, route)
 }
 
 func (*ApisixRouteCustomValidator) ValidateDelete(context.Context, 
runtime.Object) (admission.Warnings, error) {
diff --git a/internal/webhook/v1/apisixroute_webhook_test.go 
b/internal/webhook/v1/apisixroute_webhook_test.go
index b8ca3aa2..98bab580 100644
--- a/internal/webhook/v1/apisixroute_webhook_test.go
+++ b/internal/webhook/v1/apisixroute_webhook_test.go
@@ -17,6 +17,7 @@ package v1
 
 import (
        "context"
+       "net/http"
        "testing"
 
        "github.com/stretchr/testify/require"
@@ -24,9 +25,11 @@ import (
        networkingv1 "k8s.io/api/networking/v1"
        metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
        "k8s.io/apimachinery/pkg/runtime"
+       "k8s.io/apimachinery/pkg/util/intstr"
        clientgoscheme "k8s.io/client-go/kubernetes/scheme"
        "sigs.k8s.io/controller-runtime/pkg/client/fake"
 
+       apisixv1alpha1 
"github.com/apache/apisix-ingress-controller/api/v1alpha1"
        apisixv2 "github.com/apache/apisix-ingress-controller/api/v2"
        "github.com/apache/apisix-ingress-controller/internal/controller/config"
 )
@@ -37,10 +40,20 @@ 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, apisixv1alpha1.AddToScheme(scheme))
        require.NoError(t, apisixv2.AddToScheme(scheme))
 
-       managed := []runtime.Object{
-               &networkingv1.IngressClass{
+       managed := []runtime.Object{}
+       hasManagedIngressClass := false
+       for _, obj := range objects {
+               ingressClass, ok := obj.(*networkingv1.IngressClass)
+               if ok && ingressClass.Name == "apisix" {
+                       hasManagedIngressClass = true
+                       break
+               }
+       }
+       if !hasManagedIngressClass {
+               managed = append(managed, &networkingv1.IngressClass{
                        ObjectMeta: metav1.ObjectMeta{
                                Name: "apisix",
                                Annotations: map[string]string{
@@ -50,7 +63,7 @@ func buildApisixRouteValidator(t *testing.T, objects 
...runtime.Object) *ApisixR
                        Spec: networkingv1.IngressClassSpec{
                                Controller: 
config.ControllerConfig.ControllerName,
                        },
-               },
+               })
        }
        allObjects := append(managed, objects...)
        builder := 
fake.NewClientBuilder().WithScheme(scheme).WithRuntimeObjects(allObjects...)
@@ -174,3 +187,92 @@ func TestApisixRouteValidator_NoWarnings(t *testing.T) {
        require.NoError(t, err)
        require.Empty(t, warnings)
 }
+
+func TestApisixRouteValidator_DeniesOnADCValidationFailure(t *testing.T) {
+       serverURL := withMockADCServer(t, func(w http.ResponseWriter, r 
*http.Request) {
+               requireValidateRequest(t, r)
+               w.WriteHeader(http.StatusBadRequest)
+               _, _ = w.Write([]byte(`{"message":"route 
rejected","errors":[{"resource_type":"routes","resource_name":"demo","message":"invalid
 plugin config"}]}`))
+       })
+
+       route := &apisixv2.ApisixRoute{
+               ObjectMeta: metav1.ObjectMeta{
+                       Name:      "demo",
+                       Namespace: "default",
+               },
+               Spec: apisixv2.ApisixRouteSpec{
+                       IngressClassName: "apisix",
+                       HTTP: []apisixv2.ApisixRouteHTTP{{
+                               Name: "rule",
+                               Backends: []apisixv2.ApisixRouteHTTPBackend{{
+                                       ServiceName:        "backend",
+                                       ServicePort:        intstr.FromInt(80),
+                                       ResolveGranularity: 
apisixv2.ResolveGranularityService,
+                               }},
+                       }},
+               },
+       }
+
+       objects := append(managedIngressClassWithGatewayProxy(serverURL),
+               &corev1.Service{
+                       ObjectMeta: metav1.ObjectMeta{Name: "backend", 
Namespace: "default"},
+                       Spec: corev1.ServiceSpec{
+                               ClusterIP: "10.0.0.1",
+                               Ports: []corev1.ServicePort{{
+                                       Port: 80,
+                               }},
+                       },
+               },
+       )
+
+       validator := buildApisixRouteValidator(t, objects...)
+
+       warnings, err := validator.ValidateCreate(context.Background(), route)
+       require.Error(t, err)
+       require.Contains(t, err.Error(), "route rejected")
+       require.Empty(t, warnings)
+}
+
+func TestApisixRouteValidator_FailsOpenWhenADCUnavailable(t *testing.T) {
+       serverURL := withMockADCServer(t, func(w http.ResponseWriter, r 
*http.Request) {
+               requireValidateRequest(t, r)
+               w.WriteHeader(http.StatusInternalServerError)
+               _, _ = w.Write([]byte(`{"error":"backend unavailable"}`))
+       })
+
+       route := &apisixv2.ApisixRoute{
+               ObjectMeta: metav1.ObjectMeta{
+                       Name:      "demo",
+                       Namespace: "default",
+               },
+               Spec: apisixv2.ApisixRouteSpec{
+                       IngressClassName: "apisix",
+                       HTTP: []apisixv2.ApisixRouteHTTP{{
+                               Name: "rule",
+                               Backends: []apisixv2.ApisixRouteHTTPBackend{{
+                                       ServiceName:        "backend",
+                                       ServicePort:        intstr.FromInt(80),
+                                       ResolveGranularity: 
apisixv2.ResolveGranularityService,
+                               }},
+                       }},
+               },
+       }
+
+       objects := append(managedIngressClassWithGatewayProxy(serverURL),
+               &corev1.Service{
+                       ObjectMeta: metav1.ObjectMeta{Name: "backend", 
Namespace: "default"},
+                       Spec: corev1.ServiceSpec{
+                               ClusterIP: "10.0.0.1",
+                               Ports: []corev1.ServicePort{{
+                                       Port: 80,
+                               }},
+                       },
+               },
+       )
+
+       validator := buildApisixRouteValidator(t, objects...)
+
+       warnings, err := validator.ValidateCreate(context.Background(), route)
+       require.NoError(t, err)
+       require.Empty(t, warnings)
+}
diff --git a/internal/webhook/v1/apisixtls_webhook.go 
b/internal/webhook/v1/apisixtls_webhook.go
index 985c3b87..9180c809 100644
--- a/internal/webhook/v1/apisixtls_webhook.go
+++ b/internal/webhook/v1/apisixtls_webhook.go
@@ -42,19 +42,24 @@ func SetupApisixTlsWebhookWithManager(mgr ctrl.Manager) 
error {
                Complete()
 }
 
-// 
+kubebuilder:webhook:path=/validate-apisix-apache-org-v2-apisixtls,mutating=false,failurePolicy=fail,sideEffects=None,groups=apisix.apache.org,resources=apisixtlses,verbs=create;update,versions=v2,name=vapisixtls-v2.kb.io,admissionReviewVersions=v1,failurePolicy=Ignore
+// 
+kubebuilder:webhook:path=/validate-apisix-apache-org-v2-apisixtls,mutating=false,failurePolicy=Ignore,sideEffects=None,groups=apisix.apache.org,resources=apisixtlses,verbs=create;update,versions=v2,name=vapisixtls-v2.kb.io,admissionReviewVersions=v1
 
 type ApisixTlsCustomValidator struct {
-       Client  client.Client
-       checker reference.Checker
+       Client       client.Client
+       checker      reference.Checker
+       adcValidator *adcAdmissionValidator
+       initErr      error
 }
 
 var _ webhook.CustomValidator = &ApisixTlsCustomValidator{}
 
 func NewApisixTlsCustomValidator(c client.Client) *ApisixTlsCustomValidator {
+       adcValidator, err := newADCAdmissionValidator(c, apisixTlsLog)
        return &ApisixTlsCustomValidator{
-               Client:  c,
-               checker: reference.NewChecker(c, apisixTlsLog),
+               Client:       c,
+               checker:      reference.NewChecker(c, apisixTlsLog),
+               adcValidator: adcValidator,
+               initErr:      err,
        }
 }
 
@@ -74,7 +79,15 @@ func (v *ApisixTlsCustomValidator) ValidateCreate(ctx 
context.Context, obj runti
                return nil, fmt.Errorf("%s", 
sslvalidator.FormatConflicts(conflicts))
        }
 
-       return v.collectWarnings(ctx, tls), nil
+       warnings := v.collectWarnings(ctx, tls)
+       // Skip ADC validation when secrets are missing: the translator cannot
+       // load cert/key material, so validation would always fail. The missing-
+       // secret warnings are sufficient to inform the user.
+       if v.initErr != nil || len(warnings) > 0 {
+               return warnings, nil
+       }
+
+       return warnings, v.adcValidator.Validate(ctx, tls)
 }
 
 func (v *ApisixTlsCustomValidator) ValidateUpdate(ctx context.Context, oldObj, 
newObj runtime.Object) (admission.Warnings, error) {
@@ -93,7 +106,12 @@ func (v *ApisixTlsCustomValidator) ValidateUpdate(ctx 
context.Context, oldObj, n
                return nil, fmt.Errorf("%s", 
sslvalidator.FormatConflicts(conflicts))
        }
 
-       return v.collectWarnings(ctx, tls), nil
+       warnings := v.collectWarnings(ctx, tls)
+       if v.initErr != nil || len(warnings) > 0 {
+               return warnings, nil
+       }
+
+       return warnings, v.adcValidator.Validate(ctx, tls)
 }
 
 func (*ApisixTlsCustomValidator) ValidateDelete(context.Context, 
runtime.Object) (admission.Warnings, error) {
diff --git a/internal/webhook/v1/apisixtls_webhook_test.go 
b/internal/webhook/v1/apisixtls_webhook_test.go
index 205236f6..c775478d 100644
--- a/internal/webhook/v1/apisixtls_webhook_test.go
+++ b/internal/webhook/v1/apisixtls_webhook_test.go
@@ -17,6 +17,7 @@ package v1
 
 import (
        "context"
+       "net/http"
        "testing"
 
        "github.com/stretchr/testify/require"
@@ -27,6 +28,7 @@ import (
        clientgoscheme "k8s.io/client-go/kubernetes/scheme"
        "sigs.k8s.io/controller-runtime/pkg/client/fake"
 
+       apisixv1alpha1 
"github.com/apache/apisix-ingress-controller/api/v1alpha1"
        apisixv2 "github.com/apache/apisix-ingress-controller/api/v2"
        "github.com/apache/apisix-ingress-controller/internal/controller/config"
 )
@@ -37,10 +39,20 @@ 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, apisixv1alpha1.AddToScheme(scheme))
        require.NoError(t, apisixv2.AddToScheme(scheme))
 
-       managed := []runtime.Object{
-               &networkingv1.IngressClass{
+       managed := []runtime.Object{}
+       hasManagedIngressClass := false
+       for _, obj := range objects {
+               ingressClass, ok := obj.(*networkingv1.IngressClass)
+               if ok && ingressClass.Name == "apisix" {
+                       hasManagedIngressClass = true
+                       break
+               }
+       }
+       if !hasManagedIngressClass {
+               managed = append(managed, &networkingv1.IngressClass{
                        ObjectMeta: metav1.ObjectMeta{
                                Name: "apisix",
                                Annotations: map[string]string{
@@ -50,7 +62,7 @@ func buildApisixTlsValidator(t *testing.T, objects 
...runtime.Object) *ApisixTls
                        Spec: networkingv1.IngressClassSpec{
                                Controller: 
config.ControllerConfig.ControllerName,
                        },
-               },
+               })
        }
        allObjects := append(managed, objects...)
        builder := 
fake.NewClientBuilder().WithScheme(scheme).WithRuntimeObjects(allObjects...)
@@ -129,3 +141,30 @@ func TestApisixTlsValidator_NoWarningsWhenSecretsExist(t 
*testing.T) {
        require.NoError(t, err)
        require.Empty(t, warnings)
 }
+
+func TestApisixTlsValidator_DeniesOnADCValidationFailure(t *testing.T) {
+       serverURL := withMockADCServer(t, func(w http.ResponseWriter, r 
*http.Request) {
+               requireValidateRequest(t, r)
+               w.WriteHeader(http.StatusBadRequest)
+               _, _ = w.Write([]byte(`{"message":"tls 
rejected","errors":[{"resource_type":"ssls","resource_name":"demo","message":"invalid
 sni"}]}`))
+       })
+
+       tls := newApisixTls()
+
+       objects := append(managedIngressClassWithGatewayProxy(serverURL),
+               &corev1.Secret{
+                       ObjectMeta: metav1.ObjectMeta{Name: "server-cert", 
Namespace: "default"},
+                       Data: map[string][]byte{
+                               corev1.TLSCertKey:       []byte("cert"),
+                               corev1.TLSPrivateKeyKey: []byte("key"),
+                       },
+               },
+       )
+
+       validator := buildApisixTlsValidator(t, objects...)
+
+       warnings, err := validator.ValidateCreate(context.Background(), tls)
+       require.Error(t, err)
+       require.Contains(t, err.Error(), "tls rejected")
+       require.Empty(t, warnings)
+}
diff --git a/internal/webhook/v1/consumer_webhook.go 
b/internal/webhook/v1/consumer_webhook.go
index f9b3bd77..57767520 100644
--- a/internal/webhook/v1/consumer_webhook.go
+++ b/internal/webhook/v1/consumer_webhook.go
@@ -17,8 +17,11 @@ package v1
 
 import (
        "context"
+       "encoding/json"
        "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"
@@ -29,6 +32,7 @@ import (
 
        apisixv1alpha1 
"github.com/apache/apisix-ingress-controller/api/v1alpha1"
        "github.com/apache/apisix-ingress-controller/internal/controller"
+       
"github.com/apache/apisix-ingress-controller/internal/controller/indexer"
        
"github.com/apache/apisix-ingress-controller/internal/webhook/v1/reference"
 )
 
@@ -41,19 +45,24 @@ func SetupConsumerWebhookWithManager(mgr ctrl.Manager) 
error {
                Complete()
 }
 
-// 
+kubebuilder:webhook:path=/validate-apisix-apache-org-v1alpha1-consumer,mutating=false,failurePolicy=fail,sideEffects=None,groups=apisix.apache.org,resources=consumers,verbs=create;update,versions=v1alpha1,name=vconsumer-v1alpha1.kb.io,admissionReviewVersions=v1,failurePolicy=Ignore
+// 
+kubebuilder:webhook:path=/validate-apisix-apache-org-v1alpha1-consumer,mutating=false,failurePolicy=Ignore,sideEffects=None,groups=apisix.apache.org,resources=consumers,verbs=create;update,versions=v1alpha1,name=vconsumer-v1alpha1.kb.io,admissionReviewVersions=v1
 
 type ConsumerCustomValidator struct {
-       Client  client.Client
-       checker reference.Checker
+       Client       client.Client
+       checker      reference.Checker
+       adcValidator *adcAdmissionValidator
+       initErr      error
 }
 
 var _ webhook.CustomValidator = &ConsumerCustomValidator{}
 
 func NewConsumerCustomValidator(c client.Client) *ConsumerCustomValidator {
+       adcValidator, err := newADCAdmissionValidator(c, consumerLog)
        return &ConsumerCustomValidator{
-               Client:  c,
-               checker: reference.NewChecker(c, consumerLog),
+               Client:       c,
+               checker:      reference.NewChecker(c, consumerLog),
+               adcValidator: adcValidator,
+               initErr:      err,
        }
 }
 
@@ -67,7 +76,15 @@ func (v *ConsumerCustomValidator) ValidateCreate(ctx 
context.Context, obj runtim
                return nil, nil
        }
 
-       return v.collectWarnings(ctx, consumer), nil
+       warnings := v.collectWarnings(ctx, consumer)
+       if v.initErr != nil {
+               consumerLog.Error(v.initErr, "ADC validator init failed, 
skipping ADC validation")
+               return warnings, nil
+       }
+       if err := v.validateDuplicateKeyAuthCredentials(ctx, consumer); err != 
nil {
+               return warnings, err
+       }
+       return warnings, v.adcValidator.Validate(ctx, consumer)
 }
 
 func (v *ConsumerCustomValidator) ValidateUpdate(ctx context.Context, oldObj, 
newObj runtime.Object) (admission.Warnings, error) {
@@ -80,7 +97,15 @@ func (v *ConsumerCustomValidator) ValidateUpdate(ctx 
context.Context, oldObj, ne
                return nil, nil
        }
 
-       return v.collectWarnings(ctx, consumer), nil
+       warnings := v.collectWarnings(ctx, consumer)
+       if v.initErr != nil {
+               consumerLog.Error(v.initErr, "ADC validator init failed, 
skipping ADC validation")
+               return warnings, nil
+       }
+       if err := v.validateDuplicateKeyAuthCredentials(ctx, consumer); err != 
nil {
+               return warnings, err
+       }
+       return warnings, v.adcValidator.Validate(ctx, consumer)
 }
 
 func (*ConsumerCustomValidator) ValidateDelete(context.Context, 
runtime.Object) (admission.Warnings, error) {
@@ -117,3 +142,100 @@ func (v *ConsumerCustomValidator) collectWarnings(ctx 
context.Context, consumer
 
        return warnings
 }
+
+func (v *ConsumerCustomValidator) validateDuplicateKeyAuthCredentials(ctx 
context.Context, consumer *apisixv1alpha1.Consumer) error {
+       keys, err := v.extractKeyAuthKeys(ctx, consumer)
+       if err != nil {
+               return err
+       }
+       if len(keys) == 0 {
+               return nil
+       }
+
+       // Use the consumerGatewayRef field index to list only Consumers 
sharing the same gateway.
+       ns := consumer.Namespace
+       if consumer.Spec.GatewayRef.Namespace != nil && 
*consumer.Spec.GatewayRef.Namespace != "" {
+               ns = *consumer.Spec.GatewayRef.Namespace
+       }
+       indexKey := indexer.GenIndexKey(ns, consumer.Spec.GatewayRef.Name)
+
+       var consumers apisixv1alpha1.ConsumerList
+       if err := v.Client.List(ctx, &consumers, 
client.MatchingFields{indexer.ConsumerGatewayRef: indexKey}); err != nil {
+               return err
+       }
+
+       for i := range consumers.Items {
+               existing := &consumers.Items[i]
+               if existing.Namespace == consumer.Namespace && existing.Name == 
consumer.Name {
+                       continue
+               }
+
+               existingKeys, err := v.extractKeyAuthKeys(ctx, existing)
+               if err != nil {
+                       return err
+               }
+               for key := range existingKeys {
+                       if _, ok := keys[key]; ok {
+                               return fmt.Errorf("duplicate key-auth 
credential key %q already used by Consumer %s/%s", key, existing.Namespace, 
existing.Name)
+                       }
+               }
+       }
+
+       return nil
+}
+
+func (v *ConsumerCustomValidator) extractKeyAuthKeys(ctx context.Context, 
consumer *apisixv1alpha1.Consumer) (map[string]struct{}, error) {
+       keys := make(map[string]struct{})
+
+       for _, credential := range consumer.Spec.Credentials {
+               if credential.Type != "key-auth" {
+                       continue
+               }
+
+               key, err := v.extractCredentialKey(ctx, consumer, credential)
+               if err != nil {
+                       return nil, err
+               }
+               if key == "" {
+                       continue
+               }
+               keys[key] = struct{}{}
+       }
+
+       return keys, nil
+}
+
+func (v *ConsumerCustomValidator) extractCredentialKey(ctx context.Context, 
consumer *apisixv1alpha1.Consumer, credential apisixv1alpha1.Credential) 
(string, error) {
+       if credential.SecretRef != nil && credential.SecretRef.Name != "" {
+               namespace := consumer.Namespace
+               if credential.SecretRef.Namespace != nil && 
*credential.SecretRef.Namespace != "" {
+                       namespace = *credential.SecretRef.Namespace
+               }
+
+               var secret corev1.Secret
+               err := v.Client.Get(ctx, types.NamespacedName{Namespace: 
namespace, Name: credential.SecretRef.Name}, &secret)
+               if err != nil {
+                       if k8serrors.IsNotFound(err) {
+                               return "", nil
+                       }
+                       return "", err
+               }
+               return string(secret.Data["key"]), nil
+       }
+
+       if len(credential.Config.Raw) == 0 {
+               return "", nil
+       }
+
+       var cfg struct {
+               Key string `json:"key"`
+       }
+       if err := json.Unmarshal(credential.Config.Raw, &cfg); err != nil {
+               // Malformed JSON is not a hard error: skip duplicate detection 
for this
+               // credential so existing consumers with bad config are not 
suddenly denied.
+               consumerLog.V(1).Info("skipping duplicate key-auth check: 
malformed credential config",
+                       "consumer", consumer.Name, "error", err)
+               return "", nil
+       }
+       return cfg.Key, nil
+}
diff --git a/internal/webhook/v1/consumer_webhook_test.go 
b/internal/webhook/v1/consumer_webhook_test.go
index 045bc12b..4dc32b84 100644
--- a/internal/webhook/v1/consumer_webhook_test.go
+++ b/internal/webhook/v1/consumer_webhook_test.go
@@ -21,6 +21,7 @@ import (
 
        "github.com/stretchr/testify/require"
        corev1 "k8s.io/api/core/v1"
+       apiextensionsv1 
"k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/v1"
        metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
        "k8s.io/apimachinery/pkg/runtime"
        clientgoscheme "k8s.io/client-go/kubernetes/scheme"
@@ -29,6 +30,7 @@ import (
 
        apisixv1alpha1 
"github.com/apache/apisix-ingress-controller/api/v1alpha1"
        "github.com/apache/apisix-ingress-controller/internal/controller/config"
+       
"github.com/apache/apisix-ingress-controller/internal/controller/indexer"
 )
 
 func buildConsumerValidator(t *testing.T, objects ...runtime.Object) 
*ConsumerCustomValidator {
@@ -54,7 +56,10 @@ func buildConsumerValidator(t *testing.T, objects 
...runtime.Object) *ConsumerCu
                },
        }
        allObjects := append(managed, objects...)
-       builder := 
fake.NewClientBuilder().WithScheme(scheme).WithRuntimeObjects(allObjects...)
+       builder := fake.NewClientBuilder().
+               WithScheme(scheme).
+               WithRuntimeObjects(allObjects...).
+               WithIndex(&apisixv1alpha1.Consumer{}, 
indexer.ConsumerGatewayRef, indexer.ConsumerGatewayRefIndexFunc)
 
        return NewConsumerCustomValidator(builder.Build())
 }
@@ -146,3 +151,44 @@ func TestConsumerValidator_NoWarnings(t *testing.T) {
        require.NoError(t, err)
        require.Empty(t, warnings)
 }
+
+func TestConsumerValidator_DenyDuplicateKeyAuthCredential(t *testing.T) {
+       existing := &apisixv1alpha1.Consumer{
+               ObjectMeta: metav1.ObjectMeta{
+                       Name:      "existing",
+                       Namespace: "default",
+               },
+               Spec: apisixv1alpha1.ConsumerSpec{
+                       GatewayRef: apisixv1alpha1.GatewayRef{Name: 
"test-gateway"},
+                       Credentials: []apisixv1alpha1.Credential{{
+                               Type: "key-auth",
+                               Config: apiextensionsv1.JSON{
+                                       Raw: []byte(`{"key":"shared-key"}`),
+                               },
+                       }},
+               },
+       }
+       consumer := &apisixv1alpha1.Consumer{
+               ObjectMeta: metav1.ObjectMeta{
+                       Name:      "demo",
+                       Namespace: "default",
+               },
+               Spec: apisixv1alpha1.ConsumerSpec{
+                       GatewayRef: apisixv1alpha1.GatewayRef{Name: 
"test-gateway"},
+                       Credentials: []apisixv1alpha1.Credential{{
+                               Type: "key-auth",
+                               Config: apiextensionsv1.JSON{
+                                       Raw: []byte(`{"key":"shared-key"}`),
+                               },
+                       }},
+               },
+       }
+
+       validator := buildConsumerValidator(t, existing)
+
+       warnings, err := validator.ValidateCreate(context.Background(), 
consumer)
+       require.Empty(t, warnings)
+       require.Error(t, err)
+       require.Contains(t, err.Error(), `duplicate key-auth credential key 
"shared-key"`)
+       require.Contains(t, err.Error(), "default/existing")
+}
diff --git a/test/e2e/webhook/apisixconsumer.go 
b/test/e2e/webhook/apisixconsumer.go
index 7aa1a256..364e8784 100644
--- a/test/e2e/webhook/apisixconsumer.go
+++ b/test/e2e/webhook/apisixconsumer.go
@@ -19,11 +19,13 @@ package webhook
 
 import (
        "fmt"
+       "strings"
        "time"
 
        . "github.com/onsi/ginkgo/v2"
        . "github.com/onsi/gomega"
 
+       "github.com/apache/apisix-ingress-controller/test/e2e/framework"
        "github.com/apache/apisix-ingress-controller/test/e2e/scaffold"
 )
 
@@ -45,7 +47,7 @@ var _ = Describe("Test ApisixConsumer Webhook", 
Label("webhook"), func() {
                time.Sleep(5 * time.Second)
        })
 
-       It("should warn on missing authentication secrets", func() {
+       It("should warn on missing authentication secrets", func() { 
//nolint:dupl
                missingSecret := "missing-basic-secret"
                consumerName := "webhook-apisixconsumer"
                consumerYAML := `
@@ -85,4 +87,113 @@ stringData:
                Expect(err).ShouldNot(HaveOccurred())
                Expect(output).NotTo(ContainSubstring(fmt.Sprintf("Warning: 
Referenced Secret '%s/%s' not found", s.Namespace(), missingSecret)))
        })
+
+       It("should reject invalid plugin config during ADC validation", func() {
+               privateKeyYAML := "          " + 
strings.ReplaceAll(framework.TestKey, "\n", "\n          ")
+
+               firstConsumer := fmt.Sprintf(`
+apiVersion: apisix.apache.org/v2
+kind: ApisixConsumer
+metadata:
+  name: webhook-apisixconsumer-a
+  namespace: %s
+spec:
+  ingressClassName: %s
+  authParameter:
+    keyAuth:
+      value:
+        key: consumer-a-key
+`, s.Namespace(), s.Namespace())
+
+               By("creating the first ApisixConsumer with valid key-auth 
config")
+               err := s.CreateResourceFromString(firstConsumer)
+               Expect(err).NotTo(HaveOccurred(), "creating first 
ApisixConsumer")
+
+               invalidConsumer := fmt.Sprintf(`
+apiVersion: apisix.apache.org/v2
+kind: ApisixConsumer
+metadata:
+  name: webhook-apisixconsumer-b
+  namespace: %s
+spec:
+  ingressClassName: %s
+  authParameter:
+    jwtAuth:
+      value:
+        key: consumer-b-key
+        algorithm: INVALID_ALGO
+        private_key: |
+%s
+`, s.Namespace(), s.Namespace(), privateKeyYAML)
+
+               By("creating ApisixConsumer with an invalid jwt-auth algorithm")
+               err = s.CreateResourceFromString(invalidConsumer)
+               expectAdmissionDenied(s, "apisixconsumer", 
"webhook-apisixconsumer-b", err)
+
+               correctedConsumer := fmt.Sprintf(`
+apiVersion: apisix.apache.org/v2
+kind: ApisixConsumer
+metadata:
+  name: webhook-apisixconsumer-b
+  namespace: %s
+spec:
+  ingressClassName: %s
+  authParameter:
+    keyAuth:
+      value:
+        key: consumer-b-corrected-key
+`, s.Namespace(), s.Namespace())
+
+               By("creating corrected ApisixConsumer with valid auth config")
+               err = s.CreateResourceFromString(correctedConsumer)
+               Expect(err).NotTo(HaveOccurred(), "creating corrected 
ApisixConsumer")
+       })
+
+       It("should reject consumer update that fails ADC validation", func() {
+               consumerName := "webhook-apisixconsumer-update"
+
+               validConsumer := fmt.Sprintf(`
+apiVersion: apisix.apache.org/v2
+kind: ApisixConsumer
+metadata:
+  name: %s
+  namespace: %s
+spec:
+  ingressClassName: %s
+  authParameter:
+    keyAuth:
+      value:
+        key: update-test-key
+`, consumerName, s.Namespace(), s.Namespace())
+
+               By("creating valid ApisixConsumer")
+               err := s.CreateResourceFromString(validConsumer)
+               Expect(err).NotTo(HaveOccurred(), "creating initial valid 
ApisixConsumer")
+
+               privateKeyYAML := "          " + 
strings.ReplaceAll(framework.TestKey, "\n", "\n          ")
+               invalidConsumer := fmt.Sprintf(`
+apiVersion: apisix.apache.org/v2
+kind: ApisixConsumer
+metadata:
+  name: %s
+  namespace: %s
+spec:
+  ingressClassName: %s
+  authParameter:
+    jwtAuth:
+      value:
+        key: update-test-jwt-key
+        algorithm: INVALID_ALGO
+        private_key: |
+%s
+`, consumerName, s.Namespace(), s.Namespace(), privateKeyYAML)
+
+               By("updating ApisixConsumer with invalid jwt-auth algorithm")
+               err = s.CreateResourceFromString(invalidConsumer)
+               expectUpdateDenied(err)
+
+               By("updating ApisixConsumer with corrected config")
+               err = s.CreateResourceFromString(validConsumer)
+               Expect(err).NotTo(HaveOccurred(), "updating ApisixConsumer with 
corrected config")
+       })
 })
diff --git a/test/e2e/webhook/apisixroute.go b/test/e2e/webhook/apisixroute.go
index 51904f43..2e498f5f 100644
--- a/test/e2e/webhook/apisixroute.go
+++ b/test/e2e/webhook/apisixroute.go
@@ -45,9 +45,8 @@ var _ = Describe("Test ApisixRoute Webhook", 
Label("webhook"), func() {
                time.Sleep(5 * time.Second)
        })
 
-       It("should warn on missing service or secret references", func() {
+       It("should warn on missing service references", func() { //nolint:dupl
                missingService := "missing-backend"
-               missingSecret := "missing-plugin-secret"
                routeName := "webhook-apisixroute"
                routeYAML := `
 apiVersion: apisix.apache.org/v2
@@ -67,18 +66,13 @@ spec:
     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))
+               output, err := 
s.CreateResourceFromStringAndGetOutput(fmt.Sprintf(routeYAML, routeName, 
s.Namespace(), s.Namespace(), missingService))
                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")
+               By("creating referenced Service")
                serviceYAML := fmt.Sprintf(`
 apiVersion: v1
 kind: Service
@@ -96,22 +90,170 @@ spec:
                err = s.CreateResourceFromString(serviceYAML)
                Expect(err).NotTo(HaveOccurred(), "creating backend service 
placeholder")
 
-               secretYAML := fmt.Sprintf(`
+               time.Sleep(2 * time.Second)
+
+               output, err = 
s.CreateResourceFromStringAndGetOutput(fmt.Sprintf(routeYAML, routeName, 
s.Namespace(), s.Namespace(), missingService))
+               Expect(err).ShouldNot(HaveOccurred())
+               Expect(output).NotTo(ContainSubstring(fmt.Sprintf("Warning: 
Referenced Service '%s/%s' not found", s.Namespace(), missingService)))
+       })
+
+       It("should reject routes that fail ADC validation", func() {
+               backendService := "webhook-route-backend"
+               routeName := "webhook-apisixroute-invalid"
+
+               By("creating referenced Service")
+               serviceYAML := fmt.Sprintf(`
 apiVersion: v1
-kind: Secret
+kind: Service
 metadata:
   name: %s
-stringData:
-  config: enabled
-`, missingSecret)
-               err = s.CreateResourceFromString(secretYAML)
-               Expect(err).NotTo(HaveOccurred(), "creating plugin secret 
placeholder")
+spec:
+  selector:
+    app: placeholder
+  ports:
+  - name: http
+    port: 80
+    targetPort: 80
+  type: ClusterIP
+`, backendService)
+               err := s.CreateResourceFromString(serviceYAML)
+               Expect(err).NotTo(HaveOccurred(), "creating backend service")
 
-               time.Sleep(2 * time.Second)
+               invalidRouteYAML := fmt.Sprintf(`
+apiVersion: apisix.apache.org/v2
+kind: ApisixRoute
+metadata:
+  name: %s
+  namespace: %s
+spec:
+  ingressClassName: %s
+  http:
+  - name: rule-invalid
+    match:
+      hosts:
+      - webhook.example.com
+      paths:
+      - /invalid
+    backends:
+    - serviceName: %s
+      servicePort: 80
+      resolveGranularity: service
+    plugins:
+    - name: response-rewrite
+      enable: true
+      config:
+        status_code: "500"
+`, routeName, s.Namespace(), s.Namespace(), backendService)
 
-               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)))
+               By("creating ApisixRoute with invalid plugin config")
+               err = s.CreateResourceFromString(invalidRouteYAML)
+               expectAdmissionDenied(s, "apisixroute", routeName, err)
+
+               validRouteYAML := fmt.Sprintf(`
+apiVersion: apisix.apache.org/v2
+kind: ApisixRoute
+metadata:
+  name: %s
+  namespace: %s
+spec:
+  ingressClassName: %s
+  http:
+  - name: rule-valid
+    match:
+      hosts:
+      - webhook.example.com
+      paths:
+      - /valid
+    backends:
+    - serviceName: %s
+      servicePort: 80
+      resolveGranularity: service
+`, routeName, s.Namespace(), s.Namespace(), backendService)
+
+               By("creating corrected ApisixRoute")
+               err = s.CreateResourceFromString(validRouteYAML)
+               Expect(err).NotTo(HaveOccurred(), "creating corrected 
ApisixRoute")
+       })
+
+       It("should reject route update that fails ADC validation", func() {
+               backendService := "webhook-route-update-backend"
+               routeName := "webhook-apisixroute-update"
+
+               By("creating referenced Service")
+               serviceYAML := fmt.Sprintf(`
+apiVersion: v1
+kind: Service
+metadata:
+  name: %s
+spec:
+  selector:
+    app: placeholder
+  ports:
+  - name: http
+    port: 80
+    targetPort: 80
+  type: ClusterIP
+`, backendService)
+               err := s.CreateResourceFromString(serviceYAML)
+               Expect(err).NotTo(HaveOccurred(), "creating backend service")
+
+               validRouteYAML := fmt.Sprintf(`
+apiVersion: apisix.apache.org/v2
+kind: ApisixRoute
+metadata:
+  name: %s
+  namespace: %s
+spec:
+  ingressClassName: %s
+  http:
+  - name: rule-update
+    match:
+      hosts:
+      - webhook-update.example.com
+      paths:
+      - /update
+    backends:
+    - serviceName: %s
+      servicePort: 80
+      resolveGranularity: service
+`, routeName, s.Namespace(), s.Namespace(), backendService)
+
+               By("creating valid ApisixRoute")
+               err = s.CreateResourceFromString(validRouteYAML)
+               Expect(err).NotTo(HaveOccurred(), "creating initial valid 
ApisixRoute")
+
+               invalidRouteYAML := fmt.Sprintf(`
+apiVersion: apisix.apache.org/v2
+kind: ApisixRoute
+metadata:
+  name: %s
+  namespace: %s
+spec:
+  ingressClassName: %s
+  http:
+  - name: rule-update
+    match:
+      hosts:
+      - webhook-update.example.com
+      paths:
+      - /update
+    backends:
+    - serviceName: %s
+      servicePort: 80
+      resolveGranularity: service
+    plugins:
+    - name: response-rewrite
+      enable: true
+      config:
+        status_code: "500"
+`, routeName, s.Namespace(), s.Namespace(), backendService)
+
+               By("updating ApisixRoute with invalid plugin config")
+               err = s.CreateResourceFromString(invalidRouteYAML)
+               expectUpdateDenied(err)
+
+               By("updating ApisixRoute with corrected config")
+               err = s.CreateResourceFromString(validRouteYAML)
+               Expect(err).NotTo(HaveOccurred(), "updating ApisixRoute with 
corrected config")
        })
 })
diff --git a/test/e2e/webhook/apisixtls.go b/test/e2e/webhook/apisixtls.go
index 08defed9..0d24d0f8 100644
--- a/test/e2e/webhook/apisixtls.go
+++ b/test/e2e/webhook/apisixtls.go
@@ -73,8 +73,30 @@ spec:
                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(`
+               By("creating referenced TLS secrets with valid certificate 
material")
+               serverCert, serverKey := s.GenerateCert(GinkgoT(), 
[]string{"webhook.example.com"})
+               err = s.NewKubeTlsSecret(serverSecret, serverCert.String(), 
serverKey.String())
+               Expect(err).NotTo(HaveOccurred(), "creating server TLS secret")
+
+               caCert, _, _, _, _ := s.GenerateMACert(GinkgoT(), 
[]string{"webhook.example.com"})
+               err = s.NewClientCASecret(clientSecret, caCert.String(), "")
+               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)))
+       })
+
+       It("should reject invalid TLS material during ADC validation", func() {
+               serverSecret := "invalid-server-tls"
+               tlsName := "webhook-apisixtls-invalid"
+               host := "invalid-webhook.example.com"
+
+               By("creating a referenced TLS secret with invalid certificate 
data")
+               invalidServerSecretYAML := fmt.Sprintf(`
 apiVersion: v1
 kind: Secret
 metadata:
@@ -82,30 +104,113 @@ metadata:
   namespace: %s
 type: kubernetes.io/tls
 stringData:
-  tls.crt: dummy-cert
-  tls.key: dummy-key
+  tls.crt: not-a-cert
+  tls.key: not-a-key
 `, serverSecret, s.Namespace())
-               err = s.CreateResourceFromString(serverSecretYAML)
-               Expect(err).NotTo(HaveOccurred(), "creating server TLS secret")
+               err := s.CreateResourceFromString(invalidServerSecretYAML)
+               Expect(err).NotTo(HaveOccurred(), "creating invalid server TLS 
secret")
+
+               tlsYAML := fmt.Sprintf(`
+apiVersion: apisix.apache.org/v2
+kind: ApisixTls
+metadata:
+  name: %s
+  namespace: %s
+spec:
+  ingressClassName: %s
+  hosts:
+  - %s
+  secret:
+    name: %s
+    namespace: %s
+`, tlsName, s.Namespace(), s.Namespace(), host, serverSecret, s.Namespace())
+
+               By("creating ApisixTls backed by invalid certificate material")
+               err = s.CreateResourceFromString(tlsYAML)
+               expectAdmissionDenied(s, "apisixtls", tlsName, err)
+
+               By("replacing the secret with valid certificate material")
+               err = s.DeleteResource("Secret", serverSecret)
+               Expect(err).NotTo(HaveOccurred(), "deleting invalid server TLS 
secret")
+
+               serverCert, serverKey := s.GenerateCert(GinkgoT(), 
[]string{host})
+               err = s.NewKubeTlsSecret(serverSecret, serverCert.String(), 
serverKey.String())
+               Expect(err).NotTo(HaveOccurred(), "creating valid server TLS 
secret")
+
+               // Wait for the webhook cache to reflect the recreated Secret 
before submitting ApisixTls.
+               time.Sleep(2 * time.Second)
+
+               By("creating corrected ApisixTls")
+               err = s.CreateResourceFromString(tlsYAML)
+               Expect(err).NotTo(HaveOccurred(), "creating corrected 
ApisixTls")
+       })
+
+       It("should reject TLS update with invalid certificate material", func() 
{
+               validSecret := "update-valid-tls"
+               invalidSecret := "update-invalid-tls"
+               tlsName := "webhook-apisixtls-update"
+               host := "update-webhook.example.com"
 
-               clientSecretYAML := fmt.Sprintf(`
+               By("creating a valid TLS secret")
+               serverCert, serverKey := s.GenerateCert(GinkgoT(), 
[]string{host})
+               err := s.NewKubeTlsSecret(validSecret, serverCert.String(), 
serverKey.String())
+               Expect(err).NotTo(HaveOccurred(), "creating valid server TLS 
secret")
+
+               By("creating an invalid TLS secret with bad certificate 
material")
+               invalidSecretYAML := fmt.Sprintf(`
 apiVersion: v1
 kind: Secret
 metadata:
   name: %s
   namespace: %s
-type: Opaque
+type: kubernetes.io/tls
 stringData:
-  ca.crt: dummy-ca
-`, clientSecret, s.Namespace())
-               err = s.CreateResourceFromString(clientSecretYAML)
-               Expect(err).NotTo(HaveOccurred(), "creating client CA secret")
+  tls.crt: not-a-cert
+  tls.key: not-a-key
+`, invalidSecret, s.Namespace())
+               err = s.CreateResourceFromString(invalidSecretYAML)
+               Expect(err).NotTo(HaveOccurred(), "creating invalid server TLS 
secret")
 
-               time.Sleep(2 * time.Second)
+               validTLSYAML := fmt.Sprintf(`
+apiVersion: apisix.apache.org/v2
+kind: ApisixTls
+metadata:
+  name: %s
+  namespace: %s
+spec:
+  ingressClassName: %s
+  hosts:
+  - %s
+  secret:
+    name: %s
+    namespace: %s
+`, tlsName, s.Namespace(), s.Namespace(), host, validSecret, s.Namespace())
 
-               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)))
+               By("creating valid ApisixTls")
+               err = s.CreateResourceFromString(validTLSYAML)
+               Expect(err).NotTo(HaveOccurred(), "creating initial valid 
ApisixTls")
+
+               invalidTLSYAML := fmt.Sprintf(`
+apiVersion: apisix.apache.org/v2
+kind: ApisixTls
+metadata:
+  name: %s
+  namespace: %s
+spec:
+  ingressClassName: %s
+  hosts:
+  - %s
+  secret:
+    name: %s
+    namespace: %s
+`, tlsName, s.Namespace(), s.Namespace(), host, invalidSecret, s.Namespace())
+
+               By("updating ApisixTls to reference the invalid certificate 
secret")
+               err = s.CreateResourceFromString(invalidTLSYAML)
+               expectUpdateDenied(err)
+
+               By("updating ApisixTls back to the valid certificate secret")
+               err = s.CreateResourceFromString(validTLSYAML)
+               Expect(err).NotTo(HaveOccurred(), "updating ApisixTls with 
valid certificate")
        })
 })
diff --git a/test/e2e/webhook/consumer.go b/test/e2e/webhook/consumer.go
index 676adbb8..6a051e25 100644
--- a/test/e2e/webhook/consumer.go
+++ b/test/e2e/webhook/consumer.go
@@ -90,4 +90,129 @@ stringData:
                Expect(err).ShouldNot(HaveOccurred())
                Expect(output).NotTo(ContainSubstring(fmt.Sprintf("Warning: 
Referenced Secret '%s/%s' not found", s.Namespace(), missingSecret)))
        })
+
+       It("should reject invalid plugin config during ADC validation", func() {
+               gatewayName := s.Namespace()
+
+               firstConsumer := fmt.Sprintf(`
+apiVersion: apisix.apache.org/v1alpha1
+kind: Consumer
+metadata:
+  name: webhook-consumer-a
+spec:
+  gatewayRef:
+    name: %s
+  credentials:
+  - type: key-auth
+    name: key-auth-a
+    config:
+      key: consumer-a-key
+`, gatewayName)
+
+               By("creating the first Consumer with valid key-auth config")
+               err := s.CreateResourceFromString(firstConsumer)
+               Expect(err).NotTo(HaveOccurred(), "creating first Consumer")
+
+               invalidConsumer := fmt.Sprintf(`
+apiVersion: apisix.apache.org/v1alpha1
+kind: Consumer
+metadata:
+  name: webhook-consumer-b
+spec:
+  gatewayRef:
+    name: %s
+  plugins:
+  - name: jwt-auth
+    config:
+      key: consumer-b-key
+      algorithm: INVALID_ALGO
+`, gatewayName)
+
+               By("creating Consumer with an invalid jwt-auth algorithm in 
plugins")
+               err = s.CreateResourceFromString(invalidConsumer)
+               expectAdmissionDenied(s, "consumer", "webhook-consumer-b", err)
+
+               correctedConsumer := fmt.Sprintf(`
+apiVersion: apisix.apache.org/v1alpha1
+kind: Consumer
+metadata:
+  name: webhook-consumer-b
+spec:
+  gatewayRef:
+    name: %s
+  plugins:
+  - name: jwt-auth
+    config:
+      key: consumer-b-key
+      algorithm: HS256
+      secret: consumer-b-secret
+`, gatewayName)
+
+               By("creating corrected Consumer with a valid algorithm")
+               err = s.CreateResourceFromString(correctedConsumer)
+               Expect(err).NotTo(HaveOccurred(), "creating corrected Consumer")
+       })
+
+       It("should reject consumer update that fails ADC validation", func() {
+               gatewayName := s.Namespace()
+               consumerName := "webhook-consumer-update"
+
+               validConsumer := fmt.Sprintf(`
+apiVersion: apisix.apache.org/v1alpha1
+kind: Consumer
+metadata:
+  name: %s
+spec:
+  gatewayRef:
+    name: %s
+  credentials:
+  - type: key-auth
+    name: key-auth-update
+    config:
+      key: update-consumer-key
+`, consumerName, gatewayName)
+
+               By("creating valid Consumer")
+               err := s.CreateResourceFromString(validConsumer)
+               Expect(err).NotTo(HaveOccurred(), "creating initial valid 
Consumer")
+
+               invalidConsumer := fmt.Sprintf(`
+apiVersion: apisix.apache.org/v1alpha1
+kind: Consumer
+metadata:
+  name: %s
+spec:
+  gatewayRef:
+    name: %s
+  plugins:
+  - name: jwt-auth
+    config:
+      key: update-consumer-jwt-key
+      algorithm: INVALID_ALGO
+`, consumerName, gatewayName)
+
+               By("updating Consumer with an invalid jwt-auth algorithm in 
plugins")
+               err = s.CreateResourceFromString(invalidConsumer)
+               expectUpdateDenied(err)
+
+               correctedConsumer := fmt.Sprintf(`
+apiVersion: apisix.apache.org/v1alpha1
+kind: Consumer
+metadata:
+  name: %s
+spec:
+  gatewayRef:
+    name: %s
+  plugins:
+  - name: jwt-auth
+    config:
+      key: update-consumer-jwt-key
+      algorithm: HS256
+      secret: update-consumer-secret
+`, consumerName, gatewayName)
+
+               By("updating Consumer with a valid algorithm")
+               err = s.CreateResourceFromString(correctedConsumer)
+               Expect(err).NotTo(HaveOccurred(), "updating Consumer with 
corrected config")
+       })
 })
diff --git a/test/e2e/webhook/helpers.go b/test/e2e/webhook/helpers.go
index 1b21c8b7..cb42c3d1 100644
--- a/test/e2e/webhook/helpers.go
+++ b/test/e2e/webhook/helpers.go
@@ -168,6 +168,23 @@ spec:
        time.Sleep(5 * time.Second)
 }
 
+func expectAdmissionDenied(s *scaffold.Scaffold, resourceType, resourceName 
string, err error) {
+       Expect(err).To(HaveOccurred(), "expecting admission rejection")
+       Expect(err.Error()).To(ContainSubstring("denied the request"))
+
+       _, getErr := s.GetOutputFromString(resourceType, resourceName, "-o", 
"yaml")
+       Expect(getErr).To(HaveOccurred(), fmt.Sprintf("resource %s/%s should 
not exist after admission rejection", resourceType, resourceName))
+       Expect(getErr.Error()).To(ContainSubstring("not found"), 
fmt.Sprintf("expected NotFound error for %s/%s", resourceType, resourceName))
+}
+
+// expectUpdateDenied verifies that an UPDATE admission was rejected. Unlike
+// expectAdmissionDenied it does not check resource non-existence, because the
+// resource remains in its previous valid state after a denied update.
+func expectUpdateDenied(err error) {
+       Expect(err).To(HaveOccurred(), "expecting update to be rejected by 
admission webhook")
+       Expect(err.Error()).To(ContainSubstring("denied the request"))
+}
+
 func verifySimpleRouteMissingBackendWarnings(s *scaffold.Scaffold, tc 
simpleRouteWebhookTestCase) {
        gatewayName := s.Namespace()
        routeYAML := fmt.Sprintf(`

Reply via email to