Skip to content

Commit 0af7c34

Browse files
authored
fix(core): stack-related validation has an empty construct path (#38350)
CloudFormation Validate violations that trace to the stack, or to a non-resource construct, have empty construct information. Instead, trace them to the containing stack. ---- *By submitting this pull request, I confirm that my contribution is made under the terms of the Apache-2.0 license*
1 parent 01f2e16 commit 0af7c34

6 files changed

Lines changed: 109 additions & 9 deletions

File tree

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

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -329,6 +329,7 @@ function doInvokeValidationPlugins(
329329
try {
330330
const report = makeTemplatePathsRelative(plugin.validate({
331331
templatePaths: stacks.map(s => s.templateFullPath),
332+
stackTemplates: stacks.map(s => ({ stackConstructPath: s.hierarchicalId, templatePath: s.templateFullPath })),
332333
appConstruct: root,
333334
accountId: accountId !== cxapi.UNKNOWN_ACCOUNT ? accountId : undefined,
334335
region: region !== cxapi.UNKNOWN_REGION ? region : undefined,

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

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -183,6 +183,7 @@ export class Stage extends Construct {
183183
* synthesis will be interrupted and the report displayed to the user.
184184
*
185185
* @default - no validation plugins are used
186+
* @deprecated Do not use this function to look up validation plugins. Use `Validations.of(stage).plugins` instead.
186187
*/
187188
public get policyValidationBeta1(): IPolicyValidationPluginBeta1[] {
188189
return this._policyValidation.map(_toBeta1Plugin);

packages/aws-cdk-lib/core/lib/validation/cloudformation-validate-plugin.ts

Lines changed: 4 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -102,12 +102,9 @@ export class CloudFormationValidatePlugin implements IPolicyValidationPlugin {
102102
public validate(context: IPolicyValidationContext): PolicyValidationPluginReport {
103103
const violations: MutableViolation[] = [];
104104

105-
for (const templatePath of context.templatePaths) {
105+
for (const { stackConstructPath, templatePath } of context.stackTemplates) {
106106
const templateFile = new TemplateFile(templatePath);
107-
const report = this.engine.validateStandard(templateFile, {
108-
// Environment-agnostic stacks use these cx-api sentinels in the cloud assembly. Omitting
109-
// them lets the engine model the pseudo-parameters symbolically instead of validating the
110-
// sentinel text as if it were a real account or region.
107+
const report = this.engine.validateDetailed(templateFile, {
111108
pseudoParameterOverrides: {
112109
accountId: context.accountId,
113110
region: context.region,
@@ -132,6 +129,8 @@ export class CloudFormationValidatePlugin implements IPolicyValidationPlugin {
132129

133130
const violatingResource: PolicyViolatingResource = {
134131
resourceLogicalId: diagnostic.resourceId,
132+
// If this is not about any resources, best we can do is point it to the stack
133+
constructPath: !diagnostic.resourceId ? stackConstructPath : undefined,
135134
templatePath,
136135
locations: diagnostic.propertyPath ? [diagnostic.propertyPath] : [],
137136
};

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

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -72,6 +72,11 @@ export interface IPolicyValidationContext {
7272
*/
7373
readonly templatePaths: string[];
7474

75+
/**
76+
* The absolute path of all templates to be processed, along with the stack construct path for each template.
77+
*/
78+
readonly stackTemplates: PolicyValidationStack[];
79+
7580
/**
7681
* The account ID for these templates, if known
7782
*/
@@ -93,6 +98,21 @@ export interface IPolicyValidationContext {
9398
readonly appConstruct: IConstruct;
9499
}
95100

101+
/**
102+
* Information about a single stack that is being validated.
103+
*/
104+
export interface PolicyValidationStack {
105+
/**
106+
* The Stack's construct path
107+
*/
108+
readonly stackConstructPath: string;
109+
110+
/**
111+
* The path to the template file on disk
112+
*/
113+
readonly templatePath: string;
114+
}
115+
96116
/**
97117
* Represents a validation plugin that will be executed during synthesis
98118
*
@@ -171,6 +191,8 @@ export function _toBeta1Plugin(plugin: IPolicyValidationPlugin): IPolicyValidati
171191
validate(context: IPolicyValidationContextBeta1): PolicyValidationPluginReportBeta1 {
172192
const report = plugin.validate({
173193
...context,
194+
// This is incorrect information -- it doesn't matter, this function shouldn't be used regardless.
195+
stackTemplates: [],
174196
accountId: undefined,
175197
region: undefined,
176198
});

packages/aws-cdk-lib/core/test/validation/cloudformation-validate-plugin.test.ts

Lines changed: 79 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import * as fs from 'fs';
22
import * as os from 'os';
33
import * as path from 'path';
4+
import type { PolicyValidationReportJson } from '@aws-cdk/cloud-assembly-schema';
45
import { Construct } from 'constructs';
56
import * as cxapi from '../../../cx-api';
67
import * as core from '../../lib';
@@ -94,9 +95,9 @@ describe('CloudFormationValidatePlugin', () => {
9495
const asm = app.synth();
9596

9697
// THEN
97-
const validationReport = loadJson(path.join(asm.directory, 'validation-report.json'));
98+
const validationReport = loadValidationReport(asm);
9899
const report = validationReport.pluginReports.find((r: any) => r.pluginName === 'CloudFormation Validate');
99-
expect(report.violations).toEqual([
100+
expect(report?.violations).toEqual([
100101
expect.objectContaining({
101102
ruleName: 'F3002',
102103
violatingConstructs: [
@@ -134,6 +135,78 @@ describe('CloudFormationValidatePlugin', () => {
134135
expect(process.exitCode).toBeUndefined();
135136
});
136137

138+
test('correctly reports errors at stack level instead of resource level', () => {
139+
const app = new core.App({
140+
context: {
141+
[cxapi.VALIDATE_AGAINST_DEFAULT_RULES]: true,
142+
[cxapi.FAIL_SYNTH_ON_VALIDATION_ERRORS_CONTEXT]: false,
143+
},
144+
});
145+
// REmove any acknowledgements for this test, since we want to see the errors
146+
(app.node as any)._metadata = [];
147+
148+
// F0001 missing resources
149+
new core.Stack(app, 'TestStack');
150+
151+
const report = loadValidationReport(app.synth());
152+
expect(report).toEqual(expect.objectContaining({
153+
pluginReports: expect.arrayContaining([
154+
expect.objectContaining({
155+
pluginName: 'CloudFormation Validate',
156+
violations: expect.arrayContaining([
157+
expect.objectContaining({
158+
ruleName: 'F0001',
159+
violatingConstructs: expect.arrayContaining([
160+
expect.objectContaining({
161+
constructPath: 'TestStack',
162+
}),
163+
]),
164+
}),
165+
]),
166+
}),
167+
]),
168+
169+
}));
170+
});
171+
172+
test('correctly reports errors for non-resources (e.g. Parameters) instead of resource level', () => {
173+
const app = new core.App({
174+
context: {
175+
[cxapi.VALIDATE_AGAINST_DEFAULT_RULES]: true,
176+
[cxapi.FAIL_SYNTH_ON_VALIDATION_ERRORS_CONTEXT]: false,
177+
},
178+
});
179+
// REmove any acknowledgements for this test, since we want to see the errors
180+
(app.node as any)._metadata = [];
181+
182+
const stack = new core.Stack(app, 'TestStack');
183+
new core.CfnParameter(stack, 'MyParam', {
184+
type: 'Blimp',
185+
});
186+
187+
const report = loadValidationReport(app.synth());
188+
expect(report).toEqual(expect.objectContaining({
189+
pluginReports: expect.arrayContaining([
190+
expect.objectContaining({
191+
pluginName: 'CloudFormation Validate',
192+
violations: expect.arrayContaining([
193+
expect.objectContaining({
194+
ruleName: 'F0001',
195+
violatingConstructs: expect.arrayContaining([
196+
expect.objectContaining({
197+
// TODO: Currently this references the stack, in the future perhaps we have more information
198+
// to reference the actual Parameter construct: <https://github.com/aws-cloudformation/cloudformation-validate/issues/201>
199+
constructPath: 'TestStack',
200+
}),
201+
]),
202+
}),
203+
]),
204+
}),
205+
]),
206+
207+
}));
208+
});
209+
137210
test('plugin can be instantiated directly with custom rules', () => {
138211
const plugin = new core.CloudFormationValidatePlugin({
139212
regoRules: [{ name: 'my-rule', content: 'package main' }],
@@ -214,6 +287,7 @@ describe('CloudFormationValidatePlugin', () => {
214287
const plugin = new core.CloudFormationValidatePlugin();
215288
const report = plugin.validate({
216289
templatePaths: [templatePath],
290+
stackTemplates: [{ stackConstructPath: 'TestStack', templatePath }],
217291
appConstruct: new Construct(undefined as any, ''),
218292
accountId: undefined,
219293
region: undefined,
@@ -228,6 +302,7 @@ describe('CloudFormationValidatePlugin', () => {
228302
});
229303
});
230304

231-
function loadJson(filePath: string): any {
232-
return JSON.parse(fs.readFileSync(filePath, 'utf-8'));
305+
function loadValidationReport(asm: cxapi.CloudAssembly) {
306+
const p = path.join(asm.directory, 'validation-report.json');
307+
return JSON.parse(fs.readFileSync(p, { encoding: 'utf-8' })) as PolicyValidationReportJson;
233308
}

packages/awslint/bin/awslint.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -163,6 +163,8 @@ async function main() {
163163

164164
if (color) {
165165
console.error(color(`${DiagnosticLevel[diag.level].toLowerCase()}: [${chalk.bold(`awslint:${diag.rule.code}`)}:${chalk.bold(diag.scope)}] ${diag.message}`));
166+
} else {
167+
console.error(`${DiagnosticLevel[diag.level].toLowerCase()}: [${`awslint:${diag.rule.code}`}:${diag.scope}] ${diag.message}`);
166168
}
167169
}
168170

0 commit comments

Comments
 (0)