Skip to content

Commit 633fadd

Browse files
authored
feat(api7): add server-side configuration validator (#432)
1 parent 7cf7dfa commit 633fadd

8 files changed

Lines changed: 667 additions & 2 deletions

File tree

apps/cli/src/command/index.ts

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ import { IngressSyncCommand } from './ingress-sync.command';
1111
import { LintCommand } from './lint.command';
1212
import { PingCommand } from './ping.command';
1313
import { SyncCommand } from './sync.command';
14+
import { ValidateCommand } from './validate.command';
1415
import { configurePluralize } from './utils';
1516

1617
const versionCode = '0.24.3';
@@ -51,8 +52,8 @@ export const setupCommands = (): Command => {
5152
.addCommand(DiffCommand)
5253
.addCommand(SyncCommand)
5354
.addCommand(ConvertCommand)
54-
.addCommand(LintCommand);
55-
//.addCommand(ValidateCommand)
55+
.addCommand(LintCommand)
56+
.addCommand(ValidateCommand);
5657

5758
if (process.env.NODE_ENV === 'development') program.addCommand(DevCommand);
5859

Lines changed: 77 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,77 @@
1+
import { Listr } from 'listr2';
2+
3+
import {
4+
DiffResourceTask,
5+
LintTask,
6+
LoadLocalConfigurationTask,
7+
ValidateTask,
8+
} from '../tasks';
9+
import { InitializeBackendTask } from '../tasks/init_backend';
10+
import { SignaleRenderer } from '../utils/listr';
11+
import { TaskContext } from './diff.command';
12+
import { BackendCommand, NoLintOption } from './helper';
13+
import { BackendOptions } from './typing';
14+
15+
export type ValidateOptions = BackendOptions & {
16+
file: Array<string>;
17+
lint: boolean;
18+
};
19+
20+
export const ValidateCommand = new BackendCommand<ValidateOptions>(
21+
'validate',
22+
'validate the local configuration against the backend',
23+
'Validate the configuration from the local file(s) against the backend without applying any changes.',
24+
)
25+
.option(
26+
'-f, --file <file-path>',
27+
'file to validate',
28+
(filePath, files: Array<string> = []) => files.concat(filePath),
29+
)
30+
.addOption(NoLintOption)
31+
.addExamples([
32+
{
33+
title: 'Validate configuration from a single file',
34+
command: 'adc validate -f adc.yaml',
35+
},
36+
{
37+
title: 'Validate configuration from multiple files',
38+
command: 'adc validate -f service-a.yaml -f service-b.yaml',
39+
},
40+
{
41+
title: 'Validate configuration against API7 EE backend',
42+
command:
43+
'adc validate -f adc.yaml --backend api7ee --gateway-group default',
44+
},
45+
{
46+
title: 'Validate configuration without lint check',
47+
command: 'adc validate -f adc.yaml --no-lint',
48+
},
49+
])
50+
.handle(async (opts) => {
51+
const tasks = new Listr<TaskContext, typeof SignaleRenderer>(
52+
[
53+
InitializeBackendTask(opts.backend, opts),
54+
LoadLocalConfigurationTask(
55+
opts.file,
56+
opts.labelSelector,
57+
opts.includeResourceType,
58+
opts.excludeResourceType,
59+
),
60+
opts.lint ? LintTask() : { task: () => undefined },
61+
DiffResourceTask(),
62+
ValidateTask(),
63+
],
64+
{
65+
renderer: SignaleRenderer,
66+
rendererOptions: { verbose: opts.verbose },
67+
ctx: { remote: {}, local: {}, diff: [] },
68+
},
69+
);
70+
71+
try {
72+
await tasks.run();
73+
} catch (err) {
74+
if (opts.verbose === 2) console.log(err);
75+
process.exit(1);
76+
}
77+
});

apps/cli/src/tasks/index.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,4 +2,5 @@ export * from './load_local';
22
export * from './load_remote';
33
export * from './diff';
44
export * from './lint';
5+
export * from './validate';
56
export * from './experimental';

apps/cli/src/tasks/validate.ts

Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,48 @@
1+
import * as ADCSDK from '@api7/adc-sdk';
2+
import { ListrTask } from 'listr2';
3+
import { lastValueFrom } from 'rxjs';
4+
5+
export const ValidateTask = (): ListrTask<{
6+
backend: ADCSDK.Backend;
7+
diff: ADCSDK.Event[];
8+
}> => ({
9+
title: 'Validate configuration against backend',
10+
task: async (ctx) => {
11+
if (!ctx.backend.supportValidate) {
12+
throw new Error(
13+
'Validate is not supported by the current backend',
14+
);
15+
}
16+
17+
const supported = await ctx.backend.supportValidate();
18+
if (!supported) {
19+
const version = await ctx.backend.version();
20+
throw new Error(
21+
`Validate is not supported by the current backend version (${version}). Please upgrade to a newer version.`,
22+
);
23+
}
24+
25+
const result = await lastValueFrom(ctx.backend.validate!(ctx.diff));
26+
if (!result.success) {
27+
const lines: string[] = [];
28+
if (result.errorMessage) {
29+
lines.push(result.errorMessage);
30+
}
31+
for (const e of result.errors) {
32+
const parts: string[] = [e.resource_type];
33+
if (e.resource_name) {
34+
parts.push(`name="${e.resource_name}"`);
35+
} else {
36+
if (e.resource_id) parts.push(`id="${e.resource_id}"`);
37+
if (e.index !== undefined) parts.push(`index=${e.index}`);
38+
}
39+
lines.push(` - [${parts.join(', ')}]: ${e.error}`);
40+
}
41+
const error = new Error(
42+
`Configuration validation failed:\n${lines.join('\n')}`,
43+
);
44+
error.stack = '';
45+
throw error;
46+
}
47+
},
48+
});

0 commit comments

Comments
 (0)