Skip to content

Commit 9571599

Browse files
authored
feat(core): allow validation report multi-write based on context keys (#34927)
### Reason for this change It would be helpful to have more options around how the validation report is output. Currently the pretty print and JSON formatted file options are mutually exclusive. This pull request allows both to be used. The primary driver is to allow users to see their report results in a human-readable format while also having an output artifact that can be consumed by other CI/CD tools. ### Description of changes It is now possible to print multiple validation report formats. I added a new context key `@aws-cdk/core:validationReportPrettyPrint` boolean that prints the validation report to the console when `true`. I have made this value `true` by default so that users today maintain the same behavior they expect. Setting the existing `@aws-cdk/core:validationReportJson` key still results in only JSON formatted output. Users may now select both options by enabling both keys. Rather than a pretty print context key, I considered implementing a `@aws-cdk/core:validationReportBoth` key but this seemed to assume there would not be another format option added. It would allow the JSON format key to also keep its current behavior but the tradeoff of both report types appearing for those who only provide the JSON key seemed like a reasonable trade for keeping the report formats open for additional options in the future. ### Describe any new or updated permissions being added N/A ### Description of how you validated changes I have added unit tests that validate my changes. ### 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) ### EDIT Following review suggestions the logic has been updated such that: - Default: pretty print only - Enable JSON format context key: JSON only - Enable pretty print context key: pretty print only - Enable both keys: both formats ---- *By submitting this pull request, I confirm that my contribution is made under the terms of the Apache-2.0 license*
1 parent 3f695b3 commit 9571599

File tree

3 files changed

+151
-12
lines changed

3 files changed

+151
-12
lines changed

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

Lines changed: 13 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1545,7 +1545,7 @@ validation.
15451545
> etc. It's your responsibility as the consumer of a plugin to verify that it is
15461546
> secure to use.
15471547

1548-
By default, the report will be printed in a human readable format. If you want a
1548+
By default, the report will be printed in a human-readable format. If you want a
15491549
report in JSON format, enable it using the `@aws-cdk/core:validationReportJson`
15501550
context passing it directly to the application:
15511551

@@ -1559,6 +1559,18 @@ Alternatively, you can set this context key-value pair using the `cdk.json` or
15591559
`cdk.context.json` files in your project directory (see
15601560
[Runtime context](https://docs.aws.amazon.com/cdk/v2/guide/context.html)).
15611561

1562+
It is also possible to enable both JSON and human-readable formats by setting
1563+
`@aws-cdk/core:validationReportPrettyPrint` context key explicitly:
1564+
1565+
```ts
1566+
const app = new App({
1567+
context: {
1568+
'@aws-cdk/core:validationReportJson': true,
1569+
'@aws-cdk/core:validationReportPrettyPrint': true,
1570+
},
1571+
});
1572+
```
1573+
15621574
If you choose the JSON format, the CDK will print the policy validation report
15631575
to a file called `policy-validation-report.json` in the cloud assembly
15641576
directory. For the default, human-readable format, the report will be printed to

packages/aws-cdk-lib/core/lib/private/synthesis.ts

Lines changed: 13 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@ import { ConstructTree } from '../validation/private/construct-tree';
2020
import { PolicyValidationReportFormatter, NamedValidationPluginReport } from '../validation/private/report';
2121

2222
const POLICY_VALIDATION_FILE_PATH = 'policy-validation-report.json';
23+
const VALIDATION_REPORT_PRETTY_CONTEXT = '@aws-cdk/core:validationReportPrettyPrint';
2324
const VALIDATION_REPORT_JSON_CONTEXT = '@aws-cdk/core:validationReportJson';
2425

2526
/**
@@ -147,24 +148,27 @@ function invokeValidationPlugins(root: IConstruct, outdir: string, assembly: Clo
147148
if (reports.length > 0) {
148149
const tree = new ConstructTree(root);
149150
const formatter = new PolicyValidationReportFormatter(tree);
151+
let formatPretty = root.node.tryGetContext(VALIDATION_REPORT_PRETTY_CONTEXT) ?? false;
150152
const formatJson = root.node.tryGetContext(VALIDATION_REPORT_JSON_CONTEXT) ?? false;
151-
const output = formatJson
152-
? formatter.formatJson(reports)
153-
: formatter.formatPrettyPrinted(reports);
154-
153+
formatPretty = formatPretty || !(formatPretty || formatJson); // if neither is set, default to pretty print
155154
const reportFile = path.join(assembly.directory, POLICY_VALIDATION_FILE_PATH);
156-
if (formatJson) {
157-
fs.writeFileSync(reportFile, JSON.stringify(output, undefined, 2));
158-
} else {
155+
if (formatPretty) {
156+
const output = formatter.formatPrettyPrinted(reports);
159157
// eslint-disable-next-line no-console
160158
console.error(output);
161159
}
160+
if (formatJson) {
161+
const output = formatter.formatJson(reports);
162+
fs.writeFileSync(reportFile, JSON.stringify(output, undefined, 2));
163+
}
162164
const failed = reports.some(r => !r.success);
163165
if (failed) {
164-
const message = formatJson
166+
let message = formatJson
165167
? `Validation failed. See the validation report in '${reportFile}' for details`
166168
: 'Validation failed. See the validation report above for details';
167-
169+
if (formatPretty && formatJson) {
170+
message = `Validation failed. See the validation report in '${reportFile}' and above for details`;
171+
}
168172
// eslint-disable-next-line no-console
169173
console.log(message);
170174
process.exitCode = 1;

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

Lines changed: 125 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -631,7 +631,9 @@ Policy Validation Report Summary
631631
}],
632632
}]),
633633
],
634-
context: { '@aws-cdk/core:validationReportJson': true },
634+
context: {
635+
'@aws-cdk/core:validationReportJson': true,
636+
},
635637
});
636638
const stack = new core.Stack(app);
637639
new core.CfnResource(stack, 'Fake', {
@@ -642,8 +644,125 @@ Policy Validation Report Summary
642644
});
643645
app.synth();
644646
expect(process.exitCode).toEqual(1);
647+
const file = path.join(app.outdir, 'policy-validation-report.json');
648+
const report = fs.readFileSync(file).toString('utf-8');
649+
expect(JSON.parse(report)).toEqual(expect.objectContaining({
650+
title: 'Validation Report',
651+
pluginReports: [
652+
{
653+
summary: {
654+
pluginName: 'test-plugin',
655+
status: 'failure',
656+
},
657+
violations: [
658+
{
659+
ruleName: 'test-rule',
660+
description: 'test recommendation',
661+
ruleMetadata: { id: 'abcdefg' },
662+
violatingResources: [{
663+
'locations': [
664+
'test-location',
665+
],
666+
'resourceLogicalId': 'Fake',
667+
'templatePath': '/path/to/Default.template.json',
668+
}],
669+
violatingConstructs: [
670+
{
671+
constructStack: {
672+
'id': 'Default',
673+
'construct': expect.stringMatching(/(aws-cdk-lib.Stack|Construct)/),
674+
'libraryVersion': expect.any(String),
675+
'location': "Run with '--debug' to include location info",
676+
'path': 'Default',
677+
'child': {
678+
'id': 'Fake',
679+
'construct': expect.stringMatching(/(aws-cdk-lib.CfnResource|Construct)/),
680+
'libraryVersion': expect.any(String),
681+
'location': "Run with '--debug' to include location info",
682+
'path': 'Default/Fake',
683+
},
684+
},
685+
constructPath: 'Default/Fake',
686+
locations: ['test-location'],
687+
resourceLogicalId: 'Fake',
688+
templatePath: '/path/to/Default.template.json',
689+
},
690+
],
691+
},
692+
],
693+
},
694+
],
695+
}));
696+
const consoleOut = consoleLogMock.mock.calls[1][0];
697+
expect(consoleOut).toContain(`Validation failed. See the validation report in \'${file}\' for details`);
698+
});
645699

646-
const report = fs.readFileSync(path.join(app.outdir, 'policy-validation-report.json')).toString('utf-8');
700+
test('Pretty print as default', () => {
701+
const app = new core.App({
702+
policyValidationBeta1: [
703+
new FakePlugin('test-plugin', [{
704+
description: 'test recommendation',
705+
ruleName: 'test-rule',
706+
ruleMetadata: {
707+
id: 'abcdefg',
708+
},
709+
violatingResources: [{
710+
locations: ['test-location'],
711+
resourceLogicalId: 'Fake',
712+
templatePath: '/path/to/Default.template.json',
713+
}],
714+
}]),
715+
],
716+
context: {
717+
},
718+
});
719+
const stack = new core.Stack(app);
720+
new core.CfnResource(stack, 'Fake', {
721+
type: 'Test::Resource::Fake',
722+
properties: {
723+
result: 'failure',
724+
},
725+
});
726+
app.synth();
727+
expect(process.exitCode).toEqual(1);
728+
const consoleOut = consoleLogMock.mock.calls[1][0];
729+
expect(consoleOut).toContain('Validation failed. See the validation report above for details');
730+
const consoleReport = consoleErrorMock.mock.calls[0][0];
731+
expect(consoleReport).toContain('Validation Report');
732+
});
733+
734+
test('Multi format', () => {
735+
const app = new core.App({
736+
policyValidationBeta1: [
737+
new FakePlugin('test-plugin', [{
738+
description: 'test recommendation',
739+
ruleName: 'test-rule',
740+
ruleMetadata: {
741+
id: 'abcdefg',
742+
},
743+
violatingResources: [{
744+
locations: ['test-location'],
745+
resourceLogicalId: 'Fake',
746+
templatePath: '/path/to/Default.template.json',
747+
}],
748+
}]),
749+
],
750+
context: {
751+
'@aws-cdk/core:validationReportJson': true,
752+
'@aws-cdk/core:validationReportPrettyPrint': true,
753+
},
754+
});
755+
const stack = new core.Stack(app);
756+
new core.CfnResource(stack, 'Fake', {
757+
type: 'Test::Resource::Fake',
758+
properties: {
759+
result: 'failure',
760+
},
761+
});
762+
app.synth();
763+
expect(process.exitCode).toEqual(1);
764+
const file = path.join(app.outdir, 'policy-validation-report.json');
765+
const report = fs.readFileSync(file).toString('utf-8');
647766
expect(JSON.parse(report)).toEqual(expect.objectContaining({
648767
title: 'Validation Report',
649768
pluginReports: [
@@ -691,6 +810,10 @@ Policy Validation Report Summary
691810
},
692811
],
693812
}));
813+
const consoleOut = consoleLogMock.mock.calls[1][0];
814+
expect(consoleOut).toContain(`Validation failed. See the validation report in \'${file}\' and above for details`);
815+
const consoleReport = consoleErrorMock.mock.calls[0][0];
816+
expect(consoleReport).toContain('Validation Report');
694817
});
695818
});
696819

0 commit comments

Comments
 (0)