diff --git a/packages/@aws-cdk/aws-cloudformation/lib/nested-stack.ts b/packages/@aws-cdk/aws-cloudformation/lib/nested-stack.ts index 58c5b346ebe6e..8b6ec7102efb4 100644 --- a/packages/@aws-cdk/aws-cloudformation/lib/nested-stack.ts +++ b/packages/@aws-cdk/aws-cloudformation/lib/nested-stack.ts @@ -1,5 +1,7 @@ import * as sns from '@aws-cdk/aws-sns'; -import { Aws, CfnOutput, CfnParameter, CfnResource, Construct, Duration, Fn, IResolvable, IResolveContext, Lazy, Reference, Stack, Token } from '@aws-cdk/core'; +import { Aws, CfnResource, Construct, Duration, FileAssetPackaging, Fn, IResolveContext, Stack, Token } from '@aws-cdk/core'; +import { Lazy } from 'constructs'; +import * as crypto from 'crypto'; import { CfnStack } from './cloudformation.generated'; const NESTED_STACK_SYMBOL = Symbol.for('@aws-cdk/aws-cloudformation.NestedStack'); @@ -10,7 +12,6 @@ const NESTED_STACK_SYMBOL = Symbol.for('@aws-cdk/aws-cloudformation.NestedStack' * @experimental */ export interface NestedStackProps { - /** * The set value pairs that represent the parameters passed to CloudFormation * when this nested stack is created. Each parameter has a name corresponding @@ -82,12 +83,16 @@ export class NestedStack extends Stack { private readonly resource: CfnStack; private readonly _contextualStackId: string; private readonly _contextualStackName: string; + private _templateUrl?: string; + private _parentStack: Stack; constructor(scope: Construct, id: string, props: NestedStackProps = { }) { const parentStack = findParentStack(scope); super(scope, id, { env: { account: parentStack.account, region: parentStack.region } }); + this._parentStack = parentStack; + // @deprecate: remove this in v2.0 (redundent) const parentScope = new Construct(scope, id + '.NestedStack'); @@ -99,7 +104,7 @@ export class NestedStack extends Stack { this.parameters = props.parameters || {}; this.resource = new CfnStack(parentScope, `${id}.NestedStackResource`, { - templateUrl: this.templateUrl, + templateUrl: Lazy.stringValue({ produce: () => this._templateUrl || '' }), parameters: Lazy.anyValue({ produce: () => Object.keys(this.parameters).length > 0 ? this.parameters : undefined }), notificationArns: props.notifications ? props.notifications.map(n => n.topicArn) : undefined, timeoutInMinutes: props.timeout ? props.timeout.toMinutes() : undefined, @@ -144,62 +149,46 @@ export class NestedStack extends Stack { } /** - * Called by the base "prepare" method when a reference is found. + * Assign a value to one of the nested stack parameters. + * @param name The parameter name (ID) + * @param value The value to assign */ - protected prepareCrossReference(sourceStack: Stack, reference: Reference): IResolvable { - const targetStack = Stack.of(reference.target); - - // the nested stack references a resource from the parent stack: - // we pass it through a as a cloudformation parameter - if (targetStack === sourceStack.nestedStackParent) { - // we call "this.resolve" to ensure that tokens do not creep in (for example, if the reference display name includes tokens) - const paramId = this.resolve(`reference-to-${reference.target.node.uniqueId}.${reference.displayName}`); - let param = this.node.tryFindChild(paramId) as CfnParameter; - if (!param) { - param = new CfnParameter(this, paramId, { type: 'String' }); - this.parameters[param.logicalId] = Token.asString(reference); - } - - return param.value; - } - - // parent stack references a resource from the nested stack: - // we output it from the nested stack and use "Fn::GetAtt" as the reference value - if (targetStack === this && targetStack.nestedStackParent === sourceStack) { - return this.getCreateOutputForReference(reference); - } - - // sibling nested stacks (same parent): - // output from one and pass as parameter to the other - if (targetStack.nestedStackParent && targetStack.nestedStackParent === sourceStack.nestedStackParent) { - const outputValue = this.getCreateOutputForReference(reference); - return (sourceStack as NestedStack).prepareCrossReference(sourceStack, outputValue); - } - - // nested stack references a value from some other non-nested stack: - // normal export/import, with dependency between the parents - if (sourceStack.nestedStackParent && sourceStack.nestedStackParent !== targetStack) { - return super.prepareCrossReference(sourceStack, reference); - } + public setParameter(name: string, value: string) { + this.parameters[name] = value; + } - // some non-nested stack (that is not the parent) references a resource inside the nested stack: - // we output the value and let our parent export it - if (!sourceStack.nestedStackParent && targetStack.nestedStackParent && targetStack.nestedStackParent !== sourceStack) { - const outputValue = this.getCreateOutputForReference(reference); - return (targetStack.nestedStackParent as NestedStack).prepareCrossReference(sourceStack, outputValue); + /** + * Defines an asset at the parent stack which represents the template of this + * nested stack. + * + * This private API is used by `App.prepare()` within a loop that rectifies + * references every time an asset is added. This is because (at the moment) + * assets are addressed using CloudFormation parameters. + * + * @returns `true` if a new asset was added or `false` if an asset was + * previously added. When this returns `true`, App will do another reference + * rectification cycle. + * + * @internal + */ + public _prepareTemplateAsset() { + if (this._templateUrl) { + return false; } - throw new Error('unexpected nested stack cross reference'); - } + const cfn = JSON.stringify((this as any)._toCloudFormation()); + const templateHash = crypto.createHash('sha256').update(cfn).digest('hex'); - private getCreateOutputForReference(reference: Reference) { - const outputId = `${reference.target.node.uniqueId}${reference.displayName}`; - let output = this.node.tryFindChild(outputId) as CfnOutput; - if (!output) { - output = new CfnOutput(this, outputId, { value: Token.asString(reference) }); - } + const templateLocation = this._parentStack.addFileAsset({ + packaging: FileAssetPackaging.FILE, + sourceHash: templateHash, + fileName: this.templateFile + }); - return this.resource.getAtt(`Outputs.${output.logicalId}`); + // if bucketName/objectKey are cfn parameters from a stack other than the parent stack, they will + // be resolved as cross-stack references like any other (see "multi" tests). + this._templateUrl = `https://s3.${this._parentStack.region}.${this._parentStack.urlSuffix}/${templateLocation.bucketName}/${templateLocation.objectKey}`; + return true; } private contextualAttribute(innerValue: string, outerValue: string) { diff --git a/packages/@aws-cdk/aws-cloudformation/test/integ.nested-stack.expected.json b/packages/@aws-cdk/aws-cloudformation/test/integ.nested-stack.expected.json index b765d6db35ab6..2e467682966ba 100644 --- a/packages/@aws-cdk/aws-cloudformation/test/integ.nested-stack.expected.json +++ b/packages/@aws-cdk/aws-cloudformation/test/integ.nested-stack.expected.json @@ -158,7 +158,7 @@ }, "/", { - "Ref": "AssetParametersdddca70fcceefd0a4532c8eb5ad3d5da6f51d64fda9343f0b57dd664736dccafS3BucketE3660F43" + "Ref": "AssetParameters4ed2bec8961a74942e0627883abee066300275e2c2b03fe650d313898fe68f1aS3Bucket1DDC9C52" }, "/", { @@ -168,7 +168,7 @@ "Fn::Split": [ "||", { - "Ref": "AssetParametersdddca70fcceefd0a4532c8eb5ad3d5da6f51d64fda9343f0b57dd664736dccafS3VersionKeyFD0B0470" + "Ref": "AssetParameters4ed2bec8961a74942e0627883abee066300275e2c2b03fe650d313898fe68f1aS3VersionKey2B4F31C1" } ] } @@ -181,7 +181,7 @@ "Fn::Split": [ "||", { - "Ref": "AssetParametersdddca70fcceefd0a4532c8eb5ad3d5da6f51d64fda9343f0b57dd664736dccafS3VersionKeyFD0B0470" + "Ref": "AssetParameters4ed2bec8961a74942e0627883abee066300275e2c2b03fe650d313898fe68f1aS3VersionKey2B4F31C1" } ] } @@ -254,29 +254,29 @@ } }, "Parameters": { - "AssetParameters0d0404717d8867c09534f2cf382e8e24531ff64a968afa2efd7f071ad65a22dfS3BucketB322F951": { + "AssetParameters4ed2bec8961a74942e0627883abee066300275e2c2b03fe650d313898fe68f1aS3Bucket1DDC9C52": { "Type": "String", - "Description": "S3 bucket for asset \"0d0404717d8867c09534f2cf382e8e24531ff64a968afa2efd7f071ad65a22df\"" + "Description": "S3 bucket for asset \"4ed2bec8961a74942e0627883abee066300275e2c2b03fe650d313898fe68f1a\"" }, - "AssetParameters0d0404717d8867c09534f2cf382e8e24531ff64a968afa2efd7f071ad65a22dfS3VersionKeyAA9C5AF4": { + "AssetParameters4ed2bec8961a74942e0627883abee066300275e2c2b03fe650d313898fe68f1aS3VersionKey2B4F31C1": { "Type": "String", - "Description": "S3 key for asset version \"0d0404717d8867c09534f2cf382e8e24531ff64a968afa2efd7f071ad65a22df\"" + "Description": "S3 key for asset version \"4ed2bec8961a74942e0627883abee066300275e2c2b03fe650d313898fe68f1a\"" }, - "AssetParameters0d0404717d8867c09534f2cf382e8e24531ff64a968afa2efd7f071ad65a22dfArtifactHash5D335705": { + "AssetParameters4ed2bec8961a74942e0627883abee066300275e2c2b03fe650d313898fe68f1aArtifactHash3AA59378": { "Type": "String", - "Description": "Artifact hash for asset \"0d0404717d8867c09534f2cf382e8e24531ff64a968afa2efd7f071ad65a22df\"" + "Description": "Artifact hash for asset \"4ed2bec8961a74942e0627883abee066300275e2c2b03fe650d313898fe68f1a\"" }, - "AssetParametersdddca70fcceefd0a4532c8eb5ad3d5da6f51d64fda9343f0b57dd664736dccafS3BucketE3660F43": { + "AssetParameters0d0404717d8867c09534f2cf382e8e24531ff64a968afa2efd7f071ad65a22dfS3BucketB322F951": { "Type": "String", - "Description": "S3 bucket for asset \"dddca70fcceefd0a4532c8eb5ad3d5da6f51d64fda9343f0b57dd664736dccaf\"" + "Description": "S3 bucket for asset \"0d0404717d8867c09534f2cf382e8e24531ff64a968afa2efd7f071ad65a22df\"" }, - "AssetParametersdddca70fcceefd0a4532c8eb5ad3d5da6f51d64fda9343f0b57dd664736dccafS3VersionKeyFD0B0470": { + "AssetParameters0d0404717d8867c09534f2cf382e8e24531ff64a968afa2efd7f071ad65a22dfS3VersionKeyAA9C5AF4": { "Type": "String", - "Description": "S3 key for asset version \"dddca70fcceefd0a4532c8eb5ad3d5da6f51d64fda9343f0b57dd664736dccaf\"" + "Description": "S3 key for asset version \"0d0404717d8867c09534f2cf382e8e24531ff64a968afa2efd7f071ad65a22df\"" }, - "AssetParametersdddca70fcceefd0a4532c8eb5ad3d5da6f51d64fda9343f0b57dd664736dccafArtifactHashEECD8E35": { + "AssetParameters0d0404717d8867c09534f2cf382e8e24531ff64a968afa2efd7f071ad65a22dfArtifactHash5D335705": { "Type": "String", - "Description": "Artifact hash for asset \"dddca70fcceefd0a4532c8eb5ad3d5da6f51d64fda9343f0b57dd664736dccaf\"" + "Description": "Artifact hash for asset \"0d0404717d8867c09534f2cf382e8e24531ff64a968afa2efd7f071ad65a22df\"" } } } \ No newline at end of file diff --git a/packages/@aws-cdk/aws-cloudformation/test/integ.nested-stacks-multi-refs.expected.json b/packages/@aws-cdk/aws-cloudformation/test/integ.nested-stacks-multi-refs.expected.json new file mode 100644 index 0000000000000..bc546994b8e41 --- /dev/null +++ b/packages/@aws-cdk/aws-cloudformation/test/integ.nested-stacks-multi-refs.expected.json @@ -0,0 +1,116 @@ +{ + "Resources": { + "Level1ABBD39B3": { + "Type": "AWS::SNS::Topic" + }, + "Nested1NestedStackNested1NestedStackResourceCD0AD36B": { + "Type": "AWS::CloudFormation::Stack", + "Properties": { + "TemplateURL": { + "Fn::Join": [ + "", + [ + "https://s3.", + { + "Ref": "AWS::Region" + }, + ".", + { + "Ref": "AWS::URLSuffix" + }, + "/", + { + "Ref": "AssetParametersad23da1cfc8b3fd7916c6ffc7debacadf084765e62fab8acf0b8b0a9b0289f95S3BucketDB605F9E" + }, + "/", + { + "Fn::Select": [ + 0, + { + "Fn::Split": [ + "||", + { + "Ref": "AssetParametersad23da1cfc8b3fd7916c6ffc7debacadf084765e62fab8acf0b8b0a9b0289f95S3VersionKey26685906" + } + ] + } + ] + }, + { + "Fn::Select": [ + 1, + { + "Fn::Split": [ + "||", + { + "Ref": "AssetParametersad23da1cfc8b3fd7916c6ffc7debacadf084765e62fab8acf0b8b0a9b0289f95S3VersionKey26685906" + } + ] + } + ] + } + ] + ] + }, + "Parameters": { + "referencetonestedstacksmultirefsLevel19FB2466DTopicName": { + "Fn::GetAtt": [ + "Level1ABBD39B3", + "TopicName" + ] + }, + "referencetonestedstacksmultirefsAssetParameters495a6bc36c13a0adeb3778c921d18ac4a8205f5471108fcc199a291d14855c3aS3Bucket03F0C3B1Ref": { + "Ref": "AssetParameters495a6bc36c13a0adeb3778c921d18ac4a8205f5471108fcc199a291d14855c3aS3Bucket58724FCA" + }, + "referencetonestedstacksmultirefsAssetParameters495a6bc36c13a0adeb3778c921d18ac4a8205f5471108fcc199a291d14855c3aS3VersionKey5F9CF809Ref": { + "Ref": "AssetParameters495a6bc36c13a0adeb3778c921d18ac4a8205f5471108fcc199a291d14855c3aS3VersionKey2CCE0573" + }, + "referencetonestedstacksmultirefsAssetParameterscc623add53df153cf6a7df1cea4dc90740d7be087472579110754a633ec90847S3Bucket8F1E17B9Ref": { + "Ref": "AssetParameterscc623add53df153cf6a7df1cea4dc90740d7be087472579110754a633ec90847S3Bucket9A14AA6D" + }, + "referencetonestedstacksmultirefsAssetParameterscc623add53df153cf6a7df1cea4dc90740d7be087472579110754a633ec90847S3VersionKey9EEEF950Ref": { + "Ref": "AssetParameterscc623add53df153cf6a7df1cea4dc90740d7be087472579110754a633ec90847S3VersionKeyF124C0D9" + } + } + } + } + }, + "Parameters": { + "AssetParameters495a6bc36c13a0adeb3778c921d18ac4a8205f5471108fcc199a291d14855c3aS3Bucket58724FCA": { + "Type": "String", + "Description": "S3 bucket for asset \"495a6bc36c13a0adeb3778c921d18ac4a8205f5471108fcc199a291d14855c3a\"" + }, + "AssetParameters495a6bc36c13a0adeb3778c921d18ac4a8205f5471108fcc199a291d14855c3aS3VersionKey2CCE0573": { + "Type": "String", + "Description": "S3 key for asset version \"495a6bc36c13a0adeb3778c921d18ac4a8205f5471108fcc199a291d14855c3a\"" + }, + "AssetParameters495a6bc36c13a0adeb3778c921d18ac4a8205f5471108fcc199a291d14855c3aArtifactHashAE1436B7": { + "Type": "String", + "Description": "Artifact hash for asset \"495a6bc36c13a0adeb3778c921d18ac4a8205f5471108fcc199a291d14855c3a\"" + }, + "AssetParameterscc623add53df153cf6a7df1cea4dc90740d7be087472579110754a633ec90847S3Bucket9A14AA6D": { + "Type": "String", + "Description": "S3 bucket for asset \"cc623add53df153cf6a7df1cea4dc90740d7be087472579110754a633ec90847\"" + }, + "AssetParameterscc623add53df153cf6a7df1cea4dc90740d7be087472579110754a633ec90847S3VersionKeyF124C0D9": { + "Type": "String", + "Description": "S3 key for asset version \"cc623add53df153cf6a7df1cea4dc90740d7be087472579110754a633ec90847\"" + }, + "AssetParameterscc623add53df153cf6a7df1cea4dc90740d7be087472579110754a633ec90847ArtifactHashAF64C405": { + "Type": "String", + "Description": "Artifact hash for asset \"cc623add53df153cf6a7df1cea4dc90740d7be087472579110754a633ec90847\"" + }, + "AssetParametersad23da1cfc8b3fd7916c6ffc7debacadf084765e62fab8acf0b8b0a9b0289f95S3BucketDB605F9E": { + "Type": "String", + "Description": "S3 bucket for asset \"ad23da1cfc8b3fd7916c6ffc7debacadf084765e62fab8acf0b8b0a9b0289f95\"" + }, + "AssetParametersad23da1cfc8b3fd7916c6ffc7debacadf084765e62fab8acf0b8b0a9b0289f95S3VersionKey26685906": { + "Type": "String", + "Description": "S3 key for asset version \"ad23da1cfc8b3fd7916c6ffc7debacadf084765e62fab8acf0b8b0a9b0289f95\"" + }, + "AssetParametersad23da1cfc8b3fd7916c6ffc7debacadf084765e62fab8acf0b8b0a9b0289f95ArtifactHashAF8D54FC": { + "Type": "String", + "Description": "Artifact hash for asset \"ad23da1cfc8b3fd7916c6ffc7debacadf084765e62fab8acf0b8b0a9b0289f95\"" + } + } +} \ No newline at end of file diff --git a/packages/@aws-cdk/aws-cloudformation/test/integ.nested-stacks-multi-refs.ts b/packages/@aws-cdk/aws-cloudformation/test/integ.nested-stacks-multi-refs.ts new file mode 100644 index 0000000000000..321e70fbb486a --- /dev/null +++ b/packages/@aws-cdk/aws-cloudformation/test/integ.nested-stacks-multi-refs.ts @@ -0,0 +1,31 @@ +import * as sns from '@aws-cdk/aws-sns'; +import { App, Fn, Stack } from '@aws-cdk/core'; +import { NestedStack } from '../lib'; + +const app = new App(); +const top = new Stack(app, 'nested-stacks-multi-refs'); +const level1 = new sns.Topic(top, 'Level1'); +const nested1 = new NestedStack(top, 'Nested1'); +const nested2 = new NestedStack(nested1, 'Nested2'); +const nested3 = new NestedStack(nested2, 'Nested3'); + +// WHEN +const level2 = new sns.Topic(nested2, 'Level2ReferencesLevel1', { + displayName: shortName(level1.topicName) +}); + +new sns.Topic(nested3, 'Level3ReferencesLevel1', { + displayName: shortName(level1.topicName) +}); + +new sns.Topic(nested3, 'Level3ReferencesLevel2', { + displayName: shortName(level2.topicName) +}); + +app.synth(); + +// topicName is too long for displayName, so just take the second part: +// Stack1-NestedUnderStack1NestedStackNestedUnderStack1NestedStackResourceF616305B-EM64TEGA04J9-TopicInNestedUnderStack115E329C4-HEO7NLYC1AFL +function shortName(topicName: string) { + return Fn.select(1, Fn.split('-', topicName)); +} \ No newline at end of file diff --git a/packages/@aws-cdk/aws-cloudformation/test/integ.nested-stacks-nested-export-to-sibling.expected.json b/packages/@aws-cdk/aws-cloudformation/test/integ.nested-stacks-nested-export-to-sibling.expected.json new file mode 100644 index 0000000000000..3cac1b07eb420 --- /dev/null +++ b/packages/@aws-cdk/aws-cloudformation/test/integ.nested-stacks-nested-export-to-sibling.expected.json @@ -0,0 +1,106 @@ +[ + { + "Resources": { + "NestedUnderStack1NestedStackNestedUnderStack1NestedStackResourceF616305B": { + "Type": "AWS::CloudFormation::Stack", + "Properties": { + "TemplateURL": { + "Fn::Join": [ + "", + [ + "https://s3.", + { + "Ref": "AWS::Region" + }, + ".", + { + "Ref": "AWS::URLSuffix" + }, + "/", + { + "Ref": "AssetParameters5bbe5621d9656843b414db3e449d8c562b0b27bb293ef6999180dc5198c70219S3BucketF628ECFB" + }, + "/", + { + "Fn::Select": [ + 0, + { + "Fn::Split": [ + "||", + { + "Ref": "AssetParameters5bbe5621d9656843b414db3e449d8c562b0b27bb293ef6999180dc5198c70219S3VersionKey0E649F42" + } + ] + } + ] + }, + { + "Fn::Select": [ + 1, + { + "Fn::Split": [ + "||", + { + "Ref": "AssetParameters5bbe5621d9656843b414db3e449d8c562b0b27bb293ef6999180dc5198c70219S3VersionKey0E649F42" + } + ] + } + ] + } + ] + ] + } + } + } + }, + "Outputs": { + "ExportsOutputFnGetAttNestedUnderStack1NestedStackNestedUnderStack1NestedStackResourceF616305BOutputsStack1NestedUnderStack1TopicInNestedUnderStack136BDF841TopicNameD753D416": { + "Value": { + "Fn::GetAtt": [ + "NestedUnderStack1NestedStackNestedUnderStack1NestedStackResourceF616305B", + "Outputs.Stack1NestedUnderStack1TopicInNestedUnderStack136BDF841TopicName" + ] + }, + "Export": { + "Name": "Stack1:ExportsOutputFnGetAttNestedUnderStack1NestedStackNestedUnderStack1NestedStackResourceF616305BOutputsStack1NestedUnderStack1TopicInNestedUnderStack136BDF841TopicNameD753D416" + } + } + }, + "Parameters": { + "AssetParameters5bbe5621d9656843b414db3e449d8c562b0b27bb293ef6999180dc5198c70219S3BucketF628ECFB": { + "Type": "String", + "Description": "S3 bucket for asset \"5bbe5621d9656843b414db3e449d8c562b0b27bb293ef6999180dc5198c70219\"" + }, + "AssetParameters5bbe5621d9656843b414db3e449d8c562b0b27bb293ef6999180dc5198c70219S3VersionKey0E649F42": { + "Type": "String", + "Description": "S3 key for asset version \"5bbe5621d9656843b414db3e449d8c562b0b27bb293ef6999180dc5198c70219\"" + }, + "AssetParameters5bbe5621d9656843b414db3e449d8c562b0b27bb293ef6999180dc5198c70219ArtifactHash37AA0C4D": { + "Type": "String", + "Description": "Artifact hash for asset \"5bbe5621d9656843b414db3e449d8c562b0b27bb293ef6999180dc5198c70219\"" + } + } + }, + { + "Resources": { + "TopicInStack27FD9238C": { + "Type": "AWS::SNS::Topic", + "Properties": { + "DisplayName": { + "Fn::Select": [ + 1, + { + "Fn::Split": [ + "-", + { + "Fn::ImportValue": "Stack1:ExportsOutputFnGetAttNestedUnderStack1NestedStackNestedUnderStack1NestedStackResourceF616305BOutputsStack1NestedUnderStack1TopicInNestedUnderStack136BDF841TopicNameD753D416" + } + ] + } + ] + } + } + } + } + } +] \ No newline at end of file diff --git a/packages/@aws-cdk/aws-cloudformation/test/integ.nested-stacks-nested-export-to-sibling.ts b/packages/@aws-cdk/aws-cloudformation/test/integ.nested-stacks-nested-export-to-sibling.ts new file mode 100644 index 0000000000000..2363b5bb790b4 --- /dev/null +++ b/packages/@aws-cdk/aws-cloudformation/test/integ.nested-stacks-nested-export-to-sibling.ts @@ -0,0 +1,20 @@ +/// !cdk-integ Stack2 + +import * as sns from '@aws-cdk/aws-sns'; +import { App, Fn, Stack } from '@aws-cdk/core'; +import * as cfn from '../lib'; + +const app = new App(); +const stack1 = new Stack(app, 'Stack1'); +const stack2 = new Stack(app, 'Stack2'); + +const nestedUnderStack1 = new cfn.NestedStack(stack1, 'NestedUnderStack1'); +const topicInNestedUnderStack1 = new sns.Topic(nestedUnderStack1, 'TopicInNestedUnderStack1'); + +new sns.Topic(stack2, 'TopicInStack2', { + // topicName is too long for displayName, so just take the second part: + // Stack1-NestedUnderStack1NestedStackNestedUnderStack1NestedStackResourceF616305B-EM64TEGA04J9-TopicInNestedUnderStack115E329C4-HEO7NLYC1AFL + displayName: Fn.select(1, Fn.split('-', topicInNestedUnderStack1.topicName)) +}); + +app.synth(); diff --git a/packages/@aws-cdk/aws-cloudformation/test/integ.nested-stacks-refs3.expected.json b/packages/@aws-cdk/aws-cloudformation/test/integ.nested-stacks-refs3.expected.json index 25bb05cdf6ab9..80a2f7712026c 100644 --- a/packages/@aws-cdk/aws-cloudformation/test/integ.nested-stacks-refs3.expected.json +++ b/packages/@aws-cdk/aws-cloudformation/test/integ.nested-stacks-refs3.expected.json @@ -112,18 +112,6 @@ } }, "Parameters": { - "AssetParameters2e7ce09a9e0721d268d734287b72d071ed542a05451e3b53dfcb5ae4e76cc583S3Bucket72E4418F": { - "Type": "String", - "Description": "S3 bucket for asset \"2e7ce09a9e0721d268d734287b72d071ed542a05451e3b53dfcb5ae4e76cc583\"" - }, - "AssetParameters2e7ce09a9e0721d268d734287b72d071ed542a05451e3b53dfcb5ae4e76cc583S3VersionKeyC46A55B6": { - "Type": "String", - "Description": "S3 key for asset version \"2e7ce09a9e0721d268d734287b72d071ed542a05451e3b53dfcb5ae4e76cc583\"" - }, - "AssetParameters2e7ce09a9e0721d268d734287b72d071ed542a05451e3b53dfcb5ae4e76cc583ArtifactHashDF52341B": { - "Type": "String", - "Description": "Artifact hash for asset \"2e7ce09a9e0721d268d734287b72d071ed542a05451e3b53dfcb5ae4e76cc583\"" - }, "AssetParameters008e281fb3039601b8fbef60e255afe78cb00a09611d1aa7342f56328aef7d9aS3Bucket3AC5D089": { "Type": "String", "Description": "S3 bucket for asset \"008e281fb3039601b8fbef60e255afe78cb00a09611d1aa7342f56328aef7d9a\"" @@ -135,6 +123,18 @@ "AssetParameters008e281fb3039601b8fbef60e255afe78cb00a09611d1aa7342f56328aef7d9aArtifactHashEF790DCB": { "Type": "String", "Description": "Artifact hash for asset \"008e281fb3039601b8fbef60e255afe78cb00a09611d1aa7342f56328aef7d9a\"" + }, + "AssetParameters2e7ce09a9e0721d268d734287b72d071ed542a05451e3b53dfcb5ae4e76cc583S3Bucket72E4418F": { + "Type": "String", + "Description": "S3 bucket for asset \"2e7ce09a9e0721d268d734287b72d071ed542a05451e3b53dfcb5ae4e76cc583\"" + }, + "AssetParameters2e7ce09a9e0721d268d734287b72d071ed542a05451e3b53dfcb5ae4e76cc583S3VersionKeyC46A55B6": { + "Type": "String", + "Description": "S3 key for asset version \"2e7ce09a9e0721d268d734287b72d071ed542a05451e3b53dfcb5ae4e76cc583\"" + }, + "AssetParameters2e7ce09a9e0721d268d734287b72d071ed542a05451e3b53dfcb5ae4e76cc583ArtifactHashDF52341B": { + "Type": "String", + "Description": "Artifact hash for asset \"2e7ce09a9e0721d268d734287b72d071ed542a05451e3b53dfcb5ae4e76cc583\"" } } } \ No newline at end of file diff --git a/packages/@aws-cdk/aws-cloudformation/test/test.nested-stack.ts b/packages/@aws-cdk/aws-cloudformation/test/test.nested-stack.ts index e427ab45df8bb..62027ba6a37e2 100644 --- a/packages/@aws-cdk/aws-cloudformation/test/test.nested-stack.ts +++ b/packages/@aws-cdk/aws-cloudformation/test/test.nested-stack.ts @@ -1,4 +1,4 @@ -import { expect, haveResource, SynthUtils } from '@aws-cdk/assert'; +import { expect, haveResource, matchTemplate, SynthUtils } from '@aws-cdk/assert'; import * as s3_assets from '@aws-cdk/aws-s3-assets'; import * as sns from '@aws-cdk/aws-sns'; import { App, CfnParameter, CfnResource, Construct, ContextProvider, Stack } from '@aws-cdk/core'; @@ -426,6 +426,30 @@ export = { test.done(); }, + 'nested stack within a nested stack references a resource in a sibling top-level stack'(test: Test) { + // GIVEN + const app = new App(); + const consumerTopLevel = new Stack(app, 'ConsumerTopLevel'); + const consumerNested1 = new NestedStack(consumerTopLevel, 'ConsumerNested1'); + const consumerNested2 = new NestedStack(consumerNested1, 'ConsumerNested2'); + const producerTopLevel = new Stack(app, 'ProducerTopLevel'); + const producer = new CfnResource(producerTopLevel, 'Producer', { type: 'Producer' }); + + // WHEN + new CfnResource(consumerNested2, 'Consumer', { + type: 'Consumer', + properties: { + Ref: producer.ref + } + }); + + // THEN + const manifest = app.synth(); + const consumerDeps = manifest.getStackArtifact(consumerTopLevel.artifactId).dependencies.map(d => d.id); + test.deepEqual(consumerDeps, [ 'ProducerTopLevel' ]); + test.done(); + }, + 'another non-nested stack takes a reference on a resource within the nested stack (the parent exports)'(test: Test) { // GIVEN const app = new App(); @@ -909,4 +933,123 @@ export = { test.done(); }, + + 'references to a resource from a deeply nested stack'(test: Test) { + // GIVEN + const app = new App(); + const top = new Stack(app, 'stack'); + const topLevel = new CfnResource(top, 'toplevel', { type: 'TopLevel' }); + const nested1 = new NestedStack(top, 'nested1'); + const nested2 = new NestedStack(nested1, 'nested2'); + + // WHEN + new CfnResource(nested2, 'refToTopLevel', { + type: 'BottomLevel', + properties: { RefToTopLevel: topLevel.ref } + }); + + // THEN + expect(top).to(haveResource('AWS::CloudFormation::Stack', { + Parameters: { + referencetostackAssetParameters842982bd421cce9742ba27151ef12ed699d44d22801f41e8029f63f2358a3f2fS3Bucket5DA5D2E7Ref: { + Ref: 'AssetParameters842982bd421cce9742ba27151ef12ed699d44d22801f41e8029f63f2358a3f2fS3BucketDD4D96B5' + }, + referencetostackAssetParameters842982bd421cce9742ba27151ef12ed699d44d22801f41e8029f63f2358a3f2fS3VersionKey8FBE5C12Ref: { + Ref: 'AssetParameters842982bd421cce9742ba27151ef12ed699d44d22801f41e8029f63f2358a3f2fS3VersionKey83E381F3' + }, + referencetostacktoplevelBB16BF13Ref: { + Ref: 'toplevel' + } + } + })); + + expect(nested1).to(haveResource('AWS::CloudFormation::Stack', { + Parameters: { + referencetostacktoplevelBB16BF13Ref: { + Ref: 'referencetostacktoplevelBB16BF13Ref' + } + } + })); + + expect(nested2).to(matchTemplate({ + Resources: { + refToTopLevel: { + Type: 'BottomLevel', + Properties: { + RefToTopLevel: { + Ref: 'referencetostacktoplevelBB16BF13Ref' + } + } + } + }, + Parameters: { + referencetostacktoplevelBB16BF13Ref: { + Type: 'String' + }, + }, + })); + test.done(); + }, + + 'bottom nested stack consumes value from a top-level stack through a parameter in a middle nested stack'(test: Test) { + // GIVEN + const app = new App(); + const top = new Stack(app, 'Grandparent'); + const middle = new NestedStack(top, 'Parent'); + const bottom = new NestedStack(middle, 'Child'); + const resourceInGrandparent = new CfnResource(top, 'ResourceInGrandparent', { type: 'ResourceInGrandparent' }); + + // WHEN + new CfnResource(bottom, 'ResourceInChild', { + type: 'ResourceInChild', + properties: { + RefToGrandparent: resourceInGrandparent.ref + } + }); + + // THEN + + // this is the name allocated for the parameter that's propagated through + // the hierarchy. + const paramName = 'referencetoGrandparentResourceInGrandparent010E997ARef'; + + // child (bottom) references through a parameter. + expect(bottom).toMatch({ + Resources: { + ResourceInChild: { + Type: 'ResourceInChild', + Properties: { + RefToGrandparent: { Ref: paramName } + } + } + }, + Parameters: { + [paramName]: { Type: 'String' } + } + }); + + // the parent (middle) sets the value of this parameter to be a reference to another parameter + expect(middle).to(haveResource('AWS::CloudFormation::Stack', { + Parameters: { + [paramName]: { Ref: paramName } + } + })); + + // grandparent (top) assigns the actual value to the parameter + expect(top).to(haveResource('AWS::CloudFormation::Stack', { + Parameters: { + [paramName]: { Ref: 'ResourceInGrandparent' }, + + // these are for the asset of the bottom nested stack + referencetoGrandparentAssetParameters3208f43b793a1dbe28ca02cf31fb975489071beb42c492b22dc3d32decc3b4b7S3Bucket06EEE58DRef: { + Ref: 'AssetParameters3208f43b793a1dbe28ca02cf31fb975489071beb42c492b22dc3d32decc3b4b7S3Bucket01877C2E' + }, + referencetoGrandparentAssetParameters3208f43b793a1dbe28ca02cf31fb975489071beb42c492b22dc3d32decc3b4b7S3VersionKeyD3B04909Ref: { + Ref: 'AssetParameters3208f43b793a1dbe28ca02cf31fb975489071beb42c492b22dc3d32decc3b4b7S3VersionKey5765F084' + } + } + })); + + test.done(); + } }; diff --git a/packages/@aws-cdk/aws-dynamodb/test/integ.global.expected.json b/packages/@aws-cdk/aws-dynamodb/test/integ.global.expected.json index 62b67091b4886..d659f402e5372 100644 --- a/packages/@aws-cdk/aws-dynamodb/test/integ.global.expected.json +++ b/packages/@aws-cdk/aws-dynamodb/test/integ.global.expected.json @@ -91,7 +91,7 @@ }, "/", { - "Ref": "AssetParameters8daa07b80da0a0001dc3a157deb13b96170766a315b87bf649e56ee1314f03bfS3BucketEE609B7A" + "Ref": "AssetParametersceba959ae1599b891165fbd144b9f2d81a1ded6c4bd98a87ec6b1f41a76a295bS3Bucket1375170E" }, "/", { @@ -101,7 +101,7 @@ "Fn::Split": [ "||", { - "Ref": "AssetParameters8daa07b80da0a0001dc3a157deb13b96170766a315b87bf649e56ee1314f03bfS3VersionKeyAE59C268" + "Ref": "AssetParametersceba959ae1599b891165fbd144b9f2d81a1ded6c4bd98a87ec6b1f41a76a295bS3VersionKeyB1F590C9" } ] } @@ -114,7 +114,7 @@ "Fn::Split": [ "||", { - "Ref": "AssetParameters8daa07b80da0a0001dc3a157deb13b96170766a315b87bf649e56ee1314f03bfS3VersionKeyAE59C268" + "Ref": "AssetParametersceba959ae1599b891165fbd144b9f2d81a1ded6c4bd98a87ec6b1f41a76a295bS3VersionKeyB1F590C9" } ] } @@ -174,17 +174,17 @@ "Type": "String", "Description": "Artifact hash for asset \"17429b8bbbb0484d167711d8d3baf4abc55be3663b0f19233952a7fa9d9db8d4\"" }, - "AssetParameters8daa07b80da0a0001dc3a157deb13b96170766a315b87bf649e56ee1314f03bfS3BucketEE609B7A": { + "AssetParametersceba959ae1599b891165fbd144b9f2d81a1ded6c4bd98a87ec6b1f41a76a295bS3Bucket1375170E": { "Type": "String", - "Description": "S3 bucket for asset \"8daa07b80da0a0001dc3a157deb13b96170766a315b87bf649e56ee1314f03bf\"" + "Description": "S3 bucket for asset \"ceba959ae1599b891165fbd144b9f2d81a1ded6c4bd98a87ec6b1f41a76a295b\"" }, - "AssetParameters8daa07b80da0a0001dc3a157deb13b96170766a315b87bf649e56ee1314f03bfS3VersionKeyAE59C268": { + "AssetParametersceba959ae1599b891165fbd144b9f2d81a1ded6c4bd98a87ec6b1f41a76a295bS3VersionKeyB1F590C9": { "Type": "String", - "Description": "S3 key for asset version \"8daa07b80da0a0001dc3a157deb13b96170766a315b87bf649e56ee1314f03bf\"" + "Description": "S3 key for asset version \"ceba959ae1599b891165fbd144b9f2d81a1ded6c4bd98a87ec6b1f41a76a295b\"" }, - "AssetParameters8daa07b80da0a0001dc3a157deb13b96170766a315b87bf649e56ee1314f03bfArtifactHash97214390": { + "AssetParametersceba959ae1599b891165fbd144b9f2d81a1ded6c4bd98a87ec6b1f41a76a295bArtifactHash392EC608": { "Type": "String", - "Description": "Artifact hash for asset \"8daa07b80da0a0001dc3a157deb13b96170766a315b87bf649e56ee1314f03bf\"" + "Description": "Artifact hash for asset \"ceba959ae1599b891165fbd144b9f2d81a1ded6c4bd98a87ec6b1f41a76a295b\"" } } } \ No newline at end of file diff --git a/packages/@aws-cdk/aws-eks/test/integ.eks-cluster.expected.json b/packages/@aws-cdk/aws-eks/test/integ.eks-cluster.expected.json index 6294cb1892b2a..655bd172880a6 100644 --- a/packages/@aws-cdk/aws-eks/test/integ.eks-cluster.expected.json +++ b/packages/@aws-cdk/aws-eks/test/integ.eks-cluster.expected.json @@ -2231,7 +2231,7 @@ }, "/", { - "Ref": "AssetParametersc6510ec9618c99417a04af8cc6ed10b424085b76e94a020e26ae0b5cf3178cf6S3Bucket41E299BC" + "Ref": "AssetParameters0221f5cd1a0d3c0ffb2fbe4cbd2cec483bf690fb947411e20a3ab81bdb351420S3Bucket522186DC" }, "/", { @@ -2241,7 +2241,7 @@ "Fn::Split": [ "||", { - "Ref": "AssetParametersc6510ec9618c99417a04af8cc6ed10b424085b76e94a020e26ae0b5cf3178cf6S3VersionKey71B3A771" + "Ref": "AssetParameters0221f5cd1a0d3c0ffb2fbe4cbd2cec483bf690fb947411e20a3ab81bdb351420S3VersionKey49A30498" } ] } @@ -2254,7 +2254,7 @@ "Fn::Split": [ "||", { - "Ref": "AssetParametersc6510ec9618c99417a04af8cc6ed10b424085b76e94a020e26ae0b5cf3178cf6S3VersionKey71B3A771" + "Ref": "AssetParameters0221f5cd1a0d3c0ffb2fbe4cbd2cec483bf690fb947411e20a3ab81bdb351420S3VersionKey49A30498" } ] } @@ -2292,7 +2292,7 @@ }, "/", { - "Ref": "AssetParameters2377e47b4df6f3d032696e27f30e27037a5d18aeb58f9e42ce538e73a75971f7S3Bucket186FBEF7" + "Ref": "AssetParameters4879b18570e6d8e6398649d06615e96a53481f49423527777e15b54271976c83S3Bucket9B10D651" }, "/", { @@ -2302,7 +2302,7 @@ "Fn::Split": [ "||", { - "Ref": "AssetParameters2377e47b4df6f3d032696e27f30e27037a5d18aeb58f9e42ce538e73a75971f7S3VersionKey1EB57477" + "Ref": "AssetParameters4879b18570e6d8e6398649d06615e96a53481f49423527777e15b54271976c83S3VersionKey99741143" } ] } @@ -2315,7 +2315,7 @@ "Fn::Split": [ "||", { - "Ref": "AssetParameters2377e47b4df6f3d032696e27f30e27037a5d18aeb58f9e42ce538e73a75971f7S3VersionKey1EB57477" + "Ref": "AssetParameters4879b18570e6d8e6398649d06615e96a53481f49423527777e15b54271976c83S3VersionKey99741143" } ] } @@ -2449,29 +2449,29 @@ "Type": "String", "Description": "Artifact hash for asset \"a6d508eaaa0d3cddbb47a84123fc878809c8431c5466f360912f70b5b9770afb\"" }, - "AssetParameters2377e47b4df6f3d032696e27f30e27037a5d18aeb58f9e42ce538e73a75971f7S3Bucket186FBEF7": { + "AssetParameters0221f5cd1a0d3c0ffb2fbe4cbd2cec483bf690fb947411e20a3ab81bdb351420S3Bucket522186DC": { "Type": "String", - "Description": "S3 bucket for asset \"2377e47b4df6f3d032696e27f30e27037a5d18aeb58f9e42ce538e73a75971f7\"" + "Description": "S3 bucket for asset \"0221f5cd1a0d3c0ffb2fbe4cbd2cec483bf690fb947411e20a3ab81bdb351420\"" }, - "AssetParameters2377e47b4df6f3d032696e27f30e27037a5d18aeb58f9e42ce538e73a75971f7S3VersionKey1EB57477": { + "AssetParameters0221f5cd1a0d3c0ffb2fbe4cbd2cec483bf690fb947411e20a3ab81bdb351420S3VersionKey49A30498": { "Type": "String", - "Description": "S3 key for asset version \"2377e47b4df6f3d032696e27f30e27037a5d18aeb58f9e42ce538e73a75971f7\"" + "Description": "S3 key for asset version \"0221f5cd1a0d3c0ffb2fbe4cbd2cec483bf690fb947411e20a3ab81bdb351420\"" }, - "AssetParameters2377e47b4df6f3d032696e27f30e27037a5d18aeb58f9e42ce538e73a75971f7ArtifactHashD1316A92": { + "AssetParameters0221f5cd1a0d3c0ffb2fbe4cbd2cec483bf690fb947411e20a3ab81bdb351420ArtifactHash8E0FEA21": { "Type": "String", - "Description": "Artifact hash for asset \"2377e47b4df6f3d032696e27f30e27037a5d18aeb58f9e42ce538e73a75971f7\"" + "Description": "Artifact hash for asset \"0221f5cd1a0d3c0ffb2fbe4cbd2cec483bf690fb947411e20a3ab81bdb351420\"" }, - "AssetParametersc6510ec9618c99417a04af8cc6ed10b424085b76e94a020e26ae0b5cf3178cf6S3Bucket41E299BC": { + "AssetParameters4879b18570e6d8e6398649d06615e96a53481f49423527777e15b54271976c83S3Bucket9B10D651": { "Type": "String", - "Description": "S3 bucket for asset \"c6510ec9618c99417a04af8cc6ed10b424085b76e94a020e26ae0b5cf3178cf6\"" + "Description": "S3 bucket for asset \"4879b18570e6d8e6398649d06615e96a53481f49423527777e15b54271976c83\"" }, - "AssetParametersc6510ec9618c99417a04af8cc6ed10b424085b76e94a020e26ae0b5cf3178cf6S3VersionKey71B3A771": { + "AssetParameters4879b18570e6d8e6398649d06615e96a53481f49423527777e15b54271976c83S3VersionKey99741143": { "Type": "String", - "Description": "S3 key for asset version \"c6510ec9618c99417a04af8cc6ed10b424085b76e94a020e26ae0b5cf3178cf6\"" + "Description": "S3 key for asset version \"4879b18570e6d8e6398649d06615e96a53481f49423527777e15b54271976c83\"" }, - "AssetParametersc6510ec9618c99417a04af8cc6ed10b424085b76e94a020e26ae0b5cf3178cf6ArtifactHashC32772E8": { + "AssetParameters4879b18570e6d8e6398649d06615e96a53481f49423527777e15b54271976c83ArtifactHash2E06714F": { "Type": "String", - "Description": "Artifact hash for asset \"c6510ec9618c99417a04af8cc6ed10b424085b76e94a020e26ae0b5cf3178cf6\"" + "Description": "Artifact hash for asset \"4879b18570e6d8e6398649d06615e96a53481f49423527777e15b54271976c83\"" }, "SsmParameterValueawsserviceeksoptimizedami114amazonlinux2recommendedimageidC96584B6F00A464EAD1953AFF4B05118Parameter": { "Type": "AWS::SSM::Parameter::Value", diff --git a/packages/@aws-cdk/aws-kinesis/README.md b/packages/@aws-cdk/aws-kinesis/README.md index c3914e014564b..ec0a4eb19b3bc 100644 --- a/packages/@aws-cdk/aws-kinesis/README.md +++ b/packages/@aws-cdk/aws-kinesis/README.md @@ -4,11 +4,7 @@ ![cfn-resources: Stable](https://img.shields.io/badge/cfn--resources-stable-success.svg?style=for-the-badge) -> All classes with the `Cfn` prefix in this module ([CFN Resources](https://docs.aws.amazon.com/cdk/latest/guide/constructs.html#constructs_lib)) are always stable and safe to use. - -![cdk-constructs: Developer Preview](https://img.shields.io/badge/cdk--constructs-developer--preview-informational.svg?style=for-the-badge) - -> The APIs of higher level constructs in this module are in **developer preview** before they become stable. We will only make breaking changes to address unforeseen API issues. Therefore, these APIs are not subject to [Semantic Versioning](https://semver.org/), and breaking changes will be announced in release notes. This means that while you may use them, you may need to update your source code when upgrading to a newer version of this package. +![cdk-constructs: Stable](https://img.shields.io/badge/cdk--constructs-stable-success.svg?style=for-the-badge) --- diff --git a/packages/@aws-cdk/aws-kinesis/package.json b/packages/@aws-cdk/aws-kinesis/package.json index 8149ee2b133e3..7b0c6a539ca1e 100644 --- a/packages/@aws-cdk/aws-kinesis/package.json +++ b/packages/@aws-cdk/aws-kinesis/package.json @@ -87,8 +87,8 @@ "engines": { "node": ">= 10.12.0" }, - "stability": "experimental", - "maturity": "developer-preview", + "stability": "stable", + "maturity": "stable", "awscdkio": { "announce": false } diff --git a/packages/@aws-cdk/aws-stepfunctions/package.json b/packages/@aws-cdk/aws-stepfunctions/package.json index 4bbead1ec9e84..47a7e88e9e1cc 100644 --- a/packages/@aws-cdk/aws-stepfunctions/package.json +++ b/packages/@aws-cdk/aws-stepfunctions/package.json @@ -63,11 +63,9 @@ "license": "Apache-2.0", "devDependencies": { "@aws-cdk/assert": "0.0.0", - "@types/nodeunit": "^0.0.30", "cdk-build-tools": "0.0.0", "cdk-integ-tools": "0.0.0", "cfn2ts": "0.0.0", - "nodeunit": "^0.11.3", "pkglint": "0.0.0" }, "dependencies": { @@ -87,6 +85,14 @@ "@aws-cdk/core": "0.0.0", "constructs": "^2.0.0" }, + "jest": { + "coverageThreshold": { + "global": { + "branches": 75, + "statements": 80 + } + } + }, "engines": { "node": ">= 10.12.0" }, diff --git a/packages/@aws-cdk/aws-stepfunctions/test/test.activity.ts b/packages/@aws-cdk/aws-stepfunctions/test/activity.test.ts similarity index 62% rename from packages/@aws-cdk/aws-stepfunctions/test/test.activity.ts rename to packages/@aws-cdk/aws-stepfunctions/test/activity.test.ts index 34439fbac3760..05c74569e3760 100644 --- a/packages/@aws-cdk/aws-stepfunctions/test/test.activity.ts +++ b/packages/@aws-cdk/aws-stepfunctions/test/activity.test.ts @@ -1,10 +1,9 @@ -import { expect, haveResource } from '@aws-cdk/assert'; +import '@aws-cdk/assert/jest'; import * as cdk from '@aws-cdk/core'; -import { Test } from 'nodeunit'; import * as stepfunctions from '../lib'; -export = { - 'instantiate Activity'(test: Test) { +describe('Activity', () => { + test('instantiate Activity', () => { // GIVEN const stack = new cdk.Stack(); @@ -12,14 +11,12 @@ export = { new stepfunctions.Activity(stack, 'Activity'); // THEN - expect(stack).to(haveResource('AWS::StepFunctions::Activity', { + expect(stack).toHaveResource('AWS::StepFunctions::Activity', { Name: 'Activity' - })); - - test.done(); - }, + }); + }); - 'Activity exposes metrics'(test: Test) { + test('Activity exposes metrics', () => { // GIVEN const stack = new cdk.Stack(); @@ -32,18 +29,16 @@ export = { namespace: 'AWS/States', dimensions: { ActivityArn: { Ref: 'Activity04690B0A' }}, }; - test.deepEqual(stack.resolve(activity.metricRunTime()), { + expect((stack.resolve(activity.metricRunTime()))).toEqual({ ...sharedMetric, metricName: 'ActivityRunTime', statistic: 'Average' }); - test.deepEqual(stack.resolve(activity.metricFailed()), { + expect(stack.resolve(activity.metricFailed())).toEqual({ ...sharedMetric, metricName: 'ActivitiesFailed', statistic: 'Sum' }); - - test.done(); - } -}; + }); +}); diff --git a/packages/@aws-cdk/aws-stepfunctions/test/condition.test.ts b/packages/@aws-cdk/aws-stepfunctions/test/condition.test.ts new file mode 100644 index 0000000000000..084b6f28857f9 --- /dev/null +++ b/packages/@aws-cdk/aws-stepfunctions/test/condition.test.ts @@ -0,0 +1,45 @@ +import '@aws-cdk/assert/jest'; +import * as stepfunctions from '../lib'; + +describe('Condition Variables', () => { + test('Condition variables must start with $. or $[', () => { + expect(() => stepfunctions.Condition.stringEquals('a', 'b')).toThrow(); + }), + test('Condition variables can start with $.', () => { + expect(() => stepfunctions.Condition.stringEquals('$.a', 'b')).not.toThrow(); + }), + test('Condition variables can start with $[', () => { + expect(() => stepfunctions.Condition.stringEquals('$[0]', 'a')).not.toThrow(); + }), + test('NotConditon must render properly', () => { + assertRendersTo(stepfunctions.Condition.not(stepfunctions.Condition.stringEquals('$.a', 'b')), { Not: { Variable: '$.a', StringEquals: 'b' } }); + }), + test('CompoundCondition must render properly', () => { + assertRendersTo( + stepfunctions.Condition.and(stepfunctions.Condition.booleanEquals('$.a', true), stepfunctions.Condition.numberGreaterThan('$.b', 3)), + { + And: [ + { Variable: '$.a', BooleanEquals: true }, + { Variable: '$.b', NumericGreaterThan: 3 }, + ], + } + ); + }), + test('Exercise a number of other conditions', () => { + const cases: Array<[stepfunctions.Condition, object]> = [ + [stepfunctions.Condition.stringLessThan('$.a', 'foo'), { Variable: '$.a', StringLessThan: 'foo' }], + [stepfunctions.Condition.stringLessThanEquals('$.a', 'foo'), { Variable: '$.a', StringLessThanEquals: 'foo' }], + [stepfunctions.Condition.stringGreaterThan('$.a', 'foo'), { Variable: '$.a', StringGreaterThan: 'foo' }], + [stepfunctions.Condition.stringGreaterThanEquals('$.a', 'foo'), { Variable: '$.a', StringGreaterThanEquals: 'foo' }], + [stepfunctions.Condition.numberEquals('$.a', 5), { Variable: '$.a', NumericEquals: 5 }], + ]; + + for (const [cond, expected] of cases) { + assertRendersTo(cond, expected); + } + }); +}); + +function assertRendersTo(cond: stepfunctions.Condition, expected: any) { + expect(cond.renderCondition()).toStrictEqual(expected); +} diff --git a/packages/@aws-cdk/aws-stepfunctions/test/test.fail.ts b/packages/@aws-cdk/aws-stepfunctions/test/fail.test.ts similarity index 59% rename from packages/@aws-cdk/aws-stepfunctions/test/test.fail.ts rename to packages/@aws-cdk/aws-stepfunctions/test/fail.test.ts index b37d9d77ea488..4666a7a31adf1 100644 --- a/packages/@aws-cdk/aws-stepfunctions/test/test.fail.ts +++ b/packages/@aws-cdk/aws-stepfunctions/test/fail.test.ts @@ -1,13 +1,11 @@ +import '@aws-cdk/assert/jest'; import * as cdk from '@aws-cdk/core'; -import { Test } from 'nodeunit'; import * as stepfunctions from '../lib'; -export = { - 'Props are optional'(test: Test) { +describe('Fail State', () => { + test('Props are optional', () => { const stack = new cdk.Stack(); new stepfunctions.Fail(stack, 'Fail'); - - test.done(); - } -}; + }); +}); diff --git a/packages/@aws-cdk/aws-stepfunctions/test/fields.test.ts b/packages/@aws-cdk/aws-stepfunctions/test/fields.test.ts new file mode 100644 index 0000000000000..989b6046f8cf0 --- /dev/null +++ b/packages/@aws-cdk/aws-stepfunctions/test/fields.test.ts @@ -0,0 +1,129 @@ +import '@aws-cdk/assert/jest'; +import { Context, Data, FieldUtils } from '../lib'; + +describe('Fields', () => { + test('deep replace correctly handles fields in arrays', () => { + expect( + FieldUtils.renderObject({ + unknown: undefined, + bool: true, + literal: 'literal', + field: Data.stringAt('$.stringField'), + listField: Data.listAt('$.listField'), + deep: [ + 'literal', + { + deepField: Data.numberAt('$.numField'), + }, + ], + }) + ).toStrictEqual({ + 'bool': true, + 'literal': 'literal', + 'field.$': '$.stringField', + 'listField.$': '$.listField', + 'deep': [ + 'literal', + { + 'deepField.$': '$.numField', + }, + ], + }); + }), + test('exercise contextpaths', () => { + expect( + FieldUtils.renderObject({ + str: Context.stringAt('$$.Execution.StartTime'), + count: Context.numberAt('$$.State.RetryCount'), + token: Context.taskToken, + entire: Context.entireContext, + }) + ).toStrictEqual({ + 'str.$': '$$.Execution.StartTime', + 'count.$': '$$.State.RetryCount', + 'token.$': '$$.Task.Token', + 'entire.$': '$$', + }); + }), + test('find all referenced paths', () => { + expect( + FieldUtils.findReferencedPaths({ + bool: false, + literal: 'literal', + field: Data.stringAt('$.stringField'), + listField: Data.listAt('$.listField'), + deep: [ + 'literal', + { + field: Data.stringAt('$.stringField'), + deepField: Data.numberAt('$.numField'), + }, + ], + }) + ).toStrictEqual(['$.listField', '$.numField', '$.stringField']); + }), + test('cannot have JsonPath fields in arrays', () => { + expect(() => FieldUtils.renderObject({ + deep: [Data.stringAt('$.hello')], + })).toThrowError(/Cannot use JsonPath fields in an array/); + }), + test('datafield path must be correct', () => { + expect(Data.stringAt('$')).toBeDefined(); + + expect(() => Data.stringAt('$hello')).toThrowError(/exactly equal to '\$' or start with '\$.'/); + + expect(() => Data.stringAt('hello')).toThrowError(/exactly equal to '\$' or start with '\$.'/); + }), + test('context path must be correct', () => { + expect(Context.stringAt('$$')).toBeDefined(); + + expect(() => Context.stringAt('$$hello')).toThrowError(/exactly equal to '\$\$' or start with '\$\$.'/); + + expect(() => Context.stringAt('hello')).toThrowError(/exactly equal to '\$\$' or start with '\$\$.'/); + }), + test('test contains task token', () => { + expect(true).toEqual( + FieldUtils.containsTaskToken({ + field: Context.taskToken, + }) + ); + + expect(true).toEqual( + FieldUtils.containsTaskToken({ + field: Context.stringAt('$$.Task'), + }) + ); + + expect(true).toEqual( + FieldUtils.containsTaskToken({ + field: Context.entireContext, + }) + ); + + expect(false).toEqual( + FieldUtils.containsTaskToken({ + oops: 'not here', + }) + ); + + expect(false).toEqual( + FieldUtils.containsTaskToken({ + oops: Context.stringAt('$$.Execution.StartTime'), + }) + ); + }), + test('arbitrary JSONPath fields are not replaced', () => { + expect( + FieldUtils.renderObject({ + field: '$.content', + }) + ).toStrictEqual({ + field: '$.content', + }); + }), + test('fields cannot be used somewhere in a string interpolation', () => { + expect(() => FieldUtils.renderObject({ + field: `contains ${Data.stringAt('$.hello')}`, + })).toThrowError(/Field references must be the entire string/); + }); +}); diff --git a/packages/@aws-cdk/aws-stepfunctions/test/test.map.ts b/packages/@aws-cdk/aws-stepfunctions/test/map.test.ts similarity index 55% rename from packages/@aws-cdk/aws-stepfunctions/test/test.map.ts rename to packages/@aws-cdk/aws-stepfunctions/test/map.test.ts index e4cee1c8cdae9..f43b425898128 100644 --- a/packages/@aws-cdk/aws-stepfunctions/test/test.map.ts +++ b/packages/@aws-cdk/aws-stepfunctions/test/map.test.ts @@ -1,9 +1,9 @@ +import '@aws-cdk/assert/jest'; import * as cdk from '@aws-cdk/core'; -import { Test } from 'nodeunit'; import * as stepfunctions from '../lib'; -export = { - 'State Machine With Map State'(test: Test) { +describe('Map State', () => { + test('State Machine With Map State', () => { // GIVEN const stack = new cdk.Stack(); @@ -19,7 +19,7 @@ export = { map.iterator(new stepfunctions.Pass(stack, 'Pass State')); // THEN - test.deepEqual(render(map), { + expect(render(map)).toStrictEqual({ StartAt: 'Map State', States: { 'Map State': { @@ -40,10 +40,9 @@ export = { } } }); + }), - test.done(); - }, - 'synth is successful'(test: Test) { + test('synth is successful', () => { const app = createAppWithMap((stack) => { const map = new stepfunctions.Map(stack, 'Map State', { @@ -55,9 +54,9 @@ export = { }); app.synth(); - test.done(); - }, - 'fails in synthesis if iterator is missing'(test: Test) { + }), + + test('fails in synthesis if iterator is missing', () => { const app = createAppWithMap((stack) => { const map = new stepfunctions.Map(stack, 'Map State', { @@ -68,13 +67,10 @@ export = { return map; }); - test.throws(() => { - app.synth(); - }, /Map state must have a non-empty iterator/, 'A validation was expected'); + expect(() => app.synth()).toThrow(/Map state must have a non-empty iterator/); + }), - test.done(); - }, - 'fails in synthesis when maxConcurrency is a float'(test: Test) { + test('fails in synthesis when maxConcurrency is a float', () => { const app = createAppWithMap((stack) => { const map = new stepfunctions.Map(stack, 'Map State', { @@ -86,14 +82,10 @@ export = { return map; }); - test.throws(() => { - app.synth(); - }, /maxConcurrency has to be a positive integer/, 'A validation was expected'); - - test.done(); + expect(() => app.synth()).toThrow(/maxConcurrency has to be a positive integer/); + }), - }, - 'fails in synthesis when maxConcurrency is a negative integer'(test: Test) { + test('fails in synthesis when maxConcurrency is a negative integer', () => { const app = createAppWithMap((stack) => { const map = new stepfunctions.Map(stack, 'Map State', { @@ -105,13 +97,10 @@ export = { return map; }); - test.throws(() => { - app.synth(); - }, /maxConcurrency has to be a positive integer/, 'A validation was expected'); + expect(() => app.synth()).toThrow(/maxConcurrency has to be a positive integer/); + }), - test.done(); - }, - 'fails in synthesis when maxConcurrency is too big to be an integer'(test: Test) { + test('fails in synthesis when maxConcurrency is too big to be an integer', () => { const app = createAppWithMap((stack) => { const map = new stepfunctions.Map(stack, 'Map State', { @@ -123,40 +112,34 @@ export = { return map; }); - test.throws(() => { - app.synth(); - }, /maxConcurrency has to be a positive integer/, 'A validation was expected'); - - test.done(); - - }, - 'isPositiveInteger is false with negative number'(test: Test) { - test.equals(stepfunctions.isPositiveInteger(-1), false, '-1 is not a valid positive integer'); - test.done(); - }, - 'isPositiveInteger is false with decimal number'(test: Test) { - test.equals(stepfunctions.isPositiveInteger(1.2), false, '1.2 is not a valid positive integer'); - test.done(); - }, - 'isPositiveInteger is false with a value greater than safe integer '(test: Test) { + expect(() => app.synth()).toThrow(/maxConcurrency has to be a positive integer/); + }), + + test('isPositiveInteger is false with negative number', () => { + expect(stepfunctions.isPositiveInteger(-1)).toEqual(false); + }), + + test('isPositiveInteger is false with decimal number', () => { + expect(stepfunctions.isPositiveInteger(1.2)).toEqual(false); + }), + + test('isPositiveInteger is false with a value greater than safe integer', () => { const valueToTest = Number.MAX_SAFE_INTEGER + 1; - test.equals(stepfunctions.isPositiveInteger(valueToTest), false, `${valueToTest} is not a valid positive integer`); - test.done(); - }, - 'isPositiveInteger is true with 0'(test: Test) { - test.equals(stepfunctions.isPositiveInteger(0), true, '0 is expected to be a positive integer'); - test.done(); - }, - 'isPositiveInteger is true with 10'(test: Test) { - test.equals(stepfunctions.isPositiveInteger(10), true, '10 is expected to be a positive integer'); - test.done(); - }, - 'isPositiveInteger is true with max integer value'(test: Test) { - test.equals(stepfunctions.isPositiveInteger(Number.MAX_SAFE_INTEGER), true, - `${Number.MAX_SAFE_INTEGER} is expected to be a positive integer`); - test.done(); - } -}; + expect(stepfunctions.isPositiveInteger(valueToTest)).toEqual(false); + }), + + test('isPositiveInteger is true with 0', () => { + expect(stepfunctions.isPositiveInteger(0)).toEqual(true); + }), + + test('isPositiveInteger is true with 10', () => { + expect(stepfunctions.isPositiveInteger(10)).toEqual(true); + }), + + test('isPositiveInteger is true with max integer value', () => { + expect(stepfunctions.isPositiveInteger(Number.MAX_SAFE_INTEGER)).toEqual(true); + }); +}); function render(sm: stepfunctions.IChainable) { return new cdk.Stack().resolve(new stepfunctions.StateGraph(sm.startState, 'Test Graph').toGraphJson()); diff --git a/packages/@aws-cdk/aws-stepfunctions/test/test.parallel.ts b/packages/@aws-cdk/aws-stepfunctions/test/parallel.test.ts similarity index 83% rename from packages/@aws-cdk/aws-stepfunctions/test/test.parallel.ts rename to packages/@aws-cdk/aws-stepfunctions/test/parallel.test.ts index 19ca289bb78d9..a018f858290cf 100644 --- a/packages/@aws-cdk/aws-stepfunctions/test/test.parallel.ts +++ b/packages/@aws-cdk/aws-stepfunctions/test/parallel.test.ts @@ -1,9 +1,9 @@ +import '@aws-cdk/assert/jest'; import * as cdk from '@aws-cdk/core'; -import { Test } from 'nodeunit'; import * as stepfunctions from '../lib'; -export = { - 'State Machine With Parallel State'(test: Test) { +describe('Parallel State', () => { + test('State Machine With Parallel State', () => { // GIVEN const stack = new cdk.Stack(); @@ -13,7 +13,7 @@ export = { parallel.branch(new stepfunctions.Pass(stack, 'Branch 2')); // THEN - test.deepEqual(render(parallel), { + expect(render(parallel)).toStrictEqual({ StartAt: 'Parallel State', States: { 'Parallel State': { @@ -26,10 +26,8 @@ export = { } } }); - - test.done(); - } -}; + }); +}); function render(sm: stepfunctions.IChainable) { return new cdk.Stack().resolve(new stepfunctions.StateGraph(sm.startState, 'Test Graph').toGraphJson()); diff --git a/packages/@aws-cdk/aws-stepfunctions/test/pass.test.ts b/packages/@aws-cdk/aws-stepfunctions/test/pass.test.ts new file mode 100644 index 0000000000000..aa78cd4618cf2 --- /dev/null +++ b/packages/@aws-cdk/aws-stepfunctions/test/pass.test.ts @@ -0,0 +1,34 @@ +import '@aws-cdk/assert/jest'; +import { Result } from '../lib'; + +describe('Pass State', () => { + test('fromString has proper value', () => { + const testValue = 'test string'; + const result = Result.fromString(testValue); + expect(result.value).toEqual(testValue); + }), + + test('fromNumber has proper value', () => { + const testValue = 1; + const result = Result.fromNumber(testValue); + expect(result.value).toEqual(testValue); + }), + + test('fromBoolean has proper value', () => { + const testValue = false; + const result = Result.fromBoolean(testValue); + expect(result.value).toEqual(testValue); + }), + + test('fromObject has proper value', () => { + const testValue = {a: 1}; + const result = Result.fromObject(testValue); + expect(result.value).toStrictEqual(testValue); + }), + + test('fromArray has proper value', () => { + const testValue = [1]; + const result = Result.fromArray(testValue); + expect(result.value).toEqual(testValue); + }); +}); diff --git a/packages/@aws-cdk/aws-stepfunctions/test/test.state-machine-resources.ts b/packages/@aws-cdk/aws-stepfunctions/test/state-machine-resources.test.ts similarity index 82% rename from packages/@aws-cdk/aws-stepfunctions/test/test.state-machine-resources.ts rename to packages/@aws-cdk/aws-stepfunctions/test/state-machine-resources.test.ts index 16840ef821ded..3b89936fa4f34 100644 --- a/packages/@aws-cdk/aws-stepfunctions/test/test.state-machine-resources.ts +++ b/packages/@aws-cdk/aws-stepfunctions/test/state-machine-resources.test.ts @@ -1,11 +1,11 @@ -import { expect, haveResource, ResourcePart } from '@aws-cdk/assert'; +import { ResourcePart } from '@aws-cdk/assert'; +import '@aws-cdk/assert/jest'; import * as iam from '@aws-cdk/aws-iam'; import * as cdk from '@aws-cdk/core'; -import { Test } from 'nodeunit'; import * as stepfunctions from '../lib'; -export = { - 'Tasks can add permissions to the execution role'(test: Test) { +describe('State Machine Resources', () => { + test('Tasks can add permissions to the execution role', () => { // GIVEN const stack = new cdk.Stack(); const task = new stepfunctions.Task(stack, 'Task', { @@ -26,7 +26,7 @@ export = { }); // THEN - expect(stack).to(haveResource('AWS::IAM::Policy', { + expect(stack).toHaveResource('AWS::IAM::Policy', { PolicyDocument: { Version: '2012-10-17', Statement: [ @@ -37,12 +37,10 @@ export = { } ], } - })); - - test.done(); - }, + }); + }), - 'Tasks hidden inside a Parallel state are also included'(test: Test) { + test('Tasks hidden inside a Parallel state are also included', () => { // GIVEN const stack = new cdk.Stack(); const task = new stepfunctions.Task(stack, 'Task', { @@ -68,7 +66,7 @@ export = { }); // THEN - expect(stack).to(haveResource('AWS::IAM::Policy', { + expect(stack).toHaveResource('AWS::IAM::Policy', { PolicyDocument: { Version: '2012-10-17', Statement: [ @@ -79,12 +77,10 @@ export = { } ], } - })); - - test.done(); - }, + }); + }), - 'Task should render InputPath / Parameters / OutputPath correctly'(test: Test) { + test('Task should render InputPath / Parameters / OutputPath correctly', () => { // GIVEN const stack = new cdk.Stack(); const task = new stepfunctions.Task(stack, 'Task', { @@ -108,7 +104,7 @@ export = { const taskState = task.toStateJson(); // THEN - test.deepEqual(taskState, { End: true, + expect(taskState).toStrictEqual({ End: true, Retry: undefined, Catch: undefined, InputPath: '$', @@ -126,11 +122,9 @@ export = { TimeoutSeconds: undefined, HeartbeatSeconds: undefined }); + }), - test.done(); - }, - - 'Task combines taskobject parameters with direct parameters'(test: Test) { + test('Task combines taskobject parameters with direct parameters', () => { // GIVEN const stack = new cdk.Stack(); const task = new stepfunctions.Task(stack, 'Task', { @@ -153,7 +147,7 @@ export = { const taskState = task.toStateJson(); // THEN - test.deepEqual(taskState, { End: true, + expect(taskState).toStrictEqual({ End: true, Retry: undefined, Catch: undefined, InputPath: '$', @@ -168,11 +162,9 @@ export = { TimeoutSeconds: undefined, HeartbeatSeconds: undefined }); + }), - test.done(); - }, - - 'Created state machine can grant start execution to a role'(test: Test) { + test('Created state machine can grant start execution to a role', () => { // GIVEN const stack = new cdk.Stack(); const task = new stepfunctions.Task(stack, 'Task', { @@ -191,7 +183,7 @@ export = { stateMachine.grantStartExecution(role); // THEN - expect(stack).to(haveResource('AWS::IAM::Policy', { + expect(stack).toHaveResource('AWS::IAM::Policy', { PolicyDocument: { Statement: [ { @@ -210,12 +202,11 @@ export = { Ref: 'Role1ABCC5F0' } ] - })); + }); - test.done(); - }, + }), - 'Imported state machine can grant start execution to a role'(test: Test) { + test('Imported state machine can grant start execution to a role', () => { // GIVEN const stack = new cdk.Stack(); const stateMachineArn = 'arn:aws:states:::my-state-machine'; @@ -228,7 +219,7 @@ export = { stateMachine.grantStartExecution(role); // THEN - expect(stack).to(haveResource('AWS::IAM::Policy', { + expect(stack).toHaveResource('AWS::IAM::Policy', { PolicyDocument: { Statement: [ { @@ -245,12 +236,10 @@ export = { Ref: 'Role1ABCC5F0' } ] - })); - - test.done(); - }, + }); + }), - 'Pass should render InputPath / Parameters / OutputPath correctly'(test: Test) { + test('Pass should render InputPath / Parameters / OutputPath correctly', () => { // GIVEN const stack = new cdk.Stack(); const task = new stepfunctions.Pass(stack, 'Pass', { @@ -269,7 +258,7 @@ export = { const taskState = task.toStateJson(); // THEN - test.deepEqual(taskState, { End: true, + expect(taskState).toStrictEqual({ End: true, InputPath: '$', OutputPath: '$.state', Parameters: @@ -283,11 +272,9 @@ export = { Result: undefined, ResultPath: undefined, }); + }), - test.done(); - }, - - 'State machines must depend on their roles'(test: Test) { + test('State machines must depend on their roles', () => { // GIVEN const stack = new cdk.Stack(); const task = new stepfunctions.Task(stack, 'Task', { @@ -308,14 +295,12 @@ export = { }); // THEN - expect(stack).to(haveResource('AWS::StepFunctions::StateMachine', { + expect(stack).toHaveResource('AWS::StepFunctions::StateMachine', { DependsOn: [ 'StateMachineRoleDefaultPolicyDF1E6607', 'StateMachineRoleB840431D' ] - }, ResourcePart.CompleteDefinition)); - - test.done(); - }, + }, ResourcePart.CompleteDefinition); + }); -}; +}); diff --git a/packages/@aws-cdk/aws-stepfunctions/test/test.state-machine.ts b/packages/@aws-cdk/aws-stepfunctions/test/state-machine.test.ts similarity index 79% rename from packages/@aws-cdk/aws-stepfunctions/test/test.state-machine.ts rename to packages/@aws-cdk/aws-stepfunctions/test/state-machine.test.ts index 3c0741b6cbedb..78928bb7c06f4 100644 --- a/packages/@aws-cdk/aws-stepfunctions/test/test.state-machine.ts +++ b/packages/@aws-cdk/aws-stepfunctions/test/state-machine.test.ts @@ -1,11 +1,10 @@ -import { expect, haveResource } from '@aws-cdk/assert'; +import '@aws-cdk/assert/jest'; import * as logs from '@aws-cdk/aws-logs'; import * as cdk from '@aws-cdk/core'; -import { Test } from 'nodeunit'; import * as stepfunctions from '../lib'; -export = { - 'Instantiate Default State Machine'(test: Test) { +describe('State Machine', () => { + test('Instantiate Default State Machine', () => { // GIVEN const stack = new cdk.Stack(); @@ -16,15 +15,13 @@ export = { }); // THEN - expect(stack).to(haveResource('AWS::StepFunctions::StateMachine', { + expect(stack).toHaveResource('AWS::StepFunctions::StateMachine', { StateMachineName: 'MyStateMachine', DefinitionString: '{"StartAt":"Pass","States":{"Pass":{"Type":"Pass","End":true}}}' - })); - - test.done(); - }, + }); + }), - 'Instantiate Standard State Machine'(test: Test) { + test('Instantiate Standard State Machine', () => { // GIVEN const stack = new cdk.Stack(); @@ -36,16 +33,15 @@ export = { }); // THEN - expect(stack).to(haveResource('AWS::StepFunctions::StateMachine', { + expect(stack).toHaveResource('AWS::StepFunctions::StateMachine', { StateMachineName: 'MyStateMachine', StateMachineType: 'STANDARD', DefinitionString: '{"StartAt":"Pass","States":{"Pass":{"Type":"Pass","End":true}}}' - })); + }); - test.done(); - }, + }), - 'Instantiate Express State Machine'(test: Test) { + test('Instantiate Express State Machine', () => { // GIVEN const stack = new cdk.Stack(); @@ -57,16 +53,15 @@ export = { }); // THEN - expect(stack).to(haveResource('AWS::StepFunctions::StateMachine', { + expect(stack).toHaveResource('AWS::StepFunctions::StateMachine', { StateMachineName: 'MyStateMachine', StateMachineType: 'EXPRESS', DefinitionString: '{"StartAt":"Pass","States":{"Pass":{"Type":"Pass","End":true}}}' - })); + }); - test.done(); - }, + }), - 'log configuration'(test: Test) { + test('log configuration', () => { // GIVEN const stack = new cdk.Stack(); @@ -83,7 +78,7 @@ export = { }); // THEN - expect(stack).to(haveResource('AWS::StepFunctions::StateMachine', { + expect(stack).toHaveResource('AWS::StepFunctions::StateMachine', { DefinitionString: '{"StartAt":"Pass","States":{"Pass":{"Type":"Pass","End":true}}}', LoggingConfiguration: { Destinations: [{ @@ -96,9 +91,9 @@ export = { IncludeExecutionData: false, Level: 'FATAL' } - })); + }); - expect(stack).to(haveResource('AWS::IAM::Policy', { + expect(stack).toHaveResource('AWS::IAM::Policy', { PolicyDocument: { Statement: [{ Action: [ @@ -122,8 +117,7 @@ export = { Ref: 'MyStateMachineRoleD59FFEBC' } ] - })); + }); + }); - test.done(); - }, -}; +}); \ No newline at end of file diff --git a/packages/@aws-cdk/aws-stepfunctions/test/states-language.test.ts b/packages/@aws-cdk/aws-stepfunctions/test/states-language.test.ts new file mode 100644 index 0000000000000..cb39d8fdd6239 --- /dev/null +++ b/packages/@aws-cdk/aws-stepfunctions/test/states-language.test.ts @@ -0,0 +1,668 @@ +import '@aws-cdk/assert/jest'; +import * as cdk from '@aws-cdk/core'; +import * as stepfunctions from '../lib'; + +describe('States Language', () => { + test('A single task is a State Machine', () => { + // GIVEN + const stack = new cdk.Stack(); + + // WHEN + const chain = new stepfunctions.Pass(stack, 'Some State'); + + // THEN + expect(render(chain)).toStrictEqual({ + StartAt: 'Some State', + States: { + 'Some State': { Type: 'Pass', End: true } + } + }); + }), + + test('A sequence of two tasks is a State Machine', () => { + // GIVEN + const stack = new cdk.Stack(); + + // WHEN + const task1 = new stepfunctions.Pass(stack, 'State One'); + const task2 = new stepfunctions.Pass(stack, 'State Two'); + + const chain = stepfunctions.Chain + .start(task1) + .next(task2); + + // THEN + expect(render(chain)).toStrictEqual({ + StartAt: 'State One', + States: { + 'State One': { Type: 'Pass', Next: 'State Two' }, + 'State Two': { Type: 'Pass', End: true }, + } + }); + }), + + test('You dont need to hold on to the state to render the entire state machine correctly', () => { + const stack = new cdk.Stack(); + + // WHEN + const task1 = new stepfunctions.Pass(stack, 'State One'); + const task2 = new stepfunctions.Pass(stack, 'State Two'); + + task1.next(task2); + + // THEN + expect(render(task1)).toStrictEqual({ + StartAt: 'State One', + States: { + 'State One': { Type: 'Pass', Next: 'State Two' }, + 'State Two': { Type: 'Pass', End: true }, + } + }); + }), + + test('A chain can be appended to', () => { + // GIVEN + const stack = new cdk.Stack(); + + const task1 = new stepfunctions.Pass(stack, 'State One'); + const task2 = new stepfunctions.Pass(stack, 'State Two'); + const task3 = new stepfunctions.Pass(stack, 'State Three'); + + // WHEN + const chain = stepfunctions.Chain + .start(task1) + .next(task2) + .next(task3); + + // THEN + expect(render(chain)).toStrictEqual({ + StartAt: 'State One', + States: { + 'State One': { Type: 'Pass', Next: 'State Two' }, + 'State Two': { Type: 'Pass', Next: 'State Three' }, + 'State Three': { Type: 'Pass', End: true }, + } + }); + }), + + test('A state machine can be appended to another state machine', () => { + // GIVEN + const stack = new cdk.Stack(); + + const task1 = new stepfunctions.Pass(stack, 'State One'); + const task2 = new stepfunctions.Pass(stack, 'State Two'); + const task3 = new stepfunctions.Wait(stack, 'State Three', { + time: stepfunctions.WaitTime.duration(cdk.Duration.seconds(10)) + }); + + // WHEN + const chain = stepfunctions.Chain + .start(task1) + .next(stepfunctions.Chain.start(task2).next(task3)); + + // THEN + expect(render(chain)).toStrictEqual({ + StartAt: 'State One', + States: { + 'State One': { Type: 'Pass', Next: 'State Two' }, + 'State Two': { Type: 'Pass', Next: 'State Three' }, + 'State Three': { Type: 'Wait', End: true, Seconds: 10 }, + } + }); + + }), + + test('A state machine definition can be instantiated and chained', () => { + const stack = new cdk.Stack(); + const before = new stepfunctions.Pass(stack, 'Before'); + const after = new stepfunctions.Pass(stack, 'After'); + + // WHEN + const chain = before.next(new ReusableStateMachine(stack, 'Reusable')).next(after); + + // THEN + expect(render(chain)).toStrictEqual({ + StartAt: 'Before', + States: { + 'Before': { Type: 'Pass', Next: 'Choice' }, + 'Choice': { + Type: 'Choice', + Choices: [ + { Variable: '$.branch', StringEquals: 'left', Next: 'Left Branch' }, + { Variable: '$.branch', StringEquals: 'right', Next: 'Right Branch' }, + ] + }, + 'Left Branch': { Type: 'Pass', Next: 'After' }, + 'Right Branch': { Type: 'Pass', Next: 'After' }, + 'After': { Type: 'Pass', End: true }, + } + }); + }), + + test('A success state cannot be chained onto', () => { + // GIVEN + const stack = new cdk.Stack(); + + const succeed = new stepfunctions.Succeed(stack, 'Succeed'); + const pass = new stepfunctions.Pass(stack, 'Pass'); + + // WHEN + expect(() => pass.next(succeed).next(pass)).toThrow(); + }), + + test('A failure state cannot be chained onto', () => { + // GIVEN + const stack = new cdk.Stack(); + const fail = new stepfunctions.Fail(stack, 'Fail', { error: 'X', cause: 'Y' }); + const pass = new stepfunctions.Pass(stack, 'Pass'); + + // WHEN + expect(() => pass.next(fail).next(pass)).toThrow(); + }), + + test('Parallels can contain direct states', () => { + // GIVEN + const stack = new cdk.Stack(); + + const one = new stepfunctions.Pass(stack, 'One'); + const two = new stepfunctions.Pass(stack, 'Two'); + const three = new stepfunctions.Pass(stack, 'Three'); + + // WHEN + const para = new stepfunctions.Parallel(stack, 'Parallel'); + para.branch(one.next(two)); + para.branch(three); + + // THEN + expect(render(para)).toStrictEqual({ + StartAt: 'Parallel', + States: { + Parallel: { + Type: 'Parallel', + End: true, + Branches: [ + { + StartAt: 'One', + States: { + One: { Type: 'Pass', Next: 'Two' }, + Two: { Type: 'Pass', End: true }, + } + }, + { + StartAt: 'Three', + States: { + Three: { Type: 'Pass', End: true } + } + } + ] + } + } + }); + }), + + test('Parallels can contain instantiated reusable definitions', () => { + // GIVEN + const stack = new cdk.Stack(); + + // WHEN + const para = new stepfunctions.Parallel(stack, 'Parallel'); + para.branch(new ReusableStateMachine(stack, 'Reusable1').prefixStates('Reusable1/')); + para.branch(new ReusableStateMachine(stack, 'Reusable2').prefixStates('Reusable2/')); + + // THEN + expect(render(para)).toStrictEqual({ + StartAt: 'Parallel', + States: { + Parallel: { + Type: 'Parallel', + End: true, + Branches: [ + { + StartAt: 'Reusable1/Choice', + States: { + 'Reusable1/Choice': { + Type: 'Choice', + Choices: [ + { Variable: '$.branch', StringEquals: 'left', Next: 'Reusable1/Left Branch' }, + { Variable: '$.branch', StringEquals: 'right', Next: 'Reusable1/Right Branch' }, + ] + }, + 'Reusable1/Left Branch': { Type: 'Pass', End: true }, + 'Reusable1/Right Branch': { Type: 'Pass', End: true }, + } + }, + { + StartAt: 'Reusable2/Choice', + States: { + 'Reusable2/Choice': { + Type: 'Choice', + Choices: [ + { Variable: '$.branch', StringEquals: 'left', Next: 'Reusable2/Left Branch' }, + { Variable: '$.branch', StringEquals: 'right', Next: 'Reusable2/Right Branch' }, + ] + }, + 'Reusable2/Left Branch': { Type: 'Pass', End: true }, + 'Reusable2/Right Branch': { Type: 'Pass', End: true }, + } + }, + ] + } + } + }); + }), + + test('State Machine Fragments can be wrapped in a single state', () => { + // GIVEN + const stack = new cdk.Stack(); + + const reusable = new SimpleChain(stack, 'Hello'); + const state = reusable.toSingleState(); + + expect(render(state)).toStrictEqual({ + StartAt: 'Hello', + States: { + Hello: { + Type: 'Parallel', + End: true, + Branches: [ + { + StartAt: 'Hello: Task1', + States: { + 'Hello: Task1': { Type: 'Task', Next: 'Hello: Task2', Resource: 'resource' }, + 'Hello: Task2': { Type: 'Task', End: true, Resource: 'resource' }, + } + } + ], + }, + } + }); + }), + + test('Chaining onto branched failure state ignores failure state', () => { + // GIVEN + const stack = new cdk.Stack(); + + const yes = new stepfunctions.Pass(stack, 'Yes'); + const no = new stepfunctions.Fail(stack, 'No', { error: 'Failure', cause: 'Wrong branch' }); + const enfin = new stepfunctions.Pass(stack, 'Finally'); + const choice = new stepfunctions.Choice(stack, 'Choice') + .when(stepfunctions.Condition.stringEquals('$.foo', 'bar'), yes) + .otherwise(no); + + // WHEN + choice.afterwards().next(enfin); + + // THEN + expect(render(choice)).toStrictEqual({ + StartAt: 'Choice', + States: { + Choice: { + Type: 'Choice', + Choices: [ + { Variable: '$.foo', StringEquals: 'bar', Next: 'Yes' }, + ], + Default: 'No', + }, + Yes: { Type: 'Pass', Next: 'Finally' }, + No: { Type: 'Fail', Error: 'Failure', Cause: 'Wrong branch' }, + Finally: { Type: 'Pass', End: true }, + } + }); + }), + + test('Can include OTHERWISE transition for Choice in afterwards()', () => { + // GIVEN + const stack = new cdk.Stack(); + + // WHEN + const chain = new stepfunctions.Choice(stack, 'Choice') + .when(stepfunctions.Condition.stringEquals('$.foo', 'bar'), + new stepfunctions.Pass(stack, 'Yes')) + .afterwards({ includeOtherwise: true }) + .next(new stepfunctions.Pass(stack, 'Finally')); + + // THEN + expect(render(chain)).toStrictEqual({ + StartAt: 'Choice', + States: { + Choice: { + Type: 'Choice', + Choices: [ + { Variable: '$.foo', StringEquals: 'bar', Next: 'Yes' }, + ], + Default: 'Finally', + }, + Yes: { Type: 'Pass', Next: 'Finally' }, + Finally: { Type: 'Pass', End: true }, + } + }); + + }), + + test('State machines can have unconstrainted gotos', () => { + // GIVEN + const stack = new cdk.Stack(); + + const one = new stepfunctions.Pass(stack, 'One'); + const two = new stepfunctions.Pass(stack, 'Two'); + + // WHEN + const chain = one.next(two).next(one); + + // THEN + expect(render(chain)).toStrictEqual({ + StartAt: 'One', + States: { + One: { Type: 'Pass', Next: 'Two' }, + Two: { Type: 'Pass', Next: 'One' }, + } + }); + }), + + test('States can have error branches', () => { + // GIVEN + const stack = new cdk.Stack(); + const task1 = new stepfunctions.Task(stack, 'Task1', { task: new FakeTask()}); + const failure = new stepfunctions.Fail(stack, 'Failed', { error: 'DidNotWork', cause: 'We got stuck' }); + + // WHEN + const chain = task1.addCatch(failure); + + // THEN + expect(render(chain)).toStrictEqual({ + StartAt: 'Task1', + States: { + Task1: { + Type: 'Task', + Resource: 'resource', + End: true, + Catch: [ + { ErrorEquals: ['States.ALL'], Next: 'Failed' }, + ] + }, + Failed: { + Type: 'Fail', + Error: 'DidNotWork', + Cause: 'We got stuck', + } + } + }); + }), + + test('Retries and errors with a result path', () => { + // GIVEN + const stack = new cdk.Stack(); + const task1 = new stepfunctions.Task(stack, 'Task1', { task: new FakeTask() }); + const failure = new stepfunctions.Fail(stack, 'Failed', { error: 'DidNotWork', cause: 'We got stuck' }); + + // WHEN + const chain = task1.addRetry({ errors: ['HTTPError'], maxAttempts: 2 }).addCatch(failure, { resultPath: '$.some_error' }).next(failure); + + // THEN + expect(render(chain)).toStrictEqual({ + StartAt: 'Task1', + States: { + Task1: { + Type: 'Task', + Resource: 'resource', + Catch: [ { ErrorEquals: ['States.ALL'], Next: 'Failed', ResultPath: '$.some_error' } ], + Retry: [ { ErrorEquals: ['HTTPError'], MaxAttempts: 2 } ], + Next: 'Failed', + }, + Failed: { + Type: 'Fail', + Error: 'DidNotWork', + Cause: 'We got stuck', + } + } + }); + }), + + test('Can wrap chain and attach error handler', () => { + // GIVEN + const stack = new cdk.Stack(); + + const task1 = new stepfunctions.Task(stack, 'Task1', { task: new FakeTask() }); + const task2 = new stepfunctions.Task(stack, 'Task2', { task: new FakeTask() }); + const errorHandler = new stepfunctions.Pass(stack, 'ErrorHandler'); + + // WHEN + const chain = task1.next(task2).toSingleState('Wrapped').addCatch(errorHandler); + + // THEN + expect(render(chain)).toStrictEqual({ + StartAt: 'Wrapped', + States: { + Wrapped: { + Type: 'Parallel', + Branches: [ + { + StartAt: 'Task1', + States: { + Task1: { + Type: 'Task', + Resource: 'resource', + Next: 'Task2', + }, + Task2: { + Type: 'Task', + Resource: 'resource', + End: true, + }, + } + } + ], + Catch: [ + { ErrorEquals: ['States.ALL'], Next: 'ErrorHandler' }, + ], + End: true + }, + ErrorHandler: { Type: 'Pass', End: true } + }, + }); + }), + + test('Chaining does not chain onto error handler state', () => { + // GIVEN + const stack = new cdk.Stack(); + + const task1 = new stepfunctions.Task(stack, 'Task1', { task: new FakeTask() }); + const task2 = new stepfunctions.Task(stack, 'Task2', { task: new FakeTask() }); + const errorHandler = new stepfunctions.Pass(stack, 'ErrorHandler'); + + // WHEN + const chain = task1.addCatch(errorHandler).next(task2); + + // THEN + expect(render(chain)).toStrictEqual({ + StartAt: 'Task1', + States: { + Task1: { + Type: 'Task', + Resource: 'resource', + Next: 'Task2', + Catch: [ + { ErrorEquals: ['States.ALL'], Next: 'ErrorHandler' }, + ] + }, + Task2: { Type: 'Task', Resource: 'resource', End: true }, + ErrorHandler: { Type: 'Pass', End: true }, + } + }); + }), + + test('Chaining does not chain onto error handler, extended', () => { + // GIVEN + const stack = new cdk.Stack(); + + const task1 = new stepfunctions.Task(stack, 'Task1', { task: new FakeTask() }); + const task2 = new stepfunctions.Task(stack, 'Task2', { task: new FakeTask() }); + const task3 = new stepfunctions.Task(stack, 'Task3', { task: new FakeTask() }); + const errorHandler = new stepfunctions.Pass(stack, 'ErrorHandler'); + + // WHEN + const chain = task1.addCatch(errorHandler) + .next(task2.addCatch(errorHandler)) + .next(task3.addCatch(errorHandler)); + + // THEN + const sharedTaskProps = { Type: 'Task', Resource: 'resource', Catch: [ { ErrorEquals: ['States.ALL'], Next: 'ErrorHandler' } ] }; + expect(render(chain)).toStrictEqual({ + StartAt: 'Task1', + States: { + Task1: { Next: 'Task2', ...sharedTaskProps }, + Task2: { Next: 'Task3', ...sharedTaskProps }, + Task3: { End: true, ...sharedTaskProps }, + ErrorHandler: { Type: 'Pass', End: true }, + } + }); + }), + + test('Error handler with a fragment', () => { + // GIVEN + const stack = new cdk.Stack(); + + const task1 = new stepfunctions.Task(stack, 'Task1', { task: new FakeTask() }); + const task2 = new stepfunctions.Task(stack, 'Task2', { task: new FakeTask() }); + const errorHandler = new stepfunctions.Pass(stack, 'ErrorHandler'); + + // WHEN + task1.addCatch(errorHandler) + .next(new SimpleChain(stack, 'Chain').catch(errorHandler)) + .next(task2.addCatch(errorHandler)); + }), + + test('Can merge state machines with shared states', () => { + // GIVEN + const stack = new cdk.Stack(); + + const task1 = new stepfunctions.Task(stack, 'Task1', { task: new FakeTask() }); + const task2 = new stepfunctions.Task(stack, 'Task2', { task: new FakeTask() }); + const failure = new stepfunctions.Fail(stack, 'Failed', { error: 'DidNotWork', cause: 'We got stuck' }); + + // WHEN + task1.addCatch(failure); + task2.addCatch(failure); + + task1.next(task2); + + // THEN + expect(render(task1)).toStrictEqual({ + StartAt: 'Task1', + States: { + Task1: { + Type: 'Task', + Resource: 'resource', + Next: 'Task2', + Catch: [ + { ErrorEquals: ['States.ALL'], Next: 'Failed' }, + ] + }, + Task2: { + Type: 'Task', + Resource: 'resource', + End: true, + Catch: [ + { ErrorEquals: ['States.ALL'], Next: 'Failed' }, + ] + }, + Failed: { + Type: 'Fail', + Error: 'DidNotWork', + Cause: 'We got stuck', + } + } + }); + }), + + test('No duplicate state IDs', () => { + // GIVEN + const stack = new cdk.Stack(); + const intermediateParent = new cdk.Construct(stack, 'Parent'); + + const state1 = new stepfunctions.Pass(stack, 'State'); + const state2 = new stepfunctions.Pass(intermediateParent, 'State'); + + state1.next(state2); + + // WHEN + expect(() => render(state1)).toThrow(); + }), + + test('No duplicate state IDs even across Parallel branches', () => { + // GIVEN + const stack = new cdk.Stack(); + const intermediateParent = new cdk.Construct(stack, 'Parent'); + + const state1 = new stepfunctions.Pass(stack, 'State'); + const state2 = new stepfunctions.Pass(intermediateParent, 'State'); + + const parallel = new stepfunctions.Parallel(stack, 'Parallel') + .branch(state1) + .branch(state2); + + // WHEN + expect(() => render(parallel)).toThrow(); + }), + + test('No cross-parallel jumps', () => { + // GIVEN + const stack = new cdk.Stack(); + const state1 = new stepfunctions.Pass(stack, 'State1'); + const state2 = new stepfunctions.Pass(stack, 'State2'); + + expect(() => new stepfunctions.Parallel(stack, 'Parallel') + .branch(state1.next(state2)) + .branch(state2)).toThrow(); + }); +}); + +class ReusableStateMachine extends stepfunctions.StateMachineFragment { + public readonly startState: stepfunctions.State; + public readonly endStates: stepfunctions.INextable[]; + constructor(scope: cdk.Construct, id: string) { + super(scope, id); + + const choice = new stepfunctions.Choice(this, 'Choice') + .when(stepfunctions.Condition.stringEquals('$.branch', 'left'), new stepfunctions.Pass(this, 'Left Branch')) + .when(stepfunctions.Condition.stringEquals('$.branch', 'right'), new stepfunctions.Pass(this, 'Right Branch')); + + this.startState = choice; + this.endStates = choice.afterwards().endStates; + } +} + +class SimpleChain extends stepfunctions.StateMachineFragment { + public readonly startState: stepfunctions.State; + public readonly endStates: stepfunctions.INextable[]; + + private readonly task2: stepfunctions.Task; + constructor(scope: cdk.Construct, id: string) { + super(scope, id); + + const task1 = new stepfunctions.Task(this, 'Task1', { task: new FakeTask() }); + this.task2 = new stepfunctions.Task(this, 'Task2', { task: new FakeTask() }); + + task1.next(this.task2); + + this.startState = task1; + this.endStates = [this.task2]; + } + + public catch(state: stepfunctions.IChainable, props?: stepfunctions.CatchProps): SimpleChain { + this.task2.addCatch(state, props); + return this; + } +} + +function render(sm: stepfunctions.IChainable) { + return new cdk.Stack().resolve(new stepfunctions.StateGraph(sm.startState, 'Test Graph').toGraphJson()); +} + +class FakeTask implements stepfunctions.IStepFunctionsTask { + public bind(_task: stepfunctions.Task): stepfunctions.StepFunctionsTaskConfig { + return { + resourceArn: 'resource' + }; + } +} diff --git a/packages/@aws-cdk/aws-stepfunctions/test/test.condition.ts b/packages/@aws-cdk/aws-stepfunctions/test/test.condition.ts deleted file mode 100644 index edb3179de3a29..0000000000000 --- a/packages/@aws-cdk/aws-stepfunctions/test/test.condition.ts +++ /dev/null @@ -1,79 +0,0 @@ -import { Test } from 'nodeunit'; -import * as stepfunctions from '../lib'; - -export = { - 'Condition variables must start with $. or $['(test: Test) { - test.throws(() => { - stepfunctions.Condition.stringEquals('a', 'b'); - }); - - test.done(); - }, - 'Condition variables can start with $.'(test: Test) { - test.doesNotThrow(() => { - stepfunctions.Condition.stringEquals('$.a', 'b'); - }); - - test.done(); - }, - 'Condition variables can start with $['(test: Test) { - test.doesNotThrow(() => { - stepfunctions.Condition.stringEquals('$[0]', 'a'); - }); - - test.done(); - }, - 'NotConditon must render properly'(test: Test) { - assertRendersTo(test, - stepfunctions.Condition.not(stepfunctions.Condition.stringEquals('$.a', 'b')), - {Not: {Variable: '$.a', StringEquals: 'b'}} - ); - - test.done(); - }, - 'CompoundCondition must render properly'(test: Test) { - assertRendersTo(test, - stepfunctions.Condition.and( - stepfunctions.Condition.booleanEquals('$.a', true), - stepfunctions.Condition.numberGreaterThan('$.b', 3) - ), - { And: [ { Variable: '$.a', BooleanEquals: true }, { Variable: '$.b', NumericGreaterThan: 3 } ] } - ); - - test.done(); - }, - 'Exercise a number of other conditions'(test: Test) { - const cases: Array<[stepfunctions.Condition, object]> = [ - [ - stepfunctions.Condition.stringLessThan('$.a', 'foo'), - { Variable: '$.a', StringLessThan: 'foo' }, - ], - [ - stepfunctions.Condition.stringLessThanEquals('$.a', 'foo'), - { Variable: '$.a', StringLessThanEquals: 'foo' }, - ], - [ - stepfunctions.Condition.stringGreaterThan('$.a', 'foo'), - { Variable: '$.a', StringGreaterThan: 'foo' }, - ], - [ - stepfunctions.Condition.stringGreaterThanEquals('$.a', 'foo'), - { Variable: '$.a', StringGreaterThanEquals: 'foo' }, - ], - [ - stepfunctions.Condition.numberEquals('$.a', 5), - { Variable: '$.a', NumericEquals: 5 } - ], - ]; - - for (const [cond, expected] of cases) { - assertRendersTo(test, cond, expected); - } - - test.done(); - }, -}; - -function assertRendersTo(test: Test, cond: stepfunctions.Condition, expected: any) { - test.deepEqual(cond.renderCondition(), expected); -} diff --git a/packages/@aws-cdk/aws-stepfunctions/test/test.fields.ts b/packages/@aws-cdk/aws-stepfunctions/test/test.fields.ts deleted file mode 100644 index 94ac02b7051ca..0000000000000 --- a/packages/@aws-cdk/aws-stepfunctions/test/test.fields.ts +++ /dev/null @@ -1,153 +0,0 @@ -import { Test } from 'nodeunit'; -import { Context, Data, FieldUtils } from '../lib'; - -export = { - 'deep replace correctly handles fields in arrays'(test: Test) { - test.deepEqual(FieldUtils.renderObject({ - unknown: undefined, - bool: true, - literal: 'literal', - field: Data.stringAt('$.stringField'), - listField: Data.listAt('$.listField'), - deep: [ - 'literal', - { - deepField: Data.numberAt('$.numField'), - } - ] - }), { - 'bool': true, - 'literal': 'literal', - 'field.$': '$.stringField', - 'listField.$': '$.listField', - 'deep': [ - 'literal', - { - 'deepField.$': '$.numField' - } - ], - }); - - test.done(); - }, - - 'exercise contextpaths'(test: Test) { - test.deepEqual(FieldUtils.renderObject({ - str: Context.stringAt('$$.Execution.StartTime'), - count: Context.numberAt('$$.State.RetryCount'), - token: Context.taskToken, - entire: Context.entireContext - }), { - 'str.$': '$$.Execution.StartTime', - 'count.$': '$$.State.RetryCount', - 'token.$': '$$.Task.Token', - 'entire.$': '$$' - }); - - test.done(); - }, - - 'find all referenced paths'(test: Test) { - test.deepEqual(FieldUtils.findReferencedPaths({ - bool: false, - literal: 'literal', - field: Data.stringAt('$.stringField'), - listField: Data.listAt('$.listField'), - deep: [ - 'literal', - { - field: Data.stringAt('$.stringField'), - deepField: Data.numberAt('$.numField'), - } - ] - }), [ - '$.listField', - '$.numField', - '$.stringField', - ]); - - test.done(); - }, - - 'cannot have JsonPath fields in arrays'(test: Test) { - test.throws(() => { - FieldUtils.renderObject({ - deep: [Data.stringAt('$.hello')] - }); - }, /Cannot use JsonPath fields in an array/); - - test.done(); - }, - - 'datafield path must be correct'(test: Test) { - test.ok(Data.stringAt('$')); - - test.throws(() => { - Data.stringAt('$hello'); - }, /exactly equal to '\$' or start with '\$.'/); - - test.throws(() => { - Data.stringAt('hello'); - }, /exactly equal to '\$' or start with '\$.'/); - - test.done(); - }, - - 'context path must be correct'(test: Test) { - test.ok(Context.stringAt('$$')); - - test.throws(() => { - Context.stringAt('$$hello'); - }, /exactly equal to '\$\$' or start with '\$\$.'/); - - test.throws(() => { - Context.stringAt('hello'); - }, /exactly equal to '\$\$' or start with '\$\$.'/); - - test.done(); - }, - - 'test contains task token'(test: Test) { - test.equal(true, FieldUtils.containsTaskToken({ - field: Context.taskToken - })); - - test.equal(true, FieldUtils.containsTaskToken({ - field: Context.stringAt('$$.Task'), - })); - - test.equal(true, FieldUtils.containsTaskToken({ - field: Context.entireContext - })); - - test.equal(false, FieldUtils.containsTaskToken({ - oops: 'not here' - })); - - test.equal(false, FieldUtils.containsTaskToken({ - oops: Context.stringAt('$$.Execution.StartTime') - })); - - test.done(); - }, - - 'arbitrary JSONPath fields are not replaced'(test: Test) { - test.deepEqual(FieldUtils.renderObject({ - field: '$.content', - }), { - field: '$.content' - }); - - test.done(); - }, - - 'fields cannot be used somewhere in a string interpolation'(test: Test) { - test.throws(() => { - FieldUtils.renderObject({ - field: `contains ${Data.stringAt('$.hello')}` - }); - }, /Field references must be the entire string/); - - test.done(); - } -}; \ No newline at end of file diff --git a/packages/@aws-cdk/aws-stepfunctions/test/test.pass.ts b/packages/@aws-cdk/aws-stepfunctions/test/test.pass.ts deleted file mode 100644 index 5e73edd5d35ff..0000000000000 --- a/packages/@aws-cdk/aws-stepfunctions/test/test.pass.ts +++ /dev/null @@ -1,40 +0,0 @@ -import { Test } from 'nodeunit'; -import { Result } from '../lib'; - -export = { - 'fromString has proper value'(test: Test) { - const testValue = 'test string'; - const result = Result.fromString(testValue); - test.equal(result.value, testValue); - - test.done(); - }, - 'fromNumber has proper value'(test: Test) { - const testValue = 1; - const result = Result.fromNumber(testValue); - test.equal(result.value, testValue); - - test.done(); - }, - 'fromBoolean has proper value'(test: Test) { - const testValue = false; - const result = Result.fromBoolean(testValue); - test.equal(result.value, testValue); - - test.done(); - }, - 'fromObject has proper value'(test: Test) { - const testValue = {a: 1}; - const result = Result.fromObject(testValue); - test.deepEqual(result.value, testValue); - - test.done(); - }, - 'fromArray has proper value'(test: Test) { - const testValue = [1]; - const result = Result.fromArray(testValue); - test.deepEqual(result.value, testValue); - - test.done(); - }, -}; diff --git a/packages/@aws-cdk/aws-stepfunctions/test/test.states-language.ts b/packages/@aws-cdk/aws-stepfunctions/test/test.states-language.ts deleted file mode 100644 index f1552d1bcc6d5..0000000000000 --- a/packages/@aws-cdk/aws-stepfunctions/test/test.states-language.ts +++ /dev/null @@ -1,735 +0,0 @@ -import * as cdk from '@aws-cdk/core'; -import { Test } from 'nodeunit'; -import * as stepfunctions from '../lib'; - -export = { - 'Basic composition': { - 'A single task is a State Machine'(test: Test) { - // GIVEN - const stack = new cdk.Stack(); - - // WHEN - const chain = new stepfunctions.Pass(stack, 'Some State'); - - // THEN - test.deepEqual(render(chain), { - StartAt: 'Some State', - States: { - 'Some State': { Type: 'Pass', End: true } - } - }); - - test.done(); - }, - - 'A sequence of two tasks is a State Machine'(test: Test) { - // GIVEN - const stack = new cdk.Stack(); - - // WHEN - const task1 = new stepfunctions.Pass(stack, 'State One'); - const task2 = new stepfunctions.Pass(stack, 'State Two'); - - const chain = stepfunctions.Chain - .start(task1) - .next(task2); - - // THEN - test.deepEqual(render(chain), { - StartAt: 'State One', - States: { - 'State One': { Type: 'Pass', Next: 'State Two' }, - 'State Two': { Type: 'Pass', End: true }, - } - }); - - test.done(); - }, - - 'You dont need to hold on to the state to render the entire state machine correctly'(test: Test) { - // GIVEN - const stack = new cdk.Stack(); - - // WHEN - const task1 = new stepfunctions.Pass(stack, 'State One'); - const task2 = new stepfunctions.Pass(stack, 'State Two'); - - task1.next(task2); - - // THEN - test.deepEqual(render(task1), { - StartAt: 'State One', - States: { - 'State One': { Type: 'Pass', Next: 'State Two' }, - 'State Two': { Type: 'Pass', End: true }, - } - }); - - test.done(); - }, - - 'A chain can be appended to'(test: Test) { - // GIVEN - const stack = new cdk.Stack(); - - const task1 = new stepfunctions.Pass(stack, 'State One'); - const task2 = new stepfunctions.Pass(stack, 'State Two'); - const task3 = new stepfunctions.Pass(stack, 'State Three'); - - // WHEN - const chain = stepfunctions.Chain - .start(task1) - .next(task2) - .next(task3); - - // THEN - test.deepEqual(render(chain), { - StartAt: 'State One', - States: { - 'State One': { Type: 'Pass', Next: 'State Two' }, - 'State Two': { Type: 'Pass', Next: 'State Three' }, - 'State Three': { Type: 'Pass', End: true }, - } - }); - - test.done(); - }, - - 'A state machine can be appended to another state machine'(test: Test) { - // GIVEN - const stack = new cdk.Stack(); - - const task1 = new stepfunctions.Pass(stack, 'State One'); - const task2 = new stepfunctions.Pass(stack, 'State Two'); - const task3 = new stepfunctions.Wait(stack, 'State Three', { - time: stepfunctions.WaitTime.duration(cdk.Duration.seconds(10)) - }); - - // WHEN - const chain = stepfunctions.Chain - .start(task1) - .next(stepfunctions.Chain.start(task2).next(task3)); - - // THEN - test.deepEqual(render(chain), { - StartAt: 'State One', - States: { - 'State One': { Type: 'Pass', Next: 'State Two' }, - 'State Two': { Type: 'Pass', Next: 'State Three' }, - 'State Three': { Type: 'Wait', End: true, Seconds: 10 }, - } - }); - - test.done(); - }, - - 'A state machine definition can be instantiated and chained'(test: Test) { - // GIVEN - const stack = new cdk.Stack(); - const before = new stepfunctions.Pass(stack, 'Before'); - const after = new stepfunctions.Pass(stack, 'After'); - - // WHEN - const chain = before.next(new ReusableStateMachine(stack, 'Reusable')).next(after); - - // THEN - test.deepEqual(render(chain), { - StartAt: 'Before', - States: { - 'Before': { Type: 'Pass', Next: 'Choice' }, - 'Choice': { - Type: 'Choice', - Choices: [ - { Variable: '$.branch', StringEquals: 'left', Next: 'Left Branch' }, - { Variable: '$.branch', StringEquals: 'right', Next: 'Right Branch' }, - ] - }, - 'Left Branch': { Type: 'Pass', Next: 'After' }, - 'Right Branch': { Type: 'Pass', Next: 'After' }, - 'After': { Type: 'Pass', End: true }, - } - }); - - test.done(); - }, - - 'A success state cannot be chained onto'(test: Test) { - // GIVEN - const stack = new cdk.Stack(); - - const succeed = new stepfunctions.Succeed(stack, 'Succeed'); - const pass = new stepfunctions.Pass(stack, 'Pass'); - - // WHEN - test.throws(() => { - pass.next(succeed).next(pass); - }); - - test.done(); - }, - - 'A failure state cannot be chained onto'(test: Test) { - // GIVEN - const stack = new cdk.Stack(); - const fail = new stepfunctions.Fail(stack, 'Fail', { error: 'X', cause: 'Y' }); - const pass = new stepfunctions.Pass(stack, 'Pass'); - - // WHEN - test.throws(() => { - pass.next(fail).next(pass); - }); - - test.done(); - }, - - 'Parallels can contain direct states'(test: Test) { - // GIVEN - const stack = new cdk.Stack(); - - const one = new stepfunctions.Pass(stack, 'One'); - const two = new stepfunctions.Pass(stack, 'Two'); - const three = new stepfunctions.Pass(stack, 'Three'); - - // WHEN - const para = new stepfunctions.Parallel(stack, 'Parallel'); - para.branch(one.next(two)); - para.branch(three); - - // THEN - test.deepEqual(render(para), { - StartAt: 'Parallel', - States: { - Parallel: { - Type: 'Parallel', - End: true, - Branches: [ - { - StartAt: 'One', - States: { - One: { Type: 'Pass', Next: 'Two' }, - Two: { Type: 'Pass', End: true }, - } - }, - { - StartAt: 'Three', - States: { - Three: { Type: 'Pass', End: true } - } - } - ] - } - } - }); - - test.done(); - }, - - 'Parallels can contain instantiated reusable definitions'(test: Test) { - // GIVEN - const stack = new cdk.Stack(); - - // WHEN - const para = new stepfunctions.Parallel(stack, 'Parallel'); - para.branch(new ReusableStateMachine(stack, 'Reusable1').prefixStates('Reusable1/')); - para.branch(new ReusableStateMachine(stack, 'Reusable2').prefixStates('Reusable2/')); - - // THEN - test.deepEqual(render(para), { - StartAt: 'Parallel', - States: { - Parallel: { - Type: 'Parallel', - End: true, - Branches: [ - { - StartAt: 'Reusable1/Choice', - States: { - 'Reusable1/Choice': { - Type: 'Choice', - Choices: [ - { Variable: '$.branch', StringEquals: 'left', Next: 'Reusable1/Left Branch' }, - { Variable: '$.branch', StringEquals: 'right', Next: 'Reusable1/Right Branch' }, - ] - }, - 'Reusable1/Left Branch': { Type: 'Pass', End: true }, - 'Reusable1/Right Branch': { Type: 'Pass', End: true }, - } - }, - { - StartAt: 'Reusable2/Choice', - States: { - 'Reusable2/Choice': { - Type: 'Choice', - Choices: [ - { Variable: '$.branch', StringEquals: 'left', Next: 'Reusable2/Left Branch' }, - { Variable: '$.branch', StringEquals: 'right', Next: 'Reusable2/Right Branch' }, - ] - }, - 'Reusable2/Left Branch': { Type: 'Pass', End: true }, - 'Reusable2/Right Branch': { Type: 'Pass', End: true }, - } - }, - ] - } - } - }); - - test.done(); - }, - - 'State Machine Fragments can be wrapped in a single state'(test: Test) { - // GIVEN - const stack = new cdk.Stack(); - - const reusable = new SimpleChain(stack, 'Hello'); - const state = reusable.toSingleState(); - - test.deepEqual(render(state), { - StartAt: 'Hello', - States: { - Hello: { - Type: 'Parallel', - End: true, - Branches: [ - { - StartAt: 'Hello: Task1', - States: { - 'Hello: Task1': { Type: 'Task', Next: 'Hello: Task2', Resource: 'resource' }, - 'Hello: Task2': { Type: 'Task', End: true, Resource: 'resource' }, - } - } - ], - }, - } - }); - - test.done(); - }, - - 'Chaining onto branched failure state ignores failure state'(test: Test) { - // GIVEN - const stack = new cdk.Stack(); - - const yes = new stepfunctions.Pass(stack, 'Yes'); - const no = new stepfunctions.Fail(stack, 'No', { error: 'Failure', cause: 'Wrong branch' }); - const enfin = new stepfunctions.Pass(stack, 'Finally'); - const choice = new stepfunctions.Choice(stack, 'Choice') - .when(stepfunctions.Condition.stringEquals('$.foo', 'bar'), yes) - .otherwise(no); - - // WHEN - choice.afterwards().next(enfin); - - // THEN - test.deepEqual(render(choice), { - StartAt: 'Choice', - States: { - Choice: { - Type: 'Choice', - Choices: [ - { Variable: '$.foo', StringEquals: 'bar', Next: 'Yes' }, - ], - Default: 'No', - }, - Yes: { Type: 'Pass', Next: 'Finally' }, - No: { Type: 'Fail', Error: 'Failure', Cause: 'Wrong branch' }, - Finally: { Type: 'Pass', End: true }, - } - }); - - test.done(); - }, - - 'Can include OTHERWISE transition for Choice in afterwards()'(test: Test) { - // GIVEN - const stack = new cdk.Stack(); - - // WHEN - const chain = new stepfunctions.Choice(stack, 'Choice') - .when(stepfunctions.Condition.stringEquals('$.foo', 'bar'), - new stepfunctions.Pass(stack, 'Yes')) - .afterwards({ includeOtherwise: true }) - .next(new stepfunctions.Pass(stack, 'Finally')); - - // THEN - test.deepEqual(render(chain), { - StartAt: 'Choice', - States: { - Choice: { - Type: 'Choice', - Choices: [ - { Variable: '$.foo', StringEquals: 'bar', Next: 'Yes' }, - ], - Default: 'Finally', - }, - Yes: { Type: 'Pass', Next: 'Finally' }, - Finally: { Type: 'Pass', End: true }, - } - }); - - test.done(); - } - }, - - 'Goto support': { - 'State machines can have unconstrainted gotos'(test: Test) { - // GIVEN - const stack = new cdk.Stack(); - - const one = new stepfunctions.Pass(stack, 'One'); - const two = new stepfunctions.Pass(stack, 'Two'); - - // WHEN - const chain = one.next(two).next(one); - - // THEN - test.deepEqual(render(chain), { - StartAt: 'One', - States: { - One: { Type: 'Pass', Next: 'Two' }, - Two: { Type: 'Pass', Next: 'One' }, - } - }); - - test.done(); - }, - }, - - 'Catches': { - 'States can have error branches'(test: Test) { - // GIVEN - const stack = new cdk.Stack(); - const task1 = new stepfunctions.Task(stack, 'Task1', { task: new FakeTask()}); - const failure = new stepfunctions.Fail(stack, 'Failed', { error: 'DidNotWork', cause: 'We got stuck' }); - - // WHEN - const chain = task1.addCatch(failure); - - // THEN - test.deepEqual(render(chain), { - StartAt: 'Task1', - States: { - Task1: { - Type: 'Task', - Resource: 'resource', - End: true, - Catch: [ - { ErrorEquals: ['States.ALL'], Next: 'Failed' }, - ] - }, - Failed: { - Type: 'Fail', - Error: 'DidNotWork', - Cause: 'We got stuck', - } - } - }); - - test.done(); - }, - - 'Retries and errors with a result path'(test: Test) { - // GIVEN - const stack = new cdk.Stack(); - const task1 = new stepfunctions.Task(stack, 'Task1', { task: new FakeTask() }); - const failure = new stepfunctions.Fail(stack, 'Failed', { error: 'DidNotWork', cause: 'We got stuck' }); - - // WHEN - const chain = task1.addRetry({ errors: ['HTTPError'], maxAttempts: 2 }).addCatch(failure, { resultPath: '$.some_error' }).next(failure); - - // THEN - test.deepEqual(render(chain), { - StartAt: 'Task1', - States: { - Task1: { - Type: 'Task', - Resource: 'resource', - Catch: [ { ErrorEquals: ['States.ALL'], Next: 'Failed', ResultPath: '$.some_error' } ], - Retry: [ { ErrorEquals: ['HTTPError'], MaxAttempts: 2 } ], - Next: 'Failed', - }, - Failed: { - Type: 'Fail', - Error: 'DidNotWork', - Cause: 'We got stuck', - } - } - }); - - test.done(); - - }, - - 'Can wrap chain and attach error handler'(test: Test) { - // GIVEN - const stack = new cdk.Stack(); - - const task1 = new stepfunctions.Task(stack, 'Task1', { task: new FakeTask() }); - const task2 = new stepfunctions.Task(stack, 'Task2', { task: new FakeTask() }); - const errorHandler = new stepfunctions.Pass(stack, 'ErrorHandler'); - - // WHEN - const chain = task1.next(task2).toSingleState('Wrapped').addCatch(errorHandler); - - // THEN - test.deepEqual(render(chain), { - StartAt: 'Wrapped', - States: { - Wrapped: { - Type: 'Parallel', - Branches: [ - { - StartAt: 'Task1', - States: { - Task1: { - Type: 'Task', - Resource: 'resource', - Next: 'Task2', - }, - Task2: { - Type: 'Task', - Resource: 'resource', - End: true, - }, - } - } - ], - Catch: [ - { ErrorEquals: ['States.ALL'], Next: 'ErrorHandler' }, - ], - End: true - }, - ErrorHandler: { Type: 'Pass', End: true } - }, - }); - - test.done(); - }, - - 'Chaining does not chain onto error handler state'(test: Test) { - // GIVEN - const stack = new cdk.Stack(); - - const task1 = new stepfunctions.Task(stack, 'Task1', { task: new FakeTask() }); - const task2 = new stepfunctions.Task(stack, 'Task2', { task: new FakeTask() }); - const errorHandler = new stepfunctions.Pass(stack, 'ErrorHandler'); - - // WHEN - const chain = task1.addCatch(errorHandler).next(task2); - - // THEN - test.deepEqual(render(chain), { - StartAt: 'Task1', - States: { - Task1: { - Type: 'Task', - Resource: 'resource', - Next: 'Task2', - Catch: [ - { ErrorEquals: ['States.ALL'], Next: 'ErrorHandler' }, - ] - }, - Task2: { Type: 'Task', Resource: 'resource', End: true }, - ErrorHandler: { Type: 'Pass', End: true }, - } - }); - - test.done(); - }, - - 'Chaining does not chain onto error handler, extended'(test: Test) { - // GIVEN - const stack = new cdk.Stack(); - - const task1 = new stepfunctions.Task(stack, 'Task1', { task: new FakeTask() }); - const task2 = new stepfunctions.Task(stack, 'Task2', { task: new FakeTask() }); - const task3 = new stepfunctions.Task(stack, 'Task3', { task: new FakeTask() }); - const errorHandler = new stepfunctions.Pass(stack, 'ErrorHandler'); - - // WHEN - const chain = task1.addCatch(errorHandler) - .next(task2.addCatch(errorHandler)) - .next(task3.addCatch(errorHandler)); - - // THEN - const sharedTaskProps = { Type: 'Task', Resource: 'resource', Catch: [ { ErrorEquals: ['States.ALL'], Next: 'ErrorHandler' } ] }; - test.deepEqual(render(chain), { - StartAt: 'Task1', - States: { - Task1: { Next: 'Task2', ...sharedTaskProps }, - Task2: { Next: 'Task3', ...sharedTaskProps }, - Task3: { End: true, ...sharedTaskProps }, - ErrorHandler: { Type: 'Pass', End: true }, - } - }); - - test.done(); - }, - - 'Error handler with a fragment'(test: Test) { - // GIVEN - const stack = new cdk.Stack(); - - const task1 = new stepfunctions.Task(stack, 'Task1', { task: new FakeTask() }); - const task2 = new stepfunctions.Task(stack, 'Task2', { task: new FakeTask() }); - const errorHandler = new stepfunctions.Pass(stack, 'ErrorHandler'); - - // WHEN - task1.addCatch(errorHandler) - .next(new SimpleChain(stack, 'Chain').catch(errorHandler)) - .next(task2.addCatch(errorHandler)); - - test.done(); - }, - - 'Can merge state machines with shared states'(test: Test) { - // GIVEN - const stack = new cdk.Stack(); - - const task1 = new stepfunctions.Task(stack, 'Task1', { task: new FakeTask() }); - const task2 = new stepfunctions.Task(stack, 'Task2', { task: new FakeTask() }); - const failure = new stepfunctions.Fail(stack, 'Failed', { error: 'DidNotWork', cause: 'We got stuck' }); - - // WHEN - task1.addCatch(failure); - task2.addCatch(failure); - - task1.next(task2); - - // THEN - test.deepEqual(render(task1), { - StartAt: 'Task1', - States: { - Task1: { - Type: 'Task', - Resource: 'resource', - Next: 'Task2', - Catch: [ - { ErrorEquals: ['States.ALL'], Next: 'Failed' }, - ] - }, - Task2: { - Type: 'Task', - Resource: 'resource', - End: true, - Catch: [ - { ErrorEquals: ['States.ALL'], Next: 'Failed' }, - ] - }, - Failed: { - Type: 'Fail', - Error: 'DidNotWork', - Cause: 'We got stuck', - } - } - }); - - test.done(); - } - }, - - 'State machine validation': { - 'No duplicate state IDs'(test: Test) { - // GIVEN - const stack = new cdk.Stack(); - const intermediateParent = new cdk.Construct(stack, 'Parent'); - - const state1 = new stepfunctions.Pass(stack, 'State'); - const state2 = new stepfunctions.Pass(intermediateParent, 'State'); - - state1.next(state2); - - // WHEN - test.throws(() => { - render(state1); - }); - - test.done(); - }, - - 'No duplicate state IDs even across Parallel branches'(test: Test) { - // GIVEN - const stack = new cdk.Stack(); - const intermediateParent = new cdk.Construct(stack, 'Parent'); - - const state1 = new stepfunctions.Pass(stack, 'State'); - const state2 = new stepfunctions.Pass(intermediateParent, 'State'); - - const parallel = new stepfunctions.Parallel(stack, 'Parallel') - .branch(state1) - .branch(state2); - - // WHEN - test.throws(() => { - render(parallel); - }); - - test.done(); - }, - - 'No cross-parallel jumps'(test: Test) { - // GIVEN - const stack = new cdk.Stack(); - const state1 = new stepfunctions.Pass(stack, 'State1'); - const state2 = new stepfunctions.Pass(stack, 'State2'); - - test.throws(() => { - new stepfunctions.Parallel(stack, 'Parallel') - .branch(state1.next(state2)) - .branch(state2); - }); - - test.done(); - }, - }, -}; - -class ReusableStateMachine extends stepfunctions.StateMachineFragment { - public readonly startState: stepfunctions.State; - public readonly endStates: stepfunctions.INextable[]; - constructor(scope: cdk.Construct, id: string) { - super(scope, id); - - const choice = new stepfunctions.Choice(this, 'Choice') - .when(stepfunctions.Condition.stringEquals('$.branch', 'left'), new stepfunctions.Pass(this, 'Left Branch')) - .when(stepfunctions.Condition.stringEquals('$.branch', 'right'), new stepfunctions.Pass(this, 'Right Branch')); - - this.startState = choice; - this.endStates = choice.afterwards().endStates; - } -} - -class SimpleChain extends stepfunctions.StateMachineFragment { - public readonly startState: stepfunctions.State; - public readonly endStates: stepfunctions.INextable[]; - - private readonly task2: stepfunctions.Task; - constructor(scope: cdk.Construct, id: string) { - super(scope, id); - - const task1 = new stepfunctions.Task(this, 'Task1', { task: new FakeTask() }); - this.task2 = new stepfunctions.Task(this, 'Task2', { task: new FakeTask() }); - - task1.next(this.task2); - - this.startState = task1; - this.endStates = [this.task2]; - } - - public catch(state: stepfunctions.IChainable, props?: stepfunctions.CatchProps): SimpleChain { - this.task2.addCatch(state, props); - return this; - } -} - -function render(sm: stepfunctions.IChainable) { - return new cdk.Stack().resolve(new stepfunctions.StateGraph(sm.startState, 'Test Graph').toGraphJson()); -} - -class FakeTask implements stepfunctions.IStepFunctionsTask { - public bind(_task: stepfunctions.Task): stepfunctions.StepFunctionsTaskConfig { - return { - resourceArn: 'resource' - }; - } -} diff --git a/packages/@aws-cdk/core/lib/app.ts b/packages/@aws-cdk/core/lib/app.ts index 619ecc19a20da..cea43492a2e76 100644 --- a/packages/@aws-cdk/core/lib/app.ts +++ b/packages/@aws-cdk/core/lib/app.ts @@ -1,5 +1,6 @@ import * as cxapi from '@aws-cdk/cx-api'; import { Construct, ConstructNode } from './construct-compat'; +import { prepareApp } from './private/prepare-app'; import { collectRuntimeInformation } from './private/runtime-info'; import { TreeMetadata } from './private/tree-metadata'; @@ -147,6 +148,11 @@ export class App extends Construct { return assembly; } + protected prepare() { + super.prepare(); + prepareApp(this); + } + private loadContext(defaults: { [key: string]: string } = { }) { // prime with defaults passed through constructor for (const [ k, v ] of Object.entries(defaults)) { diff --git a/packages/@aws-cdk/core/lib/deps.ts b/packages/@aws-cdk/core/lib/deps.ts index fdfa4bee677f4..b26fa28cb187b 100644 --- a/packages/@aws-cdk/core/lib/deps.ts +++ b/packages/@aws-cdk/core/lib/deps.ts @@ -62,7 +62,7 @@ export function addDependency(source: T, target: T, reason?: // `source` is a direct or indirect nested stack of `target`, and this is not // possible (nested stacks cannot depend on their parents). if (commonStack === target) { - throw new Error(`Nested stack '${sourceStack.node.path}' cannot depend on a parent stack '${targetStack.node.path}'`); + throw new Error(`Nested stack '${sourceStack.node.path}' cannot depend on a parent stack '${targetStack.node.path}': ${reason}`); } // we have a common stack from which we can reach both `source` and `target` diff --git a/packages/@aws-cdk/core/lib/private/cfn-reference.ts b/packages/@aws-cdk/core/lib/private/cfn-reference.ts index 09f6b51311ec0..f563853f50982 100644 --- a/packages/@aws-cdk/core/lib/private/cfn-reference.ts +++ b/packages/@aws-cdk/core/lib/private/cfn-reference.ts @@ -77,12 +77,14 @@ export class CfnReference extends Reference { * The Tokens that should be returned for each consuming stack (as decided by the producing Stack) */ private readonly replacementTokens: Map; + private readonly targetStack: Stack; protected constructor(value: any, displayName: string, target: IConstruct) { // prepend scope path to display name super(value, target, displayName); this.replacementTokens = new Map(); + this.targetStack = Stack.of(target); Object.defineProperty(this, CFN_REFERENCE_SYMBOL, { value: true }); } @@ -106,10 +108,18 @@ export class CfnReference extends Reference { } public hasValueForStack(stack: Stack) { + if (stack === this.targetStack) { + return true; + } + return this.replacementTokens.has(stack); } public assignValueForStack(stack: Stack, value: IResolvable) { + if (stack === this.targetStack) { + throw new Error('cannot assign a value for the same stack'); + } + if (this.hasValueForStack(stack)) { throw new Error('Cannot assign a reference value twice to the same stack. Use hasValueForStack to check first'); } diff --git a/packages/@aws-cdk/core/lib/private/prepare-app.ts b/packages/@aws-cdk/core/lib/private/prepare-app.ts new file mode 100644 index 0000000000000..cb268a88b4069 --- /dev/null +++ b/packages/@aws-cdk/core/lib/private/prepare-app.ts @@ -0,0 +1,65 @@ +import { ConstructOrder } from 'constructs'; +import { Construct } from '../construct-compat'; +import { Stack } from '../stack'; +import { resolveReferences } from './refs'; + +/** + * Prepares the app for synthesis. This function is called by the root `prepare` + * (normally this the App, but if a Stack is a root, it is called by the stack), + * which means it's the last 'prepare' that executes. + * + * It takes care of reifying cross-references between stacks (or nested stacks), + * and of creating assets for nested stack templates. + * + * @param root The root of the construct tree. + */ +export function prepareApp(root: Construct) { + if (root.node.scope) { + throw new Error('prepareApp must be called on the root node'); + } + + // depth-first (children first) queue of nested stacks. We will pop a stack + // from the head of this queue to prepare it's template asset. + const queue = findAllNestedStacks(root); + + while (true) { + resolveReferences(root); + + const nested = queue.shift(); + if (!nested) { + break; + } + + defineNestedStackAsset(nested); + } +} + +/** + * Prepares the assets for nested stacks in this app. + * @returns `true` if assets were added to the parent of a nested stack, which + * implies that another round of reference resolution is in order. If this + * function returns `false`, we know we are done. + */ +function defineNestedStackAsset(nestedStack: Stack) { + // this is needed temporarily until we move NestedStack to '@aws-cdk/core'. + const nested: INestedStackPrivateApi = nestedStack as any; + nested._prepareTemplateAsset(); +} + +function findAllNestedStacks(root: Construct) { + const result = new Array(); + + // create a list of all nested stacks in depth-first post order this means + // that we first prepare the leaves and then work our way up. + for (const stack of root.node.findAll(ConstructOrder.POSTORDER /* <== important */)) { + if (Stack.isStack(stack) && stack.nested) { + result.push(stack); + } + } + + return result; +} + +interface INestedStackPrivateApi { + _prepareTemplateAsset(): boolean; +} diff --git a/packages/@aws-cdk/core/lib/private/refs.ts b/packages/@aws-cdk/core/lib/private/refs.ts new file mode 100644 index 0000000000000..51a2fc8902a30 --- /dev/null +++ b/packages/@aws-cdk/core/lib/private/refs.ts @@ -0,0 +1,273 @@ +// ---------------------------------------------------- +// CROSS REFERENCES +// ---------------------------------------------------- +import { IResolvable } from 'constructs'; +import { CfnElement } from '../cfn-element'; +import { CfnOutput } from '../cfn-output'; +import { CfnParameter } from '../cfn-parameter'; +import { Construct } from '../construct-compat'; +import { Reference } from '../reference'; +import { Stack } from '../stack'; +import { Token } from '../token'; +import { CfnReference } from './cfn-reference'; +import { Intrinsic } from './intrinsic'; +import { findTokens } from './resolve'; +import { makeUniqueId } from './uniqueid'; + +/** + * This is called from the App level to resolve all references defined. Each + * reference is resolved based on it's consumption context. + */ +export function resolveReferences(scope: Construct): void { + const edges = findAllReferences(scope); + + for (const { source, value } of edges) { + const consumer = Stack.of(source); + + // resolve the value in the context of the consumer + if (!value.hasValueForStack(consumer)) { + const resolved = resolveValue(consumer, value); + value.assignValueForStack(consumer, resolved); + } + } +} + +/** + * Resolves the value for `reference` in the context of `consumer`. + */ +function resolveValue(consumer: Stack, reference: CfnReference): IResolvable { + const producer = Stack.of(reference.target); + + // produce and consumer stacks are the same, we can just return the value itself. + if (producer === consumer) { + return reference; + } + + // unsupported: stacks from different apps + if (producer.node.root !== consumer.node.root) { + throw new Error('Cannot reference across apps. Consuming and producing stacks must be defined within the same CDK app.'); + } + + // unsupported: stacks are not in the same environment + if (producer.environment !== consumer.environment) { + throw new Error( + `Stack "${consumer.node.path}" cannot consume a cross reference from stack "${producer.node.path}". ` + + 'Cross stack references are only supported for stacks deployed to the same environment or between nested stacks and their parent stack'); + } + + // ---------------------------------------------------------------------- + // consumer is nested in the producer (directly or indirectly) + // ---------------------------------------------------------------------- + + // if the consumer is nested within the producer (directly or indirectly), + // wire through a CloudFormation parameter and then resolve the reference with + // the parent stack as the consumer. + if (consumer.nestedStackParent && isNested(consumer, producer)) { + const parameterValue = resolveValue(consumer.nestedStackParent, reference); + return createNestedStackParameter(consumer, reference, parameterValue); + } + + // ---------------------------------------------------------------------- + // producer is a nested stack + // ---------------------------------------------------------------------- + + // if the producer is nested, always publish the value through a + // cloudformation output and resolve recursively with the Fn::GetAtt + // of the output in the parent stack. + + // one might ask, if the consumer is not a parent of the producer, + // why not just use export/import? the reason is that we cannot + // generate an "export name" from a nested stack because the export + // name must contain the stack name to ensure uniqueness, and we + // don't know the stack name of a nested stack before we deploy it. + // therefore, we can only export from a top-level stack. + if (producer.nested) { + const outputValue = createNestedStackOutput(producer, reference); + return resolveValue(consumer, outputValue); + } + + // ---------------------------------------------------------------------- + // export/import + // ---------------------------------------------------------------------- + + // export the value through a cloudformation "export name" and use an + // Fn::ImportValue in the consumption site. + + // add a dependency between the producer and the consumer. dependency logic + // will take care of applying the dependency at the right level (e.g. the + // top-level stacks). + consumer.addDependency(producer, + `${consumer.node.path} -> ${reference.target.node.path}.${reference.displayName}`); + + return createImportValue(reference); +} + +/** + * Finds all the CloudFormation references in a construct tree. + */ +function findAllReferences(root: Construct) { + const result = new Array<{ source: CfnElement, value: CfnReference }>(); + for (const consumer of root.node.findAll()) { + + // include only CfnElements (i.e. resources) + if (!CfnElement.isCfnElement(consumer)) { + continue; + } + + try { + const tokens = findTokens(consumer, () => consumer._toCloudFormation()); + + // iterate over all the tokens (e.g. intrinsic functions, lazies, etc) that + // were found in the cloudformation representation of this resource. + for (const token of tokens) { + + // include only CfnReferences (i.e. "Ref" and "Fn::GetAtt") + if (!CfnReference.isCfnReference(token)) { + continue; + } + + result.push({ + source: consumer, + value: token + }); + } + } catch (e) { + // Note: it might be that the properties of the CFN object aren't valid. + // This will usually be preventatively caught in a construct's validate() + // and turned into a nicely descriptive error, but we're running prepare() + // before validate(). Swallow errors that occur because the CFN layer + // doesn't validate completely. + // + // This does make the assumption that the error will not be rectified, + // but the error will be thrown later on anyway. If the error doesn't + // get thrown down the line, we may miss references. + if (e.type === 'CfnSynthesisError') { + continue; + } + + throw e; + } + } + + return result; +} + +// ------------------------------------------------------------------------------------------------ +// export/import +// ------------------------------------------------------------------------------------------------ + +/** + * Imports a value from another stack by creating an "Output" with an "ExportName" + * and returning an "Fn::ImportValue" token. + */ +function createImportValue(reference: Reference): Intrinsic { + const exportingStack = Stack.of(reference.target); + + // Ensure a singleton "Exports" scoping Construct + // This mostly exists to trigger LogicalID munging, which would be + // disabled if we parented constructs directly under Stack. + // Also it nicely prevents likely construct name clashes + const exportsScope = getCreateExportsScope(exportingStack); + + // Ensure a singleton CfnOutput for this value + const resolved = exportingStack.resolve(reference); + const id = 'Output' + JSON.stringify(resolved); + const exportName = generateExportName(exportsScope, id); + + if (Token.isUnresolved(exportName)) { + throw new Error(`unresolved token in generated export name: ${JSON.stringify(exportingStack.resolve(exportName))}`); + } + + const output = exportsScope.node.tryFindChild(id) as CfnOutput; + if (!output) { + new CfnOutput(exportsScope, id, { value: Token.asString(reference), exportName }); + } + + // We want to return an actual FnImportValue Token here, but Fn.importValue() returns a 'string', + // so construct one in-place. + return new Intrinsic({ 'Fn::ImportValue': exportName }); +} + +function getCreateExportsScope(stack: Stack) { + const exportsName = 'Exports'; + let stackExports = stack.node.tryFindChild(exportsName) as Construct; + if (stackExports === undefined) { + stackExports = new Construct(stack, exportsName); + } + + return stackExports; +} + +function generateExportName(stackExports: Construct, id: string) { + const stack = Stack.of(stackExports); + const components = [...stackExports.node.scopes.slice(2).map(c => c.node.id), id]; + const prefix = stack.stackName ? stack.stackName + ':' : ''; + const exportName = prefix + makeUniqueId(components); + return exportName; +} + +// ------------------------------------------------------------------------------------------------ +// nested stacks +// ------------------------------------------------------------------------------------------------ + +/** + * Adds a CloudFormation parameter to a nested stack and assigns it with the + * value of the reference. + */ +function createNestedStackParameter(nested: Stack, reference: CfnReference, value: IResolvable) { + // we call "this.resolve" to ensure that tokens do not creep in (for example, if the reference display name includes tokens) + const paramId = nested.resolve(`reference-to-${reference.target.node.uniqueId}.${reference.displayName}`); + let param = nested.node.tryFindChild(paramId) as CfnParameter; + if (!param) { + param = new CfnParameter(nested, paramId, { type: 'String' }); + + // Ugly little hack until we move NestedStack to this module. + if (!('setParameter' in nested)) { + throw new Error('assertion failed: nested stack should have a "setParameter" method'); + } + + (nested as any).setParameter(param.logicalId, Token.asString(value)); + } + + return param.value as CfnReference; +} + +/** + * Adds a CloudFormation output to a nested stack and returns an "Fn::GetAtt" + * intrinsic that can be used to reference this output in the parent stack. + */ +function createNestedStackOutput(producer: Stack, reference: Reference): CfnReference { + const outputId = `${reference.target.node.uniqueId}${reference.displayName}`; + let output = producer.node.tryFindChild(outputId) as CfnOutput; + if (!output) { + output = new CfnOutput(producer, outputId, { value: Token.asString(reference) }); + } + + if (!producer.nestedStackResource) { + throw new Error('assertion failed'); + } + + return producer.nestedStackResource.getAtt(`Outputs.${output.logicalId}`) as CfnReference; +} + +/** + * @returns true if this stack is a direct or indirect parent of the nested + * stack `nested`. + * + * If `child` is not a nested stack, always returns `false` because it can't + * have a parent, dah. + */ +function isNested(nested: Stack, parent: Stack): boolean { + // if the parent is a direct parent + if (nested.nestedStackParent === parent) { + return true; + } + + // we reached a top-level (non-nested) stack without finding the parent + if (!nested.nestedStackParent) { + return false; + } + + // recurse with the child's direct parent + return isNested(nested.nestedStackParent, parent); +} diff --git a/packages/@aws-cdk/core/lib/stack.ts b/packages/@aws-cdk/core/lib/stack.ts index 104dd362b4196..162632d3a9ad4 100644 --- a/packages/@aws-cdk/core/lib/stack.ts +++ b/packages/@aws-cdk/core/lib/stack.ts @@ -1,16 +1,15 @@ import * as cxschema from '@aws-cdk/cloud-assembly-schema'; import * as cxapi from '@aws-cdk/cx-api'; -import * as crypto from 'crypto'; import * as fs from 'fs'; import * as path from 'path'; -import { DockerImageAssetLocation, DockerImageAssetSource, FileAssetLocation , FileAssetPackaging, FileAssetSource } from './assets'; +import { DockerImageAssetLocation, DockerImageAssetSource, FileAssetLocation, FileAssetSource } from './assets'; import { Construct, ConstructNode, IConstruct, ISynthesisSession } from './construct-compat'; import { ContextProvider } from './context-provider'; import { Environment } from './environment'; import { FileAssetParameters } from './private/asset-parameters'; import { CLOUDFORMATION_TOKEN_RESOLVER, CloudFormationLang } from './private/cloudformation-lang'; import { LogicalIDs } from './private/logical-id'; -import { findTokens , resolve } from './private/resolve'; +import { resolve } from './private/resolve'; import { makeUniqueId } from './private/uniqueid'; const STACK_SYMBOL = Symbol.for('@aws-cdk/core.Stack'); @@ -190,14 +189,6 @@ export class Stack extends Construct implements ITaggable { */ public readonly nestedStackResource?: CfnResource; - /** - * An attribute (late-bound) that represents the URL of the template file - * in the deployment bucket. - * - * @experimental - */ - public readonly templateUrl: string; - /** * The name of the CloudFormation template file emitted to the output * directory during synthesis. @@ -233,7 +224,6 @@ export class Stack extends Construct implements ITaggable { */ private _assetParameters?: Construct; - private _templateUrl?: string; private readonly _stackName: string; /** @@ -291,7 +281,6 @@ export class Stack extends Construct implements ITaggable { : this.stackName; this.templateFile = `${this.artifactId}.template.json`; - this.templateUrl = Lazy.stringValue({ produce: () => this._templateUrl || '' }); } /** @@ -745,36 +734,6 @@ export class Stack extends Construct implements ITaggable { * Find all dependencies as well and add the appropriate DependsOn fields. */ protected prepare() { - const tokens = this.findTokens(); - - // References (originating from this stack) - for (const reference of tokens) { - - // skip if this is not a CfnReference - if (!CfnReference.isCfnReference(reference)) { - continue; - } - - const targetStack = Stack.of(reference.target); - - // skip if this is not a cross-stack reference - if (targetStack === this) { - continue; - } - - // determine which stack should create the cross reference - const factory = this.determineCrossReferenceFactory(targetStack); - - // if one side is a nested stack (has "parentStack"), we let it create the reference - // since it has more knowledge about the world. - const consumedValue = factory.prepareCrossReference(this, reference); - - // if the reference has already been assigned a value for the consuming stack, carry on. - if (!reference.hasValueForStack(this)) { - reference.assignValueForStack(this, consumedValue); - } - } - // Resource dependencies for (const dependency of this.node.dependencies) { for (const target of findCfnResources([ dependency.target ])) { @@ -788,20 +747,10 @@ export class Stack extends Construct implements ITaggable { this.node.addMetadata(cxschema.ArtifactMetadataEntryType.STACK_TAGS, this.tags.renderTags()); } - if (this.nestedStackParent) { - // add the nested stack template as an asset - const cfn = JSON.stringify(this._toCloudFormation()); - const templateHash = crypto.createHash('sha256').update(cfn).digest('hex'); - const parent = this.nestedStackParent; - const templateLocation = parent.addFileAsset({ - packaging: FileAssetPackaging.FILE, - sourceHash: templateHash, - fileName: this.templateFile - }); - - // if bucketName/objectKey are cfn parameters from a stack other than the parent stack, they will - // be resolved as cross-stack references like any other (see "multi" tests). - this._templateUrl = `https://s3.${parent.region}.${parent.urlSuffix}/${templateLocation.bucketName}/${templateLocation.objectKey}`; + // if this stack is a roort (e.g. in unit tests), call `prepareApp` so that + // we resolve cross-references and nested stack assets. + if (!this.node.scope) { + prepareApp(this); } } @@ -898,47 +847,14 @@ export class Stack extends Construct implements ITaggable { } /** - * Exports a resolvable value for use in another stack. + * Deprecated. * - * @returns a token that can be used to reference the value from the producing stack. + * @see https://github.com/aws/aws-cdk/pull/7187 + * @returns reference itself without any change + * @deprecated cross reference handling has been moved to `App.prepare()`. */ - protected prepareCrossReference(sourceStack: Stack, reference: Reference): IResolvable { - const targetStack = Stack.of(reference.target); - - // Ensure a singleton "Exports" scoping Construct - // This mostly exists to trigger LogicalID munging, which would be - // disabled if we parented constructs directly under Stack. - // Also it nicely prevents likely construct name clashes - const exportsScope = targetStack.getCreateExportsScope(); - - // Ensure a singleton CfnOutput for this value - const resolved = targetStack.resolve(reference); - const id = 'Output' + JSON.stringify(resolved); - const exportName = targetStack.generateExportName(exportsScope, id); - const output = exportsScope.node.tryFindChild(id) as CfnOutput; - if (!output) { - new CfnOutput(exportsScope, id, { value: Token.asString(reference), exportName }); - } - - // add a dependency on the producing stack - it has to be deployed before this stack can consume the exported value - // if the producing stack is a nested stack (i.e. has a parent), the dependency is taken on the parent. - const producerDependency = targetStack.nestedStackParent ? targetStack.nestedStackParent : targetStack; - const consumerDependency = sourceStack.nestedStackParent ? sourceStack.nestedStackParent : sourceStack; - consumerDependency.addDependency(producerDependency, `${sourceStack.node.path} -> ${reference.target.node.path}.${reference.displayName}`); - - // We want to return an actual FnImportValue Token here, but Fn.importValue() returns a 'string', - // so construct one in-place. - return new Intrinsic({ 'Fn::ImportValue': exportName }); - } - - private getCreateExportsScope() { - const exportsName = 'Exports'; - let stackExports = this.node.tryFindChild(exportsName) as Construct; - if (stackExports === undefined) { - stackExports = new Construct(this, exportsName); - } - - return stackExports; + protected prepareCrossReference(_sourceStack: Stack, reference: Reference): IResolvable { + return reference; } /** @@ -1042,72 +958,12 @@ export class Stack extends Construct implements ITaggable { return makeUniqueId(ids); } - private generateExportName(stackExports: Construct, id: string) { - const stack = Stack.of(stackExports); - const components = [...stackExports.node.scopes.slice(2).map(c => c.node.id), id]; - const prefix = stack.stackName ? stack.stackName + ':' : ''; - const exportName = prefix + makeUniqueId(components); - return exportName; - } - private get assetParameters() { if (!this._assetParameters) { this._assetParameters = new Construct(this, 'AssetParameters'); } return this._assetParameters; } - - private determineCrossReferenceFactory(target: Stack) { - // unsupported: stacks from different apps - if (target.node.root !== this.node.root) { - throw new Error( - 'Cannot reference across apps. ' + - 'Consuming and producing stacks must be defined within the same CDK app.'); - } - - // unsupported: stacks are not in the same environment - if (target.environment !== this.environment) { - throw new Error( - `Stack "${this.node.path}" cannot consume a cross reference from stack "${target.node.path}". ` + - 'Cross stack references are only supported for stacks deployed to the same environment or between nested stacks and their parent stack'); - } - - // if one of the stacks is a nested stack, go ahead and give it the right to make the cross reference - if (target.nested) { return target; } - if (this.nested) { return this; } - - // both stacks are top-level (non-nested), the taret (producing stack) gets to make the reference - return target; - } - - /** - * Returns all the tokens used within the scope of the current stack. - */ - private findTokens() { - const tokens = new Array(); - - for (const element of cfnElements(this)) { - try { - tokens.push(...findTokens(element, () => element._toCloudFormation())); - } catch (e) { - // Note: it might be that the properties of the CFN object aren't valid. - // This will usually be preventatively caught in a construct's validate() - // and turned into a nicely descriptive error, but we're running prepare() - // before validate(). Swallow errors that occur because the CFN layer - // doesn't validate completely. - // - // This does make the assumption that the error will not be rectified, - // but the error will be thrown later on anyway. If the error doesn't - // get thrown down the line, we may miss references. - if (e.type === 'CfnSynthesisError') { - continue; - } - - throw e; - } - } - return tokens; - } } function merge(template: any, part: any) { @@ -1164,7 +1020,7 @@ export interface ITemplateOptions { } /** - * Collect all CfnElements from a Stack + * Collect all CfnElements from a Stack. * * @param node Root node to collect all CfnElements from * @param into Array to append CfnElements to @@ -1189,13 +1045,10 @@ function cfnElements(node: IConstruct, into: CfnElement[] = []): CfnElement[] { import { Arn, ArnComponents } from './arn'; import { CfnElement } from './cfn-element'; import { Fn } from './cfn-fn'; -import { CfnOutput } from './cfn-output'; import { Aws, ScopedAws } from './cfn-pseudo'; import { CfnResource, TagType } from './cfn-resource'; import { addDependency } from './deps'; -import { Lazy } from './lazy'; -import { CfnReference } from './private/cfn-reference'; -import { Intrinsic } from './private/intrinsic'; +import { prepareApp } from './private/prepare-app'; import { Reference } from './reference'; import { IResolvable } from './resolvable'; import { ITaggable, TagManager } from './tag-manager'; diff --git a/packages/@aws-cdk/core/lib/util.ts b/packages/@aws-cdk/core/lib/util.ts index 89204560bb919..0855c5f16b7ac 100644 --- a/packages/@aws-cdk/core/lib/util.ts +++ b/packages/@aws-cdk/core/lib/util.ts @@ -104,25 +104,6 @@ export function pathToTopLevelStack(s: Stack): Stack[] { } } -/** - * @returns true if this stack is a direct or indirect parent of the nested - * stack `nested`. If `nested` is a top-level stack, returns false. - */ -export function isParentOfNestedStack(parent: Stack, child: Stack): boolean { - // if "nested" is not a nested stack, then by definition we cannot be its parent - if (!child.nestedStackParent) { - return false; - } - - // if this is the direct parent, then we found it - if (parent === child.nestedStackParent) { - return true; - } - - // traverse up - return isParentOfNestedStack(parent, child.nestedStackParent); -} - /** * Given two arrays, returns the last common element or `undefined` if there * isn't (arrays are foriegn). diff --git a/packages/aws-cdk/bin/cdk.ts b/packages/aws-cdk/bin/cdk.ts index 66e1338619f0f..9d273d5763292 100644 --- a/packages/aws-cdk/bin/cdk.ts +++ b/packages/aws-cdk/bin/cdk.ts @@ -222,8 +222,8 @@ async function initCommandLine() { const parameterMap: { [name: string]: string | undefined } = {}; for (const parameter of args.parameters) { if (typeof parameter === 'string') { - const keyValue = (parameter as string).split('=', 2); - parameterMap[keyValue[0]] = keyValue[1]; + const keyValue = (parameter as string).split('='); + parameterMap[keyValue[0]] = keyValue.slice(1).join('='); } } return await cli.deploy({ diff --git a/packages/aws-cdk/scripts/integ-cli.sh b/packages/aws-cdk/scripts/integ-cli.sh old mode 100644 new mode 100755 diff --git a/packages/aws-cdk/test/api/deploy-stack.test.ts b/packages/aws-cdk/test/api/deploy-stack.test.ts index b1426a66ce710..2a1371c16027c 100644 --- a/packages/aws-cdk/test/api/deploy-stack.test.ts +++ b/packages/aws-cdk/test/api/deploy-stack.test.ts @@ -61,8 +61,9 @@ test('correctly passes CFN parameters, ignoring ones with empty values', async ( resolvedEnvironment: mockResolvedEnvironment(), parameters: { A: 'A-value', - B: undefined, - C: '', + B: 'B=value', + C: undefined, + D: '', }, }); @@ -70,6 +71,7 @@ test('correctly passes CFN parameters, ignoring ones with empty values', async ( expect(cfnMocks.createChangeSet).toHaveBeenCalledWith(expect.objectContaining({ Parameters: [ { ParameterKey: 'A', ParameterValue: 'A-value' }, + { ParameterKey: 'B', ParameterValue: 'B=value' }, ] })); }); diff --git a/packages/aws-cdk/test/integ/cli/app/app.js b/packages/aws-cdk/test/integ/cli/app/app.js index b1b1294cc9d81..6706ab356b4a5 100644 --- a/packages/aws-cdk/test/integ/cli/app/app.js +++ b/packages/aws-cdk/test/integ/cli/app/app.js @@ -61,6 +61,19 @@ class OtherParameterStack extends cdk.Stack { } } +class MultiParameterStack extends cdk.Stack { + constructor(parent, id, props) { + super(parent, id, props); + + new sns.Topic(this, 'TopicParameter', { + displayName: new cdk.CfnParameter(this, 'DisplayNameParam') + }); + new sns.Topic(this, 'OtherTopicParameter', { + displayName: new cdk.CfnParameter(this, 'OtherDisplayNameParam') + }); + } +} + class OutputsStack extends cdk.Stack { constructor(parent, id, props) { super(parent, id, props); @@ -236,6 +249,8 @@ new YourStack(app, `${stackPrefix}-test-2`); // Deploy wildcard with parameters does ${stackPrefix}-param-test-* new ParameterStack(app, `${stackPrefix}-param-test-1`); new OtherParameterStack(app, `${stackPrefix}-param-test-2`); +// Deploy stack with multiple parameters +new MultiParameterStack(app, `${stackPrefix}-param-test-3`); // Deploy stack with outputs does ${stackPrefix}-outputs-test-* new OutputsStack(app, `${stackPrefix}-outputs-test-1`); new AnotherOutputsStack(app, `${stackPrefix}-outputs-test-2`); diff --git a/packages/aws-cdk/test/integ/cli/common.bash b/packages/aws-cdk/test/integ/cli/common.bash index 9d4e507ef7d75..ba3836c53eba0 100644 --- a/packages/aws-cdk/test/integ/cli/common.bash +++ b/packages/aws-cdk/test/integ/cli/common.bash @@ -91,6 +91,9 @@ function prepare_fixture() { function cleanup() { cleanup_stack ${STACK_NAME_PREFIX}-test-1 cleanup_stack ${STACK_NAME_PREFIX}-test-2 + cleanup_stack ${STACK_NAME_PREFIX}-param-test-1 + cleanup_stack ${STACK_NAME_PREFIX}-param-test-2 + cleanup_stack ${STACK_NAME_PREFIX}-param-test-3 cleanup_stack ${STACK_NAME_PREFIX}-iam-test cleanup_stack ${STACK_NAME_PREFIX}-with-nested-stack cleanup_stack ${STACK_NAME_PREFIX}-outputs-test-1 diff --git a/packages/aws-cdk/test/integ/cli/test-cdk-deploy-wildcard-with-parameters.sh b/packages/aws-cdk/test/integ/cli/test-cdk-deploy-wildcard-with-parameters.sh index 30585a0ca530d..fcfaac90b870e 100755 --- a/packages/aws-cdk/test/integ/cli/test-cdk-deploy-wildcard-with-parameters.sh +++ b/packages/aws-cdk/test/integ/cli/test-cdk-deploy-wildcard-with-parameters.sh @@ -8,12 +8,12 @@ setup # STACK_NAME_PREFIX is used in OtherTopicNameParam to allow multiple instances # of this test to run in parallel, othewise they will attempt to create the same SNS topic. -stack_arns=$(cdk deploy ${STACK_NAME_PREFIX}-param-test-\* --parameters "${STACK_NAME_PREFIX}-param-test-1:TopicNameParam=${STACK_NAME_PREFIX}bazinga" --parameters "${STACK_NAME_PREFIX}-param-test-2:OtherTopicNameParam=${STACK_NAME_PREFIX}ThatsMySpot") +stack_arns=$(cdk deploy ${STACK_NAME_PREFIX}-param-test-\* --parameters "${STACK_NAME_PREFIX}-param-test-1:TopicNameParam=${STACK_NAME_PREFIX}bazinga" --parameters "${STACK_NAME_PREFIX}-param-test-2:OtherTopicNameParam=${STACK_NAME_PREFIX}ThatsMySpot" --parameters "${STACK_NAME_PREFIX}-param-test-3:DisplayNameParam=${STACK_NAME_PREFIX}HeyThere" --parameters "${STACK_NAME_PREFIX}-param-test-3:OtherDisplayNameParam=${STACK_NAME_PREFIX}AnotherOne") echo "Stack deployed successfully" # verify that we only deployed a single stack (there's a single ARN in the output) lines="$(echo "${stack_arns}" | wc -l)" -if [ "${lines}" -ne 2 ]; then +if [ "${lines}" -ne 3 ]; then echo "-- output -----------" echo "${stack_arns}" echo "---------------------" diff --git a/packages/aws-cdk/test/integ/cli/test-cdk-deploy-with-parameters-multi.sh b/packages/aws-cdk/test/integ/cli/test-cdk-deploy-with-parameters-multi.sh new file mode 100755 index 0000000000000..456cf825641ec --- /dev/null +++ b/packages/aws-cdk/test/integ/cli/test-cdk-deploy-with-parameters-multi.sh @@ -0,0 +1,40 @@ +#!/bin/bash +set -euo pipefail +scriptdir=$(cd $(dirname $0) && pwd) +source ${scriptdir}/common.bash +# ---------------------------------------------------------- + +setup + +paramVal1="${STACK_NAME_PREFIX}bazinga" +paramVal2="${STACK_NAME_PREFIX}=jagshemash" + +stack_arn=$(cdk deploy -v ${STACK_NAME_PREFIX}-param-test-3 --parameters "DisplayNameParam=${paramVal1}" --parameters "OtherDisplayNameParam=${paramVal2}") +echo "Stack deployed successfully" + +# verify that we only deployed a single stack (there's a single ARN in the output) +assert_lines "${stack_arn}" 1 + +# retrieve stack parameters +response_json=$(mktemp).json +aws cloudformation describe-stacks --stack-name ${stack_arn} > ${response_json} +parameter_count=$(node -e "console.log(require('${response_json}').Stacks[0].Parameters.length)") + +# verify stack parameter count +if [ "${parameter_count}" -ne 2 ]; then + fail "stack has ${parameter_count} parameters, and we expected two" +fi + +# verify stack parameters +for (( i=0; i<$parameter_count; i++ )); do + passedParameterVal=$(node -e "console.log(require('${response_json}').Stacks[0].Parameters[$i].ParameterValue)") + if ! [[ "${passedParameterVal}" =~ ^($paramVal1|$paramVal2)$ ]]; then + fail "Unexpected parameter: '${passedParameterVal}'. Expected parameter values: '${paramVal1}' or '${paramVal2}'" + fi +done; + +# destroy +cdk destroy -f ${STACK_NAME_PREFIX}-param-test-3 + + +echo "✅ success" diff --git a/packages/aws-cdk/test/integ/cli/test-cdk-ls.sh b/packages/aws-cdk/test/integ/cli/test-cdk-ls.sh index 05476f3b8d854..45b4fa577335e 100755 --- a/packages/aws-cdk/test/integ/cli/test-cdk-ls.sh +++ b/packages/aws-cdk/test/integ/cli/test-cdk-ls.sh @@ -19,6 +19,7 @@ ${STACK_NAME_PREFIX}-outputs-test-1 ${STACK_NAME_PREFIX}-outputs-test-2 ${STACK_NAME_PREFIX}-param-test-1 ${STACK_NAME_PREFIX}-param-test-2 +${STACK_NAME_PREFIX}-param-test-3 ${STACK_NAME_PREFIX}-test-1 ${STACK_NAME_PREFIX}-test-2 ${STACK_NAME_PREFIX}-with-nested-stack