Skip to content

Commit 19f3bff

Browse files
committed
feat: add plugin configuration validation to lint
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.
1 parent b3ef55f commit 19f3bff

26 files changed

Lines changed: 1027 additions & 44 deletions

apps/cli/package.json

Lines changed: 19 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -6,39 +6,41 @@
66
"@types/express": "^5.0.3",
77
"@types/js-yaml": "^4.0.9",
88
"@types/lodash-es": "catalog:",
9-
"@types/supertest": "^7.2.0",
109
"@types/pluralize": "^0.0.33",
1110
"@types/qs": "^6.9.15",
12-
"@types/signale": "^1.4.7",
1311
"@types/semver": "catalog:",
12+
"@types/signale": "^1.4.7",
13+
"@types/supertest": "^7.2.0",
14+
"semver": "catalog:",
1415
"supertest": "^7.1.4",
15-
"vitest": "catalog:",
16-
"semver": "catalog:"
16+
"vitest": "catalog:"
1717
},
1818
"dependencies": {
19-
"@api7/adc-sdk": "workspace:*",
2019
"@api7/adc-backend-api7": "workspace:*",
2120
"@api7/adc-backend-apisix": "workspace:*",
2221
"@api7/adc-backend-apisix-standalone": "workspace:*",
2322
"@api7/adc-converter-openapi": "workspace:*",
2423
"@api7/adc-differ": "workspace:*",
25-
"express": "^5.1.0",
26-
"winston": "^3.17.0",
24+
"@api7/adc-sdk": "workspace:*",
25+
"agentkeepalive": "^4.6.0",
26+
"ajv": "catalog:",
27+
"ajv-draft-04": "catalog:",
2728
"axios": "catalog:",
28-
"rxjs": "catalog:",
29-
"lodash-es": "catalog:",
30-
"zod": "catalog:",
31-
"pluralize": "^8.0.0",
32-
"listr2": "catalog:",
33-
"commander": "^14.0.3",
3429
"chalk": "^5.6.2",
30+
"commander": "^14.0.3",
31+
"dotenv": "^17.3.1",
32+
"express": "^5.1.0",
33+
"glob": "^13.0.0",
34+
"js-yaml": "catalog:",
35+
"listr2": "catalog:",
36+
"lodash-es": "catalog:",
3537
"parse-duration": "^2.1.5",
38+
"pluralize": "^8.0.0",
3639
"qs": "^6.14.1",
37-
"dotenv": "^17.3.1",
38-
"agentkeepalive": "^4.6.0",
40+
"rxjs": "catalog:",
3941
"signale": "^1.4.0",
40-
"glob": "^13.0.0",
41-
"js-yaml": "catalog:"
42+
"winston": "^3.17.0",
43+
"zod": "catalog:"
4244
},
4345
"nx": {
4446
"name": "cli",

apps/cli/src/command/diff.command.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ import { writeFile } from 'node:fs/promises';
66
import {
77
DiffResourceTask,
88
ExperimentalRemoteStateFileTask,
9+
FetchPluginSchemasTask,
910
LintTask,
1011
LoadLocalConfigurationTask,
1112
} from '../tasks';
@@ -64,6 +65,7 @@ export const DiffCommand = new BackendCommand<DiffOptions>(
6465
const tasks = new Listr<TaskContext, typeof SignaleRenderer>(
6566
[
6667
InitializeBackendTask(opts.backend, opts),
68+
FetchPluginSchemasTask(),
6769
LoadLocalConfigurationTask(
6870
opts.file,
6971
opts.labelSelector,
Lines changed: 55 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,19 +1,49 @@
1+
import * as ADCSDK from '@api7/adc-sdk';
2+
import { Option } from 'commander';
13
import { Listr } from 'listr2';
24

3-
import { LintTask, LoadLocalConfigurationTask } from '../tasks';
5+
import {
6+
FetchPluginSchemasTask,
7+
LintTask,
8+
LoadLocalConfigurationTask,
9+
} from '../tasks';
10+
import { InitializeBackendTask } from '../tasks/init_backend';
411
import { SignaleRenderer } from '../utils/listr';
512
import { BaseCommand } from './helper';
613

714
export const LintCommand = new BaseCommand('lint')
815
.description(
9-
'Lint the local configuration file(s) to ensure it meets ADC requirements.',
16+
'Lint the local configuration file(s) to ensure it meets ADC requirements.\n\nOptionally, provide backend connection parameters to also validate plugin configurations against the backend\'s plugin schemas.',
1017
)
1118
.summary('lint the local configuration')
1219
.option(
1320
'-f, --file <file-path>',
1421
'file to lint',
1522
(filePath, files: Array<string> = []) => files.concat(filePath),
1623
)
24+
.addOption(
25+
new Option('--backend <backend>', 'type of backend to validate plugins against')
26+
.env('ADC_BACKEND')
27+
.choices(['apisix', 'api7ee']),
28+
)
29+
.addOption(
30+
new Option('--server <string>', 'HTTP address of the backend')
31+
.env('ADC_SERVER'),
32+
)
33+
.addOption(
34+
new Option(
35+
'--token <string>',
36+
'token for ADC to connect to the backend',
37+
).env('ADC_TOKEN'),
38+
)
39+
.addOption(
40+
new Option(
41+
'--gateway-group <string>',
42+
'gateway group to operate on (only for "api7ee" backend)',
43+
)
44+
.env('ADC_GATEWAY_GROUP')
45+
.default('default'),
46+
)
1747
.addExamples([
1848
{
1949
title: 'Lint the specified configuration file',
@@ -23,12 +53,33 @@ export const LintCommand = new BaseCommand('lint')
2353
title: 'Lint multiple configuration files',
2454
command: 'adc lint -f service-a.yaml -f service-b.yaml',
2555
},
56+
{
57+
title: 'Lint with plugin validation against an APISIX backend',
58+
command: 'adc lint -f adc.yaml --backend apisix --server http://localhost:9180 --token edd1c9f034335f136f87ad84b625c8f1',
59+
},
60+
{
61+
title: 'Lint with plugin validation against an API7 EE backend',
62+
command: 'adc lint -f adc.yaml --backend api7ee --server https://dashboard.example.com --token <token>',
63+
},
2664
])
2765
.action(async () => {
2866
const opts = LintCommand.optsWithGlobals();
67+
const useBackend = !!opts.backend;
2968

3069
const tasks = new Listr(
31-
[LoadLocalConfigurationTask(opts.file, {}), LintTask()],
70+
[
71+
...(useBackend
72+
? [
73+
InitializeBackendTask(opts.backend, {
74+
...opts,
75+
cacheKey: 'lint',
76+
} as ADCSDK.BackendOptions),
77+
FetchPluginSchemasTask(),
78+
]
79+
: []),
80+
LoadLocalConfigurationTask(opts.file, {}),
81+
LintTask(),
82+
],
3283
{
3384
renderer: SignaleRenderer,
3485
rendererOptions: { verbose: opts.verbose },
@@ -37,7 +88,7 @@ export const LintCommand = new BaseCommand('lint')
3788

3889
try {
3990
await tasks.run();
40-
} catch (err) {
91+
} catch {
4192
process.exit(1);
4293
}
4394
});

apps/cli/src/command/sync.command.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ import { lastValueFrom, toArray } from 'rxjs';
44
import {
55
DiffResourceTask,
66
ExperimentalRemoteStateFileTask,
7+
FetchPluginSchemasTask,
78
LintTask,
89
LoadLocalConfigurationTask,
910
LoadRemoteConfigurationTask,
@@ -80,6 +81,7 @@ export const SyncCommand = new BackendCommand<SyncOption>(
8081
const tasks = new Listr<TaskContext, typeof SignaleRenderer>(
8182
[
8283
InitializeBackendTask(opts.backend, opts),
84+
FetchPluginSchemasTask(),
8385
LoadLocalConfigurationTask(
8486
opts.file,
8587
opts.labelSelector,

apps/cli/src/linter/index.ts

Lines changed: 47 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,51 @@
11
import { Configuration } from '@api7/adc-sdk';
22
import { ConfigurationSchema } from '@api7/adc-sdk/schema';
3+
import { z } from 'zod';
34

4-
export const check = (config: Configuration) => {
5-
return ConfigurationSchema.safeParse(config);
5+
import { validatePlugins } from './plugin-validator';
6+
import type { LintError, LintOptions, LintResult } from './types';
7+
8+
export type { LintError, LintOptions, LintResult } from './types';
9+
10+
const zodIssuesToLintErrors = (issues: z.ZodIssue[]): LintError[] =>
11+
issues.map(({ path, message, code, ...rest }) => ({
12+
path,
13+
message,
14+
code,
15+
...rest,
16+
}));
17+
18+
export const check = (
19+
config: Configuration,
20+
opts?: LintOptions,
21+
): LintResult => {
22+
// Phase 1: core resource structure validation (local, no network)
23+
const result = ConfigurationSchema.safeParse(config);
24+
if (!result.success) {
25+
return {
26+
success: false,
27+
errors: zodIssuesToLintErrors(result.error.issues),
28+
};
29+
}
30+
31+
// Phase 2: plugin config validation (requires plugin schemas from backend)
32+
if (opts?.pluginSchemas) {
33+
const pluginErrors = validatePlugins(
34+
result.data as Configuration,
35+
opts.pluginSchemas,
36+
);
37+
if (pluginErrors.length > 0) {
38+
return {
39+
success: false,
40+
errors: pluginErrors,
41+
data: result.data,
42+
};
43+
}
44+
}
45+
46+
return {
47+
success: true,
48+
errors: [],
49+
data: result.data,
50+
};
651
};

0 commit comments

Comments
 (0)