Skip to content

feat: add plugin configuration validation to lint#426

Closed
jarvis9443 wants to merge 6 commits intomainfrom
feat/lint-plugin-validation
Closed

feat: add plugin configuration validation to lint#426
jarvis9443 wants to merge 6 commits intomainfrom
feat/lint-plugin-validation

Conversation

@jarvis9443
Copy link
Copy Markdown
Contributor

@jarvis9443 jarvis9443 commented Apr 9, 2026

Summary

Add plugin configuration validation to ADC's lint functionality. Previously, lint only validated core resource structure (routes, services, upstreams, etc.) via Zod schemas, but accepted any plugin configuration without validation.

Changes

Two-Phase Validation

  • Phase 1 (local): Existing Zod schema validation for core resources (unchanged behavior)
  • Phase 2 (optional): Plugin config validation using JSON Schema fetched from APISIX/API7 EE Admin API via GET /apisix/admin/schema/plugins/{name}

New Features

  • adc lint now accepts optional --backend, --server, --token, --gateway-group flags to enable plugin validation
  • Without backend flags, lint behaves exactly as before (pure local validation)
  • adc diff and adc sync automatically validate plugin configs using the connected backend

Plugin Validation Coverage

All plugin locations in the configuration are validated:

  • services[*].plugins, routes[*].plugins, stream_routes[*].plugins
  • consumers[*].plugins, consumer_groups[*].plugins
  • consumers[*].credentials[*].config (using consumerSchema)
  • global_rules, plugin_metadata (using metadataSchema)

Backend Support

Backend Plugin Validation
APISIX ✅ Fetches schemas via Admin API
API7 EE ✅ Fetches schemas via Admin API
APISIX Standalone ⏭️ Gracefully skipped (no network)

Implementation Details

  • SDK: Added PluginSchemaEntry, PluginSchemaMap types and fetchPluginSchemas() to Backend interface
  • Backends: Implemented fetchPluginSchemas() in both BackendAPISIX and BackendAPI7
  • Validator: Uses AJV with draft-04 support (matching APISIX JSON Schema format)
  • Error reporting: Unified LintError/LintResult types for both Zod and AJV errors
  • Lint command remains a BaseCommand (no default backend connection)
  • .gitignore fix: Changed /out-tsc to out-tsc to ignore build artifacts in all subdirectories

Tests

Added 20 new test cases covering:

  • Unknown plugin detection
  • Missing required fields, type mismatches, multiple errors
  • All plugin location traversals (services, routes, consumers, credentials, global_rules, metadata)
  • Edge cases (empty plugins, no schemas, core validation priority)

All 45 tests pass (25 existing + 20 new).

Summary by CodeRabbit

  • New Features

    • Plugin schema validation added and enabled as an option for lint/sync, plus new CLI flags to configure backend-backed validation.
    • Backend schema fetching with caching to retrieve plugin schemas at runtime.
  • Improvements

    • Linter now returns structured validation results (success, errors, data) and reports path/code details.
    • Tests updated/expanded for plugin validation coverage and error-shape changes.

Add two-phase validation to ADC's lint functionality:

Phase 1 (local): Existing core resource Zod schema validation
Phase 2 (optional, backend-connected): Plugin config validation using
JSON Schema fetched from APISIX/API7 EE Admin API

Key changes:
- Add PluginSchemaEntry/PluginSchemaMap types to SDK Backend interface
- Implement fetchPluginSchemas() in both BackendAPISIX and BackendAPI7
- Create plugin-validator.ts with AJV-based draft-04 JSON Schema validation
- Introduce LintError/LintResult types for unified error reporting
- Upgrade lint command with optional --backend/--server/--token/--gateway-group
- Integrate FetchPluginSchemasTask into diff/sync commands
- Add 20 new test cases covering all plugin locations and edge cases

Plugin validation traverses all config locations:
- services[*].plugins, routes[*].plugins, stream_routes[*].plugins
- consumers[*].plugins, consumer_groups[*].plugins
- consumers[*].credentials[*].config (consumerSchema)
- global_rules, plugin_metadata (metadataSchema)

The lint command remains a BaseCommand (no default backend connection).
Backend params are optional - without them, only core validation runs.
APISIX Standalone backend gracefully skips plugin validation.
@jarvis9443 jarvis9443 requested a review from bzp2010 as a code owner April 9, 2026 06:34
@coderabbitai
Copy link
Copy Markdown

coderabbitai bot commented Apr 9, 2026

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 823301dc-504c-4ee4-89e0-6cef4ad5c89d

📥 Commits

Reviewing files that changed from the base of the PR and between 95738f8 and 5e6a6db.

📒 Files selected for processing (2)
  • apps/cli/src/command/lint.command.ts
  • apps/cli/src/tasks/fetch_plugin_schemas.ts
✅ Files skipped from review due to trivial changes (1)
  • apps/cli/src/tasks/fetch_plugin_schemas.ts
🚧 Files skipped from review as they are similar to previous changes (1)
  • apps/cli/src/command/lint.command.ts

📝 Walkthrough

Walkthrough

Adds backend-driven plugin schema fetching and integrates plugin-schema validation into the CLI linter flow, updating types, tasks, commands, tests, and backend implementations to fetch, cache, and use plugin JSON Schemas during lint/diff/sync operations.

Changes

Cohort / File(s) Summary
CLI package & workspace
apps/cli/package.json, pnpm-workspace.yaml
Dependency additions/reordering (ajv-draft-04, agentkeepalive, @types/json-schema, etc.) and workspace catalog updates / autoInstallPeers toggle.
CLI commands
apps/cli/src/command/lint.command.ts, apps/cli/src/command/diff.command.ts, apps/cli/src/command/sync.command.ts
Added backend-related CLI options to lint; conditionally insert FetchPluginSchemasTask into diff/sync/lint pipelines when backend/lint enabled; minor error-handling change in lint command.
Tasks
apps/cli/src/tasks/fetch_plugin_schemas.ts, apps/cli/src/tasks/index.ts, apps/cli/src/tasks/lint.ts
Added FetchPluginSchemasTask; re-exported it; extended LintTask context to accept pluginSchemas and changed lint error formatting to use LintError[].
Linter core & types
apps/cli/src/linter/index.ts, apps/cli/src/linter/types.ts, apps/cli/src/linter/plugin-validator.ts
check() now accepts LintOptions and returns LintResult; added LintError/LintOptions/LintResult types; new validatePlugins() uses AJV (draft-04) to validate plugin configs and metadata, returning structured lint errors.
Linter tests
apps/cli/src/linter/specs/plugin.spec.ts, .../common.spec.ts, .../consumer.spec.ts, .../route.spec.ts, .../ssl.spec.ts, .../upstream.spec.ts
Added comprehensive plugin linter tests; updated existing specs to read failures from result.errors instead of result.error.issues.
Backend SDK types
libs/sdk/src/backend/index.ts
Added PluginSchemaEntry and PluginSchemaMap types; extended Backend interface with optional fetchPluginSchemas() method.
Backend implementations
libs/backend-apisix/src/index.ts, libs/backend-api7/src/index.ts
Added fetchPluginSchemas() methods that fetch plugin lists and per-plugin schemas (config + optional consumer), cache results, emit debug events, and omit unavailable schemas.
SDK & packages
libs/sdk/package.json, libs/backend-apisix/package.json, libs/backend-api7/package.json
Package.json edits to add/types OR reorder dependencies (@types/json-schema, datum-diff, etc.).
Server sync handler
apps/cli/src/server/sync.ts
Changed lint-failure response to return fixed message and result.errors payload.
Repository config
.gitignore
Changed out-tsc ignore to unanchored form.

Sequence Diagram

sequenceDiagram
    participant User
    participant CLI as CLI Command
    participant Fetch as FetchPluginSchemasTask
    participant Backend as Backend (APISIX/API7)
    participant Lint as LintTask
    participant Validator as Plugin Validator

    User->>CLI: run lint/diff/sync with --backend
    CLI->>Fetch: enable & execute task
    Fetch->>Backend: fetchPluginSchemas()
    Backend->>Backend: request plugin list
    Backend->>Backend: concurrently fetch each plugin schema (config, optional consumer)
    Backend-->>Fetch: return PluginSchemaMap (cached)
    Fetch-->>CLI: store pluginSchemas in context
    CLI->>Lint: run lint with local config + pluginSchemas
    Lint->>Validator: validatePlugins(config, pluginSchemas)
    Validator-->>Lint: return LintError[]
    Lint-->>CLI: return LintResult { success, errors, data }
    CLI-->>User: display results
Loading

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~25 minutes

Suggested reviewers

  • juzhiyuan
🚥 Pre-merge checks | ✅ 3 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
E2e Test Quality Review ⚠️ Warning Tests are unit-level mocks, not E2E tests; missing full workflow coverage from CLI through backend API to schema validation. Add E2E tests invoking actual lint command with backend mocking; cover network failures, concurrent calls, and malformed schema scenarios.
✅ Passed checks (3 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title 'feat: add plugin configuration validation to lint' directly and clearly summarizes the main feature being added—plugin configuration validation integrated into the lint workflow.
Security Check ✅ Passed Plugin validation implementation properly isolates sensitive data; AJV errors contain only validation metadata without exposing secret values from configurations.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ 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/lint-plugin-validation

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

- Change LintError.path type from (string | number)[] to PropertyKey[]
  to match Zod's issue path type (which can include symbols)
- Remove unused direct 'ajv' dependency from CLI package (ajv-draft-04
  includes it as a peer dependency)
- Remove accidentally committed out-tsc/ directories from libs/
- Change .gitignore from '/out-tsc' to 'out-tsc' to cover all nested dirs
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: 2

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (2)
apps/cli/src/command/sync.command.ts (1)

84-91: ⚠️ Potential issue | 🟠 Major

Respect --no-lint: schema fetch should be gated with lint execution.

Line 84 currently runs FetchPluginSchemasTask() even when lint is disabled (Line 91). If schema fetch fails, sync can fail in --no-lint mode for a non-lint concern.

Suggested adjustment
-        FetchPluginSchemasTask(),
+        opts.lint ? FetchPluginSchemasTask() : { task: () => undefined },
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@apps/cli/src/command/sync.command.ts` around lines 84 - 91, The
FetchPluginSchemasTask is being executed regardless of the --no-lint flag and
should only run when linting is enabled; modify the task list in sync.command.ts
so FetchPluginSchemasTask() is included only if opts.lint is true (similar to
how LintTask() is conditionally selected), for example wrap or replace
FetchPluginSchemasTask() with a conditional expression using opts.lint so that
when lint is disabled it returns a no-op task (matching the existing { task: ()
=> undefined } pattern).
apps/cli/src/command/diff.command.ts (1)

105-107: ⚠️ Potential issue | 🟠 Major

Don’t silently swallow the caught error.

At Line 105, err is captured but never handled, which also causes the reported ESLint warning. Please at least log the error before exiting.

Suggested fix
-    } catch (err) {
-      process.exit(1);
+    } catch (err) {
+      console.error(err);
+      process.exit(1);
     }

As per coding guidelines **/*.{js,ts,jsx,tsx}: Every function return value must be checked for errors (if applicable); errors must be properly handled, not ignored or silently swallowed.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@apps/cli/src/command/diff.command.ts` around lines 105 - 107, The catch block
that currently swallows the error (catch (err) { process.exit(1); }) should log
the caught error before exiting: update the catch to log the error (e.g., using
console.error or the module's logger) with a clear message referencing the
failing operation and the err object, then call process.exit(1); ensure you
modify the catch surrounding the code in diff.command.ts so that err is not
ignored and the error details are emitted.
🧹 Nitpick comments (8)
apps/cli/src/command/lint.command.ts (1)

67-82: Consider validating required backend options when --backend is provided.

When --backend is specified, --server and --token are likely required for the backend to function. Currently, if a user provides --backend apisix without --server, the error will occur deep in InitializeBackendTask or during the first API call, which may be confusing.

💡 Suggested validation before task execution
   .action(async () => {
     const opts = LintCommand.optsWithGlobals();
     const useBackend = !!opts.backend;
+
+    if (useBackend && (!opts.server || !opts.token)) {
+      console.error('Error: --server and --token are required when --backend is specified');
+      process.exit(1);
+    }

     const tasks = new Listr(
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@apps/cli/src/command/lint.command.ts` around lines 67 - 82, When opts.backend
is provided (useBackend true) validate required backend options (e.g.,
opts.server and opts.token) before creating the Listr tasks so users get a clear
early error instead of a failure inside InitializeBackendTask or later API
calls; update the code around useBackend/InitializeBackendTask to perform a
pre-check (or add a small ValidateBackendOptionsTask) that ensures opts.server
and opts.token are present and throw or log a descriptive error if missing,
referencing opts.backend, InitializeBackendTask, and LintTask so the validation
runs prior to task execution.
apps/cli/src/linter/plugin-validator.ts (2)

134-149: Inconsistent handling of unknown plugins in consumer credentials.

When a credential references an unknown plugin type (line 136-137), validation is silently skipped. This differs from validatePluginsMap which reports an unknown_plugin error. Consider reporting unknown credential plugin types for consistency.

💡 Suggested fix for consistency
     consumer.credentials?.forEach((credential, cri) => {
       const pluginName = credential.type;
       const schema = schemas[pluginName];
-      if (schema) {
+      if (!schema) {
+        errors.push({
+          path: ['consumers', ci, 'credentials', cri, 'type'],
+          message: `Unknown credential plugin type "${pluginName}"`,
+          code: 'unknown_plugin',
+        });
+      } else {
         errors.push(
           ...validateSinglePlugin(
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@apps/cli/src/linter/plugin-validator.ts` around lines 134 - 149, The consumer
credential loop currently skips validation when schemas[pluginName] is missing,
silently ignoring unknown credential plugin types; update the loop in
plugin-validator.ts (inside consumer.credentials?.forEach) to mirror
validatePluginsMap by pushing an error when credential.type isn't found:
construct and push an 'unknown_plugin' error referencing the path ['consumers',
ci, 'credentials', cri] (or similar) and include pluginName, so unknown
credential plugin types are reported instead of silently ignored; keep using the
same error shape/fields validatePluginsMap uses for consistency with
validateSinglePlugin and the rest of the validator.

25-26: Consider handling potential ajv.compile() exceptions.

ajv.compile() can throw if the JSON schema is malformed (e.g., invalid $ref, unsupported keywords). Since schemas are fetched from the backend and may vary across versions, a try-catch would provide more graceful error handling.

💡 Suggested defensive handling
 const validateSinglePlugin = (
   ajv: Ajv,
   pluginName: string,
   pluginConfig: Record<string, unknown>,
   schema: PluginSchemaEntry,
   path: Array<string | number>,
   schemaType: 'configSchema' | 'consumerSchema' | 'metadataSchema',
 ): LintError[] => {
   const errors: LintError[] = [];
   const jsonSchema = schema[schemaType];
   if (!jsonSchema) return errors;

+  let validate;
+  try {
+    validate = ajv.compile(jsonSchema);
+  } catch (e) {
+    errors.push({
+      path: [...path, pluginName],
+      message: `Failed to compile schema for plugin: ${e instanceof Error ? e.message : 'unknown error'}`,
+      code: 'schema_compile_error',
+    });
+    return errors;
+  }
-  const validate = ajv.compile(jsonSchema);
   if (!validate(pluginConfig)) {
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@apps/cli/src/linter/plugin-validator.ts` around lines 25 - 26,
ajv.compile(jsonSchema) can throw on malformed schemas; wrap the compilation in
a try-catch around the ajv.compile call (the code creating the validate
function) so compilation errors are caught and handled gracefully instead of
propagating. In the catch, log or surface the compilation error (including the
error message and the offending jsonSchema context) and return/throw a
controlled validation failure for the calling code that uses validate and
pluginConfig, ensuring the rest of plugin-validator.ts behaves predictably when
schema compilation fails.
libs/backend-apisix/src/index.ts (1)

110-163: Same concerns as BackendAPI7.fetchPluginSchemas() apply here.

This implementation has the same issues noted in the API7 backend:

  1. Missing metadataSchema fetch (if needed for plugin metadata validation)
  2. Silent error handling in catch blocks
  3. Minor race condition on concurrent calls

Additionally, there's significant code duplication between BackendAPI7 and BackendAPISIX for fetchPluginSchemas(). Consider extracting the common logic to a shared utility function.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@libs/backend-apisix/src/index.ts` around lines 110 - 163, fetchPluginSchemas
in BackendAPISIX duplicates BackendAPI7 logic, silently swallows errors, omits
fetching metadataSchema, and has a race on concurrent callers; refactor by
extracting the shared fetching logic into a common utility (e.g., a new
fetchPluginSchemasShared function) used by both BackendAPISIX.fetchPluginSchemas
and BackendAPI7.fetchPluginSchemas, add a memoized in-flight promise property
(e.g., _pluginSchemasPromise) to prevent concurrent duplicate fetches, fetch the
metadataSchema alongside configSchema and consumerSchema (call the same endpoint
with params schema_type='metadata' and attach to the PluginSchemaEntry as
metadataSchema), and replace empty catch blocks with explicit error handling
that emits/logs the error via this.subject (use
ADCSDK.BackendEventType.AXIOS_DEBUG or a suitable ERROR type and include the
error/response and plugin name) so failures are visible while still skipping
unavailable schemas.
libs/backend-api7/src/index.ts (1)

231-284: Missing metadataSchema fetch and silent error handling.

  1. The PluginSchemaEntry interface (from libs/sdk/src/backend/index.ts:75-79) includes an optional metadataSchema, but this implementation never fetches it. If plugin metadata validation is intended (per the PR objectives mentioning metadataSchema), you'll need to add a fetch for schema_type=metadata similar to the consumer schema fetch.

  2. The empty catch blocks (lines 271-273, 276-278) silently swallow errors without any logging, making debugging difficult when plugin schemas fail to load. Consider emitting a debug event or logging a warning.

  3. Minor race condition: if fetchPluginSchemas() is called concurrently before the first call completes, both calls will proceed past the cache check (line 232) and fetch redundantly. This is a minor optimization issue but worth noting.

Suggested improvements
          // Fetch consumer schema if applicable
          try {
            const consumerResp = await this.client.get<JSONSchema4>(
              `/apisix/admin/schema/plugins/${name}`,
              { params: { schema_type: 'consumer' } },
            );
+           this.subject.next({
+             type: ADCSDK.BackendEventType.AXIOS_DEBUG,
+             event: {
+               response: consumerResp,
+               description: `Get plugin consumer schema: ${name}`,
+             },
+           });
            if (consumerResp.data && Object.keys(consumerResp.data).length > 0)
              entry.consumerSchema = consumerResp.data;
          } catch {
-           // Plugin doesn't have a consumer schema — skip
+           // Plugin doesn't have a consumer schema — skip (expected for most plugins)
          }

          schemas[name] = entry;
        } catch {
-         // Skip plugins whose schema cannot be fetched
+         // Skip plugins whose schema cannot be fetched
+         this.subject.next({
+           type: ADCSDK.BackendEventType.AXIOS_DEBUG,
+           event: { response: null, description: `Failed to fetch schema for plugin: ${name}` },
+         });
        }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@libs/backend-api7/src/index.ts` around lines 231 - 284, fetchPluginSchemas
currently never fetches PluginSchemaEntry.metadataSchema, swallows errors
silently in two empty catch blocks, and can run twice concurrently; update
fetchPluginSchemas to also request `/apisix/admin/schema/plugins/${name}` with
params { schema_type: 'metadata' } and assign it to entry.metadataSchema if
present (same pattern as consumerSchema), replace empty catch blocks with
debug/warn emits using this.subject.next (e.g., BackendEventType.AXIOS_DEBUG or
a warning event) including the caught error and context (plugin name and schema
type), and add a simple concurrency guard (introduce a private
_pluginSchemasPromise checked/assigned at the start of fetchPluginSchemas so
concurrent callers await the same promise) to prevent duplicate fetches;
reference fetchPluginSchemas, PluginSchemaEntry, this.client.get,
this.subject.next, this._pluginSchemas and introduce _pluginSchemasPromise.
apps/cli/src/linter/specs/plugin.spec.ts (3)

152-230: Add explicit min-boundary tests for rate-limiting schema constraints.

count and time_window define minimum: 1, but there’s no boundary coverage for invalid 0 and valid 1.

Suggested boundary test additions
+ it('should reject values below minimum in rate-limiting config', () => {
+   const result = check(
+     {
+       services: [
+         {
+           name: 'test-svc',
+           plugins: { 'rate-limiting': { count: 0, time_window: 1 } },
+           routes: [{ name: 'test-route', uris: ['/test'] }],
+         },
+       ],
+     },
+     { pluginSchemas: mockPluginSchemas },
+   );
+   expect(result.success).toBe(false);
+   expect(result.errors).toContainEqual(
+     expect.objectContaining({
+       path: ['services', 0, 'plugins', 'rate-limiting', 'count'],
+       code: 'plugin_schema_violation',
+     }),
+   );
+ });
+
+ it('should accept minimum boundary values in rate-limiting config', () => {
+   const result = check(
+     {
+       services: [
+         {
+           name: 'test-svc',
+           plugins: { 'rate-limiting': { count: 1, time_window: 1 } },
+           routes: [{ name: 'test-route', uris: ['/test'] }],
+         },
+       ],
+     },
+     { pluginSchemas: mockPluginSchemas },
+   );
+   expect(result.success).toBe(true);
+ });

As per coding guidelines: "Tests must cover boundary cases (empty values, min/max), invalid inputs, combination scenarios, and extreme cases (high load, failures)."

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@apps/cli/src/linter/specs/plugin.spec.ts` around lines 152 - 230, Add
explicit boundary tests to the existing "plugin config validation" suite that
exercise the rate-limiting plugin's minimum constraints: create tests (using the
same check(...) helper and mockPluginSchemas) that assert a failing result when
count is 0 (with time_window valid), failing when time_window is 0 (with count
valid), and succeeding when both count and time_window are 1; reference the
existing test cases (e.g., the "should report type mismatch" and "should report
multiple errors at once" patterns) to mirror structure and assertions
(expect(result.success).toBe(...), expect(result.errors) checks) and ensure
paths point to ['services', 0, 'plugins', 'rate-limiting', 'count'] or
['...','time_window'] as appropriate.

247-395: Use order-independent assertions instead of result.errors[0].

These assertions are brittle because they rely on error ordering. Prefer toContainEqual / arrayContaining + objectContaining so tests validate content, not position.

Suggested assertion pattern
- expect(result.errors[0].path).toEqual([
-   'services',
-   0,
-   'plugins',
-   'key-auth',
- ]);
+ expect(result.errors).toContainEqual(
+   expect.objectContaining({
+     path: ['services', 0, 'plugins', 'key-auth'],
+     code: 'plugin_schema_violation',
+   }),
+ );

As per coding guidelines: "Tests must not have hidden execution order assumptions; explicit dependencies between tests should be clear."

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@apps/cli/src/linter/specs/plugin.spec.ts` around lines 247 - 395, The tests
in plugin.spec.ts assert error positions by checking result.errors[0].path
(e.g., in the tests that validate plugins under
services/routes/stream_routes/consumers/consumer_groups/global_rules and
plugin_metadata), which is brittle; update each assertion that uses
result.errors[0] to an order-independent check by asserting the errors array
contains an element with the expected path and properties (use toContainEqual or
expect.arrayContaining + expect.objectContaining against result.errors and the
path arrays like ['services',0,'routes',0,'plugins','key-auth'],
['consumers',0,'credentials',0,'config','key-auth'], ['plugin_metadata', ...],
etc.), so tests verify content not ordering while still referencing the same
result.errors and path shapes produced by check().

440-464: Strengthen the “core validation first” assertion with a structured core-error check.

Current check proves unknown_plugin is absent, but it doesn’t explicitly verify that the returned failure is the expected core-route validation error.

As per coding guidelines: "Assertions must be readable; avoid complex regex, hard-to-understand logic, and prefer structured assertions."

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@apps/cli/src/linter/specs/plugin.spec.ts` around lines 440 - 464, The test
"should still do core validation first" currently only asserts that no
'unknown_plugin' error exists; instead, strengthen it by asserting that the
failure is the expected core route validation error: after calling check(...)
and verifying result.success is false, assert that result.errors contains a
structured core error pointing at the missing 'uris' on the route (e.g., check
result.errors.some(e => e.path && e.path.includes('uris') && e.code ===
'required' or otherwise matches the core validation shape), rather than just
checking absence of 'unknown_plugin'); reference the same symbols used in the
test (check, mockPluginSchemas, result, result.errors) so the assertion clearly
verifies the core-route validation error is present.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@apps/cli/src/linter/index.ts`:
- Around line 10-16: The mapped object in zodIssuesToLintErrors returns extra
fields from z.ZodIssue via the rest spread that conflict with the LintError
type, causing TS2322; fix it by returning an object that exactly matches the
LintError shape (do not spread unknown properties), e.g. explicitly pick/assign
only the LintError fields inside zodIssuesToLintErrors or cast the remainder to
a compatible type (e.g. Record<string, any>) before merging; refer to the
zodIssuesToLintErrors function, the z.ZodIssue type and the LintError type when
making the change so the returned object strictly conforms to LintError.

In `@package.json`:
- Around line 41-43: Remove the unused dependencies "ajv" and "ajv-draft-04"
from the dependencies section of package.json (they are the entries under
"dependencies" with keys ajv and ajv-draft-04) because `@nx/dependency-checks`
reports no imports; after removal, run your package manager to update the
lockfile (e.g., npm/yarn/pnpm install) so CI won't be blocked by the stale
entries.

---

Outside diff comments:
In `@apps/cli/src/command/diff.command.ts`:
- Around line 105-107: The catch block that currently swallows the error (catch
(err) { process.exit(1); }) should log the caught error before exiting: update
the catch to log the error (e.g., using console.error or the module's logger)
with a clear message referencing the failing operation and the err object, then
call process.exit(1); ensure you modify the catch surrounding the code in
diff.command.ts so that err is not ignored and the error details are emitted.

In `@apps/cli/src/command/sync.command.ts`:
- Around line 84-91: The FetchPluginSchemasTask is being executed regardless of
the --no-lint flag and should only run when linting is enabled; modify the task
list in sync.command.ts so FetchPluginSchemasTask() is included only if
opts.lint is true (similar to how LintTask() is conditionally selected), for
example wrap or replace FetchPluginSchemasTask() with a conditional expression
using opts.lint so that when lint is disabled it returns a no-op task (matching
the existing { task: () => undefined } pattern).

---

Nitpick comments:
In `@apps/cli/src/command/lint.command.ts`:
- Around line 67-82: When opts.backend is provided (useBackend true) validate
required backend options (e.g., opts.server and opts.token) before creating the
Listr tasks so users get a clear early error instead of a failure inside
InitializeBackendTask or later API calls; update the code around
useBackend/InitializeBackendTask to perform a pre-check (or add a small
ValidateBackendOptionsTask) that ensures opts.server and opts.token are present
and throw or log a descriptive error if missing, referencing opts.backend,
InitializeBackendTask, and LintTask so the validation runs prior to task
execution.

In `@apps/cli/src/linter/plugin-validator.ts`:
- Around line 134-149: The consumer credential loop currently skips validation
when schemas[pluginName] is missing, silently ignoring unknown credential plugin
types; update the loop in plugin-validator.ts (inside
consumer.credentials?.forEach) to mirror validatePluginsMap by pushing an error
when credential.type isn't found: construct and push an 'unknown_plugin' error
referencing the path ['consumers', ci, 'credentials', cri] (or similar) and
include pluginName, so unknown credential plugin types are reported instead of
silently ignored; keep using the same error shape/fields validatePluginsMap uses
for consistency with validateSinglePlugin and the rest of the validator.
- Around line 25-26: ajv.compile(jsonSchema) can throw on malformed schemas;
wrap the compilation in a try-catch around the ajv.compile call (the code
creating the validate function) so compilation errors are caught and handled
gracefully instead of propagating. In the catch, log or surface the compilation
error (including the error message and the offending jsonSchema context) and
return/throw a controlled validation failure for the calling code that uses
validate and pluginConfig, ensuring the rest of plugin-validator.ts behaves
predictably when schema compilation fails.

In `@apps/cli/src/linter/specs/plugin.spec.ts`:
- Around line 152-230: Add explicit boundary tests to the existing "plugin
config validation" suite that exercise the rate-limiting plugin's minimum
constraints: create tests (using the same check(...) helper and
mockPluginSchemas) that assert a failing result when count is 0 (with
time_window valid), failing when time_window is 0 (with count valid), and
succeeding when both count and time_window are 1; reference the existing test
cases (e.g., the "should report type mismatch" and "should report multiple
errors at once" patterns) to mirror structure and assertions
(expect(result.success).toBe(...), expect(result.errors) checks) and ensure
paths point to ['services', 0, 'plugins', 'rate-limiting', 'count'] or
['...','time_window'] as appropriate.
- Around line 247-395: The tests in plugin.spec.ts assert error positions by
checking result.errors[0].path (e.g., in the tests that validate plugins under
services/routes/stream_routes/consumers/consumer_groups/global_rules and
plugin_metadata), which is brittle; update each assertion that uses
result.errors[0] to an order-independent check by asserting the errors array
contains an element with the expected path and properties (use toContainEqual or
expect.arrayContaining + expect.objectContaining against result.errors and the
path arrays like ['services',0,'routes',0,'plugins','key-auth'],
['consumers',0,'credentials',0,'config','key-auth'], ['plugin_metadata', ...],
etc.), so tests verify content not ordering while still referencing the same
result.errors and path shapes produced by check().
- Around line 440-464: The test "should still do core validation first"
currently only asserts that no 'unknown_plugin' error exists; instead,
strengthen it by asserting that the failure is the expected core route
validation error: after calling check(...) and verifying result.success is
false, assert that result.errors contains a structured core error pointing at
the missing 'uris' on the route (e.g., check result.errors.some(e => e.path &&
e.path.includes('uris') && e.code === 'required' or otherwise matches the core
validation shape), rather than just checking absence of 'unknown_plugin');
reference the same symbols used in the test (check, mockPluginSchemas, result,
result.errors) so the assertion clearly verifies the core-route validation error
is present.

In `@libs/backend-api7/src/index.ts`:
- Around line 231-284: fetchPluginSchemas currently never fetches
PluginSchemaEntry.metadataSchema, swallows errors silently in two empty catch
blocks, and can run twice concurrently; update fetchPluginSchemas to also
request `/apisix/admin/schema/plugins/${name}` with params { schema_type:
'metadata' } and assign it to entry.metadataSchema if present (same pattern as
consumerSchema), replace empty catch blocks with debug/warn emits using
this.subject.next (e.g., BackendEventType.AXIOS_DEBUG or a warning event)
including the caught error and context (plugin name and schema type), and add a
simple concurrency guard (introduce a private _pluginSchemasPromise
checked/assigned at the start of fetchPluginSchemas so concurrent callers await
the same promise) to prevent duplicate fetches; reference fetchPluginSchemas,
PluginSchemaEntry, this.client.get, this.subject.next, this._pluginSchemas and
introduce _pluginSchemasPromise.

In `@libs/backend-apisix/src/index.ts`:
- Around line 110-163: fetchPluginSchemas in BackendAPISIX duplicates
BackendAPI7 logic, silently swallows errors, omits fetching metadataSchema, and
has a race on concurrent callers; refactor by extracting the shared fetching
logic into a common utility (e.g., a new fetchPluginSchemasShared function) used
by both BackendAPISIX.fetchPluginSchemas and BackendAPI7.fetchPluginSchemas, add
a memoized in-flight promise property (e.g., _pluginSchemasPromise) to prevent
concurrent duplicate fetches, fetch the metadataSchema alongside configSchema
and consumerSchema (call the same endpoint with params schema_type='metadata'
and attach to the PluginSchemaEntry as metadataSchema), and replace empty catch
blocks with explicit error handling that emits/logs the error via this.subject
(use ADCSDK.BackendEventType.AXIOS_DEBUG or a suitable ERROR type and include
the error/response and plugin name) so failures are visible while still skipping
unavailable schemas.
🪄 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: 09e2cbc9-ddd0-44af-a497-d541a016ed34

📥 Commits

Reviewing files that changed from the base of the PR and between b3ef55f and 19f3bff.

⛔ Files ignored due to path filters (1)
  • pnpm-lock.yaml is excluded by !**/pnpm-lock.yaml
📒 Files selected for processing (25)
  • apps/cli/package.json
  • apps/cli/src/command/diff.command.ts
  • apps/cli/src/command/lint.command.ts
  • apps/cli/src/command/sync.command.ts
  • apps/cli/src/linter/index.ts
  • apps/cli/src/linter/plugin-validator.ts
  • apps/cli/src/linter/specs/common.spec.ts
  • apps/cli/src/linter/specs/consumer.spec.ts
  • apps/cli/src/linter/specs/plugin.spec.ts
  • apps/cli/src/linter/specs/route.spec.ts
  • apps/cli/src/linter/specs/ssl.spec.ts
  • apps/cli/src/linter/specs/upstream.spec.ts
  • apps/cli/src/linter/types.ts
  • apps/cli/src/server/sync.ts
  • apps/cli/src/tasks/fetch_plugin_schemas.ts
  • apps/cli/src/tasks/index.ts
  • apps/cli/src/tasks/lint.ts
  • libs/backend-api7/package.json
  • libs/backend-api7/src/index.ts
  • libs/backend-apisix/package.json
  • libs/backend-apisix/src/index.ts
  • libs/sdk/package.json
  • libs/sdk/src/backend/index.ts
  • package.json
  • pnpm-workspace.yaml

These belong only in pnpm-workspace.yaml catalog and apps/cli/package.json,
not in the root workspace package.json.
When --no-lint is used, skip FetchPluginSchemasTask to avoid unnecessary
network requests and potential failures for a non-lint concern.
@nic-6443
Copy link
Copy Markdown

nic-6443 commented Apr 9, 2026

Addressed CodeRabbit review findings:

  1. FetchPluginSchemasTask gated by --no-lint (sync.command.ts, diff.command.ts): Fixed. Now uses opts.lint ? FetchPluginSchemasTask() : { task: () => undefined } pattern, consistent with how LintTask is already gated.

  2. diff.command.ts catch(err) swallowing: This is pre-existing code not introduced by this PR - will not change it here to keep the diff focused.

Nitpick comments noted but not applied to keep the PR scope focused on the core plugin validation feature.

@bzp2010
Copy link
Copy Markdown
Collaborator

bzp2010 commented Apr 9, 2026

This does not solve 90% of the problems, because configuration errors outside of any plugins cannot be checked. As a result, even if the advanced mode of the linter passes, the false negatives it reports are meaningless, since the server will reject other configuration errors.

Furthermore, even if we take a step back and assume that plugin-only validation is acceptable, the current approach cannot verify whether the plugin configuration is truly, completely correct. This is because many plugins include not only JSON Schema validation but also code-driven configuration validation, which the current method cannot check. A prime example is the CORS plugin.
Even if the lint passes, the server may still reject the configuration at any time, meaning that the lint cannot guarantee anything.

And without a once-and-for-all solution, I am unwilling to endorse the functionality claimed by the lint tool.


Even if this PR doesn’t exist, we can still prevent configuration errors through workflow design.
For example, we can synchronize changes to the test environment when a commit is pushed to a PR, meaning that modifications in the current sync are first applied to the test environment, thereby avoiding any impact on the production environment.

Once the PR is merged or a tag is pushed to the main branch, we can then initiate a sync to the production environment—all this can be achieved simply by loading different environment variables at runtime.

Any configuration errors are detected and fixed during the PR before the PR is merged. As long as all CI checks pass, the configuration can be deployed to the production environment without issues.


In fact, the lint subcommand has been intentionally designed to operate in pure local mode since its creation.
We have always reserved the validate subcommand, which is intended to perform server-side configuration validation. This is what I hope to add for the apisix-standalone backend, as it will facilitate pre-checks during AIC CRD submissions.
I believe this is the only way to effectively address all the configuration validation issues we aim to resolve. By simply running adc lint + adc validate + adc sync, users can ensure their configuration is always can be accepted.

@bzp2010
Copy link
Copy Markdown
Collaborator

bzp2010 commented Apr 9, 2026

Overall, I’m not optimistic about the content added in this PR.

Two bugs fixed:

1. FetchPluginSchemasTask destructured fetchPluginSchemas from ctx.backend,
   which detaches the method from its class instance and loses `this` binding.
   Changed to call ctx.backend.fetchPluginSchemas() directly.

2. Lint command options used .env('ADC_BACKEND') etc, which caused env vars
   (e.g. ADC_BACKEND=api7ee) to implicitly activate backend plugin validation
   even when running plain `adc lint -f file.yaml`. Removed .env() from lint
   options so backend validation is only activated via explicit CLI flags.
   This keeps lint as a local-first command that doesn't require network access
   by default.
@bzp2010 bzp2010 added test/api7 Trigger the API7 test on the PR test/apisix-standalone Trigger the APISIX standalone test on the PR labels Apr 9, 2026
@nic-6443

This comment was marked as outdated.

@nic-6443
Copy link
Copy Markdown

nic-6443 commented Apr 9, 2026

This does not solve 90% of the problems, because configuration errors outside of any plugins cannot be checked.

@bzp2010 Why is that? Even though some plugins need to be validated through Lua code, most plugin configurations are validated via json schema. What exactly do you mean by the 90% of the problems you mentioned?

If you consider those plugins that need to be verified through Lua code, we can provide an API on the backend for the ADC to perform remote verification. Would this solve the problem 100%?

@bzp2010
Copy link
Copy Markdown
Collaborator

bzp2010 commented Apr 9, 2026

@nic-6443

  • Resource configuration checks [the current lint tool does its best to do this, but no guarantee]
  • Plugin json schema checks [maybe]
  • Plugin code checks
  • Resource dependency checks, Etc.

Any issue will cause the synchronization process to be interrupted.

Local lint is useless against the latter two issues. Consequently, "false negatives" in the lint report will lead to "partial synchronization," which is effectively the same as missing synchronization at all. This results in unexpected behavior on the gateway (which I do not want to happen). No matter how many engineering resources we invest in the current approach, it is neither sustainable nor does it address the root cause of the problem.

This introduces unnecessary complexity.

And without a once-and-for-all solution, I am unwilling to endorse the functionality claimed by the lint tool.

If the solution doesn’t fully resolve (validation passed = server unconditionally accepts the configuration during synchronization) the issue, I’d rather not do it. Engineering solutions can resolve the issue hacky.

@nic-6443
Copy link
Copy Markdown

nic-6443 commented Apr 9, 2026

@bzp2010 Is there any linter tool that claims to solve all production problems? All the user needs is a configuration checking capability, so why include all so-called synchronization failure issues in the current requirement?

@bzp2010
Copy link
Copy Markdown
Collaborator

bzp2010 commented Apr 9, 2026

If we don’t do it, the sync process might be interrupted by an error during synchronization.
If we do it, the sync process continues to be interrupted by errors.
So what’s the point of adding it?

@nic-6443
Copy link
Copy Markdown

nic-6443 commented Apr 9, 2026

Why is there a lint function now? Following this reasoning, wouldn't the lint function be completely worthless? But in fact, users are using it and hoping it will be more powerful.

@bzp2010
Copy link
Copy Markdown
Collaborator

bzp2010 commented Apr 9, 2026

You are right.

The fundamental issue isn’t whether an ADC can detect and flag errors locally, but rather that we can never fully align client-side and server-side checks.
To put it bluntly, this makes any attempt at local checking laughable and completely pointless. Lint merely performs the most basic checks using a set of predefined rules. Its implementation is very limited. It’s primarily used to catch basic issues like spelling mistakes.

UPDATE: The Zod schema does not allow any undefined fields to pass validation, so the spell check is entirely valid.

@bzp2010
Copy link
Copy Markdown
Collaborator

bzp2010 commented Apr 9, 2026

export const LintCommand = new BaseCommand('lint')
.description(
'Lint the local configuration file(s) to ensure it meets ADC requirements.',
)
.summary('lint the local configuration')

Furthermore, if you read the code carefully, you’ll discover the original design intent behind the lint subcommand.

THAT IS meets ADC requirements.

It was never intended to ensure that the configuration passes server-side validation, but rather to verify that the input in the local configuration file meets ADC’s own formatting requirements—such as mandatory fields, correct data types, valid data ranges and spelling errors.

To elaborate, this is primarily to ensure that the subsequent ADC-to-Backend types conversion does not fail due to dirty data.

A predefined Zod schema handles this process quite well; while I cannot guarantee its absolute effectiveness, it is sufficient for most scenarios. Users can choose to opt out of this (by --no-lint), but I am not responsible for any program crashes that may result.

Yes, it's mainly to prevent program crashes.

@nic-6443 nic-6443 closed this Apr 9, 2026
@nic-6443 nic-6443 deleted the feat/lint-plugin-validation branch April 9, 2026 09:34
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

test/api7 Trigger the API7 test on the PR test/apisix-standalone Trigger the APISIX standalone test on the PR

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants