From 941036100f1633a5693075ea07477122bab9cbb2 Mon Sep 17 00:00:00 2001 From: Calvin Combs <66279577+comcalvi@users.noreply.github.com> Date: Thu, 3 Oct 2024 12:09:32 -0700 Subject: [PATCH] fix(core): throw on intrinsics in CFN update and create policies (#31578) ### Issue # (if applicable) Closes #27578, Closes #30740. ### Reason for this change `cfn-include` only allows Intrinsics in resource update and create policies to wrap primitive values. If Intrinsics are included anywhere else, `cfn-include` silently drops them. CDK's type system [does not allow](https://github.com/aws/aws-cdk/blob/main/packages/aws-cdk-lib/core/lib/cfn-resource-policy.ts) intrinsics in resource policies unless they define a primitive value. `cfn-include` adheres to this type system and drops any resource policies that use an intrinsic to define a complex value. This is an example of a forbidden use of intrinsics: ``` "Resources": { "ResourceSignalIntrinsic": { // .... "CreationPolicy": { "ResourceSignal": { "Fn::If": [ "UseCountParameter", { "Count": { "Ref": "CountParameter" } }, 5 ] } } } } } ``` This is forbidden because an intrinsic contains the `Count` property of this policy. CFN allows this, but CDK's type system does not permit it. ### Description of changes `cfn-include` will throw if any intrinsics break the type system, instead of silently dropping them. CDK's type system is a useful constraint around these resource update / create policies because it allows constructs that modify them, like autoscaling, to not be token-aware. Tokens are not resolved at synthesis time, so it makes it impossible to modify these with simple arithmetic if they contain tokens. The CDK will never (or at least should not) generate a token that breaks this type system. Thus, the only use-case for allowing these tokens is `cfn-include`. Supporting these customers would require the CDK type system to allow these, and thus CDK L2s should handle such cases; except, for L2 customers, this use-case does not happen. Explicitly reject templates that don't conform to this. Throwing here is a breaking change, so this is under a feature flag. Additionally add a new property, `dehydratedResources` -- a list of logical IDs that `cfn-include` will not parse. Those resources still exist in the final template. This does not impact L2 users. ### Description of how you validated changes Unit testing. Manually verified that this does not impact any L2s. ### Checklist - [x] My code adheres to the [CONTRIBUTING GUIDE](https://github.com/aws/aws-cdk/blob/main/CONTRIBUTING.md) and [DESIGN GUIDELINES](https://github.com/aws/aws-cdk/blob/main/docs/DESIGN_GUIDELINES.md) ---- *By submitting this pull request, I confirm that my contribution is made under the terms of the Apache-2.0 license* --- .../cloudformation-include/lib/cfn-include.ts | 40 +++- .../test/invalid-templates.test.ts | 184 ++++++++++++++++++ .../test/nested-stacks.test.ts | 71 +++++++ .../intrinsics-create-policy-autoscaling.json | 23 +++ .../intrinsics-create-policy-complex.json | 42 ++++ ...rinsics-create-policy-resource-signal.json | 24 +++ ...e-policy-autoscaling-replacing-update.json | 22 +++ ...ate-policy-autoscaling-rolling-update.json | 37 ++++ ...e-policy-autoscaling-scheduled-action.json | 24 +++ ...olicy-code-deploy-lambda-alias-update.json | 34 ++++ .../intrinsics-update-policy-complex.json | 33 ++++ .../intrinsics-create-policy-autoscaling.json | 28 +++ ...rinsics-create-policy-resource-signal.json | 36 ++++ .../invalid/intrinsics-create-policy.json | 42 ++++ .../intrinsics-tags-resource-validation.json | 54 +++++ ...e-policy-autoscaling-replacing-update.json | 32 +++ ...ate-policy-autoscaling-rolling-update.json | 38 ++++ ...e-policy-autoscaling-scheduled-action.json | 32 +++ ...olicy-code-deploy-lambda-alias-update.json | 39 ++++ .../invalid/intrinsics-update-policy.json | 40 ++++ .../invalid/resource-all-attributes.json | 27 +++ .../tags-with-invalid-intrinsics.json | 0 .../nested/child-dehydrated.json | 32 +++ .../nested/parent-dehydrated.json | 41 ++++ .../resource-attribute-update-policy.json | 61 ------ .../update-policy-with-intrinsics.json | 47 +++++ .../test/valid-templates.test.ts | 89 +++++++-- .../core/lib/helpers-internal/cfn-parse.ts | 37 +++- packages/aws-cdk-lib/cx-api/FEATURE_FLAGS.md | 21 +- packages/aws-cdk-lib/cx-api/lib/features.ts | 14 ++ 30 files changed, 1162 insertions(+), 82 deletions(-) create mode 100644 packages/aws-cdk-lib/cloudformation-include/test/test-templates/intrinsics-create-policy-autoscaling.json create mode 100644 packages/aws-cdk-lib/cloudformation-include/test/test-templates/intrinsics-create-policy-complex.json create mode 100644 packages/aws-cdk-lib/cloudformation-include/test/test-templates/intrinsics-create-policy-resource-signal.json create mode 100644 packages/aws-cdk-lib/cloudformation-include/test/test-templates/intrinsics-update-policy-autoscaling-replacing-update.json create mode 100644 packages/aws-cdk-lib/cloudformation-include/test/test-templates/intrinsics-update-policy-autoscaling-rolling-update.json create mode 100644 packages/aws-cdk-lib/cloudformation-include/test/test-templates/intrinsics-update-policy-autoscaling-scheduled-action.json create mode 100644 packages/aws-cdk-lib/cloudformation-include/test/test-templates/intrinsics-update-policy-code-deploy-lambda-alias-update.json create mode 100644 packages/aws-cdk-lib/cloudformation-include/test/test-templates/intrinsics-update-policy-complex.json create mode 100644 packages/aws-cdk-lib/cloudformation-include/test/test-templates/invalid/intrinsics-create-policy-autoscaling.json create mode 100644 packages/aws-cdk-lib/cloudformation-include/test/test-templates/invalid/intrinsics-create-policy-resource-signal.json create mode 100644 packages/aws-cdk-lib/cloudformation-include/test/test-templates/invalid/intrinsics-create-policy.json create mode 100644 packages/aws-cdk-lib/cloudformation-include/test/test-templates/invalid/intrinsics-tags-resource-validation.json create mode 100644 packages/aws-cdk-lib/cloudformation-include/test/test-templates/invalid/intrinsics-update-policy-autoscaling-replacing-update.json create mode 100644 packages/aws-cdk-lib/cloudformation-include/test/test-templates/invalid/intrinsics-update-policy-autoscaling-rolling-update.json create mode 100644 packages/aws-cdk-lib/cloudformation-include/test/test-templates/invalid/intrinsics-update-policy-autoscaling-scheduled-action.json create mode 100644 packages/aws-cdk-lib/cloudformation-include/test/test-templates/invalid/intrinsics-update-policy-code-deploy-lambda-alias-update.json create mode 100644 packages/aws-cdk-lib/cloudformation-include/test/test-templates/invalid/intrinsics-update-policy.json create mode 100644 packages/aws-cdk-lib/cloudformation-include/test/test-templates/invalid/resource-all-attributes.json rename packages/aws-cdk-lib/cloudformation-include/test/test-templates/{ => invalid}/tags-with-invalid-intrinsics.json (100%) create mode 100644 packages/aws-cdk-lib/cloudformation-include/test/test-templates/nested/child-dehydrated.json create mode 100644 packages/aws-cdk-lib/cloudformation-include/test/test-templates/nested/parent-dehydrated.json delete mode 100644 packages/aws-cdk-lib/cloudformation-include/test/test-templates/resource-attribute-update-policy.json create mode 100644 packages/aws-cdk-lib/cloudformation-include/test/test-templates/update-policy-with-intrinsics.json 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';