Skip to content

Commit 0c1e885

Browse files
authored
feat(core): add rule IDs to the analytics string (#25084)
Adding rule IDs to the analytics string so we can track how much each individual rule is being used. ---- *By submitting this pull request, I confirm that my contribution is made under the terms of the Apache-2.0 license*
1 parent ac40aed commit 0c1e885

File tree

5 files changed

+77
-11
lines changed

5 files changed

+77
-11
lines changed

packages/aws-cdk-lib/core/README.md

+4
Original file line numberDiff line numberDiff line change
@@ -1397,6 +1397,10 @@ validate(context: ValidationContextBeta1): ValidationReportBeta1 {
13971397
}
13981398
```
13991399

1400+
In addition to the name, plugins may optionally report their version (`version`
1401+
property ) and a list of IDs of the rules they are going to evaluate (`ruleIds`
1402+
property).
1403+
14001404
Note that plugins are not allowed to modify anything in the cloud assembly. Any
14011405
attempt to do so will result in synthesis failure.
14021406

packages/aws-cdk-lib/core/lib/private/runtime-info.ts

+21-3
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ import { IConstruct } from 'constructs';
22
import { App } from '../app';
33
import { Stack } from '../stack';
44
import { Stage } from '../stage';
5+
import { IPolicyValidationPluginBeta1 } from '../validation';
56

67
const ALLOWED_FQN_PREFIXES = [
78
// SCOPES
@@ -55,9 +56,7 @@ function addValidationPluginInfo(stack: Stack, allConstructInfos: ConstructInfo[
5556
allConstructInfos.push(...stage.policyValidationBeta1.map(
5657
plugin => {
5758
return {
58-
// the fqn can be in the format of `package.module.construct`
59-
// those get pulled out into separate fields
60-
fqn: `policyValidation.${plugin.name}`,
59+
fqn: pluginFqn(plugin),
6160
version: plugin.version ?? '0.0.0',
6261
};
6362
},
@@ -67,6 +66,25 @@ function addValidationPluginInfo(stack: Stack, allConstructInfos: ConstructInfo[
6766
} while (!done && stage);
6867
}
6968

69+
/**
70+
* Returns the fully-qualified name for a validation plugin, in the form:
71+
*
72+
* policyValidation.<plugin-name>[.<rule-ids>]
73+
*
74+
* where <rule-ids> is a pipe-separated list of rule IDs.
75+
*/
76+
function pluginFqn(plugin: IPolicyValidationPluginBeta1): string {
77+
let components = [
78+
'policyValidation',
79+
plugin.name,
80+
plugin.ruleIds?.join('|'),
81+
];
82+
83+
return components
84+
.filter(x => x != null)
85+
.join('.');
86+
}
87+
7088
/**
7189
* For a given stack, walks the tree and finds the runtime info for all constructs within the tree.
7290
* Returns the unique list of construct info present in the stack,

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

+8
Original file line numberDiff line numberDiff line change
@@ -46,6 +46,14 @@ export interface IPolicyValidationPluginBeta1 {
4646
*/
4747
readonly version?: string;
4848

49+
/**
50+
* The list of rule IDs that the plugin will evaluate. Used for analytics
51+
* purposes.
52+
*
53+
* @default - No rule is reported
54+
*/
55+
readonly ruleIds?: string[];
56+
4957
/**
5058
* The method that will be called by the CDK framework to perform
5159
* validations. This is where the plugin will evaluate the CloudFormation

packages/aws-cdk-lib/core/test/runtime-info.test.ts

+36-1
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
import * as fs from 'fs';
22
import * as path from 'path';
33
import { Construct } from 'constructs';
4-
import { App, NestedStack, Stack, Stage } from '../lib';
4+
import { App, NestedStack, Stack, Stage, IPolicyValidationPluginBeta1, PolicyViolationBeta1, PolicyValidationPluginReportBeta1, IPolicyValidationContextBeta1 } from '../lib';
55
import { constructInfoFromConstruct, constructInfoFromStack } from '../lib/private/runtime-info';
66

77
const JSII_RUNTIME_SYMBOL = Symbol.for('jsii.rtti');
@@ -157,6 +157,24 @@ describeTscSafe('constructInfoForStack', () => {
157157
expect(fqns).not.toContain('@aws-cdk/test.TestNestedStackInsideStack');
158158
expect(fqns).not.toContain('@aws-cdk/test.TestStageInsideStack');
159159
});
160+
161+
test('return info from validator plugins', () => {
162+
const validatedApp = new App({
163+
policyValidationBeta1: [new FakePlugin('fake', [], '1.0.0', ['RULE_1', 'RULE_2'])],
164+
});
165+
const validatedStack = new Stack(validatedApp, 'ValidatedStack');
166+
const constructInfos = constructInfoFromStack(validatedStack);
167+
168+
expect(constructInfos.map(info => info.fqn)).toContain('policyValidation.fake.RULE_1|RULE_2');
169+
});
170+
171+
test('does not return info from validator plugins when no plugin is registered', () => {
172+
const constructInfos = constructInfoFromStack(stack);
173+
174+
expect(constructInfos.map(info => info.fqn)).not.toEqual(expect.arrayContaining([
175+
expect.stringMatching(/^policyValidation\./),
176+
]));
177+
});
160178
});
161179

162180
class TestConstruct extends Construct {
@@ -191,3 +209,20 @@ function findParentPkgJson(dir: string, depth: number = 1, limit: number = 5): {
191209

192210
throw new Error(`No \`package.json\` file found within ${depth} parent directories`);
193211
}
212+
213+
class FakePlugin implements IPolicyValidationPluginBeta1 {
214+
constructor(
215+
public readonly name: string,
216+
private readonly violations: PolicyViolationBeta1[],
217+
public readonly version?: string,
218+
public readonly ruleIds?: string []) {
219+
}
220+
221+
validate(_context: IPolicyValidationContextBeta1): PolicyValidationPluginReportBeta1 {
222+
return {
223+
success: this.violations.length === 0,
224+
violations: this.violations,
225+
pluginVersion: this.version,
226+
};
227+
}
228+
}

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

+8-7
Original file line numberDiff line numberDiff line change
@@ -170,7 +170,7 @@ Policy Validation Report Summary
170170
resourceLogicalId: 'DefaultResource',
171171
templatePath: '/path/to/Stage1stack1DDED8B6C.template.json',
172172
}],
173-
}], '1.2.3'),
173+
}]),
174174
],
175175
});
176176
const stage1 = new core.Stage(app, 'Stage1', {
@@ -183,7 +183,7 @@ Policy Validation Report Summary
183183
resourceLogicalId: 'DefaultResource',
184184
templatePath: '/path/to/Stage1stack1DDED8B6C.template.json',
185185
}],
186-
}]),
186+
}], '1.2.3'),
187187
],
188188
});
189189
const stage2 = new core.Stage(app, 'Stage2', {
@@ -263,6 +263,7 @@ Policy Validation Report Summary
263263
],
264264
resourceLogicalId: 'DefaultResource',
265265
description: 'do something',
266+
version: '1.2.3',
266267
},
267268
{
268269
pluginName: 'test-plugin4',
@@ -695,20 +696,18 @@ Policy Validation Report Summary
695696
});
696697

697698
class FakePlugin implements core.IPolicyValidationPluginBeta1 {
698-
private _version?: string;
699-
700699
constructor(
701700
public readonly name: string,
702701
private readonly violations: PolicyViolationBeta1[],
703-
readonly version?: string) {
704-
this._version = version;
702+
public readonly version?: string,
703+
public readonly ruleIds?: string []) {
705704
}
706705

707706
validate(_context: core.IPolicyValidationContextBeta1): PolicyValidationPluginReportBeta1 {
708707
return {
709708
success: this.violations.length === 0,
710709
violations: this.violations,
711-
pluginVersion: this._version,
710+
pluginVersion: this.version,
712711
};
713712
}
714713
}
@@ -744,6 +743,7 @@ interface ValidationReportData {
744743
description?: string;
745744
resourceLogicalId: string;
746745
severity?: string;
746+
version?: string;
747747
ruleMetadata?: { [key: string]: string };
748748
}
749749

@@ -770,6 +770,7 @@ const validationReport = (data: ValidationReportData[]) => {
770770
expect.stringMatching(new RegExp('Validation Report')),
771771
expect.stringMatching(new RegExp('-----------------')),
772772
expect.stringMatching(new RegExp(`Plugin: ${d.pluginName}`)),
773+
expect.stringMatching(new RegExp(`Version: ${d.version ?? 'N/A'}`)),
773774
expect.stringMatching(new RegExp(`Status: ${d.status}`)),
774775
expect.stringMatching(new RegExp('\(Violations\)')),
775776
title,

0 commit comments

Comments
 (0)