Skip to content

feat: add ADC-backed admission validation for APISIX CRDs#2758

Open
AlinsRan wants to merge 15 commits intoapache:masterfrom
AlinsRan:feat/webhook-adc-validation
Open

feat: add ADC-backed admission validation for APISIX CRDs#2758
AlinsRan wants to merge 15 commits intoapache:masterfrom
AlinsRan:feat/webhook-adc-validation

Conversation

@AlinsRan
Copy link
Copy Markdown
Contributor

@AlinsRan AlinsRan commented May 6, 2026

Summary

This PR adds live ADC-backed admission validation to the existing webhook validators for ApisixRoute, ApisixConsumer, ApisixTls, and Consumer resources.

Previously the webhooks only emitted warnings for missing references (services, secrets). With this change, resources are also structurally validated against a live APISIX instance before being admitted.

Changes

Core infrastructure

  • internal/types/error.go: Add ADCValidationErrors, ADCValidationError, ADCValidationServerAddrError, and ADCValidationDetail types to carry structured validation error details.

  • internal/adc/client/executor.go:

    • Add Validate() to the ADCExecutor interface and HTTPADCExecutor
    • Implement runHTTPValidate / runHTTPValidateForSingleServer that POST to the /configs/validate endpoint
    • Refactor buildHTTPRequest to accept an HTTP method and path (supporting both /sync and /configs/validate)
    • Set TLS minimum version to 1.2
    • Redact full request body from logs (log length only)
  • internal/adc/client/client.go: Add Client.Validate() which calls the executor's validate path and aggregates ADCValidationErrors.

Webhook validation helpers

  • internal/controller/webhook_validation.go (new): Lightweight Prepare*ForValidation helpers for each CRD type that build a TranslateContext without running the full reconciler loop. Used by the admission webhook to resolve references before translating.

Admission validator

  • internal/webhook/v1/adc_validation.go (new): adcAdmissionValidator that:
    • Resolves the IngressClass / GatewayProxy for the resource
    • Translates the resource into an ADC payload
    • Posts the payload to APISIX via client.Validate()
    • Fails open on infrastructure / transport errors (only ADCValidationErrors cause denial)
    • Populates global_rules and plugin_metadata in the validate payload so plugin references can be resolved

Webhook wiring

  • apisixroute_webhook.go, apisixconsumer_webhook.go, apisixtls_webhook.go, consumer_webhook.go: Wire adcAdmissionValidator; ADC init errors are logged and ignored (fail-open).

  • apisixtls_webhook.go: Skip ADC validation when secrets are missing to preserve the existing warn-only behaviour for that case.

  • consumer_webhook.go: Validate duplicate key-auth credential keys scoped to the same GatewayRef using a field-index query (O(1) instead of O(N) full list). Malformed inline JSON credentials are logged and skipped rather than causing a hard denial.

Behaviour notes

Scenario Before After
Valid resource, ADC reachable Admit with warnings Admit (pass validation)
Invalid resource, ADC reachable Admit with warnings Deny with structured errors
ADC unreachable / init error Admit with warnings Admit with warnings (fail-open)
ApisixTls / ApisixConsumer with missing secrets Admit with warnings Admit with warnings (unchanged)
Consumer with duplicate key-auth key Admit Deny
Consumer with malformed inline credential JSON Admit Admit (log + skip duplicate check)

Examples

Deny: invalid plugin configuration on ApisixRoute

Applying an ApisixRoute whose response-rewrite plugin receives status_code as a string instead of an integer is rejected immediately:

apiVersion: apisix.apache.org/v2
kind: ApisixRoute
metadata:
  name: my-route
  namespace: default
spec:
  ingressClassName: apisix
  http:
  - name: rule1
    match:
      hosts: [example.com]
      paths: [/api]
    backends:
    - serviceName: my-svc
      servicePort: 80
    plugins:
    - name: response-rewrite
      enable: true
      config:
        status_code: "500"   # wrong type: must be integer
$ kubectl apply -f route.yaml
Error from server: admission webhook "vapisixroute.kb.io" denied the request:
  ADC validation errors: [ADC validation error for ApisixRoute/default/my-route:
    [ServerAddr: 127.0.0.1:9180, Err: route rejected
      (type=route, name=my-route, property 'status_code' validation failed: wrong type: expected integer, but got string)]]

Deny: invalid jwt-auth algorithm on ApisixConsumer

apiVersion: apisix.apache.org/v2
kind: ApisixConsumer
metadata:
  name: my-consumer
  namespace: default
spec:
  ingressClassName: apisix
  authParameter:
    jwtAuth:
      value:
        key: my-consumer-key
        algorithm: INVALID_ALGO   # must be one of RS256, RS512, ES256, ES512, HS256, HS512
        private_key: |
          -----BEGIN RSA PRIVATE KEY-----
          ...
          -----END RSA PRIVATE KEY-----
$ kubectl apply -f consumer.yaml
Error from server: admission webhook "vapisixconsumer.kb.io" denied the request:
  ADC validation errors: [ADC validation error for ApisixConsumer/default/my-consumer:
    [ServerAddr: 127.0.0.1:9180, Err: consumer rejected
      (type=consumer, name=my-consumer, plugin jwt-auth: property 'algorithm' validation failed: value must be one of RS256, RS512, ES256, ES512, HS256, HS512)]]

Warn + Admit: missing backend Service on ApisixRoute

When a referenced Service does not yet exist, the resource is admitted with a warning (eventual consistency):

apiVersion: apisix.apache.org/v2
kind: ApisixRoute
metadata:
  name: my-route
  namespace: default
spec:
  ingressClassName: apisix
  http:
  - name: rule1
    match:
      hosts: [example.com]
      paths: [/api]
    backends:
    - serviceName: not-yet-created-svc   # service does not exist yet
      servicePort: 80
$ kubectl apply -f route.yaml
Warning: Referenced Service 'default/not-yet-created-svc' not found
apisixroute.apisix.apache.org/my-route created

Deny: SNI conflict on ApisixTls

When two ApisixTls resources claim the same SNI, the second one is denied:

# already exists
apiVersion: apisix.apache.org/v2
kind: ApisixTls
metadata:
  name: tls-a
  namespace: default
spec:
  ingressClassName: apisix
  hosts: [example.com]
  secret:
    name: tls-secret-a
    namespace: default
---
# new resource — same host
apiVersion: apisix.apache.org/v2
kind: ApisixTls
metadata:
  name: tls-b
  namespace: default
spec:
  ingressClassName: apisix
  hosts: [example.com]   # conflicts with tls-a
  secret:
    name: tls-secret-b
    namespace: default
$ kubectl apply -f tls-b.yaml
Error from server: admission webhook "vapisixtls.kb.io" denied the request:
  SNI "example.com" is already used by ApisixTls default/tls-a

AlinsRan and others added 15 commits May 6, 2026 17:08
Validate ApisixRoute, ApisixConsumer, ApisixTls and Consumer resources
against a live APISIX instance during admission instead of only
producing warnings.

Key changes:
- Add ADCValidationErrors / ADCValidationError types (internal/types)
- Add Validate() to HTTPADCExecutor and Client; introduce the
  /configs/validate endpoint support in executor.go (TLS min-version
  set to 1.2, request body no longer logged in full)
- Add internal/controller/webhook_validation.go: lightweight helpers
  (PrepareApisixRouteForValidation, PrepareApisixConsumerForValidation,
  PrepareConsumerForValidation, PrepareApisixTlsForValidation) that
  build a TranslateContext without starting the full reconciler loop
- Add internal/webhook/v1/adc_validation.go: adcAdmissionValidator
  that translates a CRD into an ADC payload and posts it to APISIX for
  structural validation; fails open on infrastructure errors
- Wire adcAdmissionValidator into all four webhook validators; init
  errors are logged and ignored (fail-open)
- ApisixTls: skip ADC validation when secrets are missing to preserve
  the existing warn-only behaviour for that case
- Consumer: validate duplicate key-auth credential keys scoped to the
  same GatewayRef using a field-index query; malformed inline JSON is
  logged and skipped (not a hard denial)
- global_rules and plugin_metadata are now populated in the ADC
  validate payload so APISIX can resolve plugin references

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
ApisixRoute and ApisixConsumer: skip ADC validation when collectWarnings
already found missing references (services / secrets). This preserves
the existing warn-and-admit behaviour for incomplete resources, matching
the pattern already used for ApisixTls.

Also fix trailing newline in consumer_webhook.go caught by gofmt.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Add e2e tests covering ADC-backed admission validation for all four
webhook resource types. Each new test case is gated with a provider
check so it only runs against the apisix-standalone backend.

- ApisixRoute: reject route with invalid response-rewrite plugin config
- ApisixConsumer: reject consumer with invalid jwt-auth algorithm
- ApisixTls: reject TLS resource backed by invalid certificate material
- Consumer: reject consumer with invalid jwt-auth algorithm

Also add the expectAdmissionDenied helper to the webhook package and
update the ApisixTls warn-on-missing test to use real generated
certificate material for the success assertion.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Add update-path e2e coverage for all four webhook resource types.
ValidateUpdate is wired to the same ADC validator as ValidateCreate,
and these tests verify that invalid configs are rejected on update too.

- ApisixRoute: reject update with invalid response-rewrite plugin config
- ApisixConsumer: reject update with invalid jwt-auth algorithm
- ApisixTls: reject update when referenced secret contains invalid cert data
- Consumer: reject update with invalid jwt-auth algorithm

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
- Add expectUpdateDenied helper: UPDATE denials leave the resource intact
  so the resource-not-found check in expectAdmissionDenied is wrong for
  update scenarios
- Use expectUpdateDenied in all four UPDATE It blocks
- Redesign ApisixTls UPDATE test: change the secret reference in the spec
  instead of swapping secret content; spec must actually change to trigger
  the UPDATE admission webhook

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Both apisix and apisix-standalone backends now support /apisix/admin/configs/validate
via ADC PR api7/adc#434. The CI uses apache/apisix:dev and ghcr.io/api7/adc:dev
which include this support, so the skip guard is no longer needed.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Previously the apisix-standalone backend bypassed the ADC server and called
APISIX's /apisix/admin/configs/validate directly. With api7/adc#440, the ADC
server now exposes a /validate endpoint (same input format as /sync) that
handles both apisix-standalone and apisix backends uniformly.

Changes:
- Remove apisix-standalone special-case in runHTTPValidateForSingleServer;
  all backends now call ADC server POST /validate
- Fix ADCValidateResult.ErrorMessage JSON tag: errorMessage -> message
  to match the ADC server response format from api7/adc#440
- Remove buildAPISIXValidateRequest, apisixValidateRequest,
  newBackendHTTPClient, buildAPISIXValidatePayload and helpers
- Update unit tests accordingly

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
The ADC server registers PUT /validate (not POST), matching the
same HTTP method used for PUT /sync.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
APISIX validates plugin schemas but not credential schemas via /validate.
Switch the invalid Consumer config in e2e tests to use spec.plugins with
jwt-auth algorithm: INVALID_ALGO, which triggers proper ADC validation.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Remove redundant failurePolicy=fail from all 4 webhook marker lines,
keeping only failurePolicy=Ignore (kubebuilder uses the last occurrence,
having both was ambiguous and wrong).

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Remove the len(warnings) > 0 guard that was skipping ADC validation when
service or secret references were missing. ApisixRoute plugin schemas can
be validated by ADC independently of whether the backend service exists.
Running ADC even with missing references provides stronger safety guarantees.

Also update the warning test to only test missing service references,
consistent with the enterprise version.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant