Skip to content

Commit 95696b4

Browse files
authored
feat(core): Validations class is the new way to add validation plugins to CDK Apps (#37611)
### Reason for this change Part of aws/aws-cdk-rfcs#899. Depends on #37613. `Validations` is the new one-stop shop for post-synthesis validation. This PR starts with the `addPlugins()` API that mirrors the functionality of supplying `policyValidationBeta1` to your CDK App today. ### Description of how you validated changes Existing tests succeed and new tests verify that `Validations.addPlugin()` does the same thing. ### Checklist - [x] My code adheres to the [CONTRIBUTING GUIDE](https://github.com/aws/aws-cdk/blob/main/CONTRIBUTING.md) and [DESIGN GUIDELINES](https://github.com/aws/aws-cdk/blob/main/docs/DESIGN_GUIDELINES.md) ---- *By submitting this pull request, I confirm that my contribution is made under the terms of the Apache-2.0 license*
1 parent 5635c20 commit 95696b4

9 files changed

Lines changed: 152 additions & 41 deletions

File tree

packages/@aws-cdk-testing/framework-integ/test/core/test/metadata-resource.test.ts

Lines changed: 7 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
import { constructAnalyticsFromScope } from 'aws-cdk-lib/core/lib/helpers-internal';
22
import { localCdkVersion } from './util';
3-
import type { IPolicyValidationPluginBeta1, PolicyViolationBeta1, PolicyValidationPluginReportBeta1, IPolicyValidationContextBeta1 } from 'aws-cdk-lib/core';
4-
import { App, NestedStack, Stack, Stage } from 'aws-cdk-lib/core';
3+
import type { IPolicyValidationPlugin, PolicyViolation, PolicyValidationPluginReport, IPolicyValidationContext } from 'aws-cdk-lib/core';
4+
import { App, NestedStack, Stack, Stage, Validations } from 'aws-cdk-lib/core';
55
import { Construct } from 'constructs';
66

77
const JSII_RUNTIME_SYMBOL = Symbol.for('jsii.rtti');
@@ -105,9 +105,8 @@ describe('constructAnalyticsFromScope', () => {
105105
});
106106

107107
test('return info from validator plugins', () => {
108-
const validatedApp = new App({
109-
policyValidationBeta1: [new FakePlugin('fake', [], '1.0.0', ['RULE_1', 'RULE_2'])],
110-
});
108+
const validatedApp = new App();
109+
Validations.of(validatedApp).addPlugins(new FakePlugin('fake', [], '1.0.0', ['RULE_1', 'RULE_2']));
111110
const validatedStack = new Stack(validatedApp, 'ValidatedStack');
112111
const constructInfos = constructAnalyticsFromScope(validatedStack);
113112

@@ -128,15 +127,15 @@ class TestConstruct extends Construct {
128127
private static readonly [JSII_RUNTIME_SYMBOL] = { fqn: '@aws-cdk/test.TestConstruct', version: localCdkVersion() };
129128
}
130129

131-
class FakePlugin implements IPolicyValidationPluginBeta1 {
130+
class FakePlugin implements IPolicyValidationPlugin {
132131
constructor(
133132
public readonly name: string,
134-
private readonly violations: PolicyViolationBeta1[],
133+
private readonly violations: PolicyViolation[],
135134
public readonly version?: string,
136135
public readonly ruleIds?: string []) {
137136
}
138137

139-
validate(_context: IPolicyValidationContextBeta1): PolicyValidationPluginReportBeta1 {
138+
validate(_context: IPolicyValidationContext): PolicyValidationPluginReport {
140139
return {
141140
success: this.violations.length === 0,
142141
violations: this.violations,

packages/aws-cdk-lib/README.md

Lines changed: 15 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -1614,31 +1614,20 @@ generated CloudFormation templates against your policies immediately after
16141614
synthesis. If there are any violations, the synthesis will fail and a report
16151615
will be printed to the console or to a file (see below).
16161616

1617-
> [!NOTE]
1618-
> This feature is considered experimental, and both the plugin API and the
1619-
> format of the validation report are subject to change in the future.
1620-
16211617
### For application developers
16221618

16231619
To use one or more validation plugins in your application, use the
1624-
`policyValidationBeta1` property of `Stage`:
1620+
`Validations.of()` API:
16251621

16261622
```ts fixture=validation-plugin
16271623
// globally for the entire app (an app is a stage)
1628-
const app = new App({
1629-
policyValidationBeta1: [
1630-
// These hypothetical classes implement IPolicyValidationPluginBeta1:
1631-
new ThirdPartyPluginX(),
1632-
new ThirdPartyPluginY(),
1633-
],
1634-
});
1624+
const app = new App();
1625+
Validations.of(app).addPlugins(new ThirdPartyPluginX());
1626+
Validations.of(app).addPlugins(new ThirdPartyPluginY());
16351627
16361628
// only apply to a particular stage
1637-
const prodStage = new Stage(app, 'ProdStage', {
1638-
policyValidationBeta1: [
1639-
new ThirdPartyPluginX(),
1640-
],
1641-
});
1629+
const prodStage = new Stage(app, 'ProdStage');
1630+
Validations.of(prodStage).addPlugins(new ThirdPartyPluginX());
16421631
```
16431632

16441633
Immediately after synthesis, all plugins registered this way will be invoked to
@@ -1656,7 +1645,7 @@ By default, the report will be printed in a human-readable format. If you want a
16561645
report in JSON format, enable it using the `@aws-cdk/core:validationReportJson`
16571646
context passing it directly to the application:
16581647

1659-
```ts
1648+
```ts fixture=validation-plugin
16601649
const app = new App({
16611650
context: { '@aws-cdk/core:validationReportJson': true },
16621651
});
@@ -1669,7 +1658,7 @@ Alternatively, you can set this context key-value pair using the `cdk.json` or
16691658
It is also possible to enable both JSON and human-readable formats by setting
16701659
`@aws-cdk/core:validationReportPrettyPrint` context key explicitly:
16711660

1672-
```ts
1661+
```ts fixture=validation-plugin
16731662
const app = new App({
16741663
context: {
16751664
'@aws-cdk/core:validationReportJson': true,
@@ -1686,21 +1675,21 @@ the standard output.
16861675
### For plugin authors
16871676

16881677
The communication protocol between the CDK core module and your policy tool is
1689-
defined by the `IPolicyValidationPluginBeta1` interface. To create a new plugin you must
1678+
defined by the `IPolicyValidationPlugin` interface. To create a new plugin you must
16901679
write a class that implements this interface. There are two things you need to
16911680
implement: the plugin name (by overriding the `name` property), and the
16921681
`validate()` method.
16931682

1694-
The framework will call `validate()`, passing an `IPolicyValidationContextBeta1` object.
1683+
The framework will call `validate()`, passing an `IPolicyValidationContext` object.
16951684
The location of the templates to be validated is given by `templatePaths`. The
1696-
plugin should return an instance of `PolicyValidationPluginReportBeta1`. This object
1697-
represents the report that the user wil receive at the end of the synthesis.
1685+
plugin should return an instance of `PolicyValidationPluginReport`. This object
1686+
represents the report that the user will receive at the end of the synthesis.
16981687

16991688
```ts fixture=validation-plugin
1700-
class MyPlugin implements IPolicyValidationPluginBeta1 {
1689+
class MyPlugin implements IPolicyValidationPlugin {
17011690
public readonly name = 'MyPlugin';
17021691
1703-
public validate(context: IPolicyValidationContextBeta1): PolicyValidationPluginReportBeta1 {
1692+
public validate(context: IPolicyValidationContext): PolicyValidationPluginReport {
17041693
// First read the templates using context.templatePaths...
17051694
17061695
// ...then perform the validation, and then compose and return the report.
@@ -1723,7 +1712,7 @@ class MyPlugin implements IPolicyValidationPluginBeta1 {
17231712
```
17241713

17251714
In addition to the name, plugins may optionally report their version (`version`
1726-
property ) and a list of IDs of the rules they are going to evaluate (`ruleIds`
1715+
property) and a list of IDs of the rules they are going to evaluate (`ruleIds`
17271716
property).
17281717

17291718
Note that plugins are not allowed to modify anything in the cloud assembly. Any

packages/aws-cdk-lib/core/lib/app.ts

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -127,6 +127,7 @@ export interface AppProps {
127127
* Validation plugins to run after synthesis
128128
*
129129
* @default - no validation plugins
130+
* @deprecated Use `Validations.of(app).addPlugins()` instead.
130131
*/
131132
readonly policyValidationBeta1?: IPolicyValidationPluginBeta1[];
132133

@@ -185,14 +186,17 @@ export class App extends Stage {
185186
constructor(props: AppProps = {}) {
186187
super(undefined as any, '', {
187188
outdir: props.outdir ?? process.env[cxapi.OUTDIR_ENV],
188-
policyValidationBeta1: props.policyValidationBeta1,
189189
});
190190

191191
if (props.propertyInjectors) {
192192
const injectors = PropertyInjectors.of(this);
193193
injectors.add(...props.propertyInjectors);
194194
}
195195

196+
if (props.policyValidationBeta1) {
197+
this._addValidationPlugins(...props.policyValidationBeta1);
198+
}
199+
196200
Object.defineProperty(this, APP_SYMBOL, { value: true });
197201

198202
this.loadContext(props.context, props.postCliContext);

packages/aws-cdk-lib/core/lib/stage.ts

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -96,6 +96,7 @@ export interface StageProps {
9696
* synthesis will be interrupted and the report displayed to the user.
9797
*
9898
* @default - no validation plugins are used
99+
* @deprecated Use `Validations.of(stage).addPlugins()` instead.
99100
*/
100101
readonly policyValidationBeta1?: IPolicyValidationPluginBeta1[];
101102

@@ -219,6 +220,15 @@ export class Stage extends Construct {
219220
}
220221
}
221222

223+
/**
224+
* Register a validation plugin on this stage.
225+
*
226+
* @internal
227+
*/
228+
public _addValidationPlugins(...plugins: IPolicyValidationPluginBeta1[]): void {
229+
this._policyValidationBeta1.push(...plugins);
230+
}
231+
222232
/**
223233
* The cloud assembly output directory.
224234
*/
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,2 +1,3 @@
11
export * from './validation';
2+
export * from './validations';
23
export * from './report';

packages/aws-cdk-lib/core/lib/validation/validation.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -5,10 +5,10 @@ import type { PolicyValidationPluginReport, PolicyValidationPluginReportBeta1 }
55
*
66
* @example
77
* /// fixture=validation-plugin
8-
* class MyPlugin implements IPolicyValidationPluginBeta1 {
8+
* class MyPlugin implements IPolicyValidationPlugin {
99
* public readonly name = 'MyPlugin';
1010
*
11-
* public validate(context: IPolicyValidationContextBeta1): PolicyValidationPluginReportBeta1 {
11+
* public validate(context: IPolicyValidationContext): PolicyValidationPluginReport {
1212
* // First read the templates using context.templatePaths...
1313
*
1414
* // ...then perform the validation, and then compose and return the report.
Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,44 @@
1+
import type { IConstruct } from 'constructs';
2+
import type { IPolicyValidationPlugin } from './validation';
3+
import { UnscopedValidationError } from '../errors';
4+
import { lit } from '../private/literal-string';
5+
import { Stage } from '../stage';
6+
7+
/**
8+
* Manages validations for CDK constructs.
9+
*
10+
* @example
11+
* /// fixture=validation-plugin
12+
* declare const myApp: App;
13+
* declare const plugin: IPolicyValidationPlugin;
14+
* Validations.of(myApp).addPlugins(plugin);
15+
*/
16+
export class Validations {
17+
/**
18+
* Returns the Validations for the given construct scope.
19+
*
20+
* @param scope any construct
21+
*/
22+
public static of(scope: IConstruct): Validations {
23+
return new Validations(scope);
24+
}
25+
26+
private constructor(private readonly scope: IConstruct) {}
27+
28+
/**
29+
* Register one or more validation plugins that will be executed during synthesis.
30+
*
31+
* Plugins can only be registered within a Stage or App scope.
32+
* If any plugin reports a violation, synthesis will be interrupted and the
33+
* report displayed to the user.
34+
*
35+
* @param plugins the validation plugins to add
36+
*/
37+
public addPlugins(...plugins: IPolicyValidationPlugin[]): void {
38+
const stage = Stage.isStage(this.scope) ? this.scope : Stage.of(this.scope);
39+
if (!stage) {
40+
throw new UnscopedValidationError(lit`NoStageForValidationPlugins`, 'Cannot add validation plugins on a construct without an enclosing Stage');
41+
}
42+
stage._addValidationPlugins(...plugins);
43+
}
44+
}

packages/aws-cdk-lib/core/test/validation/validation.test.ts

Lines changed: 65 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -814,10 +814,75 @@ Policy Validation Report Summary
814814
});
815815

816816
test('a plugin implementing Beta1 is assignable to IPolicyValidationPlugin', () => {
817+
// GIVEN
817818
const beta1Plugin: core.IPolicyValidationPluginBeta1 = new FakePlugin('beta1-plugin', []);
819+
820+
// WHEN
818821
const plugin: core.IPolicyValidationPlugin = beta1Plugin;
822+
823+
// THEN
819824
expect(plugin.name).toEqual('beta1-plugin');
820825
});
826+
827+
describe('Validations.of()', () => {
828+
test('addPlugins adds plugin to enclosing stage', () => {
829+
// GIVEN
830+
const app = new core.App();
831+
const plugin = new FakePlugin('test-plugin', []);
832+
833+
// WHEN
834+
core.Validations.of(app).addPlugins(plugin);
835+
836+
// THEN
837+
expect(app.policyValidationBeta1).toContain(plugin);
838+
});
839+
840+
test('addPlugins from nested construct resolves to enclosing stage', () => {
841+
// GIVEN
842+
const app = new core.App();
843+
const stack = new core.Stack(app, 'MyStack');
844+
const plugin = new FakePlugin('test-plugin', []);
845+
846+
// WHEN
847+
core.Validations.of(stack).addPlugins(plugin);
848+
849+
// THEN - plugin is registered on the app (enclosing stage), not the stack
850+
expect(app.policyValidationBeta1).toContain(plugin);
851+
});
852+
853+
test('throws when addPlugins called without enclosing stage', () => {
854+
// GIVEN
855+
const construct = new Construct(undefined as any, '');
856+
857+
// THEN
858+
expect(() => core.Validations.of(construct).addPlugins(new FakePlugin('test', []))).toThrow(/without an enclosing Stage/);
859+
});
860+
861+
test('plugin added via addPlugins runs during synth', () => {
862+
// GIVEN
863+
const app = new core.App();
864+
const stack = new core.Stack(app);
865+
new core.CfnResource(stack, 'Fake', {
866+
type: 'Test::Resource::Fake',
867+
properties: { result: 'success' },
868+
});
869+
870+
// WHEN
871+
core.Validations.of(app).addPlugins(new FakePlugin('added-plugin', [{
872+
description: 'test recommendation',
873+
ruleName: 'test-rule',
874+
violatingResources: [{
875+
locations: ['test-location'],
876+
resourceLogicalId: 'Fake',
877+
templatePath: '/path/to/Default.template.json',
878+
}],
879+
}]));
880+
app.synth();
881+
882+
// THEN - exitCode 1 means the plugin ran and reported violations
883+
expect(process.exitCode).toEqual(1);
884+
});
885+
});
821886
});
822887

823888
class FakePlugin implements core.IPolicyValidationPluginBeta1 {
Lines changed: 3 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,12 @@
11
import { Construct } from 'constructs';
2-
import { App, IPolicyValidationPluginBeta1, Stage, IPolicyValidationContextBeta1, PolicyValidationPluginReportBeta1 } from 'aws-cdk-lib';
2+
import { App, IPolicyValidationPlugin, Stage, IPolicyValidationContext, PolicyValidationPluginReport, Validations } from 'aws-cdk-lib';
33

4-
declare class ThirdPartyPluginX implements IPolicyValidationPluginBeta1 {
4+
declare class ThirdPartyPluginX implements IPolicyValidationPlugin {
55
public readonly name: string;
66
public validate(x: any): any;
77
}
88

9-
declare class ThirdPartyPluginY implements IPolicyValidationPluginBeta1 {
9+
declare class ThirdPartyPluginY implements IPolicyValidationPlugin {
1010
public readonly name: string;
1111
public validate(x: any): any;
1212
}
@@ -18,4 +18,3 @@ class fixture$construct extends Construct {
1818
/// here
1919
}
2020
}
21-

0 commit comments

Comments
 (0)