diff --git a/packages/aws-cdk-lib/assertions/lib/matcher.ts b/packages/aws-cdk-lib/assertions/lib/matcher.ts index fe7462392ee3e..a4cf983884f83 100644 --- a/packages/aws-cdk-lib/assertions/lib/matcher.ts +++ b/packages/aws-cdk-lib/assertions/lib/matcher.ts @@ -182,7 +182,6 @@ export class MatchResult { */ public toHumanStrings(): string[] { const failures = new Array(); - debugger; recurse(this, []); return failures.map(r => { diff --git a/packages/aws-cdk-lib/cloudformation-include/test/test-templates/non-existent-policy-attribute.json b/packages/aws-cdk-lib/cloudformation-include/test/test-templates/non-existent-policy-attribute.json new file mode 100644 index 0000000000000..a2f1c2b9e230c --- /dev/null +++ b/packages/aws-cdk-lib/cloudformation-include/test/test-templates/non-existent-policy-attribute.json @@ -0,0 +1,17 @@ +{ + "Resources": { + "ASG": { + "Type": "AWS::AutoScaling::AutoScalingGroup", + "Properties": { + "MinSize": "1", + "MaxSize": "5" + }, + "CreationPolicy": { + "NonExistentResourceAttribute": "Bucket1" + }, + "UpdatePolicy": { + "NonExistentResourceAttribute": "Bucket1" + } + } + } +} diff --git a/packages/aws-cdk-lib/cloudformation-include/test/valid-templates.test.ts b/packages/aws-cdk-lib/cloudformation-include/test/valid-templates.test.ts index 2f7b132f4aaf8..7332314f0576f 100644 --- a/packages/aws-cdk-lib/cloudformation-include/test/valid-templates.test.ts +++ b/packages/aws-cdk-lib/cloudformation-include/test/valid-templates.test.ts @@ -707,6 +707,13 @@ describe('CDK Include', () => { ); }); + test('preserves unknown policy attributes', () => { + const cfnTemplate = includeTestTemplate(stack, 'non-existent-policy-attribute.json'); + Template.fromStack(stack).templateMatches( + loadTestFileToJsObject('non-existent-policy-attribute.json'), + ); + }); + test("correctly handles referencing the ingested template's resources across Stacks", () => { // for cross-stack sharing to work, we need an App const app = new core.App(); diff --git a/packages/aws-cdk-lib/core/lib/helpers-internal/cfn-parse.ts b/packages/aws-cdk-lib/core/lib/helpers-internal/cfn-parse.ts index 839684b2e791f..ccdf76efb2240 100644 --- a/packages/aws-cdk-lib/core/lib/helpers-internal/cfn-parse.ts +++ b/packages/aws-cdk-lib/core/lib/helpers-internal/cfn-parse.ts @@ -356,14 +356,23 @@ export class CfnParser { const cfnOptions = resource.cfnOptions; this.stack = Stack.of(resource); - cfnOptions.creationPolicy = this.parseCreationPolicy(resourceAttributes.CreationPolicy, logicalId); - cfnOptions.updatePolicy = this.parseUpdatePolicy(resourceAttributes.UpdatePolicy, logicalId); + const creationPolicy = this.parseCreationPolicy(resourceAttributes.CreationPolicy, logicalId); + const updatePolicy = this.parseUpdatePolicy(resourceAttributes.UpdatePolicy, logicalId); + cfnOptions.creationPolicy = creationPolicy.value; + cfnOptions.updatePolicy = updatePolicy.value; cfnOptions.deletionPolicy = this.parseDeletionPolicy(resourceAttributes.DeletionPolicy); cfnOptions.updateReplacePolicy = this.parseDeletionPolicy(resourceAttributes.UpdateReplacePolicy); cfnOptions.version = this.parseValue(resourceAttributes.Version); cfnOptions.description = this.parseValue(resourceAttributes.Description); cfnOptions.metadata = this.parseValue(resourceAttributes.Metadata); + for (const [key, value] of Object.entries(creationPolicy.extraProperties)) { + resource.addOverride(`CreationPolicy.${key}`, value); + } + for (const [key, value] of Object.entries(updatePolicy.extraProperties)) { + resource.addOverride(`UpdatePolicy.${key}`, value); + } + // handle Condition if (resourceAttributes.Condition) { const condition = this.finder.findCondition(resourceAttributes.Condition); @@ -386,98 +395,93 @@ export class CfnParser { } } - private parseCreationPolicy(policy: any, logicalId: string): CfnCreationPolicy | undefined { - if (typeof policy !== 'object') { return undefined; } + private parseCreationPolicy(policy: any, logicalId: string): FromCloudFormationResult { + if (typeof policy !== 'object') { return new FromCloudFormationResult(undefined); } this.throwIfIsIntrinsic(policy, logicalId); const self = this; - // change simple JS values to their CDK equivalents - policy = this.parseValue(policy); - - return undefinedIfAllValuesAreEmpty({ - autoScalingCreationPolicy: parseAutoScalingCreationPolicy(policy.AutoScalingCreationPolicy), - resourceSignal: parseResourceSignal(policy.ResourceSignal), - }); + const creaPol = new ObjectParser(this.parseValue(policy)); + creaPol.parseCase('autoScalingCreationPolicy', parseAutoScalingCreationPolicy); + creaPol.parseCase('resourceSignal', parseResourceSignal); + return creaPol.toResult(); - function parseAutoScalingCreationPolicy(p: any): CfnResourceAutoScalingCreationPolicy | undefined { + function parseAutoScalingCreationPolicy(p: any): FromCloudFormationResult { self.throwIfIsIntrinsic(p, logicalId); - if (typeof p !== 'object') { return undefined; } + if (typeof p !== 'object') { return new FromCloudFormationResult(undefined); } - return undefinedIfAllValuesAreEmpty({ - minSuccessfulInstancesPercent: FromCloudFormation.getNumber(p.MinSuccessfulInstancesPercent).value, - }); + const autoPol = new ObjectParser(p); + autoPol.parseCase('minSuccessfulInstancesPercent', FromCloudFormation.getNumber); + return autoPol.toResult(); } - function parseResourceSignal(p: any): CfnResourceSignal | undefined { - if (typeof p !== 'object') { return undefined; } + function parseResourceSignal(p: any): FromCloudFormationResult { + if (typeof p !== 'object') { return new FromCloudFormationResult(undefined); } self.throwIfIsIntrinsic(p, logicalId); - return undefinedIfAllValuesAreEmpty({ - count: FromCloudFormation.getNumber(p.Count).value, - timeout: FromCloudFormation.getString(p.Timeout).value, - }); + const sig = new ObjectParser(p); + sig.parseCase('count', FromCloudFormation.getNumber); + sig.parseCase('timeout', FromCloudFormation.getString); + return sig.toResult(); } } - private parseUpdatePolicy(policy: any, logicalId: string): CfnUpdatePolicy | undefined { - if (typeof policy !== 'object') { return undefined; } + private parseUpdatePolicy(policy: any, logicalId: string): FromCloudFormationResult { + if (typeof policy !== 'object') { return new FromCloudFormationResult(undefined); } this.throwIfIsIntrinsic(policy, logicalId); const self = this; // change simple JS values to their CDK equivalents - policy = this.parseValue(policy); - - return undefinedIfAllValuesAreEmpty({ - autoScalingReplacingUpdate: parseAutoScalingReplacingUpdate(policy.AutoScalingReplacingUpdate), - autoScalingRollingUpdate: parseAutoScalingRollingUpdate(policy.AutoScalingRollingUpdate), - autoScalingScheduledAction: parseAutoScalingScheduledAction(policy.AutoScalingScheduledAction), - codeDeployLambdaAliasUpdate: parseCodeDeployLambdaAliasUpdate(policy.CodeDeployLambdaAliasUpdate), - enableVersionUpgrade: FromCloudFormation.getBoolean(policy.EnableVersionUpgrade).value, - useOnlineResharding: FromCloudFormation.getBoolean(policy.UseOnlineResharding).value, - }); - - function parseAutoScalingReplacingUpdate(p: any): CfnAutoScalingReplacingUpdate | undefined { - if (typeof p !== 'object') { return undefined; } + const uppol = new ObjectParser(this.parseValue(policy)); + uppol.parseCase('autoScalingReplacingUpdate', parseAutoScalingReplacingUpdate); + uppol.parseCase('autoScalingRollingUpdate', parseAutoScalingRollingUpdate); + uppol.parseCase('autoScalingScheduledAction', parseAutoScalingScheduledAction); + uppol.parseCase('codeDeployLambdaAliasUpdate', parseCodeDeployLambdaAliasUpdate); + uppol.parseCase('enableVersionUpgrade', (x) => FromCloudFormation.getBoolean(x) as any); + uppol.parseCase('useOnlineResharding', (x) => FromCloudFormation.getBoolean(x) as any); + return uppol.toResult(); + + function parseAutoScalingReplacingUpdate(p: any): FromCloudFormationResult { + if (typeof p !== 'object') { return new FromCloudFormationResult(undefined); } self.throwIfIsIntrinsic(p, logicalId); - return undefinedIfAllValuesAreEmpty({ - willReplace: p.WillReplace, - }); + const repUp = new ObjectParser(p); + repUp.parseCase('willReplace', (x) => x); + return repUp.toResult(); } - function parseAutoScalingRollingUpdate(p: any): CfnAutoScalingRollingUpdate | undefined { - if (typeof p !== 'object') { return undefined; } + function parseAutoScalingRollingUpdate(p: any): FromCloudFormationResult { + if (typeof p !== 'object') { return new FromCloudFormationResult(undefined); } self.throwIfIsIntrinsic(p, logicalId); - return undefinedIfAllValuesAreEmpty({ - maxBatchSize: FromCloudFormation.getNumber(p.MaxBatchSize).value, - minInstancesInService: FromCloudFormation.getNumber(p.MinInstancesInService).value, - minSuccessfulInstancesPercent: FromCloudFormation.getNumber(p.MinSuccessfulInstancesPercent).value, - pauseTime: FromCloudFormation.getString(p.PauseTime).value, - suspendProcesses: FromCloudFormation.getStringArray(p.SuspendProcesses).value, - waitOnResourceSignals: FromCloudFormation.getBoolean(p.WaitOnResourceSignals).value, - }); + const rollUp = new ObjectParser(p); + rollUp.parseCase('maxBatchSize', FromCloudFormation.getNumber); + rollUp.parseCase('minInstancesInService', FromCloudFormation.getNumber); + rollUp.parseCase('minSuccessfulInstancesPercent', FromCloudFormation.getNumber); + rollUp.parseCase('pauseTime', FromCloudFormation.getString); + rollUp.parseCase('suspendProcesses', FromCloudFormation.getStringArray); + rollUp.parseCase('waitOnResourceSignals', FromCloudFormation.getBoolean); + return rollUp.toResult(); } - function parseCodeDeployLambdaAliasUpdate(p: any): CfnCodeDeployLambdaAliasUpdate | undefined { + function parseCodeDeployLambdaAliasUpdate(p: any): FromCloudFormationResult { + if (typeof p !== 'object') { return new FromCloudFormationResult(undefined); } self.throwIfIsIntrinsic(p, logicalId); - if (typeof p !== 'object') { return undefined; } - return { - beforeAllowTrafficHook: FromCloudFormation.getString(p.BeforeAllowTrafficHook).value, - afterAllowTrafficHook: FromCloudFormation.getString(p.AfterAllowTrafficHook).value, - applicationName: FromCloudFormation.getString(p.ApplicationName).value, - deploymentGroupName: FromCloudFormation.getString(p.DeploymentGroupName).value, - }; + const cdUp = new ObjectParser(p); + cdUp.parseCase('beforeAllowTrafficHook', FromCloudFormation.getString); + cdUp.parseCase('afterAllowTrafficHook', FromCloudFormation.getString); + cdUp.parseCase('applicationName', FromCloudFormation.getString); + cdUp.parseCase('deploymentGroupName', FromCloudFormation.getString); + return cdUp.toResult(); } - function parseAutoScalingScheduledAction(p: any): CfnAutoScalingScheduledAction | undefined { + function parseAutoScalingScheduledAction(p: any): FromCloudFormationResult { + if (typeof p !== 'object') { return new FromCloudFormationResult(undefined); } self.throwIfIsIntrinsic(p, logicalId); - if (typeof p !== 'object') { return undefined; } - return undefinedIfAllValuesAreEmpty({ - ignoreUnmodifiedGroupSizeProperties: FromCloudFormation.getBoolean(p.IgnoreUnmodifiedGroupSizeProperties).value, - }); + const schedUp = new ObjectParser(p); + schedUp.parseCase('ignoreUnmodifiedGroupSizeProperties', FromCloudFormation.getBoolean); + return schedUp.toResult(); } } @@ -853,3 +857,70 @@ export class CfnParser { return this.options.parameters || {}; } } + +class ObjectParser { + private readonly parsed: Record = {}; + private readonly unparsed: Record = {}; + + constructor(input: Record) { + this.unparsed = { ...input }; + } + + /** + * Parse a single field from the object into the target object + * + * The source key will be assumed to be the exact same as the + * target key, but with an uppercase first letter. + */ + public parseCase(targetKey: K, parser: (x: any) => T[K] | FromCloudFormationResult) { + const sourceKey = ucfirst(String(targetKey)); + this.parse(targetKey, sourceKey, parser); + } + + public parse(targetKey: K, sourceKey: string, parser: (x: any) => T[K] | FromCloudFormationResult) { + if (!(sourceKey in this.unparsed)) { + return; + } + + const value = parser(this.unparsed[sourceKey]); + delete this.unparsed[sourceKey]; + + if (value instanceof FromCloudFormationResult) { + for (const [k, v] of Object.entries(value.extraProperties ?? {})) { + this.unparsed[`${sourceKey}.${k}`] = v; + } + this.parsed[targetKey as any] = value.value; + } else { + this.parsed[targetKey as any] = value; + } + } + + public toResult(): FromCloudFormationResult { + const ret = new FromCloudFormationResult(undefinedIfAllValuesAreEmpty(this.parsed as any)); + for (const [k, v] of Object.entries(this.unparsedKeys)) { + ret.extraProperties[k] = v; + } + return ret; + } + + private get unparsedKeys(): Record { + const unparsed = { ...this.unparsed }; + + for (const [k, v] of Object.entries(this.unparsed)) { + if (v instanceof FromCloudFormationResult) { + for (const [k2, v2] of Object.entries(v.extraProperties ?? {})) { + unparsed[`${k}.${k2}`] = v2; + } + } else { + unparsed[k] = v; + } + } + + return unparsed; + } +} + +function ucfirst(x: string) { + return x.slice(0, 1).toUpperCase() + x.slice(1); +} + diff --git a/packages/aws-cdk-lib/core/lib/util.ts b/packages/aws-cdk-lib/core/lib/util.ts index c536cf535d45c..f40651f845518 100644 --- a/packages/aws-cdk-lib/core/lib/util.ts +++ b/packages/aws-cdk-lib/core/lib/util.ts @@ -121,6 +121,6 @@ export function findLastCommonElement(path1: T[], path2: T[]): T | undefined return path1[i - 1]; } -export function undefinedIfAllValuesAreEmpty(object: object): object | undefined { +export function undefinedIfAllValuesAreEmpty(object: A): A | undefined { return Object.values(object).some(v => v !== undefined) ? object : undefined; } diff --git a/packages/aws-cdk-lib/jest.config.js b/packages/aws-cdk-lib/jest.config.js index a5d279d7bf4d0..8df1f8a34a56d 100644 --- a/packages/aws-cdk-lib/jest.config.js +++ b/packages/aws-cdk-lib/jest.config.js @@ -8,6 +8,8 @@ module.exports = { testMatch: [ '/**/test/**/?(*.)+(test).ts', ], + // Massive parallellism leads to common timeouts + testTimeout: 60_000, coverageThreshold: { global: { diff --git a/packages/aws-cdk/lib/util/npm.ts b/packages/aws-cdk/lib/util/npm.ts index 17a5e099a9312..d31ac95f1a17b 100644 --- a/packages/aws-cdk/lib/util/npm.ts +++ b/packages/aws-cdk/lib/util/npm.ts @@ -5,6 +5,7 @@ import { debug } from '../../lib/logging'; const exec = promisify(_exec); +/* istanbul ignore next: not called during unit tests */ export async function getLatestVersionFromNpm(): Promise { const { stdout, stderr } = await exec('npm view aws-cdk version', { timeout: 3000 }); if (stderr && stderr.trim().length > 0) {