diff --git a/packages/@aws-cdk/aws-ec2/test/integ.core-cross-stack-string-list-references.js.snapshot/cdk.out b/packages/@aws-cdk/aws-ec2/test/integ.core-cross-stack-string-list-references.js.snapshot/cdk.out new file mode 100644 index 0000000000000..145739f539580 --- /dev/null +++ b/packages/@aws-cdk/aws-ec2/test/integ.core-cross-stack-string-list-references.js.snapshot/cdk.out @@ -0,0 +1 @@ +{"version":"22.0.0"} \ No newline at end of file diff --git a/packages/@aws-cdk/aws-ec2/test/integ.core-cross-stack-string-list-references.js.snapshot/consumer.assets.json b/packages/@aws-cdk/aws-ec2/test/integ.core-cross-stack-string-list-references.js.snapshot/consumer.assets.json new file mode 100644 index 0000000000000..be6680037c694 --- /dev/null +++ b/packages/@aws-cdk/aws-ec2/test/integ.core-cross-stack-string-list-references.js.snapshot/consumer.assets.json @@ -0,0 +1,19 @@ +{ + "version": "22.0.0", + "files": { + "0fe7f73285a0ff152665f28743981914632b5c05b361126f8af577515c058bdc": { + "source": { + "path": "consumer.template.json", + "packaging": "file" + }, + "destinations": { + "current_account-current_region": { + "bucketName": "cdk-hnb659fds-assets-${AWS::AccountId}-${AWS::Region}", + "objectKey": "0fe7f73285a0ff152665f28743981914632b5c05b361126f8af577515c058bdc.json", + "assumeRoleArn": "arn:${AWS::Partition}:iam::${AWS::AccountId}:role/cdk-hnb659fds-file-publishing-role-${AWS::AccountId}-${AWS::Region}" + } + } + } + }, + "dockerImages": {} +} \ No newline at end of file diff --git a/packages/@aws-cdk/aws-ec2/test/integ.core-cross-stack-string-list-references.js.snapshot/consumer.template.json b/packages/@aws-cdk/aws-ec2/test/integ.core-cross-stack-string-list-references.js.snapshot/consumer.template.json new file mode 100644 index 0000000000000..3aa229c39def4 --- /dev/null +++ b/packages/@aws-cdk/aws-ec2/test/integ.core-cross-stack-string-list-references.js.snapshot/consumer.template.json @@ -0,0 +1,95 @@ +{ + "Resources": { + "GetAtt0B6ACA40": { + "Type": "AWS::SSM::Parameter", + "Properties": { + "Type": "StringList", + "Value": { + "Fn::Join": [ + ",", + { + "Fn::Split": [ + "||", + { + "Fn::ImportValue": "producer:ExportsOutputFnGetAttendpointE7B9679BDnsEntries62080A34" + } + ] + } + ] + } + } + }, + "Ref47C32AF2": { + "Type": "AWS::SSM::Parameter", + "Properties": { + "Type": "StringList", + "Value": { + "Fn::Join": [ + ",", + { + "Fn::Split": [ + "||", + { + "Fn::ImportValue": "producer:ExportsOutputRefstringListParam77B646D6" + } + ] + } + ] + } + } + }, + "ManualEB2ECD12": { + "Type": "AWS::SSM::Parameter", + "Properties": { + "Type": "StringList", + "Value": { + "Fn::Join": [ + ",", + { + "Fn::Split": [ + "||", + { + "Fn::ImportValue": "ManualExport" + } + ] + } + ] + } + } + } + }, + "Parameters": { + "BootstrapVersion": { + "Type": "AWS::SSM::Parameter::Value", + "Default": "/cdk-bootstrap/hnb659fds/version", + "Description": "Version of the CDK Bootstrap resources in this environment, automatically retrieved from SSM Parameter Store. [cdk:skip]" + } + }, + "Rules": { + "CheckBootstrapVersion": { + "Assertions": [ + { + "Assert": { + "Fn::Not": [ + { + "Fn::Contains": [ + [ + "1", + "2", + "3", + "4", + "5" + ], + { + "Ref": "BootstrapVersion" + } + ] + } + ] + }, + "AssertDescription": "CDK bootstrap stack version 6 required. Please run 'cdk bootstrap' with a recent version of the CDK CLI." + } + ] + } + } +} \ No newline at end of file diff --git a/packages/@aws-cdk/aws-ec2/test/integ.core-cross-stack-string-list-references.js.snapshot/crossregionreferencesDefaultTestDeployAssertAB7415FD.assets.json b/packages/@aws-cdk/aws-ec2/test/integ.core-cross-stack-string-list-references.js.snapshot/crossregionreferencesDefaultTestDeployAssertAB7415FD.assets.json new file mode 100644 index 0000000000000..57978842e1a8a --- /dev/null +++ b/packages/@aws-cdk/aws-ec2/test/integ.core-cross-stack-string-list-references.js.snapshot/crossregionreferencesDefaultTestDeployAssertAB7415FD.assets.json @@ -0,0 +1,19 @@ +{ + "version": "22.0.0", + "files": { + "21fbb51d7b23f6a6c262b46a9caee79d744a3ac019fd45422d988b96d44b2a22": { + "source": { + "path": "crossregionreferencesDefaultTestDeployAssertAB7415FD.template.json", + "packaging": "file" + }, + "destinations": { + "current_account-current_region": { + "bucketName": "cdk-hnb659fds-assets-${AWS::AccountId}-${AWS::Region}", + "objectKey": "21fbb51d7b23f6a6c262b46a9caee79d744a3ac019fd45422d988b96d44b2a22.json", + "assumeRoleArn": "arn:${AWS::Partition}:iam::${AWS::AccountId}:role/cdk-hnb659fds-file-publishing-role-${AWS::AccountId}-${AWS::Region}" + } + } + } + }, + "dockerImages": {} +} \ No newline at end of file diff --git a/packages/@aws-cdk/aws-ec2/test/integ.core-cross-stack-string-list-references.js.snapshot/crossregionreferencesDefaultTestDeployAssertAB7415FD.template.json b/packages/@aws-cdk/aws-ec2/test/integ.core-cross-stack-string-list-references.js.snapshot/crossregionreferencesDefaultTestDeployAssertAB7415FD.template.json new file mode 100644 index 0000000000000..ad9d0fb73d1dd --- /dev/null +++ b/packages/@aws-cdk/aws-ec2/test/integ.core-cross-stack-string-list-references.js.snapshot/crossregionreferencesDefaultTestDeployAssertAB7415FD.template.json @@ -0,0 +1,36 @@ +{ + "Parameters": { + "BootstrapVersion": { + "Type": "AWS::SSM::Parameter::Value", + "Default": "/cdk-bootstrap/hnb659fds/version", + "Description": "Version of the CDK Bootstrap resources in this environment, automatically retrieved from SSM Parameter Store. [cdk:skip]" + } + }, + "Rules": { + "CheckBootstrapVersion": { + "Assertions": [ + { + "Assert": { + "Fn::Not": [ + { + "Fn::Contains": [ + [ + "1", + "2", + "3", + "4", + "5" + ], + { + "Ref": "BootstrapVersion" + } + ] + } + ] + }, + "AssertDescription": "CDK bootstrap stack version 6 required. Please run 'cdk bootstrap' with a recent version of the CDK CLI." + } + ] + } + } +} \ No newline at end of file diff --git a/packages/@aws-cdk/aws-ec2/test/integ.core-cross-stack-string-list-references.js.snapshot/integ.json b/packages/@aws-cdk/aws-ec2/test/integ.core-cross-stack-string-list-references.js.snapshot/integ.json new file mode 100644 index 0000000000000..b28db8687060c --- /dev/null +++ b/packages/@aws-cdk/aws-ec2/test/integ.core-cross-stack-string-list-references.js.snapshot/integ.json @@ -0,0 +1,14 @@ +{ + "version": "22.0.0", + "testCases": { + "cross-region-references/DefaultTest": { + "stacks": [ + "producer", + "consumer" + ], + "stackUpdateWorkflow": false, + "assertionStack": "cross-region-references/DefaultTest/DeployAssert", + "assertionStackName": "crossregionreferencesDefaultTestDeployAssertAB7415FD" + } + } +} \ No newline at end of file diff --git a/packages/@aws-cdk/aws-ec2/test/integ.core-cross-stack-string-list-references.js.snapshot/manifest.json b/packages/@aws-cdk/aws-ec2/test/integ.core-cross-stack-string-list-references.js.snapshot/manifest.json new file mode 100644 index 0000000000000..e38a5a5e53acf --- /dev/null +++ b/packages/@aws-cdk/aws-ec2/test/integ.core-cross-stack-string-list-references.js.snapshot/manifest.json @@ -0,0 +1,339 @@ +{ + "version": "22.0.0", + "artifacts": { + "producer.assets": { + "type": "cdk:asset-manifest", + "properties": { + "file": "producer.assets.json", + "requiresBootstrapStackVersion": 6, + "bootstrapStackVersionSsmParameter": "/cdk-bootstrap/hnb659fds/version" + } + }, + "producer": { + "type": "aws:cloudformation:stack", + "environment": "aws://unknown-account/unknown-region", + "properties": { + "templateFile": "producer.template.json", + "validateOnSynth": false, + "assumeRoleArn": "arn:${AWS::Partition}:iam::${AWS::AccountId}:role/cdk-hnb659fds-deploy-role-${AWS::AccountId}-${AWS::Region}", + "cloudFormationExecutionRoleArn": "arn:${AWS::Partition}:iam::${AWS::AccountId}:role/cdk-hnb659fds-cfn-exec-role-${AWS::AccountId}-${AWS::Region}", + "stackTemplateAssetObjectUrl": "s3://cdk-hnb659fds-assets-${AWS::AccountId}-${AWS::Region}/3d13409de2ca165913ea98c816c66c594e70fabb474b3eb57d271cc8e8f0fa6f.json", + "requiresBootstrapStackVersion": 6, + "bootstrapStackVersionSsmParameter": "/cdk-bootstrap/hnb659fds/version", + "additionalDependencies": [ + "producer.assets" + ], + "lookupRole": { + "arn": "arn:${AWS::Partition}:iam::${AWS::AccountId}:role/cdk-hnb659fds-lookup-role-${AWS::AccountId}-${AWS::Region}", + "requiresBootstrapStackVersion": 8, + "bootstrapStackVersionSsmParameter": "/cdk-bootstrap/hnb659fds/version" + } + }, + "dependencies": [ + "producer.assets" + ], + "metadata": { + "/producer/vpc/Resource": [ + { + "type": "aws:cdk:logicalId", + "data": "vpcA2121C38" + } + ], + "/producer/vpc/PublicSubnet1/Subnet": [ + { + "type": "aws:cdk:logicalId", + "data": "vpcPublicSubnet1Subnet2E65531E" + } + ], + "/producer/vpc/PublicSubnet1/RouteTable": [ + { + "type": "aws:cdk:logicalId", + "data": "vpcPublicSubnet1RouteTable48A2DF9B" + } + ], + "/producer/vpc/PublicSubnet1/RouteTableAssociation": [ + { + "type": "aws:cdk:logicalId", + "data": "vpcPublicSubnet1RouteTableAssociation5D3F4579" + } + ], + "/producer/vpc/PublicSubnet1/DefaultRoute": [ + { + "type": "aws:cdk:logicalId", + "data": "vpcPublicSubnet1DefaultRoute10708846" + } + ], + "/producer/vpc/PublicSubnet1/EIP": [ + { + "type": "aws:cdk:logicalId", + "data": "vpcPublicSubnet1EIPDA49DCBE" + } + ], + "/producer/vpc/PublicSubnet1/NATGateway": [ + { + "type": "aws:cdk:logicalId", + "data": "vpcPublicSubnet1NATGateway9C16659E" + } + ], + "/producer/vpc/PublicSubnet2/Subnet": [ + { + "type": "aws:cdk:logicalId", + "data": "vpcPublicSubnet2Subnet009B674F" + } + ], + "/producer/vpc/PublicSubnet2/RouteTable": [ + { + "type": "aws:cdk:logicalId", + "data": "vpcPublicSubnet2RouteTableEB40D4CB" + } + ], + "/producer/vpc/PublicSubnet2/RouteTableAssociation": [ + { + "type": "aws:cdk:logicalId", + "data": "vpcPublicSubnet2RouteTableAssociation21F81B59" + } + ], + "/producer/vpc/PublicSubnet2/DefaultRoute": [ + { + "type": "aws:cdk:logicalId", + "data": "vpcPublicSubnet2DefaultRouteA1EC0F60" + } + ], + "/producer/vpc/PublicSubnet2/EIP": [ + { + "type": "aws:cdk:logicalId", + "data": "vpcPublicSubnet2EIP9B3743B1" + } + ], + "/producer/vpc/PublicSubnet2/NATGateway": [ + { + "type": "aws:cdk:logicalId", + "data": "vpcPublicSubnet2NATGateway9B8AE11A" + } + ], + "/producer/vpc/PrivateSubnet1/Subnet": [ + { + "type": "aws:cdk:logicalId", + "data": "vpcPrivateSubnet1Subnet934893E8" + } + ], + "/producer/vpc/PrivateSubnet1/RouteTable": [ + { + "type": "aws:cdk:logicalId", + "data": "vpcPrivateSubnet1RouteTableB41A48CC" + } + ], + "/producer/vpc/PrivateSubnet1/RouteTableAssociation": [ + { + "type": "aws:cdk:logicalId", + "data": "vpcPrivateSubnet1RouteTableAssociation67945127" + } + ], + "/producer/vpc/PrivateSubnet1/DefaultRoute": [ + { + "type": "aws:cdk:logicalId", + "data": "vpcPrivateSubnet1DefaultRoute1AA8E2E5" + } + ], + "/producer/vpc/PrivateSubnet2/Subnet": [ + { + "type": "aws:cdk:logicalId", + "data": "vpcPrivateSubnet2Subnet7031C2BA" + } + ], + "/producer/vpc/PrivateSubnet2/RouteTable": [ + { + "type": "aws:cdk:logicalId", + "data": "vpcPrivateSubnet2RouteTable7280F23E" + } + ], + "/producer/vpc/PrivateSubnet2/RouteTableAssociation": [ + { + "type": "aws:cdk:logicalId", + "data": "vpcPrivateSubnet2RouteTableAssociation007E94D3" + } + ], + "/producer/vpc/PrivateSubnet2/DefaultRoute": [ + { + "type": "aws:cdk:logicalId", + "data": "vpcPrivateSubnet2DefaultRouteB0E07F99" + } + ], + "/producer/vpc/IGW": [ + { + "type": "aws:cdk:logicalId", + "data": "vpcIGWE57CBDCA" + } + ], + "/producer/vpc/VPCGW": [ + { + "type": "aws:cdk:logicalId", + "data": "vpcVPCGW7984C166" + } + ], + "/producer/endpoint/SecurityGroup/Resource": [ + { + "type": "aws:cdk:logicalId", + "data": "endpointSecurityGroup895E596E" + } + ], + "/producer/endpoint/Resource": [ + { + "type": "aws:cdk:logicalId", + "data": "endpointE7B9679B" + } + ], + "/producer/stringListParam": [ + { + "type": "aws:cdk:logicalId", + "data": "stringListParam" + } + ], + "/producer/ExportManualExport": [ + { + "type": "aws:cdk:logicalId", + "data": "ExportManualExport" + } + ], + "/producer/Exports/Output{\"Fn::GetAtt\":[\"endpointE7B9679B\",\"DnsEntries\"]}": [ + { + "type": "aws:cdk:logicalId", + "data": "ExportsOutputFnGetAttendpointE7B9679BDnsEntries62080A34" + } + ], + "/producer/Exports/Output{\"Ref\":\"stringListParam\"}": [ + { + "type": "aws:cdk:logicalId", + "data": "ExportsOutputRefstringListParam77B646D6" + } + ], + "/producer/BootstrapVersion": [ + { + "type": "aws:cdk:logicalId", + "data": "BootstrapVersion" + } + ], + "/producer/CheckBootstrapVersion": [ + { + "type": "aws:cdk:logicalId", + "data": "CheckBootstrapVersion" + } + ] + }, + "displayName": "producer" + }, + "consumer.assets": { + "type": "cdk:asset-manifest", + "properties": { + "file": "consumer.assets.json", + "requiresBootstrapStackVersion": 6, + "bootstrapStackVersionSsmParameter": "/cdk-bootstrap/hnb659fds/version" + } + }, + "consumer": { + "type": "aws:cloudformation:stack", + "environment": "aws://unknown-account/unknown-region", + "properties": { + "templateFile": "consumer.template.json", + "validateOnSynth": false, + "assumeRoleArn": "arn:${AWS::Partition}:iam::${AWS::AccountId}:role/cdk-hnb659fds-deploy-role-${AWS::AccountId}-${AWS::Region}", + "cloudFormationExecutionRoleArn": "arn:${AWS::Partition}:iam::${AWS::AccountId}:role/cdk-hnb659fds-cfn-exec-role-${AWS::AccountId}-${AWS::Region}", + "stackTemplateAssetObjectUrl": "s3://cdk-hnb659fds-assets-${AWS::AccountId}-${AWS::Region}/0fe7f73285a0ff152665f28743981914632b5c05b361126f8af577515c058bdc.json", + "requiresBootstrapStackVersion": 6, + "bootstrapStackVersionSsmParameter": "/cdk-bootstrap/hnb659fds/version", + "additionalDependencies": [ + "consumer.assets" + ], + "lookupRole": { + "arn": "arn:${AWS::Partition}:iam::${AWS::AccountId}:role/cdk-hnb659fds-lookup-role-${AWS::AccountId}-${AWS::Region}", + "requiresBootstrapStackVersion": 8, + "bootstrapStackVersionSsmParameter": "/cdk-bootstrap/hnb659fds/version" + } + }, + "dependencies": [ + "producer", + "consumer.assets" + ], + "metadata": { + "/consumer/GetAtt/Resource": [ + { + "type": "aws:cdk:logicalId", + "data": "GetAtt0B6ACA40" + } + ], + "/consumer/Ref/Resource": [ + { + "type": "aws:cdk:logicalId", + "data": "Ref47C32AF2" + } + ], + "/consumer/Manual/Resource": [ + { + "type": "aws:cdk:logicalId", + "data": "ManualEB2ECD12" + } + ], + "/consumer/BootstrapVersion": [ + { + "type": "aws:cdk:logicalId", + "data": "BootstrapVersion" + } + ], + "/consumer/CheckBootstrapVersion": [ + { + "type": "aws:cdk:logicalId", + "data": "CheckBootstrapVersion" + } + ] + }, + "displayName": "consumer" + }, + "crossregionreferencesDefaultTestDeployAssertAB7415FD.assets": { + "type": "cdk:asset-manifest", + "properties": { + "file": "crossregionreferencesDefaultTestDeployAssertAB7415FD.assets.json", + "requiresBootstrapStackVersion": 6, + "bootstrapStackVersionSsmParameter": "/cdk-bootstrap/hnb659fds/version" + } + }, + "crossregionreferencesDefaultTestDeployAssertAB7415FD": { + "type": "aws:cloudformation:stack", + "environment": "aws://unknown-account/unknown-region", + "properties": { + "templateFile": "crossregionreferencesDefaultTestDeployAssertAB7415FD.template.json", + "validateOnSynth": false, + "assumeRoleArn": "arn:${AWS::Partition}:iam::${AWS::AccountId}:role/cdk-hnb659fds-deploy-role-${AWS::AccountId}-${AWS::Region}", + "cloudFormationExecutionRoleArn": "arn:${AWS::Partition}:iam::${AWS::AccountId}:role/cdk-hnb659fds-cfn-exec-role-${AWS::AccountId}-${AWS::Region}", + "stackTemplateAssetObjectUrl": "s3://cdk-hnb659fds-assets-${AWS::AccountId}-${AWS::Region}/21fbb51d7b23f6a6c262b46a9caee79d744a3ac019fd45422d988b96d44b2a22.json", + "requiresBootstrapStackVersion": 6, + "bootstrapStackVersionSsmParameter": "/cdk-bootstrap/hnb659fds/version", + "additionalDependencies": [ + "crossregionreferencesDefaultTestDeployAssertAB7415FD.assets" + ], + "lookupRole": { + "arn": "arn:${AWS::Partition}:iam::${AWS::AccountId}:role/cdk-hnb659fds-lookup-role-${AWS::AccountId}-${AWS::Region}", + "requiresBootstrapStackVersion": 8, + "bootstrapStackVersionSsmParameter": "/cdk-bootstrap/hnb659fds/version" + } + }, + "dependencies": [ + "crossregionreferencesDefaultTestDeployAssertAB7415FD.assets" + ], + "metadata": { + "/cross-region-references/DefaultTest/DeployAssert/BootstrapVersion": [ + { + "type": "aws:cdk:logicalId", + "data": "BootstrapVersion" + } + ], + "/cross-region-references/DefaultTest/DeployAssert/CheckBootstrapVersion": [ + { + "type": "aws:cdk:logicalId", + "data": "CheckBootstrapVersion" + } + ] + }, + "displayName": "cross-region-references/DefaultTest/DeployAssert" + } + } +} \ No newline at end of file diff --git a/packages/@aws-cdk/aws-ec2/test/integ.core-cross-stack-string-list-references.js.snapshot/producer.assets.json b/packages/@aws-cdk/aws-ec2/test/integ.core-cross-stack-string-list-references.js.snapshot/producer.assets.json new file mode 100644 index 0000000000000..457eb16c010a9 --- /dev/null +++ b/packages/@aws-cdk/aws-ec2/test/integ.core-cross-stack-string-list-references.js.snapshot/producer.assets.json @@ -0,0 +1,19 @@ +{ + "version": "22.0.0", + "files": { + "3d13409de2ca165913ea98c816c66c594e70fabb474b3eb57d271cc8e8f0fa6f": { + "source": { + "path": "producer.template.json", + "packaging": "file" + }, + "destinations": { + "current_account-current_region": { + "bucketName": "cdk-hnb659fds-assets-${AWS::AccountId}-${AWS::Region}", + "objectKey": "3d13409de2ca165913ea98c816c66c594e70fabb474b3eb57d271cc8e8f0fa6f.json", + "assumeRoleArn": "arn:${AWS::Partition}:iam::${AWS::AccountId}:role/cdk-hnb659fds-file-publishing-role-${AWS::AccountId}-${AWS::Region}" + } + } + } + }, + "dockerImages": {} +} \ No newline at end of file diff --git a/packages/@aws-cdk/aws-ec2/test/integ.core-cross-stack-string-list-references.js.snapshot/producer.template.json b/packages/@aws-cdk/aws-ec2/test/integ.core-cross-stack-string-list-references.js.snapshot/producer.template.json new file mode 100644 index 0000000000000..232c38b86ff01 --- /dev/null +++ b/packages/@aws-cdk/aws-ec2/test/integ.core-cross-stack-string-list-references.js.snapshot/producer.template.json @@ -0,0 +1,552 @@ +{ + "Resources": { + "vpcA2121C38": { + "Type": "AWS::EC2::VPC", + "Properties": { + "CidrBlock": "10.0.0.0/16", + "EnableDnsHostnames": true, + "EnableDnsSupport": true, + "InstanceTenancy": "default", + "Tags": [ + { + "Key": "Name", + "Value": "producer/vpc" + } + ] + } + }, + "vpcPublicSubnet1Subnet2E65531E": { + "Type": "AWS::EC2::Subnet", + "Properties": { + "VpcId": { + "Ref": "vpcA2121C38" + }, + "AvailabilityZone": { + "Fn::Select": [ + 0, + { + "Fn::GetAZs": "" + } + ] + }, + "CidrBlock": "10.0.0.0/18", + "MapPublicIpOnLaunch": true, + "Tags": [ + { + "Key": "aws-cdk:subnet-name", + "Value": "Public" + }, + { + "Key": "aws-cdk:subnet-type", + "Value": "Public" + }, + { + "Key": "Name", + "Value": "producer/vpc/PublicSubnet1" + } + ] + } + }, + "vpcPublicSubnet1RouteTable48A2DF9B": { + "Type": "AWS::EC2::RouteTable", + "Properties": { + "VpcId": { + "Ref": "vpcA2121C38" + }, + "Tags": [ + { + "Key": "Name", + "Value": "producer/vpc/PublicSubnet1" + } + ] + } + }, + "vpcPublicSubnet1RouteTableAssociation5D3F4579": { + "Type": "AWS::EC2::SubnetRouteTableAssociation", + "Properties": { + "RouteTableId": { + "Ref": "vpcPublicSubnet1RouteTable48A2DF9B" + }, + "SubnetId": { + "Ref": "vpcPublicSubnet1Subnet2E65531E" + } + } + }, + "vpcPublicSubnet1DefaultRoute10708846": { + "Type": "AWS::EC2::Route", + "Properties": { + "RouteTableId": { + "Ref": "vpcPublicSubnet1RouteTable48A2DF9B" + }, + "DestinationCidrBlock": "0.0.0.0/0", + "GatewayId": { + "Ref": "vpcIGWE57CBDCA" + } + }, + "DependsOn": [ + "vpcVPCGW7984C166" + ] + }, + "vpcPublicSubnet1EIPDA49DCBE": { + "Type": "AWS::EC2::EIP", + "Properties": { + "Domain": "vpc", + "Tags": [ + { + "Key": "Name", + "Value": "producer/vpc/PublicSubnet1" + } + ] + } + }, + "vpcPublicSubnet1NATGateway9C16659E": { + "Type": "AWS::EC2::NatGateway", + "Properties": { + "SubnetId": { + "Ref": "vpcPublicSubnet1Subnet2E65531E" + }, + "AllocationId": { + "Fn::GetAtt": [ + "vpcPublicSubnet1EIPDA49DCBE", + "AllocationId" + ] + }, + "Tags": [ + { + "Key": "Name", + "Value": "producer/vpc/PublicSubnet1" + } + ] + }, + "DependsOn": [ + "vpcPublicSubnet1DefaultRoute10708846", + "vpcPublicSubnet1RouteTableAssociation5D3F4579" + ] + }, + "vpcPublicSubnet2Subnet009B674F": { + "Type": "AWS::EC2::Subnet", + "Properties": { + "VpcId": { + "Ref": "vpcA2121C38" + }, + "AvailabilityZone": { + "Fn::Select": [ + 1, + { + "Fn::GetAZs": "" + } + ] + }, + "CidrBlock": "10.0.64.0/18", + "MapPublicIpOnLaunch": true, + "Tags": [ + { + "Key": "aws-cdk:subnet-name", + "Value": "Public" + }, + { + "Key": "aws-cdk:subnet-type", + "Value": "Public" + }, + { + "Key": "Name", + "Value": "producer/vpc/PublicSubnet2" + } + ] + } + }, + "vpcPublicSubnet2RouteTableEB40D4CB": { + "Type": "AWS::EC2::RouteTable", + "Properties": { + "VpcId": { + "Ref": "vpcA2121C38" + }, + "Tags": [ + { + "Key": "Name", + "Value": "producer/vpc/PublicSubnet2" + } + ] + } + }, + "vpcPublicSubnet2RouteTableAssociation21F81B59": { + "Type": "AWS::EC2::SubnetRouteTableAssociation", + "Properties": { + "RouteTableId": { + "Ref": "vpcPublicSubnet2RouteTableEB40D4CB" + }, + "SubnetId": { + "Ref": "vpcPublicSubnet2Subnet009B674F" + } + } + }, + "vpcPublicSubnet2DefaultRouteA1EC0F60": { + "Type": "AWS::EC2::Route", + "Properties": { + "RouteTableId": { + "Ref": "vpcPublicSubnet2RouteTableEB40D4CB" + }, + "DestinationCidrBlock": "0.0.0.0/0", + "GatewayId": { + "Ref": "vpcIGWE57CBDCA" + } + }, + "DependsOn": [ + "vpcVPCGW7984C166" + ] + }, + "vpcPublicSubnet2EIP9B3743B1": { + "Type": "AWS::EC2::EIP", + "Properties": { + "Domain": "vpc", + "Tags": [ + { + "Key": "Name", + "Value": "producer/vpc/PublicSubnet2" + } + ] + } + }, + "vpcPublicSubnet2NATGateway9B8AE11A": { + "Type": "AWS::EC2::NatGateway", + "Properties": { + "SubnetId": { + "Ref": "vpcPublicSubnet2Subnet009B674F" + }, + "AllocationId": { + "Fn::GetAtt": [ + "vpcPublicSubnet2EIP9B3743B1", + "AllocationId" + ] + }, + "Tags": [ + { + "Key": "Name", + "Value": "producer/vpc/PublicSubnet2" + } + ] + }, + "DependsOn": [ + "vpcPublicSubnet2DefaultRouteA1EC0F60", + "vpcPublicSubnet2RouteTableAssociation21F81B59" + ] + }, + "vpcPrivateSubnet1Subnet934893E8": { + "Type": "AWS::EC2::Subnet", + "Properties": { + "VpcId": { + "Ref": "vpcA2121C38" + }, + "AvailabilityZone": { + "Fn::Select": [ + 0, + { + "Fn::GetAZs": "" + } + ] + }, + "CidrBlock": "10.0.128.0/18", + "MapPublicIpOnLaunch": false, + "Tags": [ + { + "Key": "aws-cdk:subnet-name", + "Value": "Private" + }, + { + "Key": "aws-cdk:subnet-type", + "Value": "Private" + }, + { + "Key": "Name", + "Value": "producer/vpc/PrivateSubnet1" + } + ] + } + }, + "vpcPrivateSubnet1RouteTableB41A48CC": { + "Type": "AWS::EC2::RouteTable", + "Properties": { + "VpcId": { + "Ref": "vpcA2121C38" + }, + "Tags": [ + { + "Key": "Name", + "Value": "producer/vpc/PrivateSubnet1" + } + ] + } + }, + "vpcPrivateSubnet1RouteTableAssociation67945127": { + "Type": "AWS::EC2::SubnetRouteTableAssociation", + "Properties": { + "RouteTableId": { + "Ref": "vpcPrivateSubnet1RouteTableB41A48CC" + }, + "SubnetId": { + "Ref": "vpcPrivateSubnet1Subnet934893E8" + } + } + }, + "vpcPrivateSubnet1DefaultRoute1AA8E2E5": { + "Type": "AWS::EC2::Route", + "Properties": { + "RouteTableId": { + "Ref": "vpcPrivateSubnet1RouteTableB41A48CC" + }, + "DestinationCidrBlock": "0.0.0.0/0", + "NatGatewayId": { + "Ref": "vpcPublicSubnet1NATGateway9C16659E" + } + } + }, + "vpcPrivateSubnet2Subnet7031C2BA": { + "Type": "AWS::EC2::Subnet", + "Properties": { + "VpcId": { + "Ref": "vpcA2121C38" + }, + "AvailabilityZone": { + "Fn::Select": [ + 1, + { + "Fn::GetAZs": "" + } + ] + }, + "CidrBlock": "10.0.192.0/18", + "MapPublicIpOnLaunch": false, + "Tags": [ + { + "Key": "aws-cdk:subnet-name", + "Value": "Private" + }, + { + "Key": "aws-cdk:subnet-type", + "Value": "Private" + }, + { + "Key": "Name", + "Value": "producer/vpc/PrivateSubnet2" + } + ] + } + }, + "vpcPrivateSubnet2RouteTable7280F23E": { + "Type": "AWS::EC2::RouteTable", + "Properties": { + "VpcId": { + "Ref": "vpcA2121C38" + }, + "Tags": [ + { + "Key": "Name", + "Value": "producer/vpc/PrivateSubnet2" + } + ] + } + }, + "vpcPrivateSubnet2RouteTableAssociation007E94D3": { + "Type": "AWS::EC2::SubnetRouteTableAssociation", + "Properties": { + "RouteTableId": { + "Ref": "vpcPrivateSubnet2RouteTable7280F23E" + }, + "SubnetId": { + "Ref": "vpcPrivateSubnet2Subnet7031C2BA" + } + } + }, + "vpcPrivateSubnet2DefaultRouteB0E07F99": { + "Type": "AWS::EC2::Route", + "Properties": { + "RouteTableId": { + "Ref": "vpcPrivateSubnet2RouteTable7280F23E" + }, + "DestinationCidrBlock": "0.0.0.0/0", + "NatGatewayId": { + "Ref": "vpcPublicSubnet2NATGateway9B8AE11A" + } + } + }, + "vpcIGWE57CBDCA": { + "Type": "AWS::EC2::InternetGateway", + "Properties": { + "Tags": [ + { + "Key": "Name", + "Value": "producer/vpc" + } + ] + } + }, + "vpcVPCGW7984C166": { + "Type": "AWS::EC2::VPCGatewayAttachment", + "Properties": { + "VpcId": { + "Ref": "vpcA2121C38" + }, + "InternetGatewayId": { + "Ref": "vpcIGWE57CBDCA" + } + } + }, + "endpointSecurityGroup895E596E": { + "Type": "AWS::EC2::SecurityGroup", + "Properties": { + "GroupDescription": "producer/endpoint/SecurityGroup", + "SecurityGroupEgress": [ + { + "CidrIp": "0.0.0.0/0", + "Description": "Allow all outbound traffic by default", + "IpProtocol": "-1" + } + ], + "SecurityGroupIngress": [ + { + "CidrIp": { + "Fn::GetAtt": [ + "vpcA2121C38", + "CidrBlock" + ] + }, + "Description": { + "Fn::Join": [ + "", + [ + "from ", + { + "Fn::GetAtt": [ + "vpcA2121C38", + "CidrBlock" + ] + }, + ":443" + ] + ] + }, + "FromPort": 443, + "IpProtocol": "tcp", + "ToPort": 443 + } + ], + "VpcId": { + "Ref": "vpcA2121C38" + } + } + }, + "endpointE7B9679B": { + "Type": "AWS::EC2::VPCEndpoint", + "Properties": { + "ServiceName": { + "Fn::Join": [ + "", + [ + "com.amazonaws.", + { + "Ref": "AWS::Region" + }, + ".secretsmanager" + ] + ] + }, + "VpcId": { + "Ref": "vpcA2121C38" + }, + "PrivateDnsEnabled": true, + "SecurityGroupIds": [ + { + "Fn::GetAtt": [ + "endpointSecurityGroup895E596E", + "GroupId" + ] + } + ], + "SubnetIds": [ + { + "Ref": "vpcPrivateSubnet1Subnet934893E8" + }, + { + "Ref": "vpcPrivateSubnet2Subnet7031C2BA" + } + ], + "VpcEndpointType": "Interface" + } + } + }, + "Parameters": { + "stringListParam": { + "Type": "List", + "Default": "BLAT,BLAH" + }, + "BootstrapVersion": { + "Type": "AWS::SSM::Parameter::Value", + "Default": "/cdk-bootstrap/hnb659fds/version", + "Description": "Version of the CDK Bootstrap resources in this environment, automatically retrieved from SSM Parameter Store. [cdk:skip]" + } + }, + "Outputs": { + "ExportManualExport": { + "Value": "string1||string2", + "Export": { + "Name": "ManualExport" + } + }, + "ExportsOutputFnGetAttendpointE7B9679BDnsEntries62080A34": { + "Value": { + "Fn::Join": [ + "||", + { + "Fn::GetAtt": [ + "endpointE7B9679B", + "DnsEntries" + ] + } + ] + }, + "Export": { + "Name": "producer:ExportsOutputFnGetAttendpointE7B9679BDnsEntries62080A34" + } + }, + "ExportsOutputRefstringListParam77B646D6": { + "Value": { + "Fn::Join": [ + "||", + { + "Ref": "stringListParam" + } + ] + }, + "Export": { + "Name": "producer:ExportsOutputRefstringListParam77B646D6" + } + } + }, + "Rules": { + "CheckBootstrapVersion": { + "Assertions": [ + { + "Assert": { + "Fn::Not": [ + { + "Fn::Contains": [ + [ + "1", + "2", + "3", + "4", + "5" + ], + { + "Ref": "BootstrapVersion" + } + ] + } + ] + }, + "AssertDescription": "CDK bootstrap stack version 6 required. Please run 'cdk bootstrap' with a recent version of the CDK CLI." + } + ] + } + } +} \ No newline at end of file diff --git a/packages/@aws-cdk/aws-ec2/test/integ.core-cross-stack-string-list-references.ts b/packages/@aws-cdk/aws-ec2/test/integ.core-cross-stack-string-list-references.ts new file mode 100644 index 0000000000000..fc45599493847 --- /dev/null +++ b/packages/@aws-cdk/aws-ec2/test/integ.core-cross-stack-string-list-references.ts @@ -0,0 +1,72 @@ +import * as ssm from '@aws-cdk/aws-ssm'; +import { App, CfnParameter, Stack, StackProps } from '@aws-cdk/core'; +import { IntegTest } from '@aws-cdk/integ-tests'; +import { Construct } from 'constructs'; +import { InterfaceVpcEndpoint, InterfaceVpcEndpointAwsService, Vpc } from '../lib'; + +// GIVEN +const app = new App({ + treeMetadata: false, +}); + +class ProducerStack extends Stack { + public stringListGetAtt: string[]; + public stringListRef: CfnParameter; + public manualExport: string[]; + + constructor(scope: Construct, id: string, props?: StackProps) { + super(scope, id, props); + + const vpc = new Vpc(this, 'vpc'); + this.stringListGetAtt = new InterfaceVpcEndpoint(this, 'endpoint', { + vpc, + service: InterfaceVpcEndpointAwsService.SECRETS_MANAGER, + }).vpcEndpointDnsEntries; + + this.stringListRef = new CfnParameter(this, 'stringListParam', { + default: 'BLAT,BLAH', + type: 'List', + }); + + this.manualExport = this.exportStringListValue(['string1', 'string2'], { + name: 'ManualExport', + }); + } +} + +export interface consumerDeployProps extends StackProps { + stringListGetAtt: string[], + stringListRef: CfnParameter, + manualStringList: string[] +} + +class ConsumerStack extends Stack { + constructor(scope: Construct, id: string, props: consumerDeployProps) { + super(scope, id, props); + + new ssm.StringListParameter(this, 'GetAtt', { + stringListValue: props.stringListGetAtt, + }); + + new ssm.StringListParameter(this, 'Ref', { + stringListValue: props.stringListRef.valueAsList, + }); + + new ssm.StringListParameter(this, 'Manual', { + stringListValue: props.manualStringList, + }); + } +} + +const producer = new ProducerStack(app, 'producer'); +const consumer = new ConsumerStack(app, 'consumer', { + stringListGetAtt: producer.stringListGetAtt, + stringListRef: producer.stringListRef, + manualStringList: producer.manualExport, +}); + +// THEN +new IntegTest(app, 'cross-region-references', { + testCases: [producer, consumer], + stackUpdateWorkflow: false, +}); diff --git a/packages/@aws-cdk/aws-ec2/test/vpc.test.ts b/packages/@aws-cdk/aws-ec2/test/vpc.test.ts index afe61c6e0a320..bdaffa94114de 100644 --- a/packages/@aws-cdk/aws-ec2/test/vpc.test.ts +++ b/packages/@aws-cdk/aws-ec2/test/vpc.test.ts @@ -1,6 +1,6 @@ import { Annotations, Match, Template } from '@aws-cdk/assertions'; import { testDeprecated } from '@aws-cdk/cdk-build-tools'; -import { CfnOutput, CfnResource, Fn, Lazy, Stack, Tags } from '@aws-cdk/core'; +import { App, CfnOutput, CfnResource, Fn, Lazy, Stack, Tags } from '@aws-cdk/core'; import { AclCidr, AclTraffic, @@ -27,6 +27,7 @@ import { TrafficDirection, Vpc, IpAddresses, + InterfaceVpcEndpointAwsService, } from '../lib'; describe('vpc', () => { @@ -2115,6 +2116,66 @@ describe('vpc', () => { } }); }); + + describe('can reference vpcEndpointDnsEntries across stacks', () => { + test('can reference an actual string list across stacks', () => { + const app = new App(); + const stack1 = new Stack(app, 'Stack1'); + const vpc = new Vpc(stack1, 'Vpc'); + const endpoint = new InterfaceVpcEndpoint(stack1, 'interfaceVpcEndpoint', { + vpc, + service: InterfaceVpcEndpointAwsService.SECRETS_MANAGER, + }); + + const stack2 = new Stack(app, 'Stack2'); + new CfnOutput(stack2, 'endpoint', { + value: Fn.select(0, endpoint.vpcEndpointDnsEntries), + }); + + const assembly = app.synth(); + const template1 = assembly.getStackByName(stack1.stackName).template; + const template2 = assembly.getStackByName(stack2.stackName).template; + + // THEN + expect(template1).toMatchObject({ + Outputs: { + ExportsOutputFnGetAttinterfaceVpcEndpoint89C99945DnsEntriesB1872F7A: { + Value: { + 'Fn::Join': [ + '||', { + 'Fn::GetAtt': [ + 'interfaceVpcEndpoint89C99945', + 'DnsEntries', + ], + }, + ], + }, + Export: { Name: 'Stack1:ExportsOutputFnGetAttinterfaceVpcEndpoint89C99945DnsEntriesB1872F7A' }, + }, + }, + }); + + expect(template2).toMatchObject({ + Outputs: { + endpoint: { + Value: { + 'Fn::Select': [ + 0, + { + 'Fn::Split': [ + '||', + { + 'Fn::ImportValue': 'Stack1:ExportsOutputFnGetAttinterfaceVpcEndpoint89C99945DnsEntriesB1872F7A', + }, + ], + }, + ], + }, + }, + }, + }); + }); + }); }); function getTestStack(): Stack { diff --git a/packages/@aws-cdk/aws-ssm/lib/parameter.ts b/packages/@aws-cdk/aws-ssm/lib/parameter.ts index 5721cd30e922d..cf44b574bb1ea 100644 --- a/packages/@aws-cdk/aws-ssm/lib/parameter.ts +++ b/packages/@aws-cdk/aws-ssm/lib/parameter.ts @@ -729,7 +729,7 @@ export class StringListParameter extends ParameterBase implements IStringListPar name: this.physicalName, tier: props.tier, type: ParameterType.STRING_LIST, - value: props.stringListValue.join(','), + value: Fn.join(',', props.stringListValue), }); this.parameterName = this.getResourceNameAttribute(resource.ref); this.parameterArn = arnForParameterName(this, this.parameterName, { diff --git a/packages/@aws-cdk/core/lib/cfn-output.ts b/packages/@aws-cdk/core/lib/cfn-output.ts index a9d935829fb6f..e9b8d654cb81a 100644 --- a/packages/@aws-cdk/core/lib/cfn-output.ts +++ b/packages/@aws-cdk/core/lib/cfn-output.ts @@ -51,6 +51,10 @@ export class CfnOutput extends CfnElement { if (props.value === undefined) { throw new Error(`Missing value for CloudFormation output at path "${this.node.path}"`); + } else if (Array.isArray(props.value)) { + // `props.value` is a string, but because cross-stack exports allow passing any, + // we need to check for lists here. + throw new Error(`CloudFormation output was given a string list instead of a string at path "${this.node.path}"`); } this._description = props.description; diff --git a/packages/@aws-cdk/core/lib/cfn-parameter.ts b/packages/@aws-cdk/core/lib/cfn-parameter.ts index 18182f5ef7de2..3b250415f2df7 100644 --- a/packages/@aws-cdk/core/lib/cfn-parameter.ts +++ b/packages/@aws-cdk/core/lib/cfn-parameter.ts @@ -3,6 +3,7 @@ import { CfnElement } from './cfn-element'; import { CfnReference } from './private/cfn-reference'; import { IResolvable, IResolveContext } from './resolvable'; import { Token } from './token'; +import { ResolutionTypeHint } from './type-hints'; export interface CfnParameterProps { /** @@ -108,6 +109,7 @@ export class CfnParameter extends CfnElement { private _minLength?: number; private _minValue?: number; private _noEcho?: boolean; + private typeHint: ResolutionTypeHint; /** * Creates a parameter construct. @@ -131,6 +133,7 @@ export class CfnParameter extends CfnElement { this._minLength = props.minLength; this._minValue = props.minValue; this._noEcho = props.noEcho; + this.typeHint = typeToTypeHint(this._type); } /** @@ -144,6 +147,7 @@ export class CfnParameter extends CfnElement { public set type(type: string) { this._type = type; + this.typeHint = typeToTypeHint(this._type); } /** @@ -282,7 +286,7 @@ export class CfnParameter extends CfnElement { * The parameter value as a Token */ public get value(): IResolvable { - return CfnReference.for(this, 'Ref'); + return CfnReference.for(this, 'Ref', undefined, this.typeHint); } /** @@ -363,3 +367,13 @@ function isNumberType(type: string) { function isStringType(type: string) { return !isListType(type) && !isNumberType(type); } + +function typeToTypeHint(type: string): ResolutionTypeHint { + if (isListType(type)) { + return ResolutionTypeHint.STRING_LIST; + } else if (isNumberType(type)) { + return ResolutionTypeHint.NUMBER; + } + + return ResolutionTypeHint.STRING; +} diff --git a/packages/@aws-cdk/core/lib/cfn-resource.ts b/packages/@aws-cdk/core/lib/cfn-resource.ts index 9777268a7b328..ebd6e0825fdb8 100644 --- a/packages/@aws-cdk/core/lib/cfn-resource.ts +++ b/packages/@aws-cdk/core/lib/cfn-resource.ts @@ -15,6 +15,7 @@ import { TagManager } from './tag-manager'; import { Tokenization } from './token'; import { capitalizePropertyNames, ignoreEmpty, PostResolveToken } from './util'; import { FeatureFlags } from './feature-flags'; +import { ResolutionTypeHint } from './type-hints'; export interface CfnResourceProps { /** @@ -172,8 +173,8 @@ export class CfnResource extends CfnRefElement { * in case there is no generated attribute. * @param attributeName The name of the attribute. */ - public getAtt(attributeName: string): Reference { - return CfnReference.for(this, attributeName); + public getAtt(attributeName: string, typeHint?: ResolutionTypeHint): Reference { + return CfnReference.for(this, attributeName, undefined, typeHint); } /** diff --git a/packages/@aws-cdk/core/lib/index.ts b/packages/@aws-cdk/core/lib/index.ts index c3720c49b8a82..378017a1fdefb 100644 --- a/packages/@aws-cdk/core/lib/index.ts +++ b/packages/@aws-cdk/core/lib/index.ts @@ -3,6 +3,7 @@ export * from './tag-aspect'; export * from './token'; export * from './resolvable'; +export * from './type-hints'; export * from './lazy'; export * from './tag-manager'; export * from './string-fragments'; diff --git a/packages/@aws-cdk/core/lib/private/cfn-reference.ts b/packages/@aws-cdk/core/lib/private/cfn-reference.ts index ac91c60b1c107..fb331514ee191 100644 --- a/packages/@aws-cdk/core/lib/private/cfn-reference.ts +++ b/packages/@aws-cdk/core/lib/private/cfn-reference.ts @@ -54,7 +54,7 @@ export class CfnReference extends Reference { * Lazy.string({ produce: () => new CfnReference(...) }) * */ - public static for(target: CfnElement, attribute: string, refRender?: ReferenceRendering) { + public static for(target: CfnElement, attribute: string, refRender?: ReferenceRendering, typeHint?: ResolutionTypeHint) { return CfnReference.singletonReference(target, attribute, refRender, () => { const cfnIntrinsic = refRender === ReferenceRendering.FN_SUB ? ('${' + target.logicalId + (attribute === 'Ref' ? '' : `.${attribute}`) + '}') @@ -66,7 +66,7 @@ export class CfnReference extends Reference { : [target.logicalId, attribute], } ); - return new CfnReference(cfnIntrinsic, attribute, target); + return new CfnReference(cfnIntrinsic, attribute, target, typeHint); }); } @@ -118,9 +118,9 @@ export class CfnReference extends Reference { private readonly replacementTokens: Map; private readonly targetStack: Stack; - protected constructor(value: any, displayName: string, public readonly target: IConstruct) { + protected constructor(value: any, displayName: string, public readonly target: IConstruct, typeHint?: ResolutionTypeHint) { // prepend scope path to display name - super(value, target, displayName); + super(value, target, displayName, typeHint); this.replacementTokens = new Map(); this.targetStack = Stack.of(target); @@ -180,3 +180,4 @@ import { CfnElement } from '../cfn-element'; import { IResolvable, IResolveContext } from '../resolvable'; import { Stack } from '../stack'; import { Token } from '../token'; +import { ResolutionTypeHint } from '../type-hints'; diff --git a/packages/@aws-cdk/core/lib/private/cloudformation-lang.ts b/packages/@aws-cdk/core/lib/private/cloudformation-lang.ts index 82d2809255806..c66be435ee4f4 100644 --- a/packages/@aws-cdk/core/lib/private/cloudformation-lang.ts +++ b/packages/@aws-cdk/core/lib/private/cloudformation-lang.ts @@ -2,8 +2,9 @@ import { Lazy } from '../lazy'; import { DefaultTokenResolver, IFragmentConcatenator, IResolveContext } from '../resolvable'; import { Stack } from '../stack'; import { Token } from '../token'; +import { ResolutionTypeHint } from '../type-hints'; import { CfnUtils } from './cfn-utils-provider'; -import { INTRINSIC_KEY_PREFIX, ResolutionTypeHint, resolvedTypeHint } from './resolve'; +import { INTRINSIC_KEY_PREFIX, resolvedTypeHint } from './resolve'; /** * Routines that know how to do operations at the CloudFormation document language level @@ -216,7 +217,7 @@ function tokenAwareStringify(root: any, space: number, ctx: IResolveContext) { pushLiteral('"'); return; - case ResolutionTypeHint.LIST: + case ResolutionTypeHint.STRING_LIST: // We need this to look like: // // '{"listValue":' ++ STRINGIFY(CFN_EVAL({ Ref: MyList })) ++ '}' diff --git a/packages/@aws-cdk/core/lib/private/intrinsic.ts b/packages/@aws-cdk/core/lib/private/intrinsic.ts index 8a15e38806134..c6c8892b2c934 100644 --- a/packages/@aws-cdk/core/lib/private/intrinsic.ts +++ b/packages/@aws-cdk/core/lib/private/intrinsic.ts @@ -1,6 +1,7 @@ import { IResolvable, IResolveContext } from '../resolvable'; import { captureStackTrace } from '../stack-trace'; import { Token } from '../token'; +import { ResolutionTypeHint } from '../type-hints'; /** * Customization properties for an Intrinsic token @@ -13,6 +14,14 @@ export interface IntrinsicProps { * @default true */ readonly stackTrace?: boolean; + + /** + * + * Type that this token is expected to evaluate to + * + * @default ResolutionTypeHint.STRING + */ + readonly typeHint?: ResolutionTypeHint; } /** @@ -30,6 +39,11 @@ export class Intrinsic implements IResolvable { */ public readonly creationStack: string[]; + /** + * Type that the Intrinsic is expected to evaluate to. + */ + public readonly typeHint?: ResolutionTypeHint; + private readonly value: any; constructor(value: any, options: IntrinsicProps = {}) { @@ -39,6 +53,7 @@ export class Intrinsic implements IResolvable { this.creationStack = options.stackTrace ?? true ? captureStackTrace() : []; this.value = value; + this.typeHint = options.typeHint ?? ResolutionTypeHint.STRING; } public resolve(_context: IResolveContext) { diff --git a/packages/@aws-cdk/core/lib/private/refs.ts b/packages/@aws-cdk/core/lib/private/refs.ts index 7569907e62d87..cb208a5d47a3d 100644 --- a/packages/@aws-cdk/core/lib/private/refs.ts +++ b/packages/@aws-cdk/core/lib/private/refs.ts @@ -13,6 +13,7 @@ import { Reference } from '../reference'; import { IResolvable } from '../resolvable'; import { Stack } from '../stack'; import { Token, Tokenization } from '../token'; +import { ResolutionTypeHint } from '../type-hints'; import { CfnReference } from './cfn-reference'; import { Intrinsic } from './intrinsic'; import { findTokens } from './resolve'; @@ -192,9 +193,15 @@ function findAllReferences(root: IConstruct) { */ function createImportValue(reference: Reference): Intrinsic { const exportingStack = Stack.of(reference.target); + let importExpr; - const importExpr = exportingStack.exportValue(reference); + if (reference.typeHint === ResolutionTypeHint.STRING_LIST) { + importExpr = exportingStack.exportStringListValue(reference); + // I happen to know this returns a Fn.split() which implements Intrinsic. + return Tokenization.reverseList(importExpr) as Intrinsic; + } + importExpr = exportingStack.exportValue(reference); // I happen to know this returns a Fn.importValue() which implements Intrinsic. return Tokenization.reverseCompleteString(importExpr) as Intrinsic; } diff --git a/packages/@aws-cdk/core/lib/private/resolve.ts b/packages/@aws-cdk/core/lib/private/resolve.ts index 08c2e48fea811..eb91d529dbb9c 100644 --- a/packages/@aws-cdk/core/lib/private/resolve.ts +++ b/packages/@aws-cdk/core/lib/private/resolve.ts @@ -1,6 +1,7 @@ import { IConstruct } from 'constructs'; import { DefaultTokenResolver, IPostProcessor, IResolvable, IResolveContext, ITokenResolver, ResolveChangeContextOptions, StringConcat } from '../resolvable'; import { TokenizedStringFragments } from '../string-fragments'; +import { ResolutionTypeHint } from '../type-hints'; import { containsListTokenElement, TokenString, unresolved } from './encoding'; import { TokenMap } from './token-map'; @@ -28,15 +29,6 @@ const RESOLUTION_TYPEHINT_SYM = Symbol.for('@aws-cdk/core.resolvedTypeHint'); */ export const INTRINSIC_KEY_PREFIX = '$IntrinsicKey$'; -/** - * Type hints for resolved values - */ -export enum ResolutionTypeHint { - STRING = 'string', - NUMBER = 'number', - LIST = 'list', -} - /** * Options to the resolve() operation * @@ -190,7 +182,7 @@ export function resolve(obj: any, options: IResolveOptions): any { if (Array.isArray(obj)) { if (containsListTokenElement(obj)) { - return tagResolvedValue(options.resolver.resolveList(obj, makeContext()[0]), ResolutionTypeHint.LIST); + return tagResolvedValue(options.resolver.resolveList(obj, makeContext()[0]), ResolutionTypeHint.STRING_LIST); } const arr = obj diff --git a/packages/@aws-cdk/core/lib/reference.ts b/packages/@aws-cdk/core/lib/reference.ts index ae72c3fed9d82..d19fc696ac5c4 100644 --- a/packages/@aws-cdk/core/lib/reference.ts +++ b/packages/@aws-cdk/core/lib/reference.ts @@ -1,5 +1,6 @@ import { IConstruct } from 'constructs'; import { Intrinsic } from './private/intrinsic'; +import { ResolutionTypeHint } from './type-hints'; const REFERENCE_SYMBOL = Symbol.for('@aws-cdk/core.Reference'); @@ -19,8 +20,8 @@ export abstract class Reference extends Intrinsic { public readonly target: IConstruct; public readonly displayName: string; - constructor(value: any, target: IConstruct, displayName?: string) { - super(value); + constructor(value: any, target: IConstruct, displayName?: string, typeHint?: ResolutionTypeHint) { + super(value, { typeHint }); Object.defineProperty(this, REFERENCE_SYMBOL, { value: true }); this.target = target; this.displayName = displayName || 'Reference'; diff --git a/packages/@aws-cdk/core/lib/resolvable.ts b/packages/@aws-cdk/core/lib/resolvable.ts index 466683a3169df..92630d818eee2 100644 --- a/packages/@aws-cdk/core/lib/resolvable.ts +++ b/packages/@aws-cdk/core/lib/resolvable.ts @@ -2,6 +2,7 @@ import { IConstruct } from 'constructs'; import { TokenString } from './private/encoding'; import { TokenMap } from './private/token-map'; import { TokenizedStringFragments } from './string-fragments'; +import { ResolutionTypeHint } from './type-hints'; /** * Current resolution context for tokens @@ -60,6 +61,11 @@ export interface IResolvable { */ readonly creationStack: string[]; + /** + * The type that this token will likely resolve to. + */ + readonly typeHint?: ResolutionTypeHint; + /** * Produce the Token's value at resolution time */ diff --git a/packages/@aws-cdk/core/lib/stack.ts b/packages/@aws-cdk/core/lib/stack.ts index 1e4cc5b7f0d5c..e1e71d06ebdf3 100644 --- a/packages/@aws-cdk/core/lib/stack.ts +++ b/packages/@aws-cdk/core/lib/stack.ts @@ -31,6 +31,7 @@ const VALID_STACK_NAME_REGEX = /^[A-Za-z][A-Za-z0-9-]*$/; const MAX_RESOURCES = 500; +const STRING_LIST_REFERENCE_DELIMITER = '||'; export interface StackProps { /** * A description of the stack. @@ -970,7 +971,7 @@ export class Stack extends Construct implements ITaggable { /** - * Create a CloudFormation Export for a value + * Create a CloudFormation Export for a string value * * Returns a string representing the corresponding `Fn.importValue()` * expression for this Export. You can control the name for the export by @@ -1015,7 +1016,7 @@ export class Stack extends Construct implements ITaggable { * - Don't forget to remove the `exportValue()` call as well. * - Deploy again (this time only the `producerStack` will be changed -- the bucket will be deleted). */ - public exportValue(exportedValue: any, options: ExportValueOptions = {}) { + public exportValue(exportedValue: any, options: ExportValueOptions = {}): string { if (options.name) { new CfnOutput(this, `Export${options.name}`, { value: exportedValue, @@ -1024,36 +1025,77 @@ export class Stack extends Construct implements ITaggable { return Fn.importValue(options.name); } - const resolvable = Tokenization.reverse(exportedValue); - if (!resolvable || !Reference.isReference(resolvable)) { - throw new Error('exportValue: either supply \'name\' or make sure to export a resource attribute (like \'bucket.bucketName\')'); + const { exportName, exportsScope, id, exportable } = this.resolveExportedValue(exportedValue); + + const output = exportsScope.node.tryFindChild(id) as CfnOutput; + if (!output) { + new CfnOutput(exportsScope, id, { + value: Token.asString(exportable), + exportName, + }); } - // "teleport" the value here, in case it comes from a nested stack. This will also - // ensure the value is from our own scope. - const exportable = getExportable(this, resolvable); + const importValue = Fn.importValue(exportName); - // 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(this); + if (Array.isArray(importValue)) { + throw new Error('Attempted to export a list value from `exportValue()`: use `exportStringListValue()` instead'); + } - // Ensure a singleton CfnOutput for this value - const resolved = this.resolve(exportable); - const id = 'Output' + JSON.stringify(resolved); - const exportName = generateExportName(exportsScope, id); + return importValue; + } - if (Token.isUnresolved(exportName)) { - throw new Error(`unresolved token in generated export name: ${JSON.stringify(this.resolve(exportName))}`); + /** + * Create a CloudFormation Export for a string list value + * + * Returns a string list representing the corresponding `Fn.importValue()` + * expression for this Export. The export expression is automatically wrapped with an + * `Fn::Join` and the import value with an `Fn::Split`, since CloudFormation can only + * export strings. You can control the name for the export by passing the `name` option. + * + * If you don't supply a value for `name`, the value you're exporting must be + * a Resource attribute (for example: `bucket.bucketName`) and it will be + * given the same name as the automatic cross-stack reference that would be created + * if you used the attribute in another Stack. + * + * One of the uses for this method is to *remove* the relationship between + * two Stacks established by automatic cross-stack references. It will + * temporarily ensure that the CloudFormation Export still exists while you + * remove the reference from the consuming stack. After that, you can remove + * the resource and the manual export. + * + * # See `exportValue` for an example of this process. + */ + public exportStringListValue(exportedValue: any, options: ExportValueOptions = {}): string[] { + if (options.name) { + new CfnOutput(this, `Export${options.name}`, { + value: Fn.join(STRING_LIST_REFERENCE_DELIMITER, exportedValue), + exportName: options.name, + }); + return Fn.split(STRING_LIST_REFERENCE_DELIMITER, Fn.importValue(options.name)); } + const { exportName, exportsScope, id, exportable } = this.resolveExportedValue(exportedValue); + const output = exportsScope.node.tryFindChild(id) as CfnOutput; if (!output) { - new CfnOutput(exportsScope, id, { value: Token.asString(exportable), exportName }); + new CfnOutput(exportsScope, id, { + // this is a list so export an Fn::Join expression + // and import an Fn::Split expression, + // since CloudFormation Outputs can only be strings + // (string lists are invalid) + value: Fn.join(STRING_LIST_REFERENCE_DELIMITER, Token.asList(exportable)), + exportName, + }); } - return Fn.importValue(exportName); + // we don't use `Fn.importListValue()` since this array is a CFN attribute, and we don't know how long this attribute is + const importValue = Fn.split(STRING_LIST_REFERENCE_DELIMITER, Fn.importValue(exportName)); + + if (!Array.isArray(importValue)) { + throw new Error('Attempted to export a string value from `exportStringListValue()`: use `exportValue()` instead'); + } + + return importValue; } /** @@ -1281,6 +1323,39 @@ export class Stack extends Construct implements ITaggable { return makeStackName(ids); } + private resolveExportedValue(exportedValue: any): ResolvedExport { + const resolvable = Tokenization.reverse(exportedValue); + if (!resolvable || !Reference.isReference(resolvable)) { + throw new Error('exportValue: either supply \'name\' or make sure to export a resource attribute (like \'bucket.bucketName\')'); + } + + // "teleport" the value here, in case it comes from a nested stack. This will also + // ensure the value is from our own scope. + const exportable = getExportable(this, resolvable); + + // 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(this); + + // Ensure a singleton CfnOutput for this value + const resolved = this.resolve(exportable); + 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(this.resolve(exportName))}`); + } + + return { + exportable, + exportsScope, + id, + exportName, + }; + } + /** * Indicates whether the stack requires bundling or not */ @@ -1467,6 +1542,12 @@ interface StackDependency { reasons: string[]; } +interface ResolvedExport { + exportable: Reference; + exportsScope: Construct; + id: string; + exportName: string; +} /** * Options for the `stack.exportValue()` method diff --git a/packages/@aws-cdk/core/lib/type-hints.ts b/packages/@aws-cdk/core/lib/type-hints.ts new file mode 100644 index 0000000000000..32d4e2ab3d61b --- /dev/null +++ b/packages/@aws-cdk/core/lib/type-hints.ts @@ -0,0 +1,17 @@ +/** + * Type hints for resolved values + */ +export enum ResolutionTypeHint { + /** + * This value is expected to resolve to a String + */ + STRING = 'string', + /** + * This value is expected to resolve to a Number + */ + NUMBER = 'number', + /** + * This value is expected to resolve to a String List + */ + STRING_LIST = 'string-list', +} diff --git a/packages/@aws-cdk/core/test/output.test.ts b/packages/@aws-cdk/core/test/output.test.ts index 32616e6b432de..8d28024a510a1 100644 --- a/packages/@aws-cdk/core/test/output.test.ts +++ b/packages/@aws-cdk/core/test/output.test.ts @@ -104,6 +104,13 @@ describe('output', () => { }).toThrow(/Add an exportName to the CfnOutput/); }); + test('throw if Output is passed a string list', () => { + // WHEN + expect(() => { + new CfnOutput(stack, 'SomeOutput', { value: ['listValue'] as any }); + }).toThrow(/CloudFormation output was given a string list instead of a string/); + }); + test('Verify maximum length of export name', () => { const output = new CfnOutput(stack, 'SomeOutput', { value: 'x', exportName: 'x'.repeat(260) }); const errors = output.node.validate(); diff --git a/packages/@aws-cdk/core/test/stack.test.ts b/packages/@aws-cdk/core/test/stack.test.ts index 5cb56c989ec21..8a7da057a9def 100644 --- a/packages/@aws-cdk/core/test/stack.test.ts +++ b/packages/@aws-cdk/core/test/stack.test.ts @@ -7,7 +7,7 @@ import { CfnResource, Lazy, ScopedAws, Stack, validateString, Tags, LegacyStackSynthesizer, DefaultStackSynthesizer, NestedStack, - Aws, + Aws, Fn, ResolutionTypeHint, PermissionsBoundary, PERMISSIONS_BOUNDARY_CONTEXT_KEY, Aspects, @@ -466,6 +466,190 @@ describe('stack', () => { }); }); + test('cross-stack references of lists returned from Fn::GetAtt work', () => { + // GIVEN + const app = new App(); + const stack1 = new Stack(app, 'Stack1'); + const exportResource = new CfnResource(stack1, 'exportedResource', { + type: 'BLA', + }); + const stack2 = new Stack(app, 'Stack2'); + // L1s represent attribute names with `attr${attributeName}` + (exportResource as any).attrList = ['magic-attr-value']; + + // WHEN - used in another stack + new CfnResource(stack2, 'SomeResource', { + type: 'BLA', + properties: { + Prop: exportResource.getAtt('List', ResolutionTypeHint.STRING_LIST), + }, + }); + + const assembly = app.synth(); + const template1 = assembly.getStackByName(stack1.stackName).template; + const template2 = assembly.getStackByName(stack2.stackName).template; + + // THEN + expect(template1).toMatchObject({ + Outputs: { + ExportsOutputFnGetAttexportedResourceList0EA3E0D9: { + Value: { + 'Fn::Join': [ + '||', { + 'Fn::GetAtt': [ + 'exportedResource', + 'List', + ], + }, + ], + }, + Export: { Name: 'Stack1:ExportsOutputFnGetAttexportedResourceList0EA3E0D9' }, + }, + }, + }); + + expect(template2).toMatchObject({ + Resources: { + SomeResource: { + Type: 'BLA', + Properties: { + Prop: { + 'Fn::Split': [ + '||', + { + 'Fn::ImportValue': 'Stack1:ExportsOutputFnGetAttexportedResourceList0EA3E0D9', + }, + ], + }, + }, + }, + }, + }); + }); + + test('cross-stack references of lists returned from Fn::GetAtt can be used with CFN intrinsics', () => { + // GIVEN + const app = new App(); + const stack1 = new Stack(app, 'Stack1'); + const exportResource = new CfnResource(stack1, 'exportedResource', { + type: 'BLA', + }); + const stack2 = new Stack(app, 'Stack2'); + // L1s represent attribute names with `attr${attributeName}` + (exportResource as any).attrList = ['magic-attr-value']; + + // WHEN - used in another stack + new CfnResource(stack2, 'SomeResource', { + type: 'BLA', + properties: { + Prop: Fn.select(3, exportResource.getAtt('List', ResolutionTypeHint.STRING_LIST) as any), + }, + }); + + const assembly = app.synth(); + const template1 = assembly.getStackByName(stack1.stackName).template; + const template2 = assembly.getStackByName(stack2.stackName).template; + + // THEN + expect(template1).toMatchObject({ + Outputs: { + ExportsOutputFnGetAttexportedResourceList0EA3E0D9: { + Value: { + 'Fn::Join': [ + '||', { + 'Fn::GetAtt': [ + 'exportedResource', + 'List', + ], + }, + ], + }, + Export: { Name: 'Stack1:ExportsOutputFnGetAttexportedResourceList0EA3E0D9' }, + }, + }, + }); + + expect(template2).toMatchObject({ + Resources: { + SomeResource: { + Type: 'BLA', + Properties: { + Prop: { + 'Fn::Select': [ + 3, + { + 'Fn::Split': [ + '||', + { + 'Fn::ImportValue': 'Stack1:ExportsOutputFnGetAttexportedResourceList0EA3E0D9', + }, + ], + }, + ], + }, + }, + }, + }, + }); + }); + + test('cross-stack references of lists returned from Fn::Ref work', () => { + // GIVEN + const app = new App(); + const stack1 = new Stack(app, 'Stack1'); + const param = new CfnParameter(stack1, 'magicParameter', { + default: 'BLAT,BLAH', + type: 'List', + }); + const stack2 = new Stack(app, 'Stack2'); + + // WHEN - used in another stack + new CfnResource(stack2, 'SomeResource', { + type: 'BLA', + properties: { + Prop: param.value, + }, + }); + + const assembly = app.synth(); + const template1 = assembly.getStackByName(stack1.stackName).template; + const template2 = assembly.getStackByName(stack2.stackName).template; + + // THEN + expect(template1).toMatchObject({ + Outputs: { + ExportsOutputRefmagicParameter4CC6F7BE: { + Value: { + 'Fn::Join': [ + '||', { + Ref: 'magicParameter', + }, + ], + }, + Export: { Name: 'Stack1:ExportsOutputRefmagicParameter4CC6F7BE' }, + }, + }, + }); + + expect(template2).toMatchObject({ + Resources: { + SomeResource: { + Type: 'BLA', + Properties: { + Prop: { + 'Fn::Split': [ + '||', + { + 'Fn::ImportValue': 'Stack1:ExportsOutputRefmagicParameter4CC6F7BE', + }, + ], + }, + }, + }, + }, + }); + }); + test('cross-region stack references, crossRegionReferences=true', () => { // GIVEN const app = new App(); @@ -953,6 +1137,29 @@ describe('stack', () => { expect(templateA).toEqual(templateM); }); + test('automatic cross-stack references and manual list exports look the same', () => { + // GIVEN: automatic + const appA = new App({ context: { '@aws-cdk/core:stackRelativeExports': true } }); + const producerA = new Stack(appA, 'Producer'); + const consumerA = new Stack(appA, 'Consumer'); + const resourceA = new CfnResource(producerA, 'Resource', { type: 'AWS::Resource' }); + (resourceA as any).attrAtt = ['Foo', 'Bar']; + new CfnOutput(consumerA, 'SomeOutput', { value: `${resourceA.getAtt('Att', ResolutionTypeHint.STRING_LIST)}` }); + + // GIVEN: manual + const appM = new App(); + const producerM = new Stack(appM, 'Producer'); + const resourceM = new CfnResource(producerM, 'Resource', { type: 'AWS::Resource' }); + (resourceM as any).attrAtt = ['Foo', 'Bar']; + producerM.exportStringListValue(resourceM.getAtt('Att', ResolutionTypeHint.STRING_LIST)); + + // THEN - producers are the same + const templateA = appA.synth().getStackByName(producerA.stackName).template; + const templateM = appM.synth().getStackByName(producerM.stackName).template; + + expect(templateA).toEqual(templateM); + }); + test('throw error if overrideLogicalId is used and logicalId is locked', () => { // GIVEN: manual const appM = new App(); @@ -1032,6 +1239,15 @@ describe('stack', () => { }).toThrow(/or make sure to export a resource attribute/); }); + test('manual list exports require a name if not supplying a resource attribute', () => { + const app = new App(); + const stack = new Stack(app, 'Stack'); + + expect(() => { + stack.exportStringListValue(['someValue']); + }).toThrow(/or make sure to export a resource attribute/); + }); + test('manual exports can also just be used to create an export of anything', () => { const app = new App(); const stack = new Stack(app, 'Stack'); @@ -1041,6 +1257,37 @@ describe('stack', () => { expect(stack.resolve(importV)).toEqual({ 'Fn::ImportValue': 'MyExport' }); }); + test('manual list exports can also just be used to create an export of anything', () => { + const app = new App(); + const stack = new Stack(app, 'Stack'); + + const importV = stack.exportStringListValue(['someValue', 'anotherValue'], { name: 'MyExport' }); + + expect(stack.resolve(importV)).toEqual( + { + 'Fn::Split': [ + '||', + { + 'Fn::ImportValue': 'MyExport', + }, + ], + }, + ); + + const template = app.synth().getStackByName(stack.stackName).template; + + expect(template).toMatchObject({ + Outputs: { + ExportMyExport: { + Value: 'someValue||anotherValue', + Export: { + Name: 'MyExport', + }, + }, + }, + }); + }); + test('CfnSynthesisError is ignored when preparing cross references', () => { // GIVEN const app = new App(); diff --git a/tools/@aws-cdk/cfn2ts/lib/genspec.ts b/tools/@aws-cdk/cfn2ts/lib/genspec.ts index ed0da0655e7b2..a05daef0f54d9 100644 --- a/tools/@aws-cdk/cfn2ts/lib/genspec.ts +++ b/tools/@aws-cdk/cfn2ts/lib/genspec.ts @@ -212,7 +212,13 @@ export function attributeDefinition(attributeName: string, spec: schema.Attribut attrType = TOKEN_NAME.fqn; } - const constructorArguments = `this.getAtt('${attributeName}')`; + let typeHint = 'STRING'; + if (attrType === 'number') { + typeHint = 'NUMBER'; + } else if (attrType === 'string[]') { + typeHint = 'STRING_LIST'; + } + const constructorArguments = `this.getAtt('${attributeName}', cdk.ResolutionTypeHint.${typeHint})`; return new Attribute(propertyName, attrType, constructorArguments); }