diff --git a/packages/aws-cdk-lib/cloudformation-include/lib/cfn-include.ts b/packages/aws-cdk-lib/cloudformation-include/lib/cfn-include.ts index 9a1018e79e026..a2849e5c794a6 100644 --- a/packages/aws-cdk-lib/cloudformation-include/lib/cfn-include.ts +++ b/packages/aws-cdk-lib/cloudformation-include/lib/cfn-include.ts @@ -66,6 +66,16 @@ export interface CfnIncludeProps { * @default - will throw an error on detecting any cyclical references */ readonly allowCyclicalReferences?: boolean; + + /** + * Specifies a list of LogicalIDs for resources that will be included in the CDK Stack, + * but will not be parsed and converted to CDK types. This allows you to use CFN templates + * that rely on Intrinsic placement that `cfn-include` + * would otherwise reject, such as non-primitive values in resource update policies. + * + * @default - All resources are hydrated + */ + readonly dehydratedResources?: string[]; } /** @@ -109,6 +119,7 @@ export class CfnInclude extends core.CfnElement { private readonly template: any; private readonly preserveLogicalIds: boolean; private readonly allowCyclicalReferences: boolean; + private readonly dehydratedResources: string[]; private logicalIdToPlaceholderMap: Map; constructor(scope: Construct, id: string, props: CfnIncludeProps) { @@ -125,6 +136,14 @@ export class CfnInclude extends core.CfnElement { this.preserveLogicalIds = props.preserveLogicalIds ?? true; + this.dehydratedResources = props.dehydratedResources ?? []; + + for (const logicalId of this.dehydratedResources) { + if (!Object.keys(this.template.Resources).includes(logicalId)) { + throw new Error(`Logical ID '${logicalId}' was specified in 'dehydratedResources', but does not belong to a resource in the template.`); + } + } + // check if all user specified parameter values exist in the template for (const logicalId of Object.keys(this.parametersToReplace)) { if (!(logicalId in (this.template.Parameters || {}))) { @@ -663,8 +682,27 @@ export class CfnInclude extends core.CfnElement { const resourceAttributes: any = this.template.Resources[logicalId]; let l1Instance: core.CfnResource; - if (this.nestedStacksToInclude[logicalId]) { + if (this.nestedStacksToInclude[logicalId] && this.dehydratedResources.includes(logicalId)) { + throw new Error(`nested stack '${logicalId}' was marked as dehydrated - nested stacks cannot be dehydrated`); + } else if (this.nestedStacksToInclude[logicalId]) { l1Instance = this.createNestedStack(logicalId, cfnParser); + } else if (this.dehydratedResources.includes(logicalId)) { + + l1Instance = new core.CfnResource(this, logicalId, { + type: resourceAttributes.Type, + properties: resourceAttributes.Properties, + }); + const cfnOptions = l1Instance.cfnOptions; + cfnOptions.creationPolicy = resourceAttributes.CreationPolicy; + cfnOptions.updatePolicy = resourceAttributes.UpdatePolicy; + cfnOptions.deletionPolicy = resourceAttributes.DeletionPolicy; + cfnOptions.updateReplacePolicy = resourceAttributes.UpdateReplacePolicy; + cfnOptions.version = resourceAttributes.Version; + cfnOptions.description = resourceAttributes.Description; + cfnOptions.metadata = resourceAttributes.Metadata; + this.resources[logicalId] = l1Instance; + + return l1Instance; } else { const l1ClassFqn = cfn_type_to_l1_mapping.lookup(resourceAttributes.Type); // The AWS::CloudFormation::CustomResource type corresponds to the CfnCustomResource class. diff --git a/packages/aws-cdk-lib/cloudformation-include/test/invalid-templates.test.ts b/packages/aws-cdk-lib/cloudformation-include/test/invalid-templates.test.ts index 984a394adaa05..3802c00477d37 100644 --- a/packages/aws-cdk-lib/cloudformation-include/test/invalid-templates.test.ts +++ b/packages/aws-cdk-lib/cloudformation-include/test/invalid-templates.test.ts @@ -245,17 +245,201 @@ describe('CDK Include', () => { }, ); }); + + test('throws an exception if Tags contains invalid intrinsics', () => { + expect(() => { + includeTestTemplate(stack, 'tags-with-invalid-intrinsics.json'); + }).toThrow(/expression does not exist in the template/); + }); + + test('non-leaf Intrinsics cannot be used in the top-level creation policy', () => { + stack.node.setContext(cxapi.CFN_INCLUDE_REJECT_COMPLEX_RESOURCE_UPDATE_CREATE_POLICY_INTRINSICS, true); + expect(() => { + includeTestTemplate(stack, 'intrinsics-create-policy.json'); + }).toThrow(/Cannot convert resource 'CreationPolicyIntrinsic' to CDK objects: it uses an intrinsic in a resource update or deletion policy to represent a non-primitive value. Specify 'CreationPolicyIntrinsic' in the 'dehydratedResources' prop to skip parsing this resource, while still including it in the output./); + }); + + test('Intrinsics cannot be used in the autoscaling creation policy', () => { + stack.node.setContext(cxapi.CFN_INCLUDE_REJECT_COMPLEX_RESOURCE_UPDATE_CREATE_POLICY_INTRINSICS, true); + expect(() => { + includeTestTemplate(stack, 'intrinsics-create-policy-autoscaling.json'); + }).toThrow(/Cannot convert resource 'AutoScalingCreationPolicyIntrinsic' to CDK objects: it uses an intrinsic in a resource update or deletion policy to represent a non-primitive value. Specify 'AutoScalingCreationPolicyIntrinsic' in the 'dehydratedResources' prop to skip parsing this resource, while still including it in the output./); + }); + + test('Intrinsics cannot be used in the create policy resource signal', () => { + stack.node.setContext(cxapi.CFN_INCLUDE_REJECT_COMPLEX_RESOURCE_UPDATE_CREATE_POLICY_INTRINSICS, true); + expect(() => { + includeTestTemplate(stack, 'intrinsics-create-policy-resource-signal.json'); + }).toThrow(/Cannot convert resource 'ResourceSignalIntrinsic' to CDK objects: it uses an intrinsic in a resource update or deletion policy to represent a non-primitive value. Specify 'ResourceSignalIntrinsic' in the 'dehydratedResources' prop to skip parsing this resource, while still including it in the output./); + }); + + test('Intrinsics cannot be used in the top-level update policy', () => { + stack.node.setContext(cxapi.CFN_INCLUDE_REJECT_COMPLEX_RESOURCE_UPDATE_CREATE_POLICY_INTRINSICS, true); + expect(() => { + includeTestTemplate(stack, 'intrinsics-update-policy.json'); + }).toThrow(/Cannot convert resource 'ASG' to CDK objects: it uses an intrinsic in a resource update or deletion policy to represent a non-primitive value. Specify 'ASG' in the 'dehydratedResources' prop to skip parsing this resource, while still including it in the output./); + }); + + test('Intrinsics cannot be used in the auto scaling rolling update update policy', () => { + stack.node.setContext(cxapi.CFN_INCLUDE_REJECT_COMPLEX_RESOURCE_UPDATE_CREATE_POLICY_INTRINSICS, true); + expect(() => { + includeTestTemplate(stack, 'intrinsics-update-policy-autoscaling-rolling-update.json'); + }).toThrow(/Cannot convert resource 'ASG' to CDK objects: it uses an intrinsic in a resource update or deletion policy to represent a non-primitive value. Specify 'ASG' in the 'dehydratedResources' prop to skip parsing this resource, while still including it in the output./); + }); + + test('Intrinsics cannot be used in the auto scaling replacing update update policy', () => { + stack.node.setContext(cxapi.CFN_INCLUDE_REJECT_COMPLEX_RESOURCE_UPDATE_CREATE_POLICY_INTRINSICS, true); + expect(() => { + includeTestTemplate(stack, 'intrinsics-update-policy-autoscaling-replacing-update.json'); + }).toThrow(/Cannot convert resource 'ASG' to CDK objects: it uses an intrinsic in a resource update or deletion policy to represent a non-primitive value. Specify 'ASG' in the 'dehydratedResources' prop to skip parsing this resource, while still including it in the output./); + }); + + test('Intrinsics cannot be used in the auto scaling scheduled action update policy', () => { + stack.node.setContext(cxapi.CFN_INCLUDE_REJECT_COMPLEX_RESOURCE_UPDATE_CREATE_POLICY_INTRINSICS, true); + expect(() => { + includeTestTemplate(stack, 'intrinsics-update-policy-autoscaling-scheduled-action.json'); + }).toThrow(/Cannot convert resource 'ASG' to CDK objects: it uses an intrinsic in a resource update or deletion policy to represent a non-primitive value. Specify 'ASG' in the 'dehydratedResources' prop to skip parsing this resource, while still including it in the output./); + }); + + test('Intrinsics cannot be used in the code deploy lambda alias update policy', () => { + stack.node.setContext(cxapi.CFN_INCLUDE_REJECT_COMPLEX_RESOURCE_UPDATE_CREATE_POLICY_INTRINSICS, true); + expect(() => { + includeTestTemplate(stack, 'intrinsics-update-policy-code-deploy-lambda-alias-update.json'); + }).toThrow(/Cannot convert resource 'Alias' to CDK objects: it uses an intrinsic in a resource update or deletion policy to represent a non-primitive value. Specify 'Alias' in the 'dehydratedResources' prop to skip parsing this resource, while still including it in the output./); + }); + + test('FF toggles error checking', () => { + stack.node.setContext(cxapi.CFN_INCLUDE_REJECT_COMPLEX_RESOURCE_UPDATE_CREATE_POLICY_INTRINSICS, false); + expect(() => { + includeTestTemplate(stack, 'intrinsics-update-policy-code-deploy-lambda-alias-update.json'); + }).not.toThrow(); + }); + + test('FF disabled with dehydratedResources does not throw', () => { + stack.node.setContext(cxapi.CFN_INCLUDE_REJECT_COMPLEX_RESOURCE_UPDATE_CREATE_POLICY_INTRINSICS, false); + expect(() => { + includeTestTemplate(stack, 'intrinsics-update-policy-code-deploy-lambda-alias-update.json', { + dehydratedResources: ['Alias'], + }); + }).not.toThrow(); + }); + + test('dehydrated resources retain attributes with complex Intrinsics', () => { + stack.node.setContext(cxapi.CFN_INCLUDE_REJECT_COMPLEX_RESOURCE_UPDATE_CREATE_POLICY_INTRINSICS, true); + includeTestTemplate(stack, 'intrinsics-update-policy-code-deploy-lambda-alias-update.json', { + dehydratedResources: ['Alias'], + }); + + expect(Template.fromStack(stack).hasResource('AWS::Lambda::Alias', { + UpdatePolicy: { + CodeDeployLambdaAliasUpdate: { + 'Fn::If': [ + 'SomeCondition', + { + AfterAllowTrafficHook: 'SomeOtherHook', + ApplicationName: 'SomeApp', + BeforeAllowTrafficHook: 'SomeHook', + DeploymentGroupName: 'SomeDeploymentGroup', + }, + { + AfterAllowTrafficHook: 'SomeOtherOtherHook', + ApplicationName: 'SomeOtherApp', + BeforeAllowTrafficHook: 'SomeOtherHook', + DeploymentGroupName: 'SomeOtherDeploymentGroup', + + }, + ], + }, + }, + })); + }); + + test('dehydrated resources retain all attributes', () => { + includeTestTemplate(stack, 'resource-all-attributes.json', { + dehydratedResources: ['Foo'], + }); + + expect(Template.fromStack(stack).hasResource('AWS::Foo::Bar', { + Properties: { Blinky: 'Pinky' }, + Type: 'AWS::Foo::Bar', + CreationPolicy: { Inky: 'Clyde' }, + DeletionPolicy: { DeletionPolicyKey: 'DeletionPolicyValue' }, + Metadata: { SomeKey: 'SomeValue' }, + Version: '1.2.3.4.5.6', + UpdateReplacePolicy: { Oh: 'No' }, + Description: 'This resource does not match the spec, but it does have every possible attribute', + UpdatePolicy: { + Foo: 'Bar', + }, + })); + }); + + test('synth-time validation does not run on dehydrated resources', () => { + // synth-time validation fails if resource is hydrated + expect(() => { + includeTestTemplate(stack, 'intrinsics-tags-resource-validation.json'); + Template.fromStack(stack); + }).toThrow(`Resolution error: Supplied properties not correct for \"CfnLoadBalancerProps\" + tags: element 1: {} should have a 'key' and a 'value' property.`); + + app = new core.App({ context: { [cxapi.NEW_STYLE_STACK_SYNTHESIS_CONTEXT]: false } }); + stack = new core.Stack(app); + + // synth-time validation not run if resource is dehydrated + includeTestTemplate(stack, 'intrinsics-tags-resource-validation.json', { + dehydratedResources: ['MyLoadBalancer'], + }); + + expect(Template.fromStack(stack).hasResource('AWS::ElasticLoadBalancingV2::LoadBalancer', { + Properties: { + Tags: [ + { + Key: 'Name', + Value: 'MyLoadBalancer', + }, + { + data: [ + 'IsExtraTag', + { + Key: 'Name2', + Value: 'MyLoadBalancer2', + }, + { + data: 'AWS::NoValue', + type: 'Ref', + isCfnFunction: true, + }, + ], + type: 'Fn::If', + isCfnFunction: true, + }, + ], + }, + })); + }); + + test('throws on dehydrated resources not present in the template', () => { + expect(() => { + includeTestTemplate(stack, 'intrinsics-tags-resource-validation.json', { + dehydratedResources: ['ResourceNotExistingHere'], + }); + }).toThrow(/Logical ID 'ResourceNotExistingHere' was specified in 'dehydratedResources', but does not belong to a resource in the template./); + }); }); interface IncludeTestTemplateProps { /** @default false */ readonly allowCyclicalReferences?: boolean; + + /** @default none */ + readonly dehydratedResources?: string[]; } function includeTestTemplate(scope: constructs.Construct, testTemplate: string, props: IncludeTestTemplateProps = {}): inc.CfnInclude { return new inc.CfnInclude(scope, 'MyScope', { templateFile: _testTemplateFilePath(testTemplate), allowCyclicalReferences: props.allowCyclicalReferences, + dehydratedResources: props.dehydratedResources, }); } diff --git a/packages/aws-cdk-lib/cloudformation-include/test/nested-stacks.test.ts b/packages/aws-cdk-lib/cloudformation-include/test/nested-stacks.test.ts index 6d43433c3b74b..06fb19716d3cf 100644 --- a/packages/aws-cdk-lib/cloudformation-include/test/nested-stacks.test.ts +++ b/packages/aws-cdk-lib/cloudformation-include/test/nested-stacks.test.ts @@ -743,6 +743,77 @@ describe('CDK Include for nested stacks', () => { }); }); }); + + describe('dehydrated resources', () => { + let parentStack: core.Stack; + let childStack: core.Stack; + + beforeEach(() => { + parentStack = new core.Stack(); + }); + + test('dehydrated resources are included in child templates, even if they are otherwise invalid', () => { + const parentTemplate = new inc.CfnInclude(parentStack, 'ParentStack', { + templateFile: testTemplateFilePath('parent-dehydrated.json'), + dehydratedResources: ['ASG'], + loadNestedStacks: { + 'ChildStack': { + templateFile: testTemplateFilePath('child-dehydrated.json'), + dehydratedResources: ['ChildASG'], + }, + }, + }); + childStack = parentTemplate.getNestedStack('ChildStack').stack; + + Template.fromStack(childStack).templateMatches({ + "Conditions": { + "SomeCondition": { + "Fn::Equals": [ + 2, + 2, + ], + }, + }, + "Resources": { + "ChildStackChildASGF815DFE9": { + "Type": "AWS::AutoScaling::AutoScalingGroup", + "Properties": { + "MaxSize": 10, + "MinSize": 1, + }, + "UpdatePolicy": { + "AutoScalingScheduledAction": { + "Fn::If": [ + "SomeCondition", + { + "IgnoreUnmodifiedGroupSizeProperties": true, + }, + { + "IgnoreUnmodifiedGroupSizeProperties": false, + }, + ], + }, + }, + }, + }, + }); + }); + + test('throws if a nested stack is marked dehydrated', () => { + expect(() => { + new inc.CfnInclude(parentStack, 'ParentStack', { + templateFile: testTemplateFilePath('parent-dehydrated.json'), + dehydratedResources: ['ChildStack'], + loadNestedStacks: { + 'ChildStack': { + templateFile: testTemplateFilePath('child-dehydrated.json'), + dehydratedResources: ['ChildASG'], + }, + }, + }); + }).toThrow(/nested stack 'ChildStack' was marked as dehydrated - nested stacks cannot be dehydrated/); + }); + }); }); function loadTestFileToJsObject(testTemplate: string): any { diff --git a/packages/aws-cdk-lib/cloudformation-include/test/test-templates/intrinsics-create-policy-autoscaling.json b/packages/aws-cdk-lib/cloudformation-include/test/test-templates/intrinsics-create-policy-autoscaling.json new file mode 100644 index 0000000000000..2730a163d5770 --- /dev/null +++ b/packages/aws-cdk-lib/cloudformation-include/test/test-templates/intrinsics-create-policy-autoscaling.json @@ -0,0 +1,23 @@ +{ + "Parameters": { + "MinSuccessfulInstancesPercent": { + "Type": "Number" + } + }, + "Resources": { + "AutoScalingCreationPolicyIntrinsic": { + "Type": "AWS::AutoScaling::AutoScalingGroup", + "Properties": { + "MinSize": "1", + "MaxSize": "5" + }, + "CreationPolicy": { + "AutoScalingCreationPolicy": { + "MinSuccessfulInstancesPercent": { + "Ref": "MinSuccessfulInstancesPercent" + } + } + } + } + } +} diff --git a/packages/aws-cdk-lib/cloudformation-include/test/test-templates/intrinsics-create-policy-complex.json b/packages/aws-cdk-lib/cloudformation-include/test/test-templates/intrinsics-create-policy-complex.json new file mode 100644 index 0000000000000..82f46093f68d4 --- /dev/null +++ b/packages/aws-cdk-lib/cloudformation-include/test/test-templates/intrinsics-create-policy-complex.json @@ -0,0 +1,42 @@ +{ + "Parameters": { + "CountParameter": { + "Type": "Number", + "Default": 3 + } + }, + "Conditions": { + "SomeCondition": { + "Fn::Equals": [ + 2, + 2 + ] + } + }, + "Resources": { + "ASG": { + "Type": "AWS::AutoScaling::AutoScalingGroup", + "Properties": { + "MinSize": "1", + "MaxSize": "5" + }, + "CreationPolicy": { + "AutoScalingCreationPolicy": { + "MinSuccessfulInstancesPercent": 50 + }, + "ResourceSignal": { + "Count": { + "Fn::If": [ + "SomeCondition", + { + "Ref": "CountParameter" + }, + 4 + ] + }, + "Timeout":"PT5H4M3S" + } + } + } + } +} diff --git a/packages/aws-cdk-lib/cloudformation-include/test/test-templates/intrinsics-create-policy-resource-signal.json b/packages/aws-cdk-lib/cloudformation-include/test/test-templates/intrinsics-create-policy-resource-signal.json new file mode 100644 index 0000000000000..40919f1e39b5b --- /dev/null +++ b/packages/aws-cdk-lib/cloudformation-include/test/test-templates/intrinsics-create-policy-resource-signal.json @@ -0,0 +1,24 @@ +{ + "Parameters": { + "CountParameter": { + "Type": "Number", + "Default": 3 + } + }, + "Resources": { + "ResourceSignalIntrinsic": { + "Type": "AWS::AutoScaling::AutoScalingGroup", + "Properties": { + "MinSize": "1", + "MaxSize": "5" + }, + "CreationPolicy": { + "ResourceSignal": { + "Count": { + "Ref": "CountParameter" + } + } + } + } + } +} \ No newline at end of file diff --git a/packages/aws-cdk-lib/cloudformation-include/test/test-templates/intrinsics-update-policy-autoscaling-replacing-update.json b/packages/aws-cdk-lib/cloudformation-include/test/test-templates/intrinsics-update-policy-autoscaling-replacing-update.json new file mode 100644 index 0000000000000..dd80cf6146a6c --- /dev/null +++ b/packages/aws-cdk-lib/cloudformation-include/test/test-templates/intrinsics-update-policy-autoscaling-replacing-update.json @@ -0,0 +1,22 @@ +{ + "Parameters": { + "WillReplace": { + "Type": "Boolean", + "Default": false + } + }, + "Resources": { + "ASG": { + "Type": "AWS::AutoScaling::AutoScalingGroup", + "Properties": { + "MinSize": "1", + "MaxSize": "10" + }, + "UpdatePolicy": { + "AutoScalingReplacingUpdate": { + "WillReplace" : { "Ref": "WillReplace" } + } + } + } + } +} \ No newline at end of file diff --git a/packages/aws-cdk-lib/cloudformation-include/test/test-templates/intrinsics-update-policy-autoscaling-rolling-update.json b/packages/aws-cdk-lib/cloudformation-include/test/test-templates/intrinsics-update-policy-autoscaling-rolling-update.json new file mode 100644 index 0000000000000..bd55715595887 --- /dev/null +++ b/packages/aws-cdk-lib/cloudformation-include/test/test-templates/intrinsics-update-policy-autoscaling-rolling-update.json @@ -0,0 +1,37 @@ +{ + "Parameters": { + "MinInstances": { + "Type": "Number", + "Default": 1 + }, + "MaxBatchSize": { + "Type": "Number", + "Default": 1 + }, + "PauseTime": { + "Type": "String", + "Default": "PT5M" + }, + "WaitOnResourceSignals": { + "Type": "Boolean", + "Default": true + } + }, + "Resources": { + "ASG": { + "Type": "AWS::AutoScaling::AutoScalingGroup", + "Properties": { + "MinSize": "1", + "MaxSize": "10" + }, + "UpdatePolicy": { + "AutoScalingRollingUpdate": { + "MinInstancesInService": { "Ref": "MinInstances" }, + "MaxBatchSize": { "Ref": "MaxBatchSize" }, + "PauseTime": { "Ref": "PauseTime" }, + "WaitOnResourceSignals": { "Ref": "WaitOnResourceSignals" } + } + } + } + } +} \ No newline at end of file diff --git a/packages/aws-cdk-lib/cloudformation-include/test/test-templates/intrinsics-update-policy-autoscaling-scheduled-action.json b/packages/aws-cdk-lib/cloudformation-include/test/test-templates/intrinsics-update-policy-autoscaling-scheduled-action.json new file mode 100644 index 0000000000000..27daa8a4f8972 --- /dev/null +++ b/packages/aws-cdk-lib/cloudformation-include/test/test-templates/intrinsics-update-policy-autoscaling-scheduled-action.json @@ -0,0 +1,24 @@ +{ + "Parameters": { + "IgnoreUnmodifiedGroupSizeProperties": { + "Type": "Boolean", + "Default": false + } + }, + "Resources": { + "ASG": { + "Type": "AWS::AutoScaling::AutoScalingGroup", + "Properties": { + "MinSize": "1", + "MaxSize": "10" + }, + "UpdatePolicy": { + "AutoScalingScheduledAction": { + "IgnoreUnmodifiedGroupSizeProperties": { + "Ref": "IgnoreUnmodifiedGroupSizeProperties" + } + } + } + } + } +} \ No newline at end of file diff --git a/packages/aws-cdk-lib/cloudformation-include/test/test-templates/intrinsics-update-policy-code-deploy-lambda-alias-update.json b/packages/aws-cdk-lib/cloudformation-include/test/test-templates/intrinsics-update-policy-code-deploy-lambda-alias-update.json new file mode 100644 index 0000000000000..382b04a767b89 --- /dev/null +++ b/packages/aws-cdk-lib/cloudformation-include/test/test-templates/intrinsics-update-policy-code-deploy-lambda-alias-update.json @@ -0,0 +1,34 @@ +{ + "Parameters": { + "ApplicationName": { + "Type": "String" + }, + "DeploymentGroupName": { + "Type": "String" + }, + "BeforeAllowTrafficHook": { + "Type": "String" + }, + "AfterAllowTrafficHook": { + "Type": "String" + } + }, + "Resources": { + "Alias": { + "Type": "AWS::Lambda::Alias", + "Properties": { + "FunctionName": "SomeLambda", + "FunctionVersion": "SomeVersion", + "Name": "MyAlias" + }, + "UpdatePolicy": { + "CodeDeployLambdaAliasUpdate": { + "ApplicationName": { "Ref": "ApplicationName" }, + "DeploymentGroupName": { "Ref": "DeploymentGroupName" }, + "BeforeAllowTrafficHook": { "Ref": "BeforeAllowTrafficHook" }, + "AfterAllowTrafficHook": { "Ref": "AfterAllowTrafficHook" } + } + } + } + } +} \ No newline at end of file diff --git a/packages/aws-cdk-lib/cloudformation-include/test/test-templates/intrinsics-update-policy-complex.json b/packages/aws-cdk-lib/cloudformation-include/test/test-templates/intrinsics-update-policy-complex.json new file mode 100644 index 0000000000000..6b0cdb351f00b --- /dev/null +++ b/packages/aws-cdk-lib/cloudformation-include/test/test-templates/intrinsics-update-policy-complex.json @@ -0,0 +1,33 @@ +{ + "Conditions": { + "SomeCondition": { + "Fn::Equals": [ + 2, + 2 + ] + } + }, + "Resources": { + "ASG": { + "Type": "AWS::AutoScaling::AutoScalingGroup", + "Properties": { + "MinSize": "1", + "MaxSize": "10" + }, + "UpdatePolicy": { + "AutoScalingRollingUpdate": { + "MinInstancesInService": { + "Fn::If": [ + "SomeCondition", + 1, + 2 + ] + }, + "MaxBatchSize": 2, + "PauseTime": "PT5M", + "WaitOnResourceSignals": true + } + } + } + } +} \ No newline at end of file diff --git a/packages/aws-cdk-lib/cloudformation-include/test/test-templates/invalid/intrinsics-create-policy-autoscaling.json b/packages/aws-cdk-lib/cloudformation-include/test/test-templates/invalid/intrinsics-create-policy-autoscaling.json new file mode 100644 index 0000000000000..9d6cfaddf2df2 --- /dev/null +++ b/packages/aws-cdk-lib/cloudformation-include/test/test-templates/invalid/intrinsics-create-policy-autoscaling.json @@ -0,0 +1,28 @@ +{ + "Conditions": { + "SomeCondition": { + "Fn::Equals": [ + 2, + 2 + ] + } + }, + "Resources": { + "AutoScalingCreationPolicyIntrinsic": { + "Type": "AWS::AutoScaling::AutoScalingGroup", + "Properties": { + "MinSize": 1, + "MaxSize": 5 + }, + "CreationPolicy": { + "AutoScalingCreationPolicy": { + "Fn::If": [ + "SomeCondition", + { "MinSuccessfulInstancesPercent": 50 }, + { "MinSuccessfulInstancesPercent": 25 } + ] + } + } + } + } +} diff --git a/packages/aws-cdk-lib/cloudformation-include/test/test-templates/invalid/intrinsics-create-policy-resource-signal.json b/packages/aws-cdk-lib/cloudformation-include/test/test-templates/invalid/intrinsics-create-policy-resource-signal.json new file mode 100644 index 0000000000000..5fc823d4731d5 --- /dev/null +++ b/packages/aws-cdk-lib/cloudformation-include/test/test-templates/invalid/intrinsics-create-policy-resource-signal.json @@ -0,0 +1,36 @@ +{ + "Parameters": { + "CountParameter": { + "Type": "Number", + "Default": 3 + } + }, + "Conditions": { + "UseCountParameter": { + "Fn::Equals": [ + 2, + 2 + ] + } + }, + "Resources": { + "ResourceSignalIntrinsic": { + "Type": "AWS::AutoScaling::AutoScalingGroup", + "Properties": { + "MinSize": 1, + "MaxSize": 5 + }, + "CreationPolicy": { + "ResourceSignal": { + "Fn::If": [ + "UseCountParameter", + { + "Count": { "Ref": "CountParameter" } + }, + 5 + ] + } + } + } + } +} diff --git a/packages/aws-cdk-lib/cloudformation-include/test/test-templates/invalid/intrinsics-create-policy.json b/packages/aws-cdk-lib/cloudformation-include/test/test-templates/invalid/intrinsics-create-policy.json new file mode 100644 index 0000000000000..2afe984191fcc --- /dev/null +++ b/packages/aws-cdk-lib/cloudformation-include/test/test-templates/invalid/intrinsics-create-policy.json @@ -0,0 +1,42 @@ +{ + "Parameters": { + "CountParameter": { + "Type": "Number", + "Default": 3 + } + }, + "Conditions": { + "SomeCondition": { + "Fn::Equals": [ + 2, + 2 + ] + } + }, + "Resources": { + "CreationPolicyIntrinsic": { + "Type": "AWS::AutoScaling::AutoScalingGroup", + "Properties": { + "MinSize": 1, + "MaxSize": 5 + }, + "CreationPolicy": { + "Fn::If": [ + "SomeCondition", + { + "AutoScalingCreationPolicy": { + "MinSuccessfulInstancesPercent": 50 + }, + "ResourceSignal": { + "Count": 5, + "Timeout": "PT5H4M3S" + } + }, + { + "Ref": "AWS::NoValue" + } + ] + } + } + } +} diff --git a/packages/aws-cdk-lib/cloudformation-include/test/test-templates/invalid/intrinsics-tags-resource-validation.json b/packages/aws-cdk-lib/cloudformation-include/test/test-templates/invalid/intrinsics-tags-resource-validation.json new file mode 100644 index 0000000000000..b162016bb2577 --- /dev/null +++ b/packages/aws-cdk-lib/cloudformation-include/test/test-templates/invalid/intrinsics-tags-resource-validation.json @@ -0,0 +1,54 @@ +{ + "AWSTemplateFormatVersion": "2010-09-09", + "Parameters": { + "IsExtraTag": { + "Type": "String", + "AllowedValues": [ + "true", + "false" + ], + "Default": "false" + } + }, + "Conditions": { + "AddExtraTag": { + "Fn::Equals": [ + { + "data": "IsExtraTag", + "type": "Ref", + "isCfnFunction": true + }, + "true" + ] + } + }, + "Resources": { + "MyLoadBalancer": { + "Type": "AWS::ElasticLoadBalancingV2::LoadBalancer", + "Properties": { + "Tags": [ + { + "Key": "Name", + "Value": "MyLoadBalancer" + }, + { + "data": [ + "IsExtraTag", + { + "Key": "Name2", + "Value": "MyLoadBalancer2" + }, + { + "data": "AWS::NoValue", + "type": "Ref", + "isCfnFunction": true + } + ], + "type": "Fn::If", + "isCfnFunction": true + } + ] + } + } + } +} \ No newline at end of file diff --git a/packages/aws-cdk-lib/cloudformation-include/test/test-templates/invalid/intrinsics-update-policy-autoscaling-replacing-update.json b/packages/aws-cdk-lib/cloudformation-include/test/test-templates/invalid/intrinsics-update-policy-autoscaling-replacing-update.json new file mode 100644 index 0000000000000..e302385e89139 --- /dev/null +++ b/packages/aws-cdk-lib/cloudformation-include/test/test-templates/invalid/intrinsics-update-policy-autoscaling-replacing-update.json @@ -0,0 +1,32 @@ +{ + "Conditions": { + "SomeCondition": { + "Fn::Equals": [ + 2, + 2 + ] + } + }, + "Resources": { + "ASG": { + "Type": "AWS::AutoScaling::AutoScalingGroup", + "Properties": { + "MaxSize": 10, + "MinSize": 1 + }, + "UpdatePolicy": { + "AutoScalingReplacingUpdate": { + "Fn::If": [ + "SomeCondition", + { + "WillReplace" : true + }, + { + "WillReplace" : false + } + ] + } + } + } + } +} \ No newline at end of file diff --git a/packages/aws-cdk-lib/cloudformation-include/test/test-templates/invalid/intrinsics-update-policy-autoscaling-rolling-update.json b/packages/aws-cdk-lib/cloudformation-include/test/test-templates/invalid/intrinsics-update-policy-autoscaling-rolling-update.json new file mode 100644 index 0000000000000..8d793770dda2a --- /dev/null +++ b/packages/aws-cdk-lib/cloudformation-include/test/test-templates/invalid/intrinsics-update-policy-autoscaling-rolling-update.json @@ -0,0 +1,38 @@ +{ + "Conditions": { + "SomeCondition": { + "Fn::Equals": [ + 2, + 2 + ] + } + }, + "Resources": { + "ASG": { + "Type": "AWS::AutoScaling::AutoScalingGroup", + "Properties": { + "MaxSize": 10, + "MinSize": 1 + }, + "UpdatePolicy": { + "AutoScalingRollingUpdate": { + "Fn::If": [ + "SomeCondition", + { + "MinInstancesInService": 1, + "MaxBatchSize": 2, + "PauseTime": "PT5M", + "WaitOnResourceSignals": "true" + }, + { + "MinInstancesInService": 1, + "MaxBatchSize": 2, + "PauseTime": "PT5M", + "WaitOnResourceSignals": "true" + } + ] + } + } + } + } +} \ No newline at end of file diff --git a/packages/aws-cdk-lib/cloudformation-include/test/test-templates/invalid/intrinsics-update-policy-autoscaling-scheduled-action.json b/packages/aws-cdk-lib/cloudformation-include/test/test-templates/invalid/intrinsics-update-policy-autoscaling-scheduled-action.json new file mode 100644 index 0000000000000..2a6141f18fffb --- /dev/null +++ b/packages/aws-cdk-lib/cloudformation-include/test/test-templates/invalid/intrinsics-update-policy-autoscaling-scheduled-action.json @@ -0,0 +1,32 @@ +{ + "Conditions": { + "SomeCondition": { + "Fn::Equals": [ + 2, + 2 + ] + } + }, + "Resources": { + "ASG": { + "Type": "AWS::AutoScaling::AutoScalingGroup", + "Properties": { + "MaxSize": 10, + "MinSize": 1 + }, + "UpdatePolicy": { + "AutoScalingScheduledAction": { + "Fn::If": [ + "SomeCondition", + { + "IgnoreUnmodifiedGroupSizeProperties" : true + }, + { + "IgnoreUnmodifiedGroupSizeProperties" : false + } + ] + } + } + } + } +} \ No newline at end of file diff --git a/packages/aws-cdk-lib/cloudformation-include/test/test-templates/invalid/intrinsics-update-policy-code-deploy-lambda-alias-update.json b/packages/aws-cdk-lib/cloudformation-include/test/test-templates/invalid/intrinsics-update-policy-code-deploy-lambda-alias-update.json new file mode 100644 index 0000000000000..92911296e5764 --- /dev/null +++ b/packages/aws-cdk-lib/cloudformation-include/test/test-templates/invalid/intrinsics-update-policy-code-deploy-lambda-alias-update.json @@ -0,0 +1,39 @@ +{ + "Conditions": { + "SomeCondition": { + "Fn::Equals": [ + 2, + 2 + ] + } + }, + "Resources": { + "Alias": { + "Type": "AWS::Lambda::Alias", + "Properties": { + "FunctionName": "SomeLambda", + "FunctionVersion": "SomeVersion", + "Name": "MyAlias" + }, + "UpdatePolicy": { + "CodeDeployLambdaAliasUpdate": { + "Fn::If": [ + "SomeCondition", + { + "ApplicationName": "SomeApp", + "DeploymentGroupName": "SomeDeploymentGroup", + "BeforeAllowTrafficHook": "SomeHook", + "AfterAllowTrafficHook": "SomeOtherHook" + }, + { + "ApplicationName": "SomeOtherApp", + "DeploymentGroupName": "SomeOtherDeploymentGroup", + "BeforeAllowTrafficHook": "SomeOtherHook", + "AfterAllowTrafficHook": "SomeOtherOtherHook" + } + ] + } + } + } + } +} \ No newline at end of file diff --git a/packages/aws-cdk-lib/cloudformation-include/test/test-templates/invalid/intrinsics-update-policy.json b/packages/aws-cdk-lib/cloudformation-include/test/test-templates/invalid/intrinsics-update-policy.json new file mode 100644 index 0000000000000..fe284e4e05828 --- /dev/null +++ b/packages/aws-cdk-lib/cloudformation-include/test/test-templates/invalid/intrinsics-update-policy.json @@ -0,0 +1,40 @@ +{ + "Conditions": { + "SomeCondition": { + "Fn::Equals": [ + 2, + 2 + ] + } + }, + "Resources": { + "ASG": { + "Type": "AWS::AutoScaling::AutoScalingGroup", + "Properties": { + "MaxSize": 10, + "MinSize": 1 + }, + "UpdatePolicy": { + "Fn::If": [ + "SomeCondition", + { + "AutoScalingRollingUpdate": { + "MinInstancesInService": 1, + "MaxBatchSize": 2, + "PauseTime": "PT5M", + "WaitOnResourceSignals": "true" + } + }, + { + "AutoScalingRollingUpdate": { + "MinInstancesInService": 3, + "MaxBatchSize": 4, + "PauseTime": "PT5M", + "WaitOnResourceSignals": "false" + } + } + ] + } + } + } +} \ No newline at end of file diff --git a/packages/aws-cdk-lib/cloudformation-include/test/test-templates/invalid/resource-all-attributes.json b/packages/aws-cdk-lib/cloudformation-include/test/test-templates/invalid/resource-all-attributes.json new file mode 100644 index 0000000000000..03316390e4e3b --- /dev/null +++ b/packages/aws-cdk-lib/cloudformation-include/test/test-templates/invalid/resource-all-attributes.json @@ -0,0 +1,27 @@ +{ + "Resources": { + "Foo": { + "Type": "AWS::Foo::Bar", + "Properties": { + "Blinky": "Pinky" + }, + "CreationPolicy": { + "Inky": "Clyde" + }, + "UpdatePolicy": { + "Foo": "Bar" + }, + "DeletionPolicy": { + "DeletionPolicyKey": "DeletionPolicyValue" + }, + "UpdateReplacePolicy": { + "Oh": "No" + }, + "Version": "1.2.3.4.5.6" , + "Description": "This resource does not match the spec, but it does have every possible attribute", + "Metadata": { + "SomeKey": "SomeValue" + } + } + } +} \ No newline at end of file diff --git a/packages/aws-cdk-lib/cloudformation-include/test/test-templates/tags-with-invalid-intrinsics.json b/packages/aws-cdk-lib/cloudformation-include/test/test-templates/invalid/tags-with-invalid-intrinsics.json similarity index 100% rename from packages/aws-cdk-lib/cloudformation-include/test/test-templates/tags-with-invalid-intrinsics.json rename to packages/aws-cdk-lib/cloudformation-include/test/test-templates/invalid/tags-with-invalid-intrinsics.json diff --git a/packages/aws-cdk-lib/cloudformation-include/test/test-templates/nested/child-dehydrated.json b/packages/aws-cdk-lib/cloudformation-include/test/test-templates/nested/child-dehydrated.json new file mode 100644 index 0000000000000..b390fdc70d22b --- /dev/null +++ b/packages/aws-cdk-lib/cloudformation-include/test/test-templates/nested/child-dehydrated.json @@ -0,0 +1,32 @@ +{ + "Conditions": { + "SomeCondition": { + "Fn::Equals": [ + 2, + 2 + ] + } + }, + "Resources": { + "ChildASG": { + "Type": "AWS::AutoScaling::AutoScalingGroup", + "Properties": { + "MaxSize": 10, + "MinSize": 1 + }, + "UpdatePolicy": { + "AutoScalingScheduledAction": { + "Fn::If": [ + "SomeCondition", + { + "IgnoreUnmodifiedGroupSizeProperties" : true + }, + { + "IgnoreUnmodifiedGroupSizeProperties" : false + } + ] + } + } + } + } +} \ No newline at end of file diff --git a/packages/aws-cdk-lib/cloudformation-include/test/test-templates/nested/parent-dehydrated.json b/packages/aws-cdk-lib/cloudformation-include/test/test-templates/nested/parent-dehydrated.json new file mode 100644 index 0000000000000..ee0b92688962a --- /dev/null +++ b/packages/aws-cdk-lib/cloudformation-include/test/test-templates/nested/parent-dehydrated.json @@ -0,0 +1,41 @@ +{ + "Conditions": { + "SomeCondition": { + "Fn::Equals": [ + 2, + 2 + ] + } + }, + "Resources": { + "ASG": { + "Type": "AWS::AutoScaling::AutoScalingGroup", + "Properties": { + "MaxSize": 10, + "MinSize": 1 + }, + "UpdatePolicy": { + "AutoScalingScheduledAction": { + "Fn::If": [ + "SomeCondition", + { + "IgnoreUnmodifiedGroupSizeProperties" : true + }, + { + "IgnoreUnmodifiedGroupSizeProperties" : false + } + ] + } + } + }, + "ChildStack": { + "Type": "AWS::CloudFormation::Stack", + "Properties": { + "TemplateURL": "https://cfn-templates-set.s3.amazonaws.com/child-dehydrated-stack.json", + "Parameters": { + "SomeParam": "SomeValue" + } + } + } + } +} diff --git a/packages/aws-cdk-lib/cloudformation-include/test/test-templates/resource-attribute-update-policy.json b/packages/aws-cdk-lib/cloudformation-include/test/test-templates/resource-attribute-update-policy.json deleted file mode 100644 index e1440a46193be..0000000000000 --- a/packages/aws-cdk-lib/cloudformation-include/test/test-templates/resource-attribute-update-policy.json +++ /dev/null @@ -1,61 +0,0 @@ -{ - "Parameters": { - "WaitOnResourceSignals": { - "Type": "String", - "Default": "true" - } - }, - "Resources": { - "CodeDeployApp": { - "Type": "AWS::CodeDeploy::Application" - }, - "CodeDeployDg": { - "Type": "AWS::CodeDeploy::DeploymentGroup", - "Properties": { - "ApplicationName": { "Ref": "CodeDeployApp" }, - "ServiceRoleArn": "my-role-arn" - } - }, - "Bucket": { - "Type": "AWS::S3::Bucket", - "UpdatePolicy": { - "AutoScalingReplacingUpdate": { - "WillReplace": false - }, - "AutoScalingRollingUpdate": { - "MaxBatchSize" : 1, - "MinInstancesInService" : 2, - "MinSuccessfulInstancesPercent" : 3, - "PauseTime" : "PT4M3S", - "SuspendProcesses" : [ - "Launch", - "Terminate", - "HealthCheck", - "ReplaceUnhealthy", - "AZRebalance", - "AlarmNotification", - "ScheduledActions", - "AddToLoadBalancer" - ], - "WaitOnResourceSignals" : { - "Fn::Equals": [ - "true", - { "Ref": "WaitOnResourceSignals" } - ] - } - }, - "AutoScalingScheduledAction": { - "IgnoreUnmodifiedGroupSizeProperties": true - }, - "CodeDeployLambdaAliasUpdate" : { - "AfterAllowTrafficHook" : "Lambda1", - "ApplicationName" : { "Ref": "CodeDeployApp" }, - "BeforeAllowTrafficHook" : "Lambda2", - "DeploymentGroupName" : { "Ref": "CodeDeployDg" } - }, - "EnableVersionUpgrade": true, - "UseOnlineResharding": false - } - } - } -} diff --git a/packages/aws-cdk-lib/cloudformation-include/test/test-templates/update-policy-with-intrinsics.json b/packages/aws-cdk-lib/cloudformation-include/test/test-templates/update-policy-with-intrinsics.json new file mode 100644 index 0000000000000..607443aa6c129 --- /dev/null +++ b/packages/aws-cdk-lib/cloudformation-include/test/test-templates/update-policy-with-intrinsics.json @@ -0,0 +1,47 @@ +{ + "Conditions": { + "AutoReplaceHosts": { + "Fn::Equals": [ + 2, + 2 + ] + }, + "SetMinInstancesInServiceToZero": { + "Fn::Equals": [ + 2, + 2 + ] + } + }, + "Resources": { + "ASG": { + "Type": "AWS::AutoScaling::AutoScalingGroup", + "Properties": { + "MaxSize": 10, + "MinSize": 1 + }, + "UpdatePolicy": { + "AutoScalingRollingUpdate": { + "Fn::If": [ + "AutoReplaceHosts", + { + "MinInstancesInService": { + "Fn::If": [ + "SetMinInstancesInServiceToZero", + 0, + 1 + ] + }, + "MaxBatchSize": 2, + "PauseTime": "PT5M", + "WaitOnResourceSignals": "true" + }, + { + "Ref": "AWS::NoValue" + } + ] + } + } + } + } +} \ No newline at end of file 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 8876e5c3fe50a..2f7b132f4aaf8 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 @@ -611,24 +611,91 @@ describe('CDK Include', () => { }); }); - test('correctly handles the CreationPolicy resource attribute', () => { - const cfnTemplate = includeTestTemplate(stack, 'resource-attribute-creation-policy.json'); - const cfnBucket = cfnTemplate.getResource('Bucket'); + test('Intrinsics can be used in the leaf nodes of autoscaling creation policy', () => { + const cfnTemplate = includeTestTemplate(stack, 'intrinsics-create-policy-autoscaling.json'); + const cfnBucket = cfnTemplate.getResource('AutoScalingCreationPolicyIntrinsic'); expect(cfnBucket.cfnOptions.creationPolicy).toBeDefined(); Template.fromStack(stack).templateMatches( - loadTestFileToJsObject('resource-attribute-creation-policy.json'), + loadTestFileToJsObject('intrinsics-create-policy-autoscaling.json'), ); }); - test('correctly handles the UpdatePolicy resource attribute', () => { - const cfnTemplate = includeTestTemplate(stack, 'resource-attribute-update-policy.json'); - const cfnBucket = cfnTemplate.getResource('Bucket'); + test('Nested intrinsics can be used in the leaf nodes of autoscaling creation policy', () => { + const cfnTemplate = includeTestTemplate(stack, 'intrinsics-create-policy-complex.json'); + const cfnBucket = cfnTemplate.getResource('ASG'); + + expect(cfnBucket.cfnOptions.creationPolicy).toBeDefined(); + + Template.fromStack(stack).templateMatches( + loadTestFileToJsObject('intrinsics-create-policy-complex.json'), + ); + }); + + test('intrinsics can be used in the leaf nodes of autoscaling resource signal creation policy', () => { + const cfnTemplate = includeTestTemplate(stack, 'intrinsics-create-policy-resource-signal.json'); + const cfnBucket = cfnTemplate.getResource('ResourceSignalIntrinsic'); + + expect(cfnBucket.cfnOptions.creationPolicy).toBeDefined(); + + Template.fromStack(stack).templateMatches( + loadTestFileToJsObject('intrinsics-create-policy-resource-signal.json'), + ); + }); + + test('Intrinsics can be used in the leaf nodes of autoscaling rolling update policy', () => { + const cfnTemplate = includeTestTemplate(stack, 'intrinsics-update-policy-autoscaling-rolling-update.json'); + const cfnBucket = cfnTemplate.getResource('ASG'); + + expect(cfnBucket.cfnOptions.updatePolicy).toBeDefined(); + + Template.fromStack(stack).templateMatches( + loadTestFileToJsObject('intrinsics-update-policy-autoscaling-rolling-update.json'), + ); + }); + + test('Intrinsics can be used in the leaf nodes of autoscaling replacing update policy', () => { + const cfnTemplate = includeTestTemplate(stack, 'intrinsics-update-policy-autoscaling-replacing-update.json'); + const cfnBucket = cfnTemplate.getResource('ASG'); + + expect(cfnBucket.cfnOptions.updatePolicy).toBeDefined(); + + Template.fromStack(stack).templateMatches( + loadTestFileToJsObject('intrinsics-update-policy-autoscaling-replacing-update.json'), + ); + }); + + test('Intrinsics can be used in the leaf nodes of autoscaling scheduled-action update policy', () => { + const cfnTemplate = includeTestTemplate(stack, 'intrinsics-update-policy-autoscaling-scheduled-action.json'); + const cfnBucket = cfnTemplate.getResource('ASG'); + + expect(cfnBucket.cfnOptions.updatePolicy).toBeDefined(); + + Template.fromStack(stack).templateMatches( + loadTestFileToJsObject('intrinsics-update-policy-autoscaling-scheduled-action.json'), + ); + }); + + test('Intrinsics can be used in the leaf nodes of code deploy lambda alias update policy', () => { + const cfnTemplate = includeTestTemplate(stack, 'intrinsics-update-policy-code-deploy-lambda-alias-update.json'); + const cfnBucket = cfnTemplate.getResource('Alias'); + + expect(cfnBucket.cfnOptions.updatePolicy).toBeDefined(); + + Template.fromStack(stack).templateMatches( + loadTestFileToJsObject('intrinsics-update-policy-code-deploy-lambda-alias-update.json'), + ); + }); + + test('Nested Intrinsics can be used in the leaf nodes of autoscaling rolling update policy', () => { + const cfnTemplate = includeTestTemplate(stack, 'intrinsics-update-policy-complex.json'); + const cfnBucket = cfnTemplate.getResource('ASG'); expect(cfnBucket.cfnOptions.updatePolicy).toBeDefined(); + Template.fromStack(stack).templateMatches( - loadTestFileToJsObject('resource-attribute-update-policy.json'), + loadTestFileToJsObject('intrinsics-update-policy-complex.json'), ); }); @@ -1129,12 +1196,6 @@ describe('CDK Include', () => { loadTestFileToJsObject('tags-with-intrinsics.json'), ); }); - - test('throws an exception if Tags contains invalid intrinsics', () => { - expect(() => { - includeTestTemplate(stack, 'tags-with-invalid-intrinsics.json'); - }).toThrow(/expression does not exist in the template/); - }); }); interface IncludeTestTemplateProps { 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 fbbb26c0c566e..839684b2e791f 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 @@ -1,3 +1,4 @@ +import { CFN_INCLUDE_REJECT_COMPLEX_RESOURCE_UPDATE_CREATE_POLICY_INTRINSICS } from '../../../cx-api'; import { CfnCondition } from '../cfn-condition'; import { CfnElement } from '../cfn-element'; import { Fn } from '../cfn-fn'; @@ -9,10 +10,12 @@ import { CfnCreationPolicy, CfnDeletionPolicy, CfnResourceAutoScalingCreationPolicy, CfnResourceSignal, CfnUpdatePolicy, } from '../cfn-resource-policy'; import { CfnTag } from '../cfn-tag'; +import { FeatureFlags } from '../feature-flags'; import { Lazy } from '../lazy'; import { CfnReference, ReferenceRendering } from '../private/cfn-reference'; import { IResolvable } from '../resolvable'; import { Validator } from '../runtime'; +import { Stack } from '../stack'; import { isResolvableObject, Token } from '../token'; import { undefinedIfAllValuesAreEmpty } from '../util'; @@ -343,6 +346,7 @@ export interface ParseCfnOptions { */ export class CfnParser { private readonly options: ParseCfnOptions; + private stack?: Stack; constructor(options: ParseCfnOptions) { this.options = options; @@ -350,9 +354,10 @@ export class CfnParser { public handleAttributes(resource: CfnResource, resourceAttributes: any, logicalId: string): void { const cfnOptions = resource.cfnOptions; + this.stack = Stack.of(resource); - cfnOptions.creationPolicy = this.parseCreationPolicy(resourceAttributes.CreationPolicy); - cfnOptions.updatePolicy = this.parseUpdatePolicy(resourceAttributes.UpdatePolicy); + cfnOptions.creationPolicy = this.parseCreationPolicy(resourceAttributes.CreationPolicy, logicalId); + cfnOptions.updatePolicy = this.parseUpdatePolicy(resourceAttributes.UpdatePolicy, logicalId); cfnOptions.deletionPolicy = this.parseDeletionPolicy(resourceAttributes.DeletionPolicy); cfnOptions.updateReplacePolicy = this.parseDeletionPolicy(resourceAttributes.UpdateReplacePolicy); cfnOptions.version = this.parseValue(resourceAttributes.Version); @@ -381,8 +386,10 @@ export class CfnParser { } } - private parseCreationPolicy(policy: any): CfnCreationPolicy | undefined { + private parseCreationPolicy(policy: any, logicalId: string): CfnCreationPolicy | undefined { if (typeof policy !== 'object') { return undefined; } + this.throwIfIsIntrinsic(policy, logicalId); + const self = this; // change simple JS values to their CDK equivalents policy = this.parseValue(policy); @@ -393,6 +400,7 @@ export class CfnParser { }); function parseAutoScalingCreationPolicy(p: any): CfnResourceAutoScalingCreationPolicy | undefined { + self.throwIfIsIntrinsic(p, logicalId); if (typeof p !== 'object') { return undefined; } return undefinedIfAllValuesAreEmpty({ @@ -402,6 +410,7 @@ export class CfnParser { function parseResourceSignal(p: any): CfnResourceSignal | undefined { if (typeof p !== 'object') { return undefined; } + self.throwIfIsIntrinsic(p, logicalId); return undefinedIfAllValuesAreEmpty({ count: FromCloudFormation.getNumber(p.Count).value, @@ -410,8 +419,10 @@ export class CfnParser { } } - private parseUpdatePolicy(policy: any): CfnUpdatePolicy | undefined { + private parseUpdatePolicy(policy: any, logicalId: string): CfnUpdatePolicy | undefined { if (typeof policy !== 'object') { return undefined; } + this.throwIfIsIntrinsic(policy, logicalId); + const self = this; // change simple JS values to their CDK equivalents policy = this.parseValue(policy); @@ -427,6 +438,7 @@ export class CfnParser { function parseAutoScalingReplacingUpdate(p: any): CfnAutoScalingReplacingUpdate | undefined { if (typeof p !== 'object') { return undefined; } + self.throwIfIsIntrinsic(p, logicalId); return undefinedIfAllValuesAreEmpty({ willReplace: p.WillReplace, @@ -435,6 +447,7 @@ export class CfnParser { function parseAutoScalingRollingUpdate(p: any): CfnAutoScalingRollingUpdate | undefined { if (typeof p !== 'object') { return undefined; } + self.throwIfIsIntrinsic(p, logicalId); return undefinedIfAllValuesAreEmpty({ maxBatchSize: FromCloudFormation.getNumber(p.MaxBatchSize).value, @@ -447,6 +460,7 @@ export class CfnParser { } function parseCodeDeployLambdaAliasUpdate(p: any): CfnCodeDeployLambdaAliasUpdate | undefined { + self.throwIfIsIntrinsic(p, logicalId); if (typeof p !== 'object') { return undefined; } return { @@ -458,6 +472,7 @@ export class CfnParser { } function parseAutoScalingScheduledAction(p: any): CfnAutoScalingScheduledAction | undefined { + self.throwIfIsIntrinsic(p, logicalId); if (typeof p !== 'object') { return undefined; } return undefinedIfAllValuesAreEmpty({ @@ -674,6 +689,20 @@ export class CfnParser { } } + private throwIfIsIntrinsic(object: object, logicalId: string): void { + // Top-level parsing functions check before we call `parseValue`, which requires + // calling `looksLikeCfnIntrinsic`. Helper parsing functions check after we call + // `parseValue`, which requires calling `isResolvableObject`. + if (!this.stack) { + throw new Error('cannot call this method before handleAttributes!'); + } + if (FeatureFlags.of(this.stack).isEnabled(CFN_INCLUDE_REJECT_COMPLEX_RESOURCE_UPDATE_CREATE_POLICY_INTRINSICS)) { + if (isResolvableObject(object ?? {}) || this.looksLikeCfnIntrinsic(object ?? {})) { + throw new Error(`Cannot convert resource '${logicalId}' to CDK objects: it uses an intrinsic in a resource update or deletion policy to represent a non-primitive value. Specify '${logicalId}' in the 'dehydratedResources' prop to skip parsing this resource, while still including it in the output.`); + } + } + } + private looksLikeCfnIntrinsic(object: object): string | undefined { const objectKeys = Object.keys(object); // a CFN intrinsic is always an object with a single key diff --git a/packages/aws-cdk-lib/cx-api/FEATURE_FLAGS.md b/packages/aws-cdk-lib/cx-api/FEATURE_FLAGS.md index 2de4a12515cb1..c2813160d9f03 100644 --- a/packages/aws-cdk-lib/cx-api/FEATURE_FLAGS.md +++ b/packages/aws-cdk-lib/cx-api/FEATURE_FLAGS.md @@ -77,6 +77,7 @@ Flags come in three types: | [@aws-cdk/aws-ec2:ec2SumTImeoutEnabled](#aws-cdkaws-ec2ec2sumtimeoutenabled) | When enabled, initOptions.timeout and resourceSignalTimeout values will be summed together. | 2.160.0 | (fix) | | [@aws-cdk/aws-appsync:appSyncGraphQLAPIScopeLambdaPermission](#aws-cdkaws-appsyncappsyncgraphqlapiscopelambdapermission) | When enabled, a Lambda authorizer Permission created when using GraphqlApi will be properly scoped with a SourceArn. | V2NEXT | (fix) | | [@aws-cdk/aws-rds:setCorrectValueForDatabaseInstanceReadReplicaInstanceResourceId](#aws-cdkaws-rdssetcorrectvaluefordatabaseinstancereadreplicainstanceresourceid) | When enabled, the value of property `instanceResourceId` in construct `DatabaseInstanceReadReplica` will be set to the correct value which is `DbiResourceId` instead of currently `DbInstanceArn` | V2NEXT | (fix) | +| [@aws-cdk/core:cfnIncludeRejectComplexResourceUpdateCreatePolicyIntrinsics](#aws-cdkcorecfnincluderejectcomplexresourceupdatecreatepolicyintrinsics) | When enabled, CFN templates added with `cfn-include` will error if the template contains Resource Update or Create policies with CFN Intrinsics that include non-primitive values. | V2NEXT | (fix) | @@ -141,8 +142,9 @@ The following json shows the current recommended set of flags, as `cdk init` wou "@aws-cdk/aws-s3:keepNotificationInImportedBucket": false, "@aws-cdk/aws-ecs:reduceEc2FargateCloudWatchPermissions": true, "@aws-cdk/aws-ec2:ec2SumTImeoutEnabled": true, - "@aws-cdk/aws-appsync:appSyncGraphQLAPIScopeLambdaPermission": true - "@aws-cdk/aws-rds:setCorrectValueForDatabaseInstanceReadReplicaInstanceResourceId": true + "@aws-cdk/aws-appsync:appSyncGraphQLAPIScopeLambdaPermission": true, + "@aws-cdk/aws-rds:setCorrectValueForDatabaseInstanceReadReplicaInstanceResourceId": true, + "@aws-cdk/core:cfnIncludeRejectComplexResourceUpdateCreatePolicyIntrinsics": true } } ``` @@ -1455,4 +1457,19 @@ When this feature flag is enabled, the value of that property will be as expecte **Compatibility with old behavior:** Disable the feature flag to use `DbInstanceArn` as value for property `instanceResourceId` +### @aws-cdk/core:cfnIncludeRejectComplexResourceUpdateCreatePolicyIntrinsics + +*When enabled, CFN templates added with `cfn-include` will error if the template contains Resource Update or Create policies with CFN Intrinsics that include non-primitive values.* (fix) + +Without enabling this feature flag, `cfn-include` will silently drop resource update or create policies that contain CFN Intrinsics if they include non-primitive values. + +Enabling this feature flag will make `cfn-include` throw on these templates, unless you specify the logical ID of the resource in the 'unhydratedResources' property. + + +| Since | Default | Recommended | +| ----- | ----- | ----- | +| (not in v1) | | | +| V2NEXT | `false` | `true` | + + diff --git a/packages/aws-cdk-lib/cx-api/lib/features.ts b/packages/aws-cdk-lib/cx-api/lib/features.ts index b89ad272b63a4..f31d2d3b78687 100644 --- a/packages/aws-cdk-lib/cx-api/lib/features.ts +++ b/packages/aws-cdk-lib/cx-api/lib/features.ts @@ -111,6 +111,7 @@ export const REDUCE_EC2_FARGATE_CLOUDWATCH_PERMISSIONS = '@aws-cdk/aws-ecs:reduc export const EC2_SUM_TIMEOUT_ENABLED = '@aws-cdk/aws-ec2:ec2SumTImeoutEnabled'; export const APPSYNC_GRAPHQLAPI_SCOPE_LAMBDA_FUNCTION_PERMISSION = '@aws-cdk/aws-appsync:appSyncGraphQLAPIScopeLambdaPermission'; export const USE_CORRECT_VALUE_FOR_INSTANCE_RESOURCE_ID_PROPERTY = '@aws-cdk/aws-rds:setCorrectValueForDatabaseInstanceReadReplicaInstanceResourceId'; +export const CFN_INCLUDE_REJECT_COMPLEX_RESOURCE_UPDATE_CREATE_POLICY_INTRINSICS = '@aws-cdk/core:cfnIncludeRejectComplexResourceUpdateCreatePolicyIntrinsics'; export const FLAGS: Record = { ////////////////////////////////////////////////////////////////////// @@ -1189,6 +1190,19 @@ export const FLAGS: Record = { recommendedValue: true, compatibilityWithOldBehaviorMd: 'Disable the feature flag to use `DbInstanceArn` as value for property `instanceResourceId`', }, + + ////////////////////////////////////////////////////////////////////// + [CFN_INCLUDE_REJECT_COMPLEX_RESOURCE_UPDATE_CREATE_POLICY_INTRINSICS]: { + type: FlagType.BugFix, + summary: 'When enabled, CFN templates added with `cfn-include` will error if the template contains Resource Update or Create policies with CFN Intrinsics that include non-primitive values.', + detailsMd: ` + Without enabling this feature flag, \`cfn-include\` will silently drop resource update or create policies that contain CFN Intrinsics if they include non-primitive values. + + Enabling this feature flag will make \`cfn-include\` throw on these templates, unless you specify the logical ID of the resource in the 'unhydratedResources' property. + `, + recommendedValue: true, + introducedIn: { v2: 'V2NEXT' }, + }, }; const CURRENT_MV = 'v2';