Skip to content

feat(config-validate): add --remote flag for live backend validation#65

Open
shreemaan-abhishek wants to merge 1 commit into
masterfrom
feat/validate-remote
Open

feat(config-validate): add --remote flag for live backend validation#65
shreemaan-abhishek wants to merge 1 commit into
masterfrom
feat/validate-remote

Conversation

@shreemaan-abhishek
Copy link
Copy Markdown
Contributor

@shreemaan-abhishek shreemaan-abhishek commented May 28, 2026

Summary

  • a7 config validate --remote posts the parsed config to POST /apisix/admin/configs/validate?gateway_group_id=<id> so API7 EE can report errors caught by the live backend schema (plugin-config shapes, cross-resource references, etc) on top of a7's local checks.
  • Local errors short-circuit before any HTTP call — remote step never sees a known-bad input (mirrors sync validation gating).
  • Remote errors prefixed with [remote] to distinguish from local output.
  • Issues a raw HTTP request instead of going through api.Client.Post so the 400 body's structured errors[] array is preserved (api.Client.do would collapse it into a single error string).

Part of the adc → a7 parity gap tracked in docs/adc-test-parity-plan.md.

Endpoint contract

POST /apisix/admin/configs/validate?gateway_group_id=<id> with flat JSON body containing routes, services, consumers, ssls, global_rules, stream_routes, plugin_metadata. Inferred from adc/libs/backend-api7/src/validator.ts (request body shape) and adc/.../e2e/validate.e2e-spec.ts (success/failure semantics, confirms dry-run).

Test plan

  • TestConfigValidate_Remote_HappyPath
  • TestConfigValidate_Remote_CollectsErrors
  • TestConfigValidate_Remote_LocalErrorsSkipRemote
  • TestConfigValidate_NoRemoteFlag_BehavesAsBefore
  • go test ./pkg/... ./internal/... passes
  • go vet ./...; gofmt -l . clean
  • Smoke against a real API7 EE (>=3.9.10) instance to confirm endpoint shape

Caveat

Endpoint shape is inferred, not confirmed against API7 EE docs. If the real admin API differs, the contract change is localized to validate/remote.go. adc gates this feature on API7 EE >= 3.9.10; a7 currently surfaces an older-version 404 as-is rather than checking version first.

Summary by CodeRabbit

  • New Features
    • Added --remote flag to the config validate command for validation against API7 EE backend.
    • Remote validation errors are clearly identified with [remote] prefix.
    • Remote validation only runs if local validation succeeds.

Review Change Stack

`a7 config validate --remote` posts the parsed config to
`/apisix/admin/configs/validate?gateway_group_id=<id>` so API7 EE can
report errors caught by the live backend schema (plugin-config shapes,
cross-resource references, etc) on top of a7's local checks.

Local errors short-circuit before any HTTP call so the remote step
never sees a known-bad input. Each remote error is prefixed with
[remote] to distinguish it from local validation output.

Endpoint shape inferred from adc's backend-api7 validator (request
body keys, error array). Implementation issues a raw HTTP request
instead of going through api.Client.Post so the 400 body's structured
`errors[]` array is preserved instead of being collapsed into a single
error string.
@coderabbitai
Copy link
Copy Markdown

coderabbitai Bot commented May 28, 2026

📝 Walkthrough

Walkthrough

This PR adds optional remote validation to the config validate command. It introduces new HTTP client logic to call API7 EE's dry-run endpoint, integrates the remote validation into the command flow behind a --remote flag, and includes comprehensive tests for both success and error scenarios.

Changes

Remote Configuration Validation

Layer / File(s) Summary
Remote validation data contracts and request payload construction
pkg/cmd/config/validate/remote.go
Request/response types mirror the EE backend's dry-run API. Request type wraps routes, services, and plugin metadata. buildRemoteValidateBody clones and transforms plugin metadata entries, translating plugin_name pseudo-field into id while removing the original field.
Remote validation HTTP client and error formatting
pkg/cmd/config/validate/remote.go
validateRemote marshals the request, POSTs to /apisix/admin/configs/validate with optional gateway_group_id, and handles responses: nil for 2xx, structured per-resource errors for 400 (unmarshaled from JSON), raw-body strings for other non-2xx. formatRemoteError converts backend error objects to human-readable strings using message/reason fields and resource/index/field prefix.
Command options and remote flag registration
pkg/cmd/config/validate/validate.go
Options struct gains File and Remote fields. NewCmdValidate registers --remote boolean flag to enable remote dry-run validation after local checks pass.
Command execution flow with remote validation
pkg/cmd/config/validate/validate.go
validateRun now conditionally calls runRemoteValidation when opts.Remote is set. runRemoteValidation resolves Config and http.Client, dispatches the remote call, and aggregates returned errors with [remote] prefix. Command prints "Config is valid" only when both local and remote validations succeed.
Test infrastructure and remote validation coverage
pkg/cmd/config/validate/validate_test.go
Adds httpmock-backed factoryWithHTTP for intercepting remote endpoint calls. validRemoteConfig YAML fixture provides request payload for tests. Four test functions verify: (1) remote validator is called with flattened routes/services, (2) remote errors include [remote] prefix and field-level details, (3) local validation failures prevent remote call, (4) omitting --remote flag skips remote endpoint and preserves backward compatibility.

Sequence Diagram

sequenceDiagram
  participant CLI as CLI / validateRun
  participant Local as Local Validator
  participant Remote as Remote Client
  participant EE as API7 EE Endpoint
  CLI->>Local: ValidateConfigFile(config)
  Local-->>CLI: nil or local errors
  alt local errors
    CLI-->>CLI: return early
  else local success & opts.Remote
    CLI->>Remote: runRemoteValidation(opts)
    Remote->>Remote: buildRemoteValidateBody(config)
    Remote->>EE: POST /apisix/admin/configs/validate (JSON)
    EE-->>Remote: 400 or 2xx with body
    alt status 2xx
      Remote-->>CLI: nil
    else status 400
      Remote->>Remote: unmarshal & formatRemoteError for each entry
      Remote-->>CLI: aggregated errors with [remote] prefix
    end
  end
  alt all validations pass
    CLI-->>CLI: print "Config is valid"
  else any validation failed
    CLI-->>CLI: return error
  end
Loading

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~20 minutes


Important

Pre-merge checks failed

Please resolve all errors before merging. Addressing warnings is optional.

❌ Failed checks (1 error, 1 warning)

Check name Status Explanation Resolution
Security Check ❌ Error Missing query parameter escaping allows URL injection; response bodies exposed without redaction leak sensitive data; error handling conflates transport failures with validation. Use net/url.Values for parameters; redact/parse error responses; return ([]string, error) to distinguish operational errors from validation rejections.
E2e Test Quality Review ⚠️ Warning Three blocking issues from review comments remain: (1) Token not passed to validateRemote; (2) Error handling conflates operational vs validation errors; (3) Gateway group ID not URL-escaped. Add Token() parameter, set Authorization header; change validateRemote return type to ([]string, error); use net/url.Values for escaping.
✅ Passed checks (4 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title accurately summarizes the main change: adding a --remote flag to the config validate command for remote backend validation.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch feat/validate-remote

Comment @coderabbitai help to get the list of available commands and usage tips.

Copy link
Copy Markdown

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 3

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@pkg/cmd/config/validate/remote.go`:
- Around line 85-128: validateRemote mixes transport/serialization failures and
non-400 HTTP errors into the []string validation slice; change its API to return
([]string, error) so operational failures (json.Marshal, http.NewRequest,
httpClient.Do, io.ReadAll, and any non-2xx non-400 status) return a non-nil
error while only a 400 with a parsed remoteValidationResponse returns ([]string,
nil); update validateRemote to return detailed errors (wrap context) for
marshal/build/Do/read and non-400 statuses, keep the existing parsing/unmarshal
path to return the per-resource []string, and then update the caller
runRemoteValidation to handle the new (results, err) tuple and treat err as
"couldn't reach/use remote" and results as schema rejections.
- Around line 88-91: The URL is built by string concatenation which allows
reserved chars in gatewayGroup to break the request; update the construction
around the url variable (the baseURL + "/apisix/admin/configs/validate" block)
to use net/url and url.Values: create values := url.Values{},
values.Set("gateway_group_id", gatewayGroup) when gatewayGroup is non-empty,
then append "?" + values.Encode() (or build a url.URL and set RawQuery) so
gatewayGroup is properly escaped instead of concatenated.

In `@pkg/cmd/config/validate/validate.go`:
- Around line 114-115: The call to validateRemote is not passing the auth token,
so modify the call site in validate.go to forward configReader.Token() along
with BaseURL() and GatewayGroup() (i.e., call validateRemote(httpClient,
configReader.BaseURL(), cfg, configReader.GatewayGroup(),
configReader.Token())). Then update the validateRemote function signature in
pkg/cmd/config/validate/remote.go to accept the token parameter and ensure the
implementation sets the Authorization header (or appropriate auth header) on the
outgoing request before dispatching to the remote admin validate endpoint.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 775bd14b-4a6f-49f0-86b8-5721ffa930c8

📥 Commits

Reviewing files that changed from the base of the PR and between d99f5fa and fa43ebf.

📒 Files selected for processing (3)
  • pkg/cmd/config/validate/remote.go
  • pkg/cmd/config/validate/validate.go
  • pkg/cmd/config/validate/validate_test.go

Comment on lines +85 to +128
func validateRemote(httpClient *http.Client, baseURL string, cfg api.ConfigFile, gatewayGroup string) []string {
body := buildRemoteValidateBody(cfg)

url := baseURL + "/apisix/admin/configs/validate"
if gatewayGroup != "" {
url += "?gateway_group_id=" + gatewayGroup
}

encoded, err := json.Marshal(body)
if err != nil {
return []string{fmt.Sprintf("failed to encode remote validate request: %v", err)}
}

req, err := http.NewRequest(http.MethodPost, url, bytes.NewReader(encoded))
if err != nil {
return []string{fmt.Sprintf("failed to build remote validate request: %v", err)}
}
req.Header.Set("Content-Type", "application/json")

resp, err := httpClient.Do(req)
if err != nil {
return []string{fmt.Sprintf("remote validation request failed: %v", err)}
}
defer resp.Body.Close()

respBody, err := io.ReadAll(resp.Body)
if err != nil {
return []string{fmt.Sprintf("failed to read remote validate response: %v", err)}
}

if resp.StatusCode >= 200 && resp.StatusCode < 300 {
return nil
}

// Only 400 carries the structured per-resource error list. For other
// non-2xx statuses surface the raw body so auth or availability issues
// aren't swallowed.
if resp.StatusCode != http.StatusBadRequest {
return []string{fmt.Sprintf("remote validation failed (status %d): %s", resp.StatusCode, string(respBody))}
}

var parsed remoteValidationResponse
if jsonErr := json.Unmarshal(respBody, &parsed); jsonErr != nil {
return []string{fmt.Sprintf("remote validation failed (status 400): %s", string(respBody))}
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Propagate transport/setup failures through error, not the validation slice.

validateRemote currently turns marshal/build/Do/read/non-400 failures into []string. That breaks the contract used by runRemoteValidation in pkg/cmd/config/validate/validate.go, Lines 97-115, where error means “couldn’t reach/use the remote” and []string means “remote schema rejected the config.” As written, auth/network/404/5xx paths are reported as [remote] validation failures instead of operational errors.

Suggested direction
-func validateRemote(httpClient *http.Client, baseURL string, cfg api.ConfigFile, gatewayGroup string) []string {
+func validateRemote(httpClient *http.Client, baseURL string, cfg api.ConfigFile, gatewayGroup string) ([]string, error) {
 	body := buildRemoteValidateBody(cfg)

 	encoded, err := json.Marshal(body)
 	if err != nil {
-		return []string{fmt.Sprintf("failed to encode remote validate request: %v", err)}
+		return nil, fmt.Errorf("failed to encode remote validate request: %w", err)
 	}

 	req, err := http.NewRequest(http.MethodPost, url, bytes.NewReader(encoded))
 	if err != nil {
-		return []string{fmt.Sprintf("failed to build remote validate request: %v", err)}
+		return nil, fmt.Errorf("failed to build remote validate request: %w", err)
 	}

 	resp, err := httpClient.Do(req)
 	if err != nil {
-		return []string{fmt.Sprintf("remote validation request failed: %v", err)}
+		return nil, fmt.Errorf("remote validation request failed: %w", err)
 	}

 	respBody, err := io.ReadAll(resp.Body)
 	if err != nil {
-		return []string{fmt.Sprintf("failed to read remote validate response: %v", err)}
+		return nil, fmt.Errorf("failed to read remote validate response: %w", err)
 	}

 	if resp.StatusCode >= 200 && resp.StatusCode < 300 {
-		return nil
+		return nil, nil
 	}

 	if resp.StatusCode != http.StatusBadRequest {
-		return []string{fmt.Sprintf("remote validation failed (status %d): %s", resp.StatusCode, string(respBody))}
+		return nil, fmt.Errorf("remote validation failed (status %d): %s", resp.StatusCode, string(respBody))
 	}

 	// keep returning []string only for decoded 400 validation errors
-	return out
+	return out, nil
 }

As per coding guidelines, **/*.go: Never suppress errors. Always handle and propagate errors explicitly.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@pkg/cmd/config/validate/remote.go` around lines 85 - 128, validateRemote
mixes transport/serialization failures and non-400 HTTP errors into the []string
validation slice; change its API to return ([]string, error) so operational
failures (json.Marshal, http.NewRequest, httpClient.Do, io.ReadAll, and any
non-2xx non-400 status) return a non-nil error while only a 400 with a parsed
remoteValidationResponse returns ([]string, nil); update validateRemote to
return detailed errors (wrap context) for marshal/build/Do/read and non-400
statuses, keep the existing parsing/unmarshal path to return the per-resource
[]string, and then update the caller runRemoteValidation to handle the new
(results, err) tuple and treat err as "couldn't reach/use remote" and results as
schema rejections.

Comment on lines +88 to +91
url := baseURL + "/apisix/admin/configs/validate"
if gatewayGroup != "" {
url += "?gateway_group_id=" + gatewayGroup
}
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -euo pipefail

# Show the relevant section of the file with line numbers
sed -n '1,200p' pkg/cmd/config/validate/remote.go | nl -ba | sed -n '1,200p'

# Focus around the cited lines 70-120
sed -n '60,130p' pkg/cmd/config/validate/remote.go | nl -ba

# Find validateRemote signature and any URL parsing/values usage in the file
rg -n "func validateRemote|validateRemote\(" -n pkg/cmd/config/validate/remote.go
rg -n "baseURL|gatewayGroup|gateway_group_id|url\.Values|net/url|Query\(" pkg/cmd/config/validate/remote.go

Repository: api7/a7

Length of output: 94


🏁 Script executed:

#!/bin/bash
set -euo pipefail

# Show the relevant section of the file with line numbers (cat -n is available)
sed -n '1,220p' pkg/cmd/config/validate/remote.go | cat -n

# Focus around the cited lines 70-120 (with cat -n to keep line numbers)
sed -n '60,130p' pkg/cmd/config/validate/remote.go | cat -n

# Find validateRemote signature and URL parsing usage
rg -n "func validateRemote|validateRemote\(" pkg/cmd/config/validate/remote.go
rg -n "baseURL|gatewayGroup|gateway_group_id|net/url|url\.Values|Query\(" pkg/cmd/config/validate/remote.go

Repository: api7/a7

Length of output: 9893


Escape gateway_group_id instead of concatenating it into the URL.

	url := baseURL + "/apisix/admin/configs/validate"
	if gatewayGroup != "" {
		url += "?gateway_group_id=" + gatewayGroup
	}

Build the query using net/url + url.Values (e.g., values.Set("gateway_group_id", gatewayGroup); ... + "?" + values.Encode()) so reserved characters in gatewayGroup can’t break or alter the request target.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@pkg/cmd/config/validate/remote.go` around lines 88 - 91, The URL is built by
string concatenation which allows reserved chars in gatewayGroup to break the
request; update the construction around the url variable (the baseURL +
"/apisix/admin/configs/validate" block) to use net/url and url.Values: create
values := url.Values{}, values.Set("gateway_group_id", gatewayGroup) when
gatewayGroup is non-empty, then append "?" + values.Encode() (or build a url.URL
and set RawQuery) so gatewayGroup is properly escaped instead of concatenated.

Comment on lines +114 to +115
return validateRemote(httpClient, configReader.BaseURL(), cfg, configReader.GatewayGroup()), nil
}
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Pass auth context into remote validate dispatch.

Line 114 forwards only BaseURL() and GatewayGroup(). The remote admin validate endpoint is authenticated; not threading Token() into the request builder can make --remote fail (401/forbidden) on real EE environments.

Suggested fix
- return validateRemote(httpClient, configReader.BaseURL(), cfg, configReader.GatewayGroup()), nil
+ return validateRemote(
+   httpClient,
+   configReader.BaseURL(),
+   configReader.Token(),
+   cfg,
+   configReader.GatewayGroup(),
+ ), nil

Then update validateRemote(...) in pkg/cmd/config/validate/remote.go to accept/use the token when setting request headers.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@pkg/cmd/config/validate/validate.go` around lines 114 - 115, The call to
validateRemote is not passing the auth token, so modify the call site in
validate.go to forward configReader.Token() along with BaseURL() and
GatewayGroup() (i.e., call validateRemote(httpClient, configReader.BaseURL(),
cfg, configReader.GatewayGroup(), configReader.Token())). Then update the
validateRemote function signature in pkg/cmd/config/validate/remote.go to accept
the token parameter and ensure the implementation sets the Authorization header
(or appropriate auth header) on the outgoing request before dispatching to the
remote admin validate endpoint.

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