Skip to content

Commit 20edc7f

Browse files
authored
fix(cloudformation-include): drops unknown policy attributes (#32321)
Unknown fields in `CreationPolicy`/`UpdatePolicy` were dropped while loading resources using `CfnInclude`. In this PR, handle unknown fields in those places like unknown fields in other places: retain them during parsing and add them as overrides. ---- *By submitting this pull request, I confirm that my contribution is made under the terms of the Apache-2.0 license*
1 parent 24adca3 commit 20edc7f

File tree

7 files changed

+162
-65
lines changed

7 files changed

+162
-65
lines changed

packages/aws-cdk-lib/assertions/lib/matcher.ts

-1
Original file line numberDiff line numberDiff line change
@@ -182,7 +182,6 @@ export class MatchResult {
182182
*/
183183
public toHumanStrings(): string[] {
184184
const failures = new Array<MatchFailure>();
185-
debugger;
186185
recurse(this, []);
187186

188187
return failures.map(r => {
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
{
2+
"Resources": {
3+
"ASG": {
4+
"Type": "AWS::AutoScaling::AutoScalingGroup",
5+
"Properties": {
6+
"MinSize": "1",
7+
"MaxSize": "5"
8+
},
9+
"CreationPolicy": {
10+
"NonExistentResourceAttribute": "Bucket1"
11+
},
12+
"UpdatePolicy": {
13+
"NonExistentResourceAttribute": "Bucket1"
14+
}
15+
}
16+
}
17+
}

packages/aws-cdk-lib/cloudformation-include/test/valid-templates.test.ts

+7
Original file line numberDiff line numberDiff line change
@@ -707,6 +707,13 @@ describe('CDK Include', () => {
707707
);
708708
});
709709

710+
test('preserves unknown policy attributes', () => {
711+
const cfnTemplate = includeTestTemplate(stack, 'non-existent-policy-attribute.json');
712+
Template.fromStack(stack).templateMatches(
713+
loadTestFileToJsObject('non-existent-policy-attribute.json'),
714+
);
715+
});
716+
710717
test("correctly handles referencing the ingested template's resources across Stacks", () => {
711718
// for cross-stack sharing to work, we need an App
712719
const app = new core.App();

packages/aws-cdk-lib/core/lib/helpers-internal/cfn-parse.ts

+134-63
Original file line numberDiff line numberDiff line change
@@ -356,14 +356,23 @@ export class CfnParser {
356356
const cfnOptions = resource.cfnOptions;
357357
this.stack = Stack.of(resource);
358358

359-
cfnOptions.creationPolicy = this.parseCreationPolicy(resourceAttributes.CreationPolicy, logicalId);
360-
cfnOptions.updatePolicy = this.parseUpdatePolicy(resourceAttributes.UpdatePolicy, logicalId);
359+
const creationPolicy = this.parseCreationPolicy(resourceAttributes.CreationPolicy, logicalId);
360+
const updatePolicy = this.parseUpdatePolicy(resourceAttributes.UpdatePolicy, logicalId);
361+
cfnOptions.creationPolicy = creationPolicy.value;
362+
cfnOptions.updatePolicy = updatePolicy.value;
361363
cfnOptions.deletionPolicy = this.parseDeletionPolicy(resourceAttributes.DeletionPolicy);
362364
cfnOptions.updateReplacePolicy = this.parseDeletionPolicy(resourceAttributes.UpdateReplacePolicy);
363365
cfnOptions.version = this.parseValue(resourceAttributes.Version);
364366
cfnOptions.description = this.parseValue(resourceAttributes.Description);
365367
cfnOptions.metadata = this.parseValue(resourceAttributes.Metadata);
366368

369+
for (const [key, value] of Object.entries(creationPolicy.extraProperties)) {
370+
resource.addOverride(`CreationPolicy.${key}`, value);
371+
}
372+
for (const [key, value] of Object.entries(updatePolicy.extraProperties)) {
373+
resource.addOverride(`UpdatePolicy.${key}`, value);
374+
}
375+
367376
// handle Condition
368377
if (resourceAttributes.Condition) {
369378
const condition = this.finder.findCondition(resourceAttributes.Condition);
@@ -386,98 +395,93 @@ export class CfnParser {
386395
}
387396
}
388397

389-
private parseCreationPolicy(policy: any, logicalId: string): CfnCreationPolicy | undefined {
390-
if (typeof policy !== 'object') { return undefined; }
398+
private parseCreationPolicy(policy: any, logicalId: string): FromCloudFormationResult<CfnCreationPolicy | undefined> {
399+
if (typeof policy !== 'object') { return new FromCloudFormationResult(undefined); }
391400
this.throwIfIsIntrinsic(policy, logicalId);
392401
const self = this;
393402

394-
// change simple JS values to their CDK equivalents
395-
policy = this.parseValue(policy);
396-
397-
return undefinedIfAllValuesAreEmpty({
398-
autoScalingCreationPolicy: parseAutoScalingCreationPolicy(policy.AutoScalingCreationPolicy),
399-
resourceSignal: parseResourceSignal(policy.ResourceSignal),
400-
});
403+
const creaPol = new ObjectParser<CfnCreationPolicy>(this.parseValue(policy));
404+
creaPol.parseCase('autoScalingCreationPolicy', parseAutoScalingCreationPolicy);
405+
creaPol.parseCase('resourceSignal', parseResourceSignal);
406+
return creaPol.toResult();
401407

402-
function parseAutoScalingCreationPolicy(p: any): CfnResourceAutoScalingCreationPolicy | undefined {
408+
function parseAutoScalingCreationPolicy(p: any): FromCloudFormationResult<CfnResourceAutoScalingCreationPolicy | undefined> {
403409
self.throwIfIsIntrinsic(p, logicalId);
404-
if (typeof p !== 'object') { return undefined; }
410+
if (typeof p !== 'object') { return new FromCloudFormationResult(undefined); }
405411

406-
return undefinedIfAllValuesAreEmpty({
407-
minSuccessfulInstancesPercent: FromCloudFormation.getNumber(p.MinSuccessfulInstancesPercent).value,
408-
});
412+
const autoPol = new ObjectParser<CfnResourceAutoScalingCreationPolicy>(p);
413+
autoPol.parseCase('minSuccessfulInstancesPercent', FromCloudFormation.getNumber);
414+
return autoPol.toResult();
409415
}
410416

411-
function parseResourceSignal(p: any): CfnResourceSignal | undefined {
412-
if (typeof p !== 'object') { return undefined; }
417+
function parseResourceSignal(p: any): FromCloudFormationResult<CfnResourceSignal | undefined> {
418+
if (typeof p !== 'object') { return new FromCloudFormationResult(undefined); }
413419
self.throwIfIsIntrinsic(p, logicalId);
414420

415-
return undefinedIfAllValuesAreEmpty({
416-
count: FromCloudFormation.getNumber(p.Count).value,
417-
timeout: FromCloudFormation.getString(p.Timeout).value,
418-
});
421+
const sig = new ObjectParser<CfnResourceSignal>(p);
422+
sig.parseCase('count', FromCloudFormation.getNumber);
423+
sig.parseCase('timeout', FromCloudFormation.getString);
424+
return sig.toResult();
419425
}
420426
}
421427

422-
private parseUpdatePolicy(policy: any, logicalId: string): CfnUpdatePolicy | undefined {
423-
if (typeof policy !== 'object') { return undefined; }
428+
private parseUpdatePolicy(policy: any, logicalId: string): FromCloudFormationResult<CfnUpdatePolicy | undefined> {
429+
if (typeof policy !== 'object') { return new FromCloudFormationResult(undefined); }
424430
this.throwIfIsIntrinsic(policy, logicalId);
425431
const self = this;
426432

427433
// change simple JS values to their CDK equivalents
428-
policy = this.parseValue(policy);
429-
430-
return undefinedIfAllValuesAreEmpty({
431-
autoScalingReplacingUpdate: parseAutoScalingReplacingUpdate(policy.AutoScalingReplacingUpdate),
432-
autoScalingRollingUpdate: parseAutoScalingRollingUpdate(policy.AutoScalingRollingUpdate),
433-
autoScalingScheduledAction: parseAutoScalingScheduledAction(policy.AutoScalingScheduledAction),
434-
codeDeployLambdaAliasUpdate: parseCodeDeployLambdaAliasUpdate(policy.CodeDeployLambdaAliasUpdate),
435-
enableVersionUpgrade: FromCloudFormation.getBoolean(policy.EnableVersionUpgrade).value,
436-
useOnlineResharding: FromCloudFormation.getBoolean(policy.UseOnlineResharding).value,
437-
});
438-
439-
function parseAutoScalingReplacingUpdate(p: any): CfnAutoScalingReplacingUpdate | undefined {
440-
if (typeof p !== 'object') { return undefined; }
434+
const uppol = new ObjectParser<CfnUpdatePolicy>(this.parseValue(policy));
435+
uppol.parseCase('autoScalingReplacingUpdate', parseAutoScalingReplacingUpdate);
436+
uppol.parseCase('autoScalingRollingUpdate', parseAutoScalingRollingUpdate);
437+
uppol.parseCase('autoScalingScheduledAction', parseAutoScalingScheduledAction);
438+
uppol.parseCase('codeDeployLambdaAliasUpdate', parseCodeDeployLambdaAliasUpdate);
439+
uppol.parseCase('enableVersionUpgrade', (x) => FromCloudFormation.getBoolean(x) as any);
440+
uppol.parseCase('useOnlineResharding', (x) => FromCloudFormation.getBoolean(x) as any);
441+
return uppol.toResult();
442+
443+
function parseAutoScalingReplacingUpdate(p: any): FromCloudFormationResult<CfnAutoScalingReplacingUpdate | undefined> {
444+
if (typeof p !== 'object') { return new FromCloudFormationResult(undefined); }
441445
self.throwIfIsIntrinsic(p, logicalId);
442446

443-
return undefinedIfAllValuesAreEmpty({
444-
willReplace: p.WillReplace,
445-
});
447+
const repUp = new ObjectParser<CfnAutoScalingReplacingUpdate>(p);
448+
repUp.parseCase('willReplace', (x) => x);
449+
return repUp.toResult();
446450
}
447451

448-
function parseAutoScalingRollingUpdate(p: any): CfnAutoScalingRollingUpdate | undefined {
449-
if (typeof p !== 'object') { return undefined; }
452+
function parseAutoScalingRollingUpdate(p: any): FromCloudFormationResult<CfnAutoScalingRollingUpdate | undefined> {
453+
if (typeof p !== 'object') { return new FromCloudFormationResult(undefined); }
450454
self.throwIfIsIntrinsic(p, logicalId);
451455

452-
return undefinedIfAllValuesAreEmpty({
453-
maxBatchSize: FromCloudFormation.getNumber(p.MaxBatchSize).value,
454-
minInstancesInService: FromCloudFormation.getNumber(p.MinInstancesInService).value,
455-
minSuccessfulInstancesPercent: FromCloudFormation.getNumber(p.MinSuccessfulInstancesPercent).value,
456-
pauseTime: FromCloudFormation.getString(p.PauseTime).value,
457-
suspendProcesses: FromCloudFormation.getStringArray(p.SuspendProcesses).value,
458-
waitOnResourceSignals: FromCloudFormation.getBoolean(p.WaitOnResourceSignals).value,
459-
});
456+
const rollUp = new ObjectParser<CfnAutoScalingRollingUpdate>(p);
457+
rollUp.parseCase('maxBatchSize', FromCloudFormation.getNumber);
458+
rollUp.parseCase('minInstancesInService', FromCloudFormation.getNumber);
459+
rollUp.parseCase('minSuccessfulInstancesPercent', FromCloudFormation.getNumber);
460+
rollUp.parseCase('pauseTime', FromCloudFormation.getString);
461+
rollUp.parseCase('suspendProcesses', FromCloudFormation.getStringArray);
462+
rollUp.parseCase('waitOnResourceSignals', FromCloudFormation.getBoolean);
463+
return rollUp.toResult();
460464
}
461465

462-
function parseCodeDeployLambdaAliasUpdate(p: any): CfnCodeDeployLambdaAliasUpdate | undefined {
466+
function parseCodeDeployLambdaAliasUpdate(p: any): FromCloudFormationResult<CfnCodeDeployLambdaAliasUpdate | undefined> {
467+
if (typeof p !== 'object') { return new FromCloudFormationResult(undefined); }
463468
self.throwIfIsIntrinsic(p, logicalId);
464-
if (typeof p !== 'object') { return undefined; }
465469

466-
return {
467-
beforeAllowTrafficHook: FromCloudFormation.getString(p.BeforeAllowTrafficHook).value,
468-
afterAllowTrafficHook: FromCloudFormation.getString(p.AfterAllowTrafficHook).value,
469-
applicationName: FromCloudFormation.getString(p.ApplicationName).value,
470-
deploymentGroupName: FromCloudFormation.getString(p.DeploymentGroupName).value,
471-
};
470+
const cdUp = new ObjectParser<CfnCodeDeployLambdaAliasUpdate>(p);
471+
cdUp.parseCase('beforeAllowTrafficHook', FromCloudFormation.getString);
472+
cdUp.parseCase('afterAllowTrafficHook', FromCloudFormation.getString);
473+
cdUp.parseCase('applicationName', FromCloudFormation.getString);
474+
cdUp.parseCase('deploymentGroupName', FromCloudFormation.getString);
475+
return cdUp.toResult();
472476
}
473477

474-
function parseAutoScalingScheduledAction(p: any): CfnAutoScalingScheduledAction | undefined {
478+
function parseAutoScalingScheduledAction(p: any): FromCloudFormationResult<CfnAutoScalingScheduledAction | undefined> {
479+
if (typeof p !== 'object') { return new FromCloudFormationResult(undefined); }
475480
self.throwIfIsIntrinsic(p, logicalId);
476-
if (typeof p !== 'object') { return undefined; }
477481

478-
return undefinedIfAllValuesAreEmpty({
479-
ignoreUnmodifiedGroupSizeProperties: FromCloudFormation.getBoolean(p.IgnoreUnmodifiedGroupSizeProperties).value,
480-
});
482+
const schedUp = new ObjectParser<CfnAutoScalingScheduledAction>(p);
483+
schedUp.parseCase('ignoreUnmodifiedGroupSizeProperties', FromCloudFormation.getBoolean);
484+
return schedUp.toResult();
481485
}
482486
}
483487

@@ -853,3 +857,70 @@ export class CfnParser {
853857
return this.options.parameters || {};
854858
}
855859
}
860+
861+
class ObjectParser<T extends object> {
862+
private readonly parsed: Record<string, unknown> = {};
863+
private readonly unparsed: Record<string, unknown> = {};
864+
865+
constructor(input: Record<string, unknown>) {
866+
this.unparsed = { ...input };
867+
}
868+
869+
/**
870+
* Parse a single field from the object into the target object
871+
*
872+
* The source key will be assumed to be the exact same as the
873+
* target key, but with an uppercase first letter.
874+
*/
875+
public parseCase<K extends keyof T>(targetKey: K, parser: (x: any) => T[K] | FromCloudFormationResult<T[K] | IResolvable>) {
876+
const sourceKey = ucfirst(String(targetKey));
877+
this.parse(targetKey, sourceKey, parser);
878+
}
879+
880+
public parse<K extends keyof T>(targetKey: K, sourceKey: string, parser: (x: any) => T[K] | FromCloudFormationResult<T[K] | IResolvable>) {
881+
if (!(sourceKey in this.unparsed)) {
882+
return;
883+
}
884+
885+
const value = parser(this.unparsed[sourceKey]);
886+
delete this.unparsed[sourceKey];
887+
888+
if (value instanceof FromCloudFormationResult) {
889+
for (const [k, v] of Object.entries(value.extraProperties ?? {})) {
890+
this.unparsed[`${sourceKey}.${k}`] = v;
891+
}
892+
this.parsed[targetKey as any] = value.value;
893+
} else {
894+
this.parsed[targetKey as any] = value;
895+
}
896+
}
897+
898+
public toResult(): FromCloudFormationResult<T | undefined> {
899+
const ret = new FromCloudFormationResult(undefinedIfAllValuesAreEmpty(this.parsed as any));
900+
for (const [k, v] of Object.entries(this.unparsedKeys)) {
901+
ret.extraProperties[k] = v;
902+
}
903+
return ret;
904+
}
905+
906+
private get unparsedKeys(): Record<string, unknown> {
907+
const unparsed = { ...this.unparsed };
908+
909+
for (const [k, v] of Object.entries(this.unparsed)) {
910+
if (v instanceof FromCloudFormationResult) {
911+
for (const [k2, v2] of Object.entries(v.extraProperties ?? {})) {
912+
unparsed[`${k}.${k2}`] = v2;
913+
}
914+
} else {
915+
unparsed[k] = v;
916+
}
917+
}
918+
919+
return unparsed;
920+
}
921+
}
922+
923+
function ucfirst(x: string) {
924+
return x.slice(0, 1).toUpperCase() + x.slice(1);
925+
}
926+

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

+1-1
Original file line numberDiff line numberDiff line change
@@ -121,6 +121,6 @@ export function findLastCommonElement<T>(path1: T[], path2: T[]): T | undefined
121121
return path1[i - 1];
122122
}
123123

124-
export function undefinedIfAllValuesAreEmpty(object: object): object | undefined {
124+
export function undefinedIfAllValuesAreEmpty<A extends object>(object: A): A | undefined {
125125
return Object.values(object).some(v => v !== undefined) ? object : undefined;
126126
}

packages/aws-cdk-lib/jest.config.js

+2
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,8 @@ module.exports = {
88
testMatch: [
99
'<rootDir>/**/test/**/?(*.)+(test).ts',
1010
],
11+
// Massive parallellism leads to common timeouts
12+
testTimeout: 60_000,
1113

1214
coverageThreshold: {
1315
global: {

packages/aws-cdk/lib/util/npm.ts

+1
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ import { debug } from '../../lib/logging';
55

66
const exec = promisify(_exec);
77

8+
/* istanbul ignore next: not called during unit tests */
89
export async function getLatestVersionFromNpm(): Promise<string> {
910
const { stdout, stderr } = await exec('npm view aws-cdk version', { timeout: 3000 });
1011
if (stderr && stderr.trim().length > 0) {

0 commit comments

Comments
 (0)