From c75d245b229f8c88f890fc51597319d217896ddd Mon Sep 17 00:00:00 2001 From: Elad Ben-Israel Date: Wed, 19 Jun 2019 23:11:28 +0300 Subject: [PATCH] feat(core): environment-agnostic cloud assemblies (#2922) Formalize the simple use case for synthesizing cloudformation templates that are not pre-associated with a specific AWS account/region. When a CDK stack is defined without an explicit `env` configuration, or if `env.account` and/or `env.region` are set to `Aws.accountId`/`Aws.region`, the stack is said to be "environment-agnostic". This means that when a template is synthesized, we will use the CloudFormation intrinsics `AWS::AccountId` and `AWS::Region` instead of concrete account/region. The cloud assembly manifest for such stacks will indicate `aws://unknown-account/unknown region` to represent that this stack is environment-agnostic, and tooling should rely on external configuration to determine the deployment environment. Environment-agnostic stacks have limitations. For example, their resources cannot be referenced across accounts or regions, and context providers such as SSM, AZs, VPC and Route53 lookup cannot be used since they won't know which environment to query. To faciliate the env-agnostic use case at the AWS Construct Library level, this change removes any dependency on concrete environment specification. Namely: - The AZ provider, which is now accessible through `stack.availabilityZones` will fall back to use `[ Fn::GetAZs[0], Fn::GetAZs[1] ]` in case the stack is env-agnostic. This is a safe fallback since all AWS regions have at least two AZs. - The use of the SSM context provider by the EC2 and ECS libraries to retrieve AMIs was replaced by deploy-time resolution of SSM parameters, so no fallback is required. See list of breaking API changes below. Added a few static methods to `ssm.StringParameter` to make it easier to reference values directly: * `valueFromLookup` will read a value during synthesis using the SSM context provider. * `valueForStringParameter` will return a deploy-time resolved value. * `valueForSecureStringParameter` will return a deploy-time resolved secure string value. Fixes #2866 BREAKING CHANGE: `ContextProvider` is no longer designed to be extended. Use `ContextProvider.getValue` and `ContextProvider.getKey` as utilities. * **core:** `Context.getSsmParameter` has been removed. Use `ssm.StringParameter.valueFromLookup` * **core:** `Context.getAvailabilityZones` has been removed. Use `stack.availabilityZones` * **core:** `Context.getDefaultAccount` and `getDefaultRegion` have been removed an no longer available. * **route52:** `HostedZoneProvider` has been removed. Use `HostedZone.fromLookup`. * **ec2:** `VpcNetworkProvider` has been removed. Use `Vpc.fromLookup`. * **ec2:** `ec2.MachineImage` will now resolve AMIs from SSM during deployment. * **ecs:** `ecs.EcsOptimizedAmi` will now resolve AMis from SSM during deployment. --- .../test/integ.restapi.expected.json | 1 + .../aws-apigateway/test/integ.restapi.ts | 1 + .../test/integ.amazonlinux2.expected.json | 10 +- ...g.asg-w-classic-loadbalancer.expected.json | 10 +- .../test/integ.asg-w-elbv2.expected.json | 10 +- .../test/integ.custom-scaling.expected.json | 10 +- .../test/integ.external-role.expected.json | 10 +- .../test/integ.spot-instances.expected.json | 10 +- .../test/test.auto-scaling-group.ts | 200 +++++++------ .../test/test.scheduled-action.ts | 1 - .../aws-codebuild/test/test.codebuild.ts | 3 - .../integ.deployment-group.expected.json | 10 +- packages/@aws-cdk/aws-ec2/lib/index.ts | 2 +- .../@aws-cdk/aws-ec2/lib/machine-image.ts | 12 +- packages/@aws-cdk/aws-ec2/lib/vpc-lookup.ts | 41 +++ .../aws-ec2/lib/vpc-network-provider.ts | 85 ------ packages/@aws-cdk/aws-ec2/lib/vpc.ts | 88 ++++-- packages/@aws-cdk/aws-ec2/package.json | 2 + .../test/integ.import-default-vpc.lit.ts | 12 +- .../aws-ec2/test/integ.share-vpcs.lit.ts | 5 + .../aws-ec2/test/test.vpc-endpoint.ts | 12 - packages/@aws-cdk/aws-ec2/test/test.vpc.ts | 8 +- ...integ.scheduled-ecs-task.lit.expected.json | 18 +- packages/@aws-cdk/aws-ecs/lib/cluster.ts | 8 +- packages/@aws-cdk/aws-ecs/package.json | 6 +- .../test/ec2/integ.lb-awsvpc-nw.expected.json | 12 +- .../test/ec2/integ.lb-bridge-nw.expected.json | 12 +- .../test/ec2/integ.sd-awsvpc-nw.expected.json | 10 +- .../test/ec2/integ.sd-bridge-nw.expected.json | 10 +- .../aws-ecs/test/ec2/test.ec2-service.ts | 3 - .../test/fargate/test.fargate-service.ts | 3 - .../@aws-cdk/aws-ecs/test/test.ecs-cluster.ts | 11 +- .../aws-eks/test/integ.eks-cluster.lit.ts | 9 +- .../@aws-cdk/aws-eks/test/test.cluster.ts | 2 - .../test/alb/test.load-balancer.ts | 3 - .../test/nlb/test.load-balancer.ts | 2 - .../integ.event-ec2-task.lit.expected.json | 8 +- .../aws-lambda/test/test.vpc-lambda.ts | 1 - .../@aws-cdk/aws-rds/test/test.instance.ts | 3 - .../aws-rds/test/test.secret-rotation.ts | 8 - .../aws-route53/lib/hosted-zone-provider.ts | 50 ---- .../@aws-cdk/aws-route53/lib/hosted-zone.ts | 35 ++- .../test/test.hosted-zone-provider.ts | 15 +- packages/@aws-cdk/aws-ssm/lib/parameter.ts | 56 +++- packages/@aws-cdk/aws-ssm/package.json | 2 + .../@aws-cdk/aws-ssm/test/test.parameter.ts | 76 ++++- packages/@aws-cdk/aws-ssm/test/test.ssm.ts | 8 - .../test/ecs-tasks.test.ts | 1 - .../test/integ.ec2-task.expected.json | 85 +----- .../test/integ.ec2-task.ts | 7 +- .../test/integ.fargate-task.expected.json | 67 +---- .../test/integ.fargate-task.ts | 7 +- .../test/sagemaker-training-job.test.ts | 1 - packages/@aws-cdk/cdk/lib/construct.ts | 8 + packages/@aws-cdk/cdk/lib/context-provider.ts | 124 ++++++++ packages/@aws-cdk/cdk/lib/context.ts | 282 ------------------ packages/@aws-cdk/cdk/lib/environment.ts | 20 +- packages/@aws-cdk/cdk/lib/index.ts | 2 +- packages/@aws-cdk/cdk/lib/stack.ts | 115 +++++-- packages/@aws-cdk/cdk/test/test.construct.ts | 9 +- packages/@aws-cdk/cdk/test/test.context.ts | 132 ++++---- .../@aws-cdk/cdk/test/test.environment.ts | 78 ++--- packages/@aws-cdk/cdk/test/test.stack.ts | 30 +- .../cx-api/lib/context/availability-zones.ts | 11 +- packages/@aws-cdk/cx-api/lib/cxapi.ts | 8 +- packages/@aws-cdk/cx-api/lib/environment.ts | 3 + packages/aws-cdk/lib/api/cxapp/exec.ts | 18 +- packages/aws-cdk/lib/api/deploy-stack.ts | 4 +- packages/aws-cdk/lib/api/deployment-target.ts | 2 +- packages/aws-cdk/lib/api/toolkit-info.ts | 10 +- packages/aws-cdk/lib/api/util/sdk.ts | 57 +++- .../test/__snapshots__/synth.test.js.snap | 213 ++++--------- tools/cdk-integ-tools/bin/cdk-integ-assert.ts | 10 +- tools/cdk-integ-tools/bin/cdk-integ.ts | 13 +- tools/cdk-integ-tools/lib/integ-helpers.ts | 41 +-- 75 files changed, 1098 insertions(+), 1174 deletions(-) create mode 100644 packages/@aws-cdk/aws-ec2/lib/vpc-lookup.ts delete mode 100644 packages/@aws-cdk/aws-ec2/lib/vpc-network-provider.ts delete mode 100644 packages/@aws-cdk/aws-ssm/test/test.ssm.ts create mode 100644 packages/@aws-cdk/cdk/lib/context-provider.ts delete mode 100644 packages/@aws-cdk/cdk/lib/context.ts diff --git a/packages/@aws-cdk/aws-apigateway/test/integ.restapi.expected.json b/packages/@aws-cdk/aws-apigateway/test/integ.restapi.expected.json index e5298cbfc2bb2..0519a38ee286b 100644 --- a/packages/@aws-cdk/aws-apigateway/test/integ.restapi.expected.json +++ b/packages/@aws-cdk/aws-apigateway/test/integ.restapi.expected.json @@ -644,6 +644,7 @@ } } ], + "Throttle": { "RateLimit": 5 }, "Description": "Free tier monthly usage plan", "Quota": { "Limit": 10000, diff --git a/packages/@aws-cdk/aws-apigateway/test/integ.restapi.ts b/packages/@aws-cdk/aws-apigateway/test/integ.restapi.ts index 689374700d7fd..91815bffd75d5 100644 --- a/packages/@aws-cdk/aws-apigateway/test/integ.restapi.ts +++ b/packages/@aws-cdk/aws-apigateway/test/integ.restapi.ts @@ -58,6 +58,7 @@ class Test extends cdk.Stack { name: 'Basic', apiKey: key, description: 'Free tier monthly usage plan', + throttle: { rateLimit: 5 }, quota: { limit: 10000, period: apigateway.Period.Month diff --git a/packages/@aws-cdk/aws-autoscaling/test/integ.amazonlinux2.expected.json b/packages/@aws-cdk/aws-autoscaling/test/integ.amazonlinux2.expected.json index 8245dbc572179..36919e49f9646 100644 --- a/packages/@aws-cdk/aws-autoscaling/test/integ.amazonlinux2.expected.json +++ b/packages/@aws-cdk/aws-autoscaling/test/integ.amazonlinux2.expected.json @@ -1,4 +1,10 @@ { + "Parameters": { + "SsmParameterValueawsserviceamiamazonlinuxlatestamzn2amihvmx8664gp2C96584B6F00A464EAD1953AFF4B05118Parameter": { + "Type": "AWS::SSM::Parameter::Value", + "Default": "/aws/service/ami-amazon-linux-latest/amzn2-ami-hvm-x86_64-gp2" + } + }, "Resources": { "VPCB9E5F0B4": { "Type": "AWS::EC2::VPC", @@ -406,7 +412,9 @@ "FleetLaunchConfig59F79D36": { "Type": "AWS::AutoScaling::LaunchConfiguration", "Properties": { - "ImageId": "ami-1234", + "ImageId": { + "Ref": "SsmParameterValueawsserviceamiamazonlinuxlatestamzn2amihvmx8664gp2C96584B6F00A464EAD1953AFF4B05118Parameter" + }, "InstanceType": "t2.micro", "IamInstanceProfile": { "Ref": "FleetInstanceProfileC6192A66" diff --git a/packages/@aws-cdk/aws-autoscaling/test/integ.asg-w-classic-loadbalancer.expected.json b/packages/@aws-cdk/aws-autoscaling/test/integ.asg-w-classic-loadbalancer.expected.json index c5482ded0fe5b..c8f20ad8aa6bd 100644 --- a/packages/@aws-cdk/aws-autoscaling/test/integ.asg-w-classic-loadbalancer.expected.json +++ b/packages/@aws-cdk/aws-autoscaling/test/integ.asg-w-classic-loadbalancer.expected.json @@ -1,4 +1,10 @@ { + "Parameters": { + "SsmParameterValueawsserviceamiamazonlinuxlatestamznamihvmx8664gp2C96584B6F00A464EAD1953AFF4B05118Parameter": { + "Type": "AWS::SSM::Parameter::Value", + "Default": "/aws/service/ami-amazon-linux-latest/amzn-ami-hvm-x86_64-gp2" + } + }, "Resources": { "VPCB9E5F0B4": { "Type": "AWS::EC2::VPC", @@ -580,7 +586,9 @@ "FleetLaunchConfig59F79D36": { "Type": "AWS::AutoScaling::LaunchConfiguration", "Properties": { - "ImageId": "ami-1234", + "ImageId": { + "Ref": "SsmParameterValueawsserviceamiamazonlinuxlatestamznamihvmx8664gp2C96584B6F00A464EAD1953AFF4B05118Parameter" + }, "InstanceType": "t2.micro", "IamInstanceProfile": { "Ref": "FleetInstanceProfileC6192A66" diff --git a/packages/@aws-cdk/aws-autoscaling/test/integ.asg-w-elbv2.expected.json b/packages/@aws-cdk/aws-autoscaling/test/integ.asg-w-elbv2.expected.json index a79170c34a022..cf18393b3d0cf 100644 --- a/packages/@aws-cdk/aws-autoscaling/test/integ.asg-w-elbv2.expected.json +++ b/packages/@aws-cdk/aws-autoscaling/test/integ.asg-w-elbv2.expected.json @@ -1,4 +1,10 @@ { + "Parameters": { + "SsmParameterValueawsserviceamiamazonlinuxlatestamznamihvmx8664gp2C96584B6F00A464EAD1953AFF4B05118Parameter": { + "Type": "AWS::SSM::Parameter::Value", + "Default": "/aws/service/ami-amazon-linux-latest/amzn-ami-hvm-x86_64-gp2" + } + }, "Resources": { "VPCB9E5F0B4": { "Type": "AWS::EC2::VPC", @@ -427,7 +433,9 @@ "FleetLaunchConfig59F79D36": { "Type": "AWS::AutoScaling::LaunchConfiguration", "Properties": { - "ImageId": "ami-1234", + "ImageId": { + "Ref": "SsmParameterValueawsserviceamiamazonlinuxlatestamznamihvmx8664gp2C96584B6F00A464EAD1953AFF4B05118Parameter" + }, "InstanceType": "t2.micro", "IamInstanceProfile": { "Ref": "FleetInstanceProfileC6192A66" diff --git a/packages/@aws-cdk/aws-autoscaling/test/integ.custom-scaling.expected.json b/packages/@aws-cdk/aws-autoscaling/test/integ.custom-scaling.expected.json index f9c511ea4d59c..af6f77223f4cb 100644 --- a/packages/@aws-cdk/aws-autoscaling/test/integ.custom-scaling.expected.json +++ b/packages/@aws-cdk/aws-autoscaling/test/integ.custom-scaling.expected.json @@ -1,4 +1,10 @@ { + "Parameters": { + "SsmParameterValueawsserviceamiamazonlinuxlatestamzn2amihvmx8664gp2C96584B6F00A464EAD1953AFF4B05118Parameter": { + "Type": "AWS::SSM::Parameter::Value", + "Default": "/aws/service/ami-amazon-linux-latest/amzn2-ami-hvm-x86_64-gp2" + } + }, "Resources": { "VPCB9E5F0B4": { "Type": "AWS::EC2::VPC", @@ -406,7 +412,9 @@ "FleetLaunchConfig59F79D36": { "Type": "AWS::AutoScaling::LaunchConfiguration", "Properties": { - "ImageId": "ami-1234", + "ImageId": { + "Ref": "SsmParameterValueawsserviceamiamazonlinuxlatestamzn2amihvmx8664gp2C96584B6F00A464EAD1953AFF4B05118Parameter" + }, "InstanceType": "t2.micro", "IamInstanceProfile": { "Ref": "FleetInstanceProfileC6192A66" diff --git a/packages/@aws-cdk/aws-autoscaling/test/integ.external-role.expected.json b/packages/@aws-cdk/aws-autoscaling/test/integ.external-role.expected.json index 2870f0a110040..44e040031454c 100644 --- a/packages/@aws-cdk/aws-autoscaling/test/integ.external-role.expected.json +++ b/packages/@aws-cdk/aws-autoscaling/test/integ.external-role.expected.json @@ -1,4 +1,10 @@ { + "Parameters": { + "SsmParameterValueawsserviceamiamazonlinuxlatestamznamihvmx8664gp2C96584B6F00A464EAD1953AFF4B05118Parameter": { + "Type": "AWS::SSM::Parameter::Value", + "Default": "/aws/service/ami-amazon-linux-latest/amzn-ami-hvm-x86_64-gp2" + } + }, "Resources": { "VPCB9E5F0B4": { "Type": "AWS::EC2::VPC", @@ -559,7 +565,9 @@ "ASGLaunchConfigC00AF12B": { "Type": "AWS::AutoScaling::LaunchConfiguration", "Properties": { - "ImageId": "ami-1234", + "ImageId": { + "Ref": "SsmParameterValueawsserviceamiamazonlinuxlatestamznamihvmx8664gp2C96584B6F00A464EAD1953AFF4B05118Parameter" + }, "InstanceType": "t2.micro", "IamInstanceProfile": { "Ref": "ASGInstanceProfile0A2834D7" diff --git a/packages/@aws-cdk/aws-autoscaling/test/integ.spot-instances.expected.json b/packages/@aws-cdk/aws-autoscaling/test/integ.spot-instances.expected.json index d3f188cd6266b..8dd9fdaf2d9da 100644 --- a/packages/@aws-cdk/aws-autoscaling/test/integ.spot-instances.expected.json +++ b/packages/@aws-cdk/aws-autoscaling/test/integ.spot-instances.expected.json @@ -1,4 +1,10 @@ { + "Parameters": { + "SsmParameterValueawsserviceamiamazonlinuxlatestamzn2amihvmx8664gp2C96584B6F00A464EAD1953AFF4B05118Parameter": { + "Type": "AWS::SSM::Parameter::Value", + "Default": "/aws/service/ami-amazon-linux-latest/amzn2-ami-hvm-x86_64-gp2" + } + }, "Resources": { "VPCB9E5F0B4": { "Type": "AWS::EC2::VPC", @@ -406,7 +412,9 @@ "FleetLaunchConfig59F79D36": { "Type": "AWS::AutoScaling::LaunchConfiguration", "Properties": { - "ImageId": "ami-1234", + "ImageId": { + "Ref": "SsmParameterValueawsserviceamiamazonlinuxlatestamzn2amihvmx8664gp2C96584B6F00A464EAD1953AFF4B05118Parameter" + }, "InstanceType": "t2.micro", "IamInstanceProfile": { "Ref": "FleetInstanceProfileC6192A66" diff --git a/packages/@aws-cdk/aws-autoscaling/test/test.auto-scaling-group.ts b/packages/@aws-cdk/aws-autoscaling/test/test.auto-scaling-group.ts index 60b44894ebeee..1327627868723 100644 --- a/packages/@aws-cdk/aws-autoscaling/test/test.auto-scaling-group.ts +++ b/packages/@aws-cdk/aws-autoscaling/test/test.auto-scaling-group.ts @@ -19,6 +19,12 @@ export = { }); expect(stack).toMatch({ + "Parameters": { + "SsmParameterValueawsserviceamiamazonlinuxlatestamznamihvmx8664gp2C96584B6F00A464EAD1953AFF4B05118Parameter": { + "Type": "AWS::SSM::Parameter::Value", + "Default": "/aws/service/ami-amazon-linux-latest/amzn-ami-hvm-x86_64-gp2" + } + }, "Resources": { "MyFleetInstanceSecurityGroup774E8234": { "Type": "AWS::EC2::SecurityGroup", @@ -47,78 +53,78 @@ export = { "Properties": { "AssumeRolePolicyDocument": { "Statement": [ - { - "Action": "sts:AssumeRole", - "Effect": "Allow", - "Principal": { - "Service": "ec2.amazonaws.com" - } + { + "Action": "sts:AssumeRole", + "Effect": "Allow", + "Principal": { + "Service": "ec2.amazonaws.com" + } + } + ], + "Version": "2012-10-17" } - ], - "Version": "2012-10-17" } - } }, "MyFleetInstanceProfile70A58496": { - "Type": "AWS::IAM::InstanceProfile", - "Properties": { - "Roles": [ - { - "Ref": "MyFleetInstanceRole25A84AB8" + "Type": "AWS::IAM::InstanceProfile", + "Properties": { + "Roles": [ + { + "Ref": "MyFleetInstanceRole25A84AB8" + } + ] } - ] - } }, "MyFleetLaunchConfig5D7F9801": { - "Type": "AWS::AutoScaling::LaunchConfiguration", - "Properties": { - "IamInstanceProfile": { - "Ref": "MyFleetInstanceProfile70A58496" + "Type": "AWS::AutoScaling::LaunchConfiguration", + "Properties": { + "IamInstanceProfile": { + "Ref": "MyFleetInstanceProfile70A58496" + }, + "ImageId": { "Ref": "SsmParameterValueawsserviceamiamazonlinuxlatestamznamihvmx8664gp2C96584B6F00A464EAD1953AFF4B05118Parameter" }, + "InstanceType": "m4.micro", + "SecurityGroups": [ + { + "Fn::GetAtt": [ + "MyFleetInstanceSecurityGroup774E8234", + "GroupId" + ] + } + ], + "UserData": { + "Fn::Base64": "#!/bin/bash\n" + } }, - "ImageId": "dummy", - "InstanceType": "m4.micro", - "SecurityGroups": [ - { - "Fn::GetAtt": [ - "MyFleetInstanceSecurityGroup774E8234", - "GroupId" - ] - } - ], - "UserData": { - "Fn::Base64": "#!/bin/bash\n" - } - }, - "DependsOn": [ - "MyFleetInstanceRole25A84AB8" - ] + "DependsOn": [ + "MyFleetInstanceRole25A84AB8" + ] }, "MyFleetASG88E55886": { - "Type": "AWS::AutoScaling::AutoScalingGroup", - "UpdatePolicy": { - "AutoScalingScheduledAction": { - "IgnoreUnmodifiedGroupSizeProperties": true - } - }, - "Properties": { - "DesiredCapacity": "1", - "LaunchConfigurationName": { - "Ref": "MyFleetLaunchConfig5D7F9801" - }, - "Tags": [ - { - "Key": "Name", - "PropagateAtLaunch": true, - "Value": "MyFleet" + "Type": "AWS::AutoScaling::AutoScalingGroup", + "UpdatePolicy": { + "AutoScalingScheduledAction": { + "IgnoreUnmodifiedGroupSizeProperties": true } - ], + }, + "Properties": { + "DesiredCapacity": "1", + "LaunchConfigurationName": { + "Ref": "MyFleetLaunchConfig5D7F9801" + }, + "Tags": [ + { + "Key": "Name", + "PropagateAtLaunch": true, + "Value": "MyFleet" + } + ], - "MaxSize": "1", - "MinSize": "1", - "VPCZoneIdentifier": [ - "pri1" - ] - } + "MaxSize": "1", + "MinSize": "1", + "VPCZoneIdentifier": [ + "pri1" + ] + } } } }); @@ -127,7 +133,7 @@ export = { }, 'can set minCapacity, maxCapacity, desiredCapacity to 0'(test: Test) { - const stack = new cdk.Stack(undefined, 'MyStack', { env: { region: 'us-east-1', account: '1234' }}); + const stack = new cdk.Stack(undefined, 'MyStack', { env: { region: 'us-east-1', account: '1234' } }); const vpc = mockVpc(stack); new autoscaling.AutoScalingGroup(stack, 'MyFleet', { @@ -140,10 +146,10 @@ export = { }); expect(stack).to(haveResource("AWS::AutoScaling::AutoScalingGroup", { - MinSize: "0", - MaxSize: "0", - DesiredCapacity: "0", - } + MinSize: "0", + MaxSize: "0", + DesiredCapacity: "0", + } )); test.done(); @@ -164,10 +170,10 @@ export = { // THEN expect(stack).to(haveResource("AWS::AutoScaling::AutoScalingGroup", { - MinSize: "10", - MaxSize: "10", - DesiredCapacity: "10", - } + MinSize: "10", + MaxSize: "10", + DesiredCapacity: "10", + } )); test.done(); @@ -188,10 +194,10 @@ export = { // THEN expect(stack).to(haveResource("AWS::AutoScaling::AutoScalingGroup", { - MinSize: "1", - MaxSize: "10", - DesiredCapacity: "10", - } + MinSize: "1", + MaxSize: "10", + DesiredCapacity: "10", + } )); test.done(); @@ -212,17 +218,17 @@ export = { // THEN expect(stack).to(haveResource("AWS::AutoScaling::AutoScalingGroup", { - MinSize: "1", - MaxSize: "10", - DesiredCapacity: "10", - } + MinSize: "1", + MaxSize: "10", + DesiredCapacity: "10", + } )); test.done(); }, 'addToRolePolicy can be used to add statements to the role policy'(test: Test) { - const stack = new cdk.Stack(undefined, 'MyStack', { env: { region: 'us-east-1', account: '1234' }}); + const stack = new cdk.Stack(undefined, 'MyStack', { env: { region: 'us-east-1', account: '1234' } }); const vpc = mockVpc(stack); const fleet = new autoscaling.AutoScalingGroup(stack, 'MyFleet', { @@ -253,7 +259,7 @@ export = { 'can configure replacing update'(test: Test) { // GIVEN - const stack = new cdk.Stack(undefined, 'MyStack', { env: { region: 'us-east-1', account: '1234' }}); + const stack = new cdk.Stack(undefined, 'MyStack', { env: { region: 'us-east-1', account: '1234' } }); const vpc = mockVpc(stack); // WHEN @@ -269,12 +275,12 @@ export = { expect(stack).to(haveResourceLike("AWS::AutoScaling::AutoScalingGroup", { UpdatePolicy: { AutoScalingReplacingUpdate: { - WillReplace: true + WillReplace: true } }, CreationPolicy: { AutoScalingCreationPolicy: { - MinSuccessfulInstancesPercent: 50 + MinSuccessfulInstancesPercent: 50 } } }, ResourcePart.CompleteDefinition)); @@ -284,7 +290,7 @@ export = { 'can configure rolling update'(test: Test) { // GIVEN - const stack = new cdk.Stack(undefined, 'MyStack', { env: { region: 'us-east-1', account: '1234' }}); + const stack = new cdk.Stack(undefined, 'MyStack', { env: { region: 'us-east-1', account: '1234' } }); const vpc = mockVpc(stack); // WHEN @@ -303,10 +309,10 @@ export = { expect(stack).to(haveResourceLike("AWS::AutoScaling::AutoScalingGroup", { UpdatePolicy: { "AutoScalingRollingUpdate": { - "MinSuccessfulInstancesPercent": 50, - "WaitOnResourceSignals": true, - "PauseTime": "PT5M45S", - "SuspendProcesses": [ "HealthCheck", "ReplaceUnhealthy", "AZRebalance", "AlarmNotification", "ScheduledActions" ] + "MinSuccessfulInstancesPercent": 50, + "WaitOnResourceSignals": true, + "PauseTime": "PT5M45S", + "SuspendProcesses": ["HealthCheck", "ReplaceUnhealthy", "AZRebalance", "AlarmNotification", "ScheduledActions"] }, } }, ResourcePart.CompleteDefinition)); @@ -316,7 +322,7 @@ export = { 'can configure resource signals'(test: Test) { // GIVEN - const stack = new cdk.Stack(undefined, 'MyStack', { env: { region: 'us-east-1', account: '1234' }}); + const stack = new cdk.Stack(undefined, 'MyStack', { env: { region: 'us-east-1', account: '1234' } }); const vpc = mockVpc(stack); // WHEN @@ -343,7 +349,7 @@ export = { 'can add Security Group to Fleet'(test: Test) { // GIVEN - const stack = new cdk.Stack(undefined, 'MyStack', { env: { region: 'us-east-1', account: '1234' }}); + const stack = new cdk.Stack(undefined, 'MyStack', { env: { region: 'us-east-1', account: '1234' } }); const vpc = mockVpc(stack); // WHEN @@ -369,7 +375,7 @@ export = { 'can set tags'(test: Test) { // GIVEN const stack = getTestStack(); - // new cdk.Stack(undefined, 'MyStack', { env: { region: 'us-east-1', account: '1234' }}); + // new cdk.Stack(undefined, 'MyStack', { env: { region: 'us-east-1', account: '1234' }}); const vpc = mockVpc(stack); // WHEN @@ -451,8 +457,8 @@ export = { // THEN expect(stack).to(haveResource("AWS::AutoScaling::LaunchConfiguration", { - AssociatePublicIpAddress: true, - } + AssociatePublicIpAddress: true, + } )); test.done(); }, @@ -495,8 +501,8 @@ export = { // THEN expect(stack).to(haveResource("AWS::AutoScaling::LaunchConfiguration", { - AssociatePublicIpAddress: false, - } + AssociatePublicIpAddress: false, + } )); test.done(); }, @@ -546,7 +552,7 @@ export = { // THEN test.same(asg.role, importedRole); expect(stack).to(haveResource('AWS::IAM::InstanceProfile', { - "Roles": [ "HelloDude" ] + "Roles": ["HelloDude"] })); test.done(); } @@ -555,9 +561,9 @@ export = { function mockVpc(stack: cdk.Stack) { return ec2.Vpc.fromVpcAttributes(stack, 'MyVpc', { vpcId: 'my-vpc', - availabilityZones: [ 'az1' ], - publicSubnetIds: [ 'pub1' ], - privateSubnetIds: [ 'pri1' ], + availabilityZones: ['az1'], + publicSubnetIds: ['pub1'], + privateSubnetIds: ['pri1'], isolatedSubnetIds: [], }); } diff --git a/packages/@aws-cdk/aws-autoscaling/test/test.scheduled-action.ts b/packages/@aws-cdk/aws-autoscaling/test/test.scheduled-action.ts index 49b576145f214..76bb56827422c 100644 --- a/packages/@aws-cdk/aws-autoscaling/test/test.scheduled-action.ts +++ b/packages/@aws-cdk/aws-autoscaling/test/test.scheduled-action.ts @@ -76,7 +76,6 @@ export = { VPCZoneIdentifier: [ { Ref: "VPCPrivateSubnet1Subnet8BCA10E0" }, { Ref: "VPCPrivateSubnet2SubnetCFCDAA7A" }, - { Ref: "VPCPrivateSubnet3Subnet3EDCD457" } ] }, UpdatePolicy: { diff --git a/packages/@aws-cdk/aws-codebuild/test/test.codebuild.ts b/packages/@aws-cdk/aws-codebuild/test/test.codebuild.ts index ac25c92d3279a..a9ff68a1cc876 100644 --- a/packages/@aws-cdk/aws-codebuild/test/test.codebuild.ts +++ b/packages/@aws-cdk/aws-codebuild/test/test.codebuild.ts @@ -657,9 +657,6 @@ export = { }, { "Ref": "MyVPCPrivateSubnet2SubnetA420D3F0" - }, - { - "Ref": "MyVPCPrivateSubnet3SubnetE1B8B1B4" } ], "VpcId": { diff --git a/packages/@aws-cdk/aws-codedeploy/test/server/integ.deployment-group.expected.json b/packages/@aws-cdk/aws-codedeploy/test/server/integ.deployment-group.expected.json index 807f10d06b551..883ce81ac3f31 100644 --- a/packages/@aws-cdk/aws-codedeploy/test/server/integ.deployment-group.expected.json +++ b/packages/@aws-cdk/aws-codedeploy/test/server/integ.deployment-group.expected.json @@ -1,4 +1,10 @@ { + "Parameters": { + "SsmParameterValueawsserviceamiamazonlinuxlatestamznamihvmx8664gp2C96584B6F00A464EAD1953AFF4B05118Parameter": { + "Type": "AWS::SSM::Parameter::Value", + "Default": "/aws/service/ami-amazon-linux-latest/amzn-ami-hvm-x86_64-gp2" + } + }, "Resources": { "VPCB9E5F0B4": { "Type": "AWS::EC2::VPC", @@ -616,7 +622,7 @@ "ASGLaunchConfigC00AF12B": { "Type": "AWS::AutoScaling::LaunchConfiguration", "Properties": { - "ImageId": "ami-1234", + "ImageId": { "Ref": "SsmParameterValueawsserviceamiamazonlinuxlatestamznamihvmx8664gp2C96584B6F00A464EAD1953AFF4B05118Parameter" }, "InstanceType": "m5.large", "IamInstanceProfile": { "Ref": "ASGInstanceProfile0A2834D7" @@ -854,4 +860,4 @@ } } } -} +} \ No newline at end of file diff --git a/packages/@aws-cdk/aws-ec2/lib/index.ts b/packages/@aws-cdk/aws-ec2/lib/index.ts index d086304362bec..4bbfedd98c42e 100644 --- a/packages/@aws-cdk/aws-ec2/lib/index.ts +++ b/packages/@aws-cdk/aws-ec2/lib/index.ts @@ -4,7 +4,7 @@ export * from './machine-image'; export * from './security-group'; export * from './security-group-rule'; export * from './vpc'; -export * from './vpc-network-provider'; +export * from './vpc-lookup'; export * from './vpn'; export * from './vpc-endpoint'; diff --git a/packages/@aws-cdk/aws-ec2/lib/machine-image.ts b/packages/@aws-cdk/aws-ec2/lib/machine-image.ts index 3fe42fbd6abfb..de885d2197e7a 100644 --- a/packages/@aws-cdk/aws-ec2/lib/machine-image.ts +++ b/packages/@aws-cdk/aws-ec2/lib/machine-image.ts @@ -1,4 +1,5 @@ -import { Construct, Context, Stack, Token } from '@aws-cdk/cdk'; +import ssm = require('@aws-cdk/aws-ssm'); +import { Construct, Stack, Token } from '@aws-cdk/cdk'; /** * Interface for classes that can select an appropriate machine image to use @@ -25,7 +26,8 @@ export class WindowsImage implements IMachineImageSource { * Return the image to use in the given context */ public getImage(scope: Construct): MachineImage { - const ami = Context.getSsmParameter(scope, this.imageParameterName(this.version)); + const parameterName = this.imageParameterName(this.version); + const ami = ssm.StringParameter.valueForStringParameter(scope, parameterName); return new MachineImage(ami, new WindowsOS()); } @@ -102,7 +104,7 @@ export class AmazonLinuxImage implements IMachineImageSource { ].filter(x => x !== undefined); // Get rid of undefineds const parameterName = '/aws/service/ami-amazon-linux-latest/' + parts.join('-'); - const ami = Context.getSsmParameter(scope, parameterName); + const ami = ssm.StringParameter.valueForStringParameter(scope, parameterName); return new MachineImage(ami, new LinuxOS()); } } @@ -180,9 +182,9 @@ export class GenericLinuxImage implements IMachineImageSource { } public getImage(scope: Construct): MachineImage { - let region = Stack.of(scope).region; + const region = Stack.of(scope).region; if (Token.isUnresolved(region)) { - region = Context.getDefaultRegion(scope); + throw new Error(`Unable to determine AMI from AMI map since stack is region-agnostic`); } const ami = region !== 'test-region' ? this.amiMap[region] : 'ami-12345'; diff --git a/packages/@aws-cdk/aws-ec2/lib/vpc-lookup.ts b/packages/@aws-cdk/aws-ec2/lib/vpc-lookup.ts new file mode 100644 index 0000000000000..573ff75538e2e --- /dev/null +++ b/packages/@aws-cdk/aws-ec2/lib/vpc-lookup.ts @@ -0,0 +1,41 @@ +/** + * Properties for looking up an existing VPC. + * + * The combination of properties must specify filter down to exactly one + * non-default VPC, otherwise an error is raised. + */ +export interface VpcLookupOptions { + /** + * The ID of the VPC + * + * If given, will import exactly this VPC. + * + * @default Don't filter on vpcId + */ + readonly vpcId?: string; + + /** + * The name of the VPC + * + * If given, will import the VPC with this name. + * + * @default Don't filter on vpcName + */ + readonly vpcName?: string; + + /** + * Tags on the VPC + * + * The VPC must have all of these tags + * + * @default Don't filter on tags + */ + readonly tags?: {[key: string]: string}; + + /** + * Whether to match the default VPC + * + * @default Don't care whether we return the default VPC + */ + readonly isDefault?: boolean; +} diff --git a/packages/@aws-cdk/aws-ec2/lib/vpc-network-provider.ts b/packages/@aws-cdk/aws-ec2/lib/vpc-network-provider.ts deleted file mode 100644 index 574059f78887f..0000000000000 --- a/packages/@aws-cdk/aws-ec2/lib/vpc-network-provider.ts +++ /dev/null @@ -1,85 +0,0 @@ -import cdk = require('@aws-cdk/cdk'); -import cxapi = require('@aws-cdk/cx-api'); -import { VpcAttributes } from './vpc'; - -/** - * Properties for looking up an existing VPC. - * - * The combination of properties must specify filter down to exactly one - * non-default VPC, otherwise an error is raised. - */ -export interface VpcLookupOptions { - /** - * The ID of the VPC - * - * If given, will import exactly this VPC. - * - * @default Don't filter on vpcId - */ - readonly vpcId?: string; - - /** - * The name of the VPC - * - * If given, will import the VPC with this name. - * - * @default Don't filter on vpcName - */ - readonly vpcName?: string; - - /** - * Tags on the VPC - * - * The VPC must have all of these tags - * - * @default Don't filter on tags - */ - readonly tags?: {[key: string]: string}; - - /** - * Whether to match the default VPC - * - * @default Don't care whether we return the default VPC - */ - readonly isDefault?: boolean; -} - -/** - * Context provider to discover and import existing VPCs - */ -export class VpcNetworkProvider { - private provider: cdk.ContextProvider; - - constructor(context: cdk.Construct, options: VpcLookupOptions) { - const filter: {[key: string]: string} = options.tags || {}; - - // We give special treatment to some tags - if (options.vpcId) { filter['vpc-id'] = options.vpcId; } - if (options.vpcName) { filter['tag:Name'] = options.vpcName; } - if (options.isDefault !== undefined) { - filter.isDefault = options.isDefault ? 'true' : 'false'; - } - - this.provider = new cdk.ContextProvider(context, cxapi.VPC_PROVIDER, { filter } as cxapi.VpcContextQuery); - } - - /** - * Return the VPC import props matching the filter - */ - public get vpcProps(): VpcAttributes { - const ret: cxapi.VpcContextResponse = this.provider.getValue(DUMMY_VPC_PROPS); - return ret; - } -} - -/** - * There are returned when the provider has not supplied props yet - * - * It's only used for testing and on the first run-through. - */ -const DUMMY_VPC_PROPS: cxapi.VpcContextResponse = { - availabilityZones: ['dummy-1a', 'dummy-1b'], - vpcId: 'vpc-12345', - publicSubnetIds: ['s-12345', 's-67890'], - privateSubnetIds: ['p-12345', 'p-67890'], -}; diff --git a/packages/@aws-cdk/aws-ec2/lib/vpc.ts b/packages/@aws-cdk/aws-ec2/lib/vpc.ts index b8bf5f146ce68..50006a700a30b 100644 --- a/packages/@aws-cdk/aws-ec2/lib/vpc.ts +++ b/packages/@aws-cdk/aws-ec2/lib/vpc.ts @@ -1,12 +1,22 @@ -import cdk = require('@aws-cdk/cdk'); -import { ConcreteDependable, Construct, IConstruct, IDependable, IResource, Resource, Stack } from '@aws-cdk/cdk'; +import { + ConcreteDependable, + Construct, + ContextProvider, + DependableTrait, + IConstruct, + IDependable, + IResource, + Resource, + Stack, + Tag } from '@aws-cdk/cdk'; +import cxapi = require('@aws-cdk/cx-api'); import { CfnEIP, CfnInternetGateway, CfnNatGateway, CfnRoute, CfnVPNGateway, CfnVPNGatewayRoutePropagation } from './ec2.generated'; import { CfnRouteTable, CfnSubnet, CfnSubnetRouteTableAssociation, CfnVPC, CfnVPCGatewayAttachment } from './ec2.generated'; import { NetworkBuilder } from './network-util'; import { defaultSubnetName, ImportSubnetGroup, subnetId, subnetName } from './util'; import { GatewayVpcEndpoint, GatewayVpcEndpointAwsService, GatewayVpcEndpointOptions } from './vpc-endpoint'; import { InterfaceVpcEndpoint, InterfaceVpcEndpointOptions } from './vpc-endpoint'; -import { VpcLookupOptions, VpcNetworkProvider } from './vpc-network-provider'; +import { VpcLookupOptions } from './vpc-lookup'; import { VpnConnection, VpnConnectionOptions, VpnConnectionType } from './vpn'; const VPC_SUBNET_SYMBOL = Symbol.for('@aws-cdk/aws-ec2.VpcSubnet'); @@ -666,15 +676,30 @@ export class Vpc extends VpcBase { /** * Import an exported VPC */ - public static fromVpcAttributes(scope: cdk.Construct, id: string, attrs: VpcAttributes): IVpc { + public static fromVpcAttributes(scope: Construct, id: string, attrs: VpcAttributes): IVpc { return new ImportedVpc(scope, id, attrs); } /** * Import an existing VPC from by querying the AWS environment this stack is deployed to. */ - public static fromLookup(scope: cdk.Construct, id: string, options: VpcLookupOptions): IVpc { - return Vpc.fromVpcAttributes(scope, id, new VpcNetworkProvider(scope, options).vpcProps); + public static fromLookup(scope: Construct, id: string, options: VpcLookupOptions): IVpc { + const filter: {[key: string]: string} = options.tags || {}; + + // We give special treatment to some tags + if (options.vpcId) { filter['vpc-id'] = options.vpcId; } + if (options.vpcName) { filter['tag:Name'] = options.vpcName; } + if (options.isDefault !== undefined) { + filter.isDefault = options.isDefault ? 'true' : 'false'; + } + + const attributes = ContextProvider.getValue(scope, { + provider: cxapi.VPC_PROVIDER, + props: { filter } as cxapi.VpcContextQuery, + dummyValue: DUMMY_VPC_PROPS + }); + + return this.fromVpcAttributes(scope, id, attributes); } /** @@ -758,9 +783,11 @@ export class Vpc extends VpcBase { * Network routing for the public subnets will be configured to allow outbound access directly via an Internet Gateway. * Network routing for the private subnets will be configured to allow outbound access via a set of resilient NAT Gateways (one per AZ). */ - constructor(scope: cdk.Construct, id: string, props: VpcProps = {}) { + constructor(scope: Construct, id: string, props: VpcProps = {}) { super(scope, id); + const stack = Stack.of(this); + // Can't have enabledDnsHostnames without enableDnsSupport if (props.enableDnsHostnames && !props.enableDnsSupport) { throw new Error('To use DNS Hostnames, DNS Support must be enabled, however, it was explicitly disabled.'); @@ -787,9 +814,9 @@ export class Vpc extends VpcBase { this.vpcDefaultSecurityGroup = this.resource.attrDefaultSecurityGroup; this.vpcIpv6CidrBlocks = this.resource.attrIpv6CidrBlocks; - this.node.applyAspect(new cdk.Tag(NAME_TAG, this.node.path)); + this.node.applyAspect(new Tag(NAME_TAG, this.node.path)); - this.availabilityZones = cdk.Context.getAvailabilityZones(this); + this.availabilityZones = stack.availabilityZones; const maxAZs = props.maxAZs !== undefined ? props.maxAZs : 3; this.availabilityZones = this.availabilityZones.slice(0, maxAZs); @@ -877,7 +904,6 @@ export class Vpc extends VpcBase { } } } - /** * Adds a new gateway endpoint to this VPC */ @@ -999,8 +1025,8 @@ export class Vpc extends VpcBase { // These values will be used to recover the config upon provider import const includeResourceTypes = [CfnSubnet.cfnResourceTypeName]; - subnet.node.applyAspect(new cdk.Tag(SUBNETNAME_TAG, subnetConfig.name, {includeResourceTypes})); - subnet.node.applyAspect(new cdk.Tag(SUBNETTYPE_TAG, subnetTypeTagValue(subnetConfig.subnetType), {includeResourceTypes})); + subnet.node.applyAspect(new Tag(SUBNETNAME_TAG, subnetConfig.name, {includeResourceTypes})); + subnet.node.applyAspect(new Tag(SUBNETTYPE_TAG, subnetTypeTagValue(subnetConfig.subnetType), {includeResourceTypes})); }); } } @@ -1049,13 +1075,13 @@ export interface SubnetProps { * * @resource AWS::EC2::Subnet */ -export class Subnet extends cdk.Resource implements ISubnet { +export class Subnet extends Resource implements ISubnet { public static isVpcSubnet(x: any): x is Subnet { return VPC_SUBNET_SYMBOL in x; } - public static fromSubnetAttributes(scope: cdk.Construct, id: string, attrs: SubnetAttributes): ISubnet { + public static fromSubnetAttributes(scope: Construct, id: string, attrs: SubnetAttributes): ISubnet { return new ImportedSubnet(scope, id, attrs); } @@ -1092,7 +1118,7 @@ export class Subnet extends cdk.Resource implements ISubnet { /** * Parts of this VPC subnet */ - public readonly dependencyElements: cdk.IDependable[] = []; + public readonly dependencyElements: IDependable[] = []; /** * The routeTableId attached to this subnet. @@ -1101,12 +1127,12 @@ export class Subnet extends cdk.Resource implements ISubnet { private readonly internetDependencies = new ConcreteDependable(); - constructor(scope: cdk.Construct, id: string, props: SubnetProps) { + constructor(scope: Construct, id: string, props: SubnetProps) { super(scope, id); Object.defineProperty(this, VPC_SUBNET_SYMBOL, { value: true }); - this.node.applyAspect(new cdk.Tag(NAME_TAG, this.node.path)); + this.node.applyAspect(new Tag(NAME_TAG, this.node.path)); this.availabilityZone = props.availabilityZone; const subnet = new CfnSubnet(this, 'Subnet', { @@ -1144,7 +1170,7 @@ export class Subnet extends cdk.Resource implements ISubnet { * @param gatewayId the logical ID (ref) of the gateway attached to your VPC * @param gatewayAttachment the gateway attachment construct to be added as a dependency */ - public addDefaultInternetRoute(gatewayId: string, gatewayAttachment: cdk.IDependable) { + public addDefaultInternetRoute(gatewayId: string, gatewayAttachment: IDependable) { const route = new CfnRoute(this, `DefaultRoute`, { routeTableId: this.routeTableId!, destinationCidrBlock: '0.0.0.0/0', @@ -1188,7 +1214,7 @@ export class PublicSubnet extends Subnet implements IPublicSubnet { return new ImportedSubnet(scope, id, attrs); } - constructor(scope: cdk.Construct, id: string, props: PublicSubnetProps) { + constructor(scope: Construct, id: string, props: PublicSubnetProps) { super(scope, id, props); } @@ -1227,7 +1253,7 @@ export class PrivateSubnet extends Subnet implements IPrivateSubnet { return new ImportedSubnet(scope, id, attrs); } - constructor(scope: cdk.Construct, id: string, props: PrivateSubnetProps) { + constructor(scope: Construct, id: string, props: PrivateSubnetProps) { super(scope, id, props); } } @@ -1244,7 +1270,7 @@ class ImportedVpc extends VpcBase { public readonly availabilityZones: string[]; public readonly vpnGatewayId?: string; - constructor(scope: cdk.Construct, id: string, props: VpcAttributes) { + constructor(scope: Construct, id: string, props: VpcAttributes) { super(scope, id); this.vpcId = props.vpcId; @@ -1299,11 +1325,11 @@ class CompositeDependable implements IDependable { constructor() { const self = this; - cdk.DependableTrait.implement(this, { + DependableTrait.implement(this, { get dependencyRoots() { const ret = []; for (const dep of self.dependables) { - ret.push(...cdk.DependableTrait.get(dep).dependencyRoots); + ret.push(...DependableTrait.get(dep).dependencyRoots); } return ret; } @@ -1330,8 +1356,8 @@ function notUndefined(x: T | undefined): x is T { return x !== undefined; } -class ImportedSubnet extends cdk.Resource implements ISubnet, IPublicSubnet, IPrivateSubnet { - public readonly internetConnectivityEstablished: cdk.IDependable = new cdk.ConcreteDependable(); +class ImportedSubnet extends Resource implements ISubnet, IPublicSubnet, IPrivateSubnet { + public readonly internetConnectivityEstablished: IDependable = new ConcreteDependable(); public readonly availabilityZone: string; public readonly subnetId: string; public readonly routeTableId?: string = undefined; @@ -1343,3 +1369,15 @@ class ImportedSubnet extends cdk.Resource implements ISubnet, IPublicSubnet, IPr this.subnetId = attrs.subnetId; } } + +/** + * There are returned when the provider has not supplied props yet + * + * It's only used for testing and on the first run-through. + */ +const DUMMY_VPC_PROPS: cxapi.VpcContextResponse = { + availabilityZones: ['dummy-1a', 'dummy-1b'], + vpcId: 'vpc-12345', + publicSubnetIds: ['s-12345', 's-67890'], + privateSubnetIds: ['p-12345', 'p-67890'], +}; diff --git a/packages/@aws-cdk/aws-ec2/package.json b/packages/@aws-cdk/aws-ec2/package.json index b9472c2654e77..b3095ad095b97 100644 --- a/packages/@aws-cdk/aws-ec2/package.json +++ b/packages/@aws-cdk/aws-ec2/package.json @@ -71,6 +71,7 @@ }, "dependencies": { "@aws-cdk/aws-cloudwatch": "^0.35.0", + "@aws-cdk/aws-ssm": "^0.35.0", "@aws-cdk/aws-iam": "^0.35.0", "@aws-cdk/cdk": "^0.35.0", "@aws-cdk/cx-api": "^0.35.0" @@ -78,6 +79,7 @@ "homepage": "https://github.com/awslabs/aws-cdk", "peerDependencies": { "@aws-cdk/aws-cloudwatch": "^0.35.0", + "@aws-cdk/aws-ssm": "^0.35.0", "@aws-cdk/aws-iam": "^0.35.0", "@aws-cdk/cdk": "^0.35.0", "@aws-cdk/cx-api": "^0.35.0" diff --git a/packages/@aws-cdk/aws-ec2/test/integ.import-default-vpc.lit.ts b/packages/@aws-cdk/aws-ec2/test/integ.import-default-vpc.lit.ts index 0bb6d9edd4193..c47ea57c8b839 100644 --- a/packages/@aws-cdk/aws-ec2/test/integ.import-default-vpc.lit.ts +++ b/packages/@aws-cdk/aws-ec2/test/integ.import-default-vpc.lit.ts @@ -3,7 +3,17 @@ import cdk = require('@aws-cdk/cdk'); import ec2 = require("../lib"); const app = new cdk.App(); -const stack = new cdk.Stack(app, 'aws-cdk-ec2-import'); + +// we associate this stack with an explicit environment since this is required by the +// environmental context provider used in `fromLookup`. CDK_INTEG_XXX are set +// when producing the .expected file and CDK_DEFAULT_XXX is passed in through from +// the CLI in actual deployment. +const env = { + account: process.env.CDK_INTEG_ACCOUNT || process.env.CDK_DEFAULT_ACCOUNT, + region: process.env.CDK_INTEG_REGION || process.env.CDK_DEFAULT_REGION +}; + +const stack = new cdk.Stack(app, 'aws-cdk-ec2-import', { env }); /// !show const vpc = ec2.Vpc.fromLookup(stack, 'VPC', { diff --git a/packages/@aws-cdk/aws-ec2/test/integ.share-vpcs.lit.ts b/packages/@aws-cdk/aws-ec2/test/integ.share-vpcs.lit.ts index af632fd9ffd4a..8931bdb19e874 100644 --- a/packages/@aws-cdk/aws-ec2/test/integ.share-vpcs.lit.ts +++ b/packages/@aws-cdk/aws-ec2/test/integ.share-vpcs.lit.ts @@ -11,6 +11,11 @@ interface ConstructThatTakesAVpcProps { class ConstructThatTakesAVpc extends cdk.Construct { constructor(scope: cdk.Construct, id: string, _props: ConstructThatTakesAVpcProps) { super(scope, id); + + // new ec2.CfnInstance(this, 'Instance', { + // subnetId: props.vpc.privateSubnets[0].subnetId, + // imageId: new ec2.AmazonLinuxImage().getImage(this).imageId, + // }); } } diff --git a/packages/@aws-cdk/aws-ec2/test/test.vpc-endpoint.ts b/packages/@aws-cdk/aws-ec2/test/test.vpc-endpoint.ts index 69fd0670a8977..a30369a980163 100644 --- a/packages/@aws-cdk/aws-ec2/test/test.vpc-endpoint.ts +++ b/packages/@aws-cdk/aws-ec2/test/test.vpc-endpoint.ts @@ -44,9 +44,6 @@ export = { { Ref: 'VpcNetworkPrivateSubnet2RouteTableE97B328B' }, - { - Ref: 'VpcNetworkPrivateSubnet3RouteTableE0C661A2' - } ], VpcEndpointType: 'Gateway' })); @@ -99,18 +96,12 @@ export = { { Ref: 'VpcNetworkPublicSubnet2RouteTableE5F348DF' }, - { - Ref: 'VpcNetworkPublicSubnet3RouteTable36E30B07' - }, { Ref: 'VpcNetworkPrivateSubnet1RouteTableCD085FF1' }, { Ref: 'VpcNetworkPrivateSubnet2RouteTableE97B328B' }, - { - Ref: 'VpcNetworkPrivateSubnet3RouteTableE0C661A2' - } ], VpcEndpointType: 'Gateway' })); @@ -289,9 +280,6 @@ export = { { Ref: 'VpcNetworkPrivateSubnet2Subnet5E4189D6' }, - { - Ref: 'VpcNetworkPrivateSubnet3Subnet5D16E0FB' - } ], VpcEndpointType: 'Interface' })); diff --git a/packages/@aws-cdk/aws-ec2/test/test.vpc.ts b/packages/@aws-cdk/aws-ec2/test/test.vpc.ts index 4600d5721f0d3..e0e19f4246443 100644 --- a/packages/@aws-cdk/aws-ec2/test/test.vpc.ts +++ b/packages/@aws-cdk/aws-ec2/test/test.vpc.ts @@ -1,5 +1,5 @@ import { countResources, expect, haveResource, haveResourceLike, isSuperObject } from '@aws-cdk/assert'; -import { Construct, Context, Stack, Tag } from '@aws-cdk/cdk'; +import { Construct, Stack, Tag } from '@aws-cdk/cdk'; import { Test } from 'nodeunit'; import { CfnVPC, DefaultInstanceTenancy, IVpc, SubnetType, Vpc } from '../lib'; import { exportVpc } from './export-helper'; @@ -63,7 +63,7 @@ export = { "contains the correct number of subnets"(test: Test) { const stack = getTestStack(); const vpc = new Vpc(stack, 'TheVPC'); - const zones = Context.getAvailabilityZones(stack).length; + const zones = stack.availabilityZones.length; test.equal(vpc.publicSubnets.length, zones); test.equal(vpc.privateSubnets.length, zones); test.deepEqual(stack.resolve(vpc.vpcId), { Ref: 'TheVPC92636AB0' }); @@ -109,7 +109,7 @@ export = { "with no subnets defined, the VPC should have an IGW, and a NAT Gateway per AZ"(test: Test) { const stack = getTestStack(); - const zones = Context.getAvailabilityZones(stack).length; + const zones = stack.availabilityZones.length; new Vpc(stack, 'TheVPC', { }); expect(stack).to(countResources("AWS::EC2::InternetGateway", 1)); expect(stack).to(countResources("AWS::EC2::NatGateway", zones)); @@ -186,7 +186,7 @@ export = { }, "with custom subnets, the VPC should have the right number of subnets, an IGW, and a NAT Gateway per AZ"(test: Test) { const stack = getTestStack(); - const zones = Context.getAvailabilityZones(stack).length; + const zones = stack.availabilityZones.length; new Vpc(stack, 'TheVPC', { cidr: '10.0.0.0/21', subnetConfiguration: [ diff --git a/packages/@aws-cdk/aws-ecs-patterns/test/ec2/integ.scheduled-ecs-task.lit.expected.json b/packages/@aws-cdk/aws-ecs-patterns/test/ec2/integ.scheduled-ecs-task.lit.expected.json index bf0e626ef96ea..5d7c2f56c5e2c 100644 --- a/packages/@aws-cdk/aws-ecs-patterns/test/ec2/integ.scheduled-ecs-task.lit.expected.json +++ b/packages/@aws-cdk/aws-ecs-patterns/test/ec2/integ.scheduled-ecs-task.lit.expected.json @@ -288,7 +288,9 @@ "EcsClusterDefaultAutoScalingGroupLaunchConfigB7E376C1": { "Type": "AWS::AutoScaling::LaunchConfiguration", "Properties": { - "ImageId": "ami-1234", + "ImageId": { + "Ref": "SsmParameterValueawsserviceecsoptimizedamiamazonlinuxrecommendedimageidC96584B6F00A464EAD1953AFF4B05118Parameter" + }, "InstanceType": "t2.micro", "IamInstanceProfile": { "Ref": "EcsClusterDefaultAutoScalingGroupInstanceProfile2CE606B3" @@ -352,9 +354,6 @@ } } }, - "EcsClusterDefaultAutoScalingGroupLifecycleHookDrainHookTopicACD2D4A4": { - "Type": "AWS::SNS::Topic" - }, "EcsClusterDefaultAutoScalingGroupDrainECSHookFunctionServiceRole94543EDA": { "Type": "AWS::IAM::Role", "Properties": { @@ -588,6 +587,9 @@ ] } }, + "EcsClusterDefaultAutoScalingGroupLifecycleHookDrainHookTopicACD2D4A4": { + "Type": "AWS::SNS::Topic" + }, "EcsClusterDefaultAutoScalingGroupLifecycleHookDrainHookFFA63029": { "Type": "AWS::AutoScaling::LifecycleHook", "Properties": { @@ -859,5 +861,11 @@ ] } } + }, + "Parameters": { + "SsmParameterValueawsserviceecsoptimizedamiamazonlinuxrecommendedimageidC96584B6F00A464EAD1953AFF4B05118Parameter": { + "Type": "AWS::SSM::Parameter::Value", + "Default": "/aws/service/ecs/optimized-ami/amazon-linux/recommended/image_id" + } } -} +} \ No newline at end of file diff --git a/packages/@aws-cdk/aws-ecs/lib/cluster.ts b/packages/@aws-cdk/aws-ecs/lib/cluster.ts index 9d65e43a5d059..d3981b4bc9314 100644 --- a/packages/@aws-cdk/aws-ecs/lib/cluster.ts +++ b/packages/@aws-cdk/aws-ecs/lib/cluster.ts @@ -3,7 +3,8 @@ import cloudwatch = require ('@aws-cdk/aws-cloudwatch'); import ec2 = require('@aws-cdk/aws-ec2'); import iam = require('@aws-cdk/aws-iam'); import cloudmap = require('@aws-cdk/aws-servicediscovery'); -import { Construct, Context, IResource, Resource, Stack } from '@aws-cdk/cdk'; +import ssm = require('@aws-cdk/aws-ssm'); +import { Construct, IResource, Resource, Stack } from '@aws-cdk/cdk'; import { InstanceDrainHook } from './drain-hook/instance-drain-hook'; import { CfnCluster } from './ecs.generated'; @@ -264,15 +265,14 @@ export class EcsOptimizedAmi implements ec2.IMachineImageSource { + ( this.generation === ec2.AmazonLinuxGeneration.AmazonLinux2 ? "amazon-linux-2/" : "" ) + ( this.hwType === AmiHardwareType.Gpu ? "gpu/" : "" ) + ( this.hwType === AmiHardwareType.Arm ? "arm64/" : "" ) - + "recommended"; + + "recommended/image_id"; } /** * Return the correct image */ public getImage(scope: Construct): ec2.MachineImage { - const json = Context.getSsmParameter(scope, this.amiParameterName, { defaultValue: "{\"image_id\": \"\"}" }); - const ami = JSON.parse(json).image_id; + const ami = ssm.StringParameter.valueForStringParameter(scope, this.amiParameterName); return new ec2.MachineImage(ami, new ec2.LinuxOS()); } } diff --git a/packages/@aws-cdk/aws-ecs/package.json b/packages/@aws-cdk/aws-ecs/package.json index 4be72beea642f..6d3f9c61abaab 100644 --- a/packages/@aws-cdk/aws-ecs/package.json +++ b/packages/@aws-cdk/aws-ecs/package.json @@ -72,6 +72,7 @@ "proxyquire": "^2.1.0" }, "dependencies": { + "@aws-cdk/aws-ecr-assets": "^0.35.0", "@aws-cdk/aws-applicationautoscaling": "^0.35.0", "@aws-cdk/aws-autoscaling": "^0.35.0", "@aws-cdk/aws-autoscaling-hooktargets": "^0.35.0", @@ -80,7 +81,7 @@ "@aws-cdk/aws-cloudwatch": "^0.35.0", "@aws-cdk/aws-ec2": "^0.35.0", "@aws-cdk/aws-ecr": "^0.35.0", - "@aws-cdk/aws-ecr-assets": "^0.35.0", + "@aws-cdk/aws-ssm": "^0.35.0", "@aws-cdk/aws-elasticloadbalancing": "^0.35.0", "@aws-cdk/aws-elasticloadbalancingv2": "^0.35.0", "@aws-cdk/aws-iam": "^0.35.0", @@ -97,6 +98,7 @@ }, "homepage": "https://github.com/awslabs/aws-cdk", "peerDependencies": { + "@aws-cdk/aws-ecr-assets": "^0.35.0", "@aws-cdk/aws-applicationautoscaling": "^0.35.0", "@aws-cdk/aws-autoscaling": "^0.35.0", "@aws-cdk/aws-autoscaling-hooktargets": "^0.35.0", @@ -105,8 +107,8 @@ "@aws-cdk/aws-cloudwatch": "^0.35.0", "@aws-cdk/aws-ec2": "^0.35.0", "@aws-cdk/aws-ecr": "^0.35.0", - "@aws-cdk/aws-ecr-assets": "^0.35.0", "@aws-cdk/aws-elasticloadbalancing": "^0.35.0", + "@aws-cdk/aws-ssm": "^0.35.0", "@aws-cdk/aws-elasticloadbalancingv2": "^0.35.0", "@aws-cdk/aws-iam": "^0.35.0", "@aws-cdk/aws-lambda": "^0.35.0", diff --git a/packages/@aws-cdk/aws-ecs/test/ec2/integ.lb-awsvpc-nw.expected.json b/packages/@aws-cdk/aws-ecs/test/ec2/integ.lb-awsvpc-nw.expected.json index 45625b2d4a15e..e3b29d4b8aae2 100644 --- a/packages/@aws-cdk/aws-ecs/test/ec2/integ.lb-awsvpc-nw.expected.json +++ b/packages/@aws-cdk/aws-ecs/test/ec2/integ.lb-awsvpc-nw.expected.json @@ -1,4 +1,10 @@ { + "Parameters": { + "SsmParameterValueawsserviceecsoptimizedamiamazonlinuxrecommendedimageidC96584B6F00A464EAD1953AFF4B05118Parameter": { + "Type": "AWS::SSM::Parameter::Value", + "Default": "/aws/service/ecs/optimized-ami/amazon-linux/recommended/image_id" + } + }, "Resources": { "Vpc8378EB38": { "Type": "AWS::EC2::VPC", @@ -441,7 +447,9 @@ "EcsClusterDefaultAutoScalingGroupLaunchConfigB7E376C1": { "Type": "AWS::AutoScaling::LaunchConfiguration", "Properties": { - "ImageId": "ami-1234", + "ImageId": { + "Ref": "SsmParameterValueawsserviceecsoptimizedamiamazonlinuxrecommendedimageidC96584B6F00A464EAD1953AFF4B05118Parameter" + }, "InstanceType": "t2.micro", "IamInstanceProfile": { "Ref": "EcsClusterDefaultAutoScalingGroupInstanceProfile2CE606B3" @@ -1032,4 +1040,4 @@ } } } -} +} \ No newline at end of file diff --git a/packages/@aws-cdk/aws-ecs/test/ec2/integ.lb-bridge-nw.expected.json b/packages/@aws-cdk/aws-ecs/test/ec2/integ.lb-bridge-nw.expected.json index 4316141bd1952..64cd3a75c59a0 100644 --- a/packages/@aws-cdk/aws-ecs/test/ec2/integ.lb-bridge-nw.expected.json +++ b/packages/@aws-cdk/aws-ecs/test/ec2/integ.lb-bridge-nw.expected.json @@ -1,4 +1,10 @@ { + "Parameters": { + "SsmParameterValueawsserviceecsoptimizedamiamazonlinuxrecommendedimageidC96584B6F00A464EAD1953AFF4B05118Parameter": { + "Type": "AWS::SSM::Parameter::Value", + "Default": "/aws/service/ecs/optimized-ami/amazon-linux/recommended/image_id" + } + }, "Resources": { "Vpc8378EB38": { "Type": "AWS::EC2::VPC", @@ -462,7 +468,9 @@ "EcsClusterDefaultAutoScalingGroupLaunchConfigB7E376C1": { "Type": "AWS::AutoScaling::LaunchConfiguration", "Properties": { - "ImageId": "ami-1234", + "ImageId": { + "Ref": "SsmParameterValueawsserviceecsoptimizedamiamazonlinuxrecommendedimageidC96584B6F00A464EAD1953AFF4B05118Parameter" + }, "InstanceType": "t2.micro", "IamInstanceProfile": { "Ref": "EcsClusterDefaultAutoScalingGroupInstanceProfile2CE606B3" @@ -995,4 +1003,4 @@ } } } -} +} \ No newline at end of file diff --git a/packages/@aws-cdk/aws-ecs/test/ec2/integ.sd-awsvpc-nw.expected.json b/packages/@aws-cdk/aws-ecs/test/ec2/integ.sd-awsvpc-nw.expected.json index 82b202d4f6551..0793f9813976a 100644 --- a/packages/@aws-cdk/aws-ecs/test/ec2/integ.sd-awsvpc-nw.expected.json +++ b/packages/@aws-cdk/aws-ecs/test/ec2/integ.sd-awsvpc-nw.expected.json @@ -1,4 +1,10 @@ { + "Parameters": { + "SsmParameterValueawsserviceecsoptimizedamiamazonlinuxrecommendedimageidC96584B6F00A464EAD1953AFF4B05118Parameter": { + "Type": "AWS::SSM::Parameter::Value", + "Default": "/aws/service/ecs/optimized-ami/amazon-linux/recommended/image_id" + } + }, "Resources": { "Vpc8378EB38": { "Type": "AWS::EC2::VPC", @@ -441,7 +447,9 @@ "EcsClusterDefaultAutoScalingGroupLaunchConfigB7E376C1": { "Type": "AWS::AutoScaling::LaunchConfiguration", "Properties": { - "ImageId": "ami-1234", + "ImageId": { + "Ref": "SsmParameterValueawsserviceecsoptimizedamiamazonlinuxrecommendedimageidC96584B6F00A464EAD1953AFF4B05118Parameter" + }, "InstanceType": "t2.micro", "IamInstanceProfile": { "Ref": "EcsClusterDefaultAutoScalingGroupInstanceProfile2CE606B3" diff --git a/packages/@aws-cdk/aws-ecs/test/ec2/integ.sd-bridge-nw.expected.json b/packages/@aws-cdk/aws-ecs/test/ec2/integ.sd-bridge-nw.expected.json index c8770a3740c09..efd377829f8ce 100644 --- a/packages/@aws-cdk/aws-ecs/test/ec2/integ.sd-bridge-nw.expected.json +++ b/packages/@aws-cdk/aws-ecs/test/ec2/integ.sd-bridge-nw.expected.json @@ -1,4 +1,10 @@ { + "Parameters": { + "SsmParameterValueawsserviceecsoptimizedamiamazonlinuxrecommendedimageidC96584B6F00A464EAD1953AFF4B05118Parameter": { + "Type": "AWS::SSM::Parameter::Value", + "Default": "/aws/service/ecs/optimized-ami/amazon-linux/recommended/image_id" + } + }, "Resources": { "Vpc8378EB38": { "Type": "AWS::EC2::VPC", @@ -441,7 +447,9 @@ "EcsClusterDefaultAutoScalingGroupLaunchConfigB7E376C1": { "Type": "AWS::AutoScaling::LaunchConfiguration", "Properties": { - "ImageId": "ami-1234", + "ImageId": { + "Ref": "SsmParameterValueawsserviceecsoptimizedamiamazonlinuxrecommendedimageidC96584B6F00A464EAD1953AFF4B05118Parameter" + }, "InstanceType": "t2.micro", "IamInstanceProfile": { "Ref": "EcsClusterDefaultAutoScalingGroupInstanceProfile2CE606B3" diff --git a/packages/@aws-cdk/aws-ecs/test/ec2/test.ec2-service.ts b/packages/@aws-cdk/aws-ecs/test/ec2/test.ec2-service.ts index 891d3592325bc..571ca2579cd0e 100644 --- a/packages/@aws-cdk/aws-ecs/test/ec2/test.ec2-service.ts +++ b/packages/@aws-cdk/aws-ecs/test/ec2/test.ec2-service.ts @@ -300,9 +300,6 @@ export = { { Ref: "MyVpcPrivateSubnet2Subnet0040C983" }, - { - Ref: "MyVpcPrivateSubnet3Subnet772D6AD7" - } ] } } diff --git a/packages/@aws-cdk/aws-ecs/test/fargate/test.fargate-service.ts b/packages/@aws-cdk/aws-ecs/test/fargate/test.fargate-service.ts index 16950b85d80ef..d050d3b3f9987 100644 --- a/packages/@aws-cdk/aws-ecs/test/fargate/test.fargate-service.ts +++ b/packages/@aws-cdk/aws-ecs/test/fargate/test.fargate-service.ts @@ -58,9 +58,6 @@ export = { { Ref: "MyVpcPrivateSubnet2Subnet0040C983" }, - { - Ref: "MyVpcPrivateSubnet3Subnet772D6AD7" - } ] } } diff --git a/packages/@aws-cdk/aws-ecs/test/test.ecs-cluster.ts b/packages/@aws-cdk/aws-ecs/test/test.ecs-cluster.ts index a2986621549a7..40ebfd0db6377 100644 --- a/packages/@aws-cdk/aws-ecs/test/test.ecs-cluster.ts +++ b/packages/@aws-cdk/aws-ecs/test/test.ecs-cluster.ts @@ -36,7 +36,9 @@ export = { })); expect(stack).to(haveResource("AWS::AutoScaling::LaunchConfiguration", { - ImageId: "", // Should this not be the latest image ID? + ImageId: { + Ref: "SsmParameterValueawsserviceecsoptimizedamiamazonlinuxrecommendedimageidC96584B6F00A464EAD1953AFF4B05118Parameter" + }, InstanceType: "t2.micro", IamInstanceProfile: { Ref: "EcsClusterDefaultAutoScalingGroupInstanceProfile2CE606B3" @@ -86,9 +88,6 @@ export = { }, { Ref: "MyVpcPrivateSubnet2Subnet0040C983" - }, - { - Ref: "MyVpcPrivateSubnet3Subnet772D6AD7" } ] })); @@ -235,7 +234,9 @@ export = { // THEN expect(stack).to(haveResource("AWS::AutoScaling::LaunchConfiguration", { - ImageId: "" + ImageId: { + Ref: "SsmParameterValueawsserviceecsoptimizedamiamazonlinux2gpurecommendedimageidC96584B6F00A464EAD1953AFF4B05118Parameter" + } })); test.done(); diff --git a/packages/@aws-cdk/aws-eks/test/integ.eks-cluster.lit.ts b/packages/@aws-cdk/aws-eks/test/integ.eks-cluster.lit.ts index aabdb7a5ffd19..c642b878f4cbb 100644 --- a/packages/@aws-cdk/aws-eks/test/integ.eks-cluster.lit.ts +++ b/packages/@aws-cdk/aws-eks/test/integ.eks-cluster.lit.ts @@ -23,6 +23,13 @@ class EksClusterStack extends cdk.Stack { const app = new cdk.App(); -new EksClusterStack(app, 'eks-integ-test'); +// since the EKS optimized AMI is hard-coded here based on the region, +// we need to actually pass in a specific region. +new EksClusterStack(app, 'eks-integ-test', { + env: { + region: process.env.CDK_INTEG_REGION || process.env.CDK_DEFAULT_REGION, + account: process.env.CDK_INTEG_ACCOUNT || process.env.CDK_DEFAULT_ACCOUNT, + } +}); app.synth(); \ No newline at end of file diff --git a/packages/@aws-cdk/aws-eks/test/test.cluster.ts b/packages/@aws-cdk/aws-eks/test/test.cluster.ts index 5a212d571a77d..839833905e8b4 100644 --- a/packages/@aws-cdk/aws-eks/test/test.cluster.ts +++ b/packages/@aws-cdk/aws-eks/test/test.cluster.ts @@ -19,10 +19,8 @@ export = { SubnetIds: [ { Ref: "VPCPublicSubnet1SubnetB4246D30" }, { Ref: "VPCPublicSubnet2Subnet74179F39" }, - { Ref: "VPCPublicSubnet3Subnet631C5E25" }, { Ref: "VPCPrivateSubnet1Subnet8BCA10E0" }, { Ref: "VPCPrivateSubnet2SubnetCFCDAA7A" }, - { Ref: "VPCPrivateSubnet3Subnet3EDCD457" } ] } })); diff --git a/packages/@aws-cdk/aws-elasticloadbalancingv2/test/alb/test.load-balancer.ts b/packages/@aws-cdk/aws-elasticloadbalancingv2/test/alb/test.load-balancer.ts index 0b607002a2562..98b536ffd7045 100644 --- a/packages/@aws-cdk/aws-elasticloadbalancingv2/test/alb/test.load-balancer.ts +++ b/packages/@aws-cdk/aws-elasticloadbalancingv2/test/alb/test.load-balancer.ts @@ -24,7 +24,6 @@ export = { Subnets: [ { Ref: "StackPublicSubnet1Subnet0AD81D22" }, { Ref: "StackPublicSubnet2Subnet3C7D2288" }, - { Ref: "StackPublicSubnet3SubnetCC1055D9" } ], Type: "application" })); @@ -48,7 +47,6 @@ export = { DependsOn: [ 'StackPublicSubnet1DefaultRoute16154E3D', 'StackPublicSubnet2DefaultRoute0319539B', - 'StackPublicSubnet3DefaultRouteBC0DA152' ] }, ResourcePart.CompleteDefinition)); @@ -69,7 +67,6 @@ export = { Subnets: [ { Ref: "StackPrivateSubnet1Subnet47AC2BC7" }, { Ref: "StackPrivateSubnet2SubnetA2F8EDD8" }, - { Ref: "StackPrivateSubnet3Subnet28548F2E" } ], Type: "application" })); diff --git a/packages/@aws-cdk/aws-elasticloadbalancingv2/test/nlb/test.load-balancer.ts b/packages/@aws-cdk/aws-elasticloadbalancingv2/test/nlb/test.load-balancer.ts index 96da83363c6e9..b04d7198b98ce 100644 --- a/packages/@aws-cdk/aws-elasticloadbalancingv2/test/nlb/test.load-balancer.ts +++ b/packages/@aws-cdk/aws-elasticloadbalancingv2/test/nlb/test.load-balancer.ts @@ -22,7 +22,6 @@ export = { Subnets: [ { Ref: "StackPublicSubnet1Subnet0AD81D22" }, { Ref: "StackPublicSubnet2Subnet3C7D2288" }, - { Ref: "StackPublicSubnet3SubnetCC1055D9" } ], Type: "network" })); @@ -44,7 +43,6 @@ export = { Subnets: [ { Ref: "StackPrivateSubnet1Subnet47AC2BC7" }, { Ref: "StackPrivateSubnet2SubnetA2F8EDD8" }, - { Ref: "StackPrivateSubnet3Subnet28548F2E" } ], Type: "network" })); diff --git a/packages/@aws-cdk/aws-events-targets/test/ecs/integ.event-ec2-task.lit.expected.json b/packages/@aws-cdk/aws-events-targets/test/ecs/integ.event-ec2-task.lit.expected.json index 939626dcd5b44..82973ff075d5f 100644 --- a/packages/@aws-cdk/aws-events-targets/test/ecs/integ.event-ec2-task.lit.expected.json +++ b/packages/@aws-cdk/aws-events-targets/test/ecs/integ.event-ec2-task.lit.expected.json @@ -288,7 +288,9 @@ "EcsClusterDefaultAutoScalingGroupLaunchConfigB7E376C1": { "Type": "AWS::AutoScaling::LaunchConfiguration", "Properties": { - "ImageId": "ami-1234", + "ImageId": { + "Ref": "SsmParameterValueawsserviceecsoptimizedamiamazonlinuxrecommendedimageidC96584B6F00A464EAD1953AFF4B05118Parameter" + }, "InstanceType": "t2.micro", "IamInstanceProfile": { "Ref": "EcsClusterDefaultAutoScalingGroupInstanceProfile2CE606B3" @@ -1174,6 +1176,10 @@ } }, "Parameters": { + "SsmParameterValueawsserviceecsoptimizedamiamazonlinuxrecommendedimageidC96584B6F00A464EAD1953AFF4B05118Parameter": { + "Type": "AWS::SSM::Parameter::Value", + "Default": "/aws/service/ecs/optimized-ami/amazon-linux/recommended/image_id" + }, "TaskDefTheContainerAssetImageImageName92ECAC22": { "Type": "String", "Description": "ECR repository name and tag asset \"aws-ecs-integ-ecs/TaskDef/TheContainer/AssetImage\"" diff --git a/packages/@aws-cdk/aws-lambda/test/test.vpc-lambda.ts b/packages/@aws-cdk/aws-lambda/test/test.vpc-lambda.ts index f53312d02ab54..b96ef156bf1c6 100644 --- a/packages/@aws-cdk/aws-lambda/test/test.vpc-lambda.ts +++ b/packages/@aws-cdk/aws-lambda/test/test.vpc-lambda.ts @@ -37,7 +37,6 @@ export = { SubnetIds: [ {Ref: "VPCPrivateSubnet1Subnet8BCA10E0"}, {Ref: "VPCPrivateSubnet2SubnetCFCDAA7A"}, - {Ref: "VPCPrivateSubnet3Subnet3EDCD457"} ] } })); diff --git a/packages/@aws-cdk/aws-rds/test/test.instance.ts b/packages/@aws-cdk/aws-rds/test/test.instance.ts index eab88f7d721f8..c6bc1b64792c4 100644 --- a/packages/@aws-cdk/aws-rds/test/test.instance.ts +++ b/packages/@aws-cdk/aws-rds/test/test.instance.ts @@ -121,9 +121,6 @@ export = { }, { Ref: 'VPCPrivateSubnet2SubnetCFCDAA7A' - }, - { - Ref: 'VPCPrivateSubnet3Subnet3EDCD457' } ] })); diff --git a/packages/@aws-cdk/aws-rds/test/test.secret-rotation.ts b/packages/@aws-cdk/aws-rds/test/test.secret-rotation.ts index edaf88c0af0b1..ecf4e761c2b10 100644 --- a/packages/@aws-cdk/aws-rds/test/test.secret-rotation.ts +++ b/packages/@aws-cdk/aws-rds/test/test.secret-rotation.ts @@ -130,10 +130,6 @@ export = { { "Ref": "VPCPrivateSubnet2SubnetCFCDAA7A" }, - ",", - { - "Ref": "VPCPrivateSubnet3Subnet3EDCD457" - } ] ] } @@ -324,10 +320,6 @@ export = { { "Ref": "VPCPrivateSubnet2SubnetCFCDAA7A" }, - ",", - { - "Ref": "VPCPrivateSubnet3Subnet3EDCD457" - } ] ] } diff --git a/packages/@aws-cdk/aws-route53/lib/hosted-zone-provider.ts b/packages/@aws-cdk/aws-route53/lib/hosted-zone-provider.ts index 0bf6f903d8523..0106187ec492c 100644 --- a/packages/@aws-cdk/aws-route53/lib/hosted-zone-provider.ts +++ b/packages/@aws-cdk/aws-route53/lib/hosted-zone-provider.ts @@ -1,8 +1,3 @@ -import cdk = require('@aws-cdk/cdk'); -import cxapi = require('@aws-cdk/cx-api'); -import { HostedZone } from './hosted-zone'; -import { HostedZoneAttributes, IHostedZone } from './hosted-zone-ref'; - /** * Zone properties for looking up the Hosted Zone */ @@ -22,48 +17,3 @@ export interface HostedZoneProviderProps { */ readonly vpcId?: string; } - -const DEFAULT_HOSTED_ZONE: HostedZoneContextResponse = { - Id: '/hostedzone/DUMMY', - Name: 'example.com', -}; - -/** - * Context provider that will lookup the Hosted Zone ID for the given arguments - */ -export class HostedZoneProvider { - private provider: cdk.ContextProvider; - constructor(context: cdk.Construct, props: HostedZoneProviderProps) { - this.provider = new cdk.ContextProvider(context, cxapi.HOSTED_ZONE_PROVIDER, props); - } - - /** - * This method calls `findHostedZone` and returns the imported hosted zone - */ - public findAndImport(scope: cdk.Construct, id: string): IHostedZone { - return HostedZone.fromHostedZoneAttributes(scope, id, this.findHostedZone()); - } - /** - * Return the hosted zone meeting the filter - */ - public findHostedZone(): HostedZoneAttributes { - const zone = this.provider.getValue(DEFAULT_HOSTED_ZONE) as HostedZoneContextResponse; - // CDK handles the '.' at the end, so remove it here - if (zone.Name.endsWith('.')) { - zone.Name = zone.Name.substring(0, zone.Name.length - 1); - } - return { - hostedZoneId: zone.Id, - zoneName: zone.Name, - }; - } -} - -/** - * A mirror of the definition in cxapi, but can use the capital letters - * since it doesn't need to be published via JSII. - */ -interface HostedZoneContextResponse { - Id: string; - Name: string; -} \ No newline at end of file diff --git a/packages/@aws-cdk/aws-route53/lib/hosted-zone.ts b/packages/@aws-cdk/aws-route53/lib/hosted-zone.ts index 1f86cfcff00fa..87d2f346b103e 100644 --- a/packages/@aws-cdk/aws-route53/lib/hosted-zone.ts +++ b/packages/@aws-cdk/aws-route53/lib/hosted-zone.ts @@ -1,5 +1,7 @@ import ec2 = require('@aws-cdk/aws-ec2'); -import { Construct, Lazy, Resource } from '@aws-cdk/cdk'; +import { Construct, ContextProvider, Lazy, Resource } from '@aws-cdk/cdk'; +import cxapi = require('@aws-cdk/cx-api'); +import { HostedZoneProviderProps } from './hosted-zone-provider'; import { HostedZoneAttributes, IHostedZone } from './hosted-zone-ref'; import { CaaAmazonRecord, ZoneDelegationRecord } from './record-set'; import { CfnHostedZone } from './route53.generated'; @@ -67,6 +69,37 @@ export class HostedZone extends Resource implements IHostedZone { return new Import(scope, id); } + /** + * Lookup a hosted zone in the current account/region based on query parameters. + */ + public static fromLookup(scope: Construct, id: string, query: HostedZoneProviderProps): IHostedZone { + const DEFAULT_HOSTED_ZONE: HostedZoneContextResponse = { + Id: '/hostedzone/DUMMY', + Name: 'example.com', + }; + + interface HostedZoneContextResponse { + Id: string; + Name: string; + } + + const response: HostedZoneContextResponse = ContextProvider.getValue(scope, { + provider: cxapi.HOSTED_ZONE_PROVIDER, + dummyValue: DEFAULT_HOSTED_ZONE, + props: query + }); + + // CDK handles the '.' at the end, so remove it here + if (response.Name.endsWith('.')) { + response.Name = response.Name.substring(0, response.Name.length - 1); + } + + return this.fromHostedZoneAttributes(scope, id, { + hostedZoneId: response.Id, + zoneName: response.Name, + }); + } + public readonly hostedZoneId: string; public readonly zoneName: string; public readonly hostedZoneNameServers?: string[]; diff --git a/packages/@aws-cdk/aws-route53/test/test.hosted-zone-provider.ts b/packages/@aws-cdk/aws-route53/test/test.hosted-zone-provider.ts index c9cb57a4e4aee..c83b163fc01a8 100644 --- a/packages/@aws-cdk/aws-route53/test/test.hosted-zone-provider.ts +++ b/packages/@aws-cdk/aws-route53/test/test.hosted-zone-provider.ts @@ -1,7 +1,7 @@ import { SynthUtils } from '@aws-cdk/assert'; import cdk = require('@aws-cdk/cdk'); import { Test } from 'nodeunit'; -import { HostedZone, HostedZoneAttributes, HostedZoneProvider } from '../lib'; +import { HostedZone, HostedZoneAttributes } from '../lib'; export = { 'Hosted Zone Provider': { @@ -9,7 +9,8 @@ export = { // GIVEN const stack = new cdk.Stack(undefined, 'TestStack', { env: { account: '12345', region: 'us-east-1' } }); const filter = {domainName: 'test.com'}; - new HostedZoneProvider(stack, filter).findHostedZone(); + + HostedZone.fromLookup(stack, 'Ref', filter); const missing = SynthUtils.synthesize(stack).assembly.manifest.missing!; test.ok(missing && missing.length === 1); @@ -25,22 +26,20 @@ export = { ResourceRecordSetCount: 3 }; - stack.node.setContext(missing[0].key, fakeZone); + const stack2 = new cdk.Stack(undefined, 'TestStack', { env: { account: '12345', region: 'us-east-1' } }); + stack2.node.setContext(missing[0].key, fakeZone); const cdkZoneProps: HostedZoneAttributes = { hostedZoneId: fakeZone.Id, zoneName: 'example.com', }; - const cdkZone = HostedZone.fromHostedZoneAttributes(stack, 'MyZone', cdkZoneProps); + const cdkZone = HostedZone.fromHostedZoneAttributes(stack2, 'MyZone', cdkZoneProps); // WHEN - const provider = new HostedZoneProvider(stack, filter); - const zoneProps = stack.resolve(provider.findHostedZone()); - const zoneRef = provider.findAndImport(stack, 'MyZoneProvider'); + const zoneRef = HostedZone.fromLookup(stack2, 'MyZoneProvider', filter); // THEN - test.deepEqual(zoneProps, cdkZoneProps); test.deepEqual(zoneRef.hostedZoneId, cdkZone.hostedZoneId); test.done(); }, diff --git a/packages/@aws-cdk/aws-ssm/lib/parameter.ts b/packages/@aws-cdk/aws-ssm/lib/parameter.ts index e79729cf26120..ccd405abef105 100644 --- a/packages/@aws-cdk/aws-ssm/lib/parameter.ts +++ b/packages/@aws-cdk/aws-ssm/lib/parameter.ts @@ -1,5 +1,8 @@ import iam = require('@aws-cdk/aws-iam'); -import { CfnDynamicReference, CfnDynamicReferenceService, CfnParameter, Construct, Fn, IResource, Resource, Stack, Token } from '@aws-cdk/cdk'; +import { + CfnDynamicReference, CfnDynamicReferenceService, CfnParameter, + Construct, ContextProvider, Fn, IResource, Resource, Stack, Token } from '@aws-cdk/cdk'; +import cxapi = require('@aws-cdk/cx-api'); import ssm = require('./ssm.generated'); /** @@ -223,6 +226,53 @@ export class StringParameter extends ParameterBase implements IStringParameter { return new Import(scope, id); } + /** + * Reads the value of an SSM parameter during synthesis through an + * environmental context provider. + * + * Requires that the stack this scope is defined in will have explicit + * account/region information. Otherwise, it will fail during synthesis. + */ + public static valueFromLookup(scope: Construct, parameterName: string): string { + const value = ContextProvider.getValue(scope, { + provider: cxapi.SSM_PARAMETER_PROVIDER, + props: { parameterName }, + dummyValue: `dummy-value-for-${parameterName}` + }); + + return value; + } + + /** + * Returns a token that will resolve (during deployment) to the string value of an SSM string parameter. + * @param scope Some scope within a stack + * @param parameterName The name of the SSM parameter. + * @param version The parameter version (recommended in order to ensure that the value won't change during deployment) + */ + public static valueForStringParameter(scope: Construct, parameterName: string, version?: number): string { + const stack = Stack.of(scope); + const id = makeIdentityForImportedValue(parameterName); + const exists = stack.node.tryFindChild(id) as IStringParameter; + if (exists) { return exists.stringValue; } + + return this.fromStringParameterAttributes(stack, id, { parameterName, version }).stringValue; + } + + /** + * Returns a token that will resolve (during deployment) + * @param scope Some scope within a stack + * @param parameterName The name of the SSM parameter + * @param version The parameter version (required for secure strings) + */ + public static valueForSecureStringParameter(scope: Construct, parameterName: string, version: number): string { + const stack = Stack.of(scope); + const id = makeIdentityForImportedValue(parameterName); + const exists = stack.node.tryFindChild(id) as IStringParameter; + if (exists) { return exists.stringValue; } + + return this.fromSecureStringParameterAttributes(stack, id, { parameterName, version }).stringValue; + } + public readonly parameterName: string; public readonly parameterType: string; public readonly stringValue: string; @@ -314,3 +364,7 @@ function _assertValidValue(value: string, allowedPattern: string): void { throw new Error(`The supplied value (${value}) does not match the specified allowedPattern (${allowedPattern})`); } } + +function makeIdentityForImportedValue(parameterName: string) { + return `SsmParameterValue:${parameterName}:C96584B6-F00A-464E-AD19-53AFF4B05118`; +} \ No newline at end of file diff --git a/packages/@aws-cdk/aws-ssm/package.json b/packages/@aws-cdk/aws-ssm/package.json index 2e40164c05cc2..892048ea63fbd 100644 --- a/packages/@aws-cdk/aws-ssm/package.json +++ b/packages/@aws-cdk/aws-ssm/package.json @@ -70,11 +70,13 @@ "pkglint": "^0.35.0" }, "dependencies": { + "@aws-cdk/cx-api": "^0.35.0", "@aws-cdk/aws-iam": "^0.35.0", "@aws-cdk/cdk": "^0.35.0" }, "homepage": "https://github.com/awslabs/aws-cdk", "peerDependencies": { + "@aws-cdk/cx-api": "^0.35.0", "@aws-cdk/aws-iam": "^0.35.0", "@aws-cdk/cdk": "^0.35.0" }, diff --git a/packages/@aws-cdk/aws-ssm/test/test.parameter.ts b/packages/@aws-cdk/aws-ssm/test/test.parameter.ts index 5fcaebf1b679f..95fcfa71fa285 100644 --- a/packages/@aws-cdk/aws-ssm/test/test.parameter.ts +++ b/packages/@aws-cdk/aws-ssm/test/test.parameter.ts @@ -1,6 +1,6 @@ import { expect, haveResource } from '@aws-cdk/assert'; import cdk = require('@aws-cdk/cdk'); -import { Stack } from '@aws-cdk/cdk'; +import { App, Stack } from '@aws-cdk/cdk'; import { Test } from 'nodeunit'; import ssm = require('../lib'); @@ -209,5 +209,79 @@ export = { test.deepEqual(stack.resolve(param.parameterType), 'StringList'); test.deepEqual(stack.resolve(param.stringListValue), { 'Fn::Split': [ ',', '{{resolve:ssm:MyParamName}}' ] }); test.done(); + }, + + 'fromLookup will use the SSM context provider to read value during synthesis'(test: Test) { + // GIVEN + const app = new App(); + const stack = new Stack(app, 'my-staq', { env: { region: 'us-east-1', account: '12344' }}); + + // WHEN + const value = ssm.StringParameter.valueFromLookup(stack, 'my-param-name'); + + // THEN + test.deepEqual(value, 'dummy-value-for-my-param-name'); + test.deepEqual(app.synth().manifest.missing, [ + { + key: 'ssm:account=12344:parameterName=my-param-name:region=us-east-1', + props: { + account: '12344', + region: 'us-east-1', + parameterName: 'my-param-name' + }, + provider: 'ssm' + } + ]); + test.done(); + }, + + 'valueForStringParameter': { + + 'returns a token that represents the SSM parameter value'(test: Test) { + // GIVEN + const stack = new Stack(); + + // WHEN + const value = ssm.StringParameter.valueForStringParameter(stack, 'my-param-name'); + + // THEN + expect(stack).toMatch({ + Parameters: { + SsmParameterValuemyparamnameC96584B6F00A464EAD1953AFF4B05118Parameter: { + Type: "AWS::SSM::Parameter::Value", + Default: "my-param-name" + } + } + }); + test.deepEqual(stack.resolve(value), { Ref: 'SsmParameterValuemyparamnameC96584B6F00A464EAD1953AFF4B05118Parameter' }); + test.done(); + }, + + 'de-dup based on parameter name'(test: Test) { + // GIVEN + const stack = new Stack(); + + // WHEN + ssm.StringParameter.valueForStringParameter(stack, 'my-param-name'); + ssm.StringParameter.valueForStringParameter(stack, 'my-param-name'); + ssm.StringParameter.valueForStringParameter(stack, 'my-param-name-2'); + ssm.StringParameter.valueForStringParameter(stack, 'my-param-name'); + + // THEN + expect(stack).toMatch({ + Parameters: { + SsmParameterValuemyparamnameC96584B6F00A464EAD1953AFF4B05118Parameter: { + Type: "AWS::SSM::Parameter::Value", + Default: "my-param-name" + }, + SsmParameterValuemyparamname2C96584B6F00A464EAD1953AFF4B05118Parameter: { + Type: "AWS::SSM::Parameter::Value", + Default: "my-param-name-2" + } + } + }); + test.done(); + } + } }; diff --git a/packages/@aws-cdk/aws-ssm/test/test.ssm.ts b/packages/@aws-cdk/aws-ssm/test/test.ssm.ts deleted file mode 100644 index 820f6b467f38f..0000000000000 --- a/packages/@aws-cdk/aws-ssm/test/test.ssm.ts +++ /dev/null @@ -1,8 +0,0 @@ -import { Test, testCase } from 'nodeunit'; - -export = testCase({ - notTested(test: Test) { - test.ok(true, 'No tests are specified for this package.'); - test.done(); - } -}); diff --git a/packages/@aws-cdk/aws-stepfunctions-tasks/test/ecs-tasks.test.ts b/packages/@aws-cdk/aws-stepfunctions-tasks/test/ecs-tasks.test.ts index ef5f6c817ec15..91eb19e440882 100644 --- a/packages/@aws-cdk/aws-stepfunctions-tasks/test/ecs-tasks.test.ts +++ b/packages/@aws-cdk/aws-stepfunctions-tasks/test/ecs-tasks.test.ts @@ -85,7 +85,6 @@ test('Running a Fargate Task', () => { Subnets: [ {Ref: "VpcPrivateSubnet1Subnet536B997A"}, {Ref: "VpcPrivateSubnet2Subnet3788AAA1"}, - {Ref: "VpcPrivateSubnet3SubnetF258B56E"}, ] }, }, diff --git a/packages/@aws-cdk/aws-stepfunctions-tasks/test/integ.ec2-task.expected.json b/packages/@aws-cdk/aws-stepfunctions-tasks/test/integ.ec2-task.expected.json index cddf481381a26..31cb13d038435 100644 --- a/packages/@aws-cdk/aws-stepfunctions-tasks/test/integ.ec2-task.expected.json +++ b/packages/@aws-cdk/aws-stepfunctions-tasks/test/integ.ec2-task.expected.json @@ -96,7 +96,9 @@ "FargateClusterDefaultAutoScalingGroupLaunchConfig57306899": { "Type": "AWS::AutoScaling::LaunchConfiguration", "Properties": { - "ImageId": "ami-1234", + "ImageId": { + "Ref": "SsmParameterValueawsserviceecsoptimizedamiamazonlinuxrecommendedimageidC96584B6F00A464EAD1953AFF4B05118Parameter" + }, "InstanceType": "t2.micro", "IamInstanceProfile": { "Ref": "FargateClusterDefaultAutoScalingGroupInstanceProfile2C0FEF3B" @@ -227,15 +229,7 @@ { "Ref": "AWS::Partition" }, - ":autoscaling:", - { - "Ref": "AWS::Region" - }, - ":", - { - "Ref": "AWS::AccountId" - }, - ":autoScalingGroup:*:autoScalingGroupName/", + ":autoscaling:test-region:12345678:autoScalingGroup:*:autoScalingGroupName/", { "Ref": "FargateClusterDefaultAutoScalingGroupASG36A4948F" } @@ -471,15 +465,7 @@ { "Ref": "AWS::Partition" }, - ":ecr:", - { - "Ref": "AWS::Region" - }, - ":", - { - "Ref": "AWS::AccountId" - }, - ":repository/", + ":ecr:test-region:12345678:repository/", { "Fn::GetAtt": [ "TaskDefTheContainerAssetImageAdoptRepository997406C3", @@ -508,15 +494,7 @@ { "Ref": "AWS::Partition" }, - ":ecr:", - { - "Ref": "AWS::Region" - }, - ":", - { - "Ref": "AWS::AccountId" - }, - ":repository/", + ":ecr:test-region:12345678:repository/", { "Fn::GetAtt": [ "TaskDefTheContainerAssetImageAdoptRepository997406C3", @@ -562,9 +540,7 @@ "Ref": "TaskLoggingLogGroupC7E938D4" }, "awslogs-stream-prefix": "EventDemo", - "awslogs-region": { - "Ref": "AWS::Region" - } + "awslogs-region": "test-region" } }, "Memory": 256, @@ -671,15 +647,7 @@ { "Ref": "AWS::Partition" }, - ":ecr:", - { - "Ref": "AWS::Region" - }, - ":", - { - "Ref": "AWS::AccountId" - }, - ":repository/", + ":ecr:test-region:12345678:repository/", { "Fn::GetAtt": [ "TaskDefTheContainerAssetImageAdoptRepository997406C3", @@ -786,15 +754,7 @@ { "Ref": "AWS::Partition" }, - ":ecr:", - { - "Ref": "AWS::Region" - }, - ":", - { - "Ref": "AWS::AccountId" - }, - ":repository/", + ":ecr:test-region:12345678:repository/", { "Fn::Select": [ 0, @@ -888,18 +848,7 @@ "Action": "sts:AssumeRole", "Effect": "Allow", "Principal": { - "Service": { - "Fn::Join": [ - "", - [ - "states.", - { - "Ref": "AWS::Region" - }, - ".amazonaws.com" - ] - ] - } + "Service": "states.test-region.amazonaws.com" } } ], @@ -960,15 +909,7 @@ { "Ref": "AWS::Partition" }, - ":events:", - { - "Ref": "AWS::Region" - }, - ":", - { - "Ref": "AWS::AccountId" - }, - ":rule/StepFunctionsGetEventsForECSTaskRule" + ":events:test-region:12345678:rule/StepFunctionsGetEventsForECSTaskRule" ] ] } @@ -1016,6 +957,10 @@ } }, "Parameters": { + "SsmParameterValueawsserviceecsoptimizedamiamazonlinuxrecommendedimageidC96584B6F00A464EAD1953AFF4B05118Parameter": { + "Type": "AWS::SSM::Parameter::Value", + "Default": "/aws/service/ecs/optimized-ami/amazon-linux/recommended/image_id" + }, "TaskDefTheContainerAssetImageImageName92ECAC22": { "Type": "String", "Description": "ECR repository name and tag asset \"aws-ecs-integ2/TaskDef/TheContainer/AssetImage\"" diff --git a/packages/@aws-cdk/aws-stepfunctions-tasks/test/integ.ec2-task.ts b/packages/@aws-cdk/aws-stepfunctions-tasks/test/integ.ec2-task.ts index 290e17ae26a56..d7d26f48c0743 100644 --- a/packages/@aws-cdk/aws-stepfunctions-tasks/test/integ.ec2-task.ts +++ b/packages/@aws-cdk/aws-stepfunctions-tasks/test/integ.ec2-task.ts @@ -6,7 +6,12 @@ import path = require('path'); import tasks = require('../lib'); const app = new cdk.App(); -const stack = new cdk.Stack(app, 'aws-ecs-integ2'); +const stack = new cdk.Stack(app, 'aws-ecs-integ2', { + env: { + account: process.env.CDK_INTEG_ACCOUNT || process.env.CDK_DEFAULT_ACCOUNT, + region: process.env.CDK_INTEG_REGION || process.env.CDK_DEFAULT_REGION + } +}); const vpc = ec2.Vpc.fromLookup(stack, 'Vpc', { isDefault: true diff --git a/packages/@aws-cdk/aws-stepfunctions-tasks/test/integ.fargate-task.expected.json b/packages/@aws-cdk/aws-stepfunctions-tasks/test/integ.fargate-task.expected.json index b2fe109c399c2..b37ca00c1fc4b 100644 --- a/packages/@aws-cdk/aws-stepfunctions-tasks/test/integ.fargate-task.expected.json +++ b/packages/@aws-cdk/aws-stepfunctions-tasks/test/integ.fargate-task.expected.json @@ -54,15 +54,7 @@ { "Ref": "AWS::Partition" }, - ":ecr:", - { - "Ref": "AWS::Region" - }, - ":", - { - "Ref": "AWS::AccountId" - }, - ":repository/", + ":ecr:test-region:12345678:repository/", { "Fn::GetAtt": [ "TaskDefTheContainerAssetImageAdoptRepository997406C3", @@ -91,15 +83,7 @@ { "Ref": "AWS::Partition" }, - ":ecr:", - { - "Ref": "AWS::Region" - }, - ":", - { - "Ref": "AWS::AccountId" - }, - ":repository/", + ":ecr:test-region:12345678:repository/", { "Fn::GetAtt": [ "TaskDefTheContainerAssetImageAdoptRepository997406C3", @@ -145,9 +129,7 @@ "Ref": "TaskLoggingLogGroupC7E938D4" }, "awslogs-stream-prefix": "EventDemo", - "awslogs-region": { - "Ref": "AWS::Region" - } + "awslogs-region": "test-region" } }, "Memory": 256, @@ -256,15 +238,7 @@ { "Ref": "AWS::Partition" }, - ":ecr:", - { - "Ref": "AWS::Region" - }, - ":", - { - "Ref": "AWS::AccountId" - }, - ":repository/", + ":ecr:test-region:12345678:repository/", { "Fn::GetAtt": [ "TaskDefTheContainerAssetImageAdoptRepository997406C3", @@ -371,15 +345,7 @@ { "Ref": "AWS::Partition" }, - ":ecr:", - { - "Ref": "AWS::Region" - }, - ":", - { - "Ref": "AWS::AccountId" - }, - ":repository/", + ":ecr:test-region:12345678:repository/", { "Fn::Select": [ 0, @@ -488,18 +454,7 @@ "Action": "sts:AssumeRole", "Effect": "Allow", "Principal": { - "Service": { - "Fn::Join": [ - "", - [ - "states.", - { - "Ref": "AWS::Region" - }, - ".amazonaws.com" - ] - ] - } + "Service": "states.test-region.amazonaws.com" } } ], @@ -560,15 +515,7 @@ { "Ref": "AWS::Partition" }, - ":events:", - { - "Ref": "AWS::Region" - }, - ":", - { - "Ref": "AWS::AccountId" - }, - ":rule/StepFunctionsGetEventsForECSTaskRule" + ":events:test-region:12345678:rule/StepFunctionsGetEventsForECSTaskRule" ] ] } diff --git a/packages/@aws-cdk/aws-stepfunctions-tasks/test/integ.fargate-task.ts b/packages/@aws-cdk/aws-stepfunctions-tasks/test/integ.fargate-task.ts index f3ec0a6fd26aa..37647931c92e4 100644 --- a/packages/@aws-cdk/aws-stepfunctions-tasks/test/integ.fargate-task.ts +++ b/packages/@aws-cdk/aws-stepfunctions-tasks/test/integ.fargate-task.ts @@ -6,7 +6,12 @@ import path = require('path'); import tasks = require('../lib'); const app = new cdk.App(); -const stack = new cdk.Stack(app, 'aws-ecs-integ2'); +const stack = new cdk.Stack(app, 'aws-ecs-integ2', { + env: { + account: process.env.CDK_INTEG_ACCOUNT || process.env.CDK_DEFAULT_ACCOUNT, + region: process.env.CDK_INTEG_REGION || process.env.CDK_DEFAULT_REGION + } +}); const vpc = ec2.Vpc.fromLookup(stack, 'Vpc', { isDefault: true diff --git a/packages/@aws-cdk/aws-stepfunctions-tasks/test/sagemaker-training-job.test.ts b/packages/@aws-cdk/aws-stepfunctions-tasks/test/sagemaker-training-job.test.ts index 66575a68dfc04..057799772878e 100644 --- a/packages/@aws-cdk/aws-stepfunctions-tasks/test/sagemaker-training-job.test.ts +++ b/packages/@aws-cdk/aws-stepfunctions-tasks/test/sagemaker-training-job.test.ts @@ -217,7 +217,6 @@ test('create complex training job', () => { Subnets: [ { Ref: "VPCPrivateSubnet1Subnet8BCA10E0" }, { Ref: "VPCPrivateSubnet2SubnetCFCDAA7A" }, - { Ref: "VPCPrivateSubnet3Subnet3EDCD457" } ] } }, diff --git a/packages/@aws-cdk/cdk/lib/construct.ts b/packages/@aws-cdk/cdk/lib/construct.ts index a0696ec0241c8..3a94d1eabe3d3 100644 --- a/packages/@aws-cdk/cdk/lib/construct.ts +++ b/packages/@aws-cdk/cdk/lib/construct.ts @@ -267,6 +267,10 @@ export class ConstructNode { * @param value The context value */ public setContext(key: string, value: any) { + if (Token.isUnresolved(key)) { + throw new Error(`Invalid context key "${key}". It contains unresolved tokens`); + } + if (this.children.length > 0) { const names = this.children.map(c => c.node.id); throw new Error('Cannot set context after children have been added: ' + names.join(',')); @@ -283,6 +287,10 @@ export class ConstructNode { * @returns The context value or `undefined` if there is no context value for thie key. */ public tryGetContext(key: string): any { + if (Token.isUnresolved(key)) { + throw new Error(`Invalid context key "${key}". It contains unresolved tokens`); + } + const value = this._context[key]; if (value !== undefined) { return value; } diff --git a/packages/@aws-cdk/cdk/lib/context-provider.ts b/packages/@aws-cdk/cdk/lib/context-provider.ts new file mode 100644 index 0000000000000..4353218580111 --- /dev/null +++ b/packages/@aws-cdk/cdk/lib/context-provider.ts @@ -0,0 +1,124 @@ +import { Construct } from './construct'; +import { Stack } from './stack'; +import { Token } from './token'; + +export interface GetContextKeyOptions { + /** + * The context provider to query. + */ + readonly provider: string; + + /** + * Provider-specific properties. + */ + readonly props?: { [key: string]: any }; +} + +export interface GetContextValueOptions extends GetContextKeyOptions { + /** + * The value to return if the context value was not found and a missing + * context is reported. This should be a dummy value that should preferably + * fail during deployment since it represents an invalid state. + */ + readonly dummyValue: any; +} + +export interface GetContextKeyResult { + readonly key: string; + readonly props: { [key: string]: any }; +} + +export interface GetContextValueResult { + readonly value?: any; +} + +/** + * Base class for the model side of context providers + * + * Instances of this class communicate with context provider plugins in the 'cdk + * toolkit' via context variables (input), outputting specialized queries for + * more context variables (output). + * + * ContextProvider needs access to a Construct to hook into the context mechanism. + */ +export class ContextProvider { + /** + * @returns the context key or undefined if a key cannot be rendered (due to tokens used in any of the props) + */ + public static getKey(scope: Construct, options: GetContextKeyOptions): GetContextKeyResult { + const stack = Stack.of(scope); + + const props = { + account: stack.account, + region: stack.region, + ...options.props || {}, + }; + + if (Object.values(props).find(x => Token.isUnresolved(x))) { + throw new Error( + `Cannot determine scope for context provider ${options.provider}.\n` + + `This usually happens when one or more of the provider props have unresolved tokens`); + } + + const propStrings = propsToArray(props); + return { + key: `${options.provider}:${propStrings.join(':')}`, + props + }; + } + + public static getValue(scope: Construct, options: GetContextValueOptions): any { + const stack = Stack.of(scope); + + if (Token.isUnresolved(stack.account) || Token.isUnresolved(stack.region)) { + throw new Error(`Cannot retrieve value from context provider ${options.provider} since account/region are not specified at the stack level`); + } + + const { key, props } = this.getKey(scope, options); + const value = scope.node.tryGetContext(key); + + // if context is missing, report and return a dummy value + if (value === undefined) { + stack.reportMissingContext({ key, props, provider: options.provider, }); + return options.dummyValue; + } + + return value; + } + + private constructor() { } +} + +/** + * Quote colons in all strings so that we can undo the quoting at a later point + * + * We'll use $ as a quoting character, for no particularly good reason other + * than that \ is going to lead to quoting hell when the keys are stored in JSON. + */ +function colonQuote(xs: string): string { + return xs.replace('$', '$$').replace(':', '$:'); +} + +function propsToArray(props: {[key: string]: any}, keyPrefix = ''): string[] { + const ret: string[] = []; + + for (const key of Object.keys(props)) { + switch (typeof props[key]) { + case 'object': { + ret.push(...propsToArray(props[key], `${keyPrefix}${key}.`)); + break; + } + case 'string': { + ret.push(`${keyPrefix}${key}=${colonQuote(props[key])}`); + break; + } + default: { + ret.push(`${keyPrefix}${key}=${JSON.stringify(props[key])}`); + break; + } + } + } + + ret.sort(); + return ret; +} diff --git a/packages/@aws-cdk/cdk/lib/context.ts b/packages/@aws-cdk/cdk/lib/context.ts deleted file mode 100644 index 7e85c422eb381..0000000000000 --- a/packages/@aws-cdk/cdk/lib/context.ts +++ /dev/null @@ -1,282 +0,0 @@ -import cxapi = require('@aws-cdk/cx-api'); -import { Construct } from './construct'; -import { Stack } from './stack'; -import { Token } from './token'; - -type ContextProviderProps = {[key: string]: any}; - -/** - * Methods for CDK-related context information. - */ -export class Context { - /** - * Returns the default region as passed in through the CDK CLI. - * - * @returns The default region as specified in context or `undefined` if the region is not specified. - */ - public static getDefaultRegion(scope: Construct) { return scope.node.tryGetContext(cxapi.DEFAULT_REGION_CONTEXT_KEY); } - - /** - * Returns the default account ID as passed in through the CDK CLI. - * - * @returns The default account ID as specified in context or `undefined` if the account ID is not specified. - */ - public static getDefaultAccount(scope: Construct) { return scope.node.tryGetContext(cxapi.DEFAULT_ACCOUNT_CONTEXT_KEY); } - - /** - * Returnst the list of AZs in the scope's environment (account/region). - * - * If they are not available in the context, returns a set of dummy values and - * reports them as missing, and let the CLI resolve them by calling EC2 - * `DescribeAvailabilityZones` on the target environment. - */ - public static getAvailabilityZones(scope: Construct) { - return new AvailabilityZoneProvider(scope).availabilityZones; - } - - /** - * Retrieves the value of an SSM parameter. - * @param scope Some construct scope. - * @param parameterName The name of the parameter - * @param options Options - */ - public static getSsmParameter(scope: Construct, parameterName: string, options: SsmParameterOptions = { }) { - return new SsmParameterProvider(scope, parameterName).parameterValue(options.defaultValue); - } - - private constructor() { } -} - -export interface SsmParameterOptions { - /** - * The default/dummy value to return if the SSM parameter is not available in the context. - */ - readonly defaultValue?: string; -} - -/** - * Base class for the model side of context providers - * - * Instances of this class communicate with context provider plugins in the 'cdk - * toolkit' via context variables (input), outputting specialized queries for - * more context variables (output). - * - * ContextProvider needs access to a Construct to hook into the context mechanism. - */ -export class ContextProvider { - private readonly props: ContextProviderProps; - - constructor(private readonly context: Construct, - private readonly provider: string, - props: ContextProviderProps = {}) { - - const stack = Stack.of(context); - - let account: undefined | string = stack.account; - let region: undefined | string = stack.region; - - // stack.account and stack.region will defer to deploy-time resolution - // (AWS::Region, AWS::AccountId) if user did not explicitly specify them - // when they defined the stack, but this is not good enough for - // environmental context because we need concrete values during synthesis. - if (!account || Token.isUnresolved(account)) { - account = Context.getDefaultAccount(this.context); - } - - if (!region || Token.isUnresolved(region)) { - region = Context.getDefaultRegion(this.context); - } - - // this is probably an issue. we can't have only account but no region specified - if (account && !region) { - throw new Error(`A region must be specified in order to obtain environmental context: ${provider}`); - } - - this.props = { - account, - region, - ...props, - }; - } - - public get key(): string { - const propStrings: string[] = propsToArray(this.props); - return `${this.provider}:${propStrings.join(':')}`; - } - - /** - * Read a provider value and verify it is not `null` - */ - public getValue(defaultValue: any): any { - const value = this.context.node.tryGetContext(this.key); - if (value != null) { - return value; - } - - // if account or region is not defined this is probably a test mode, so we just - // return the default value - if (!this.props.account || !this.props.region) { - this.context.node.addError(formatMissingScopeError(this.provider, this.props)); - return defaultValue; - } - - this.reportMissingContext({ - key: this.key, - provider: this.provider, - props: this.props, - }); - - return defaultValue; - } - /** - * Read a provider value, verifying it's a string - * @param defaultValue The value to return if there is no value defined for this context key - */ - public getStringValue( defaultValue: string): string { - const value = this.context.node.tryGetContext(this.key); - - if (value != null) { - if (typeof value !== 'string') { - throw new TypeError(`Expected context parameter '${this.key}' to be a string, but got '${JSON.stringify(value)}'`); - } - return value; - } - - // if scope is undefined, this is probably a test mode, so we just - // return the default value - if (!this.props.account || !this.props.region) { - this.context.node.addError(formatMissingScopeError(this.provider, this.props)); - return defaultValue; - } - - this.reportMissingContext({ - key: this.key, - provider: this.provider, - props: this.props, - }); - - return defaultValue; - } - - /** - * Read a provider value, verifying it's a list - * @param defaultValue The value to return if there is no value defined for this context key - */ - public getStringListValue(defaultValue: string[]): string[] { - const value = this.context.node.tryGetContext(this.key); - - if (value != null) { - if (!value.map) { - throw new Error(`Context value '${this.key}' is supposed to be a list, got '${JSON.stringify(value)}'`); - } - return value; - } - - // if scope is undefined, this is probably a test mode, so we just - // return the default value and report an error so this in not accidentally used - // in the toolkit - if (!this.props.account || !this.props.region) { - this.context.node.addError(formatMissingScopeError(this.provider, this.props)); - return defaultValue; - } - - this.reportMissingContext({ - key: this.key, - provider: this.provider, - props: this.props, - }); - - return defaultValue; - } - - protected reportMissingContext(report: cxapi.MissingContext) { - Stack.of(this.context).reportMissingContext(report); - } -} - -/** - * Quote colons in all strings so that we can undo the quoting at a later point - * - * We'll use $ as a quoting character, for no particularly good reason other - * than that \ is going to lead to quoting hell when the keys are stored in JSON. - */ -function colonQuote(xs: string): string { - return xs.replace('$', '$$').replace(':', '$:'); -} - -/** - * Context provider that will return the availability zones for the current account and region - */ -class AvailabilityZoneProvider { - private provider: ContextProvider; - - constructor(context: Construct) { - this.provider = new ContextProvider(context, cxapi.AVAILABILITY_ZONE_PROVIDER); - } - - /** - * Returns the context key the AZ provider looks up in the context to obtain - * the list of AZs in the current environment. - */ - public get key() { - return this.provider.key; - } - - /** - * Return the list of AZs for the current account and region - */ - public get availabilityZones(): string[] { - return this.provider.getStringListValue(['dummy1a', 'dummy1b', 'dummy1c']); - } -} - -/** - * Context provider that will read values from the SSM parameter store in the indicated account and region - */ -class SsmParameterProvider { - private provider: ContextProvider; - - constructor(context: Construct, parameterName: string) { - this.provider = new ContextProvider(context, cxapi.SSM_PARAMETER_PROVIDER, { parameterName }); - } - - /** - * Return the SSM parameter string with the indicated key - */ - public parameterValue(defaultValue = 'dummy'): any { - return this.provider.getStringValue(defaultValue); - } -} - -function formatMissingScopeError(provider: string, props: {[key: string]: string}) { - let s = `Cannot determine scope for context provider ${provider}`; - const propsString = Object.keys(props).map( key => (`${key}=${props[key]}`)); - s += ` with props: ${propsString}.`; - s += '\n'; - s += 'This usually happens when AWS credentials are not available and the default account/region cannot be determined.'; - return s; -} - -function propsToArray(props: {[key: string]: any}, keyPrefix = ''): string[] { - const ret: string[] = []; - - for (const key of Object.keys(props)) { - switch (typeof props[key]) { - case 'object': { - ret.push(...propsToArray(props[key], `${keyPrefix}${key}.`)); - break; - } - case 'string': { - ret.push(`${keyPrefix}${key}=${colonQuote(props[key])}`); - break; - } - default: { - ret.push(`${keyPrefix}${key}=${JSON.stringify(props[key])}`); - break; - } - } - } - - ret.sort(); - return ret; -} diff --git a/packages/@aws-cdk/cdk/lib/environment.ts b/packages/@aws-cdk/cdk/lib/environment.ts index 6df08fb48fac5..837d7546c46fb 100644 --- a/packages/@aws-cdk/cdk/lib/environment.ts +++ b/packages/@aws-cdk/cdk/lib/environment.ts @@ -4,13 +4,29 @@ export interface Environment { /** * The AWS account ID for this environment. - * If not specified, the context parameter `default-account` is used. + * + * This can be either a concrete value such as `585191031104` or `Aws.accountId` which + * indicates that account ID will only be determined during deployment (it + * will resolve to the CloudFormation intrinsic `{"Ref":"AWS::AccountId"}`). + * Note that certain features, such as cross-stack references and + * environmental context providers require concerete region information and + * will cause this stack to emit synthesis errors. + * + * @default Aws.accountId which means that the stack will be account-agnostic. */ readonly account?: string; /** * The AWS region for this environment. - * If not specified, the context parameter `default-region` is used. + * + * This can be either a concrete value such as `eu-west-2` or `Aws.region` + * which indicates that account ID will only be determined during deployment + * (it will resolve to the CloudFormation intrinsic `{"Ref":"AWS::Region"}`). + * Note that certain features, such as cross-stack references and + * environmental context providers require concerete region information and + * will cause this stack to emit synthesis errors. + * + * @default Aws.region which means that the stack will be region-agnostic. */ readonly region?: string; } diff --git a/packages/@aws-cdk/cdk/lib/index.ts b/packages/@aws-cdk/cdk/lib/index.ts index ad156c94b63d4..a624d4cdf9892 100644 --- a/packages/@aws-cdk/cdk/lib/index.ts +++ b/packages/@aws-cdk/cdk/lib/index.ts @@ -29,7 +29,7 @@ export * from './arn'; export * from './stack-trace'; export * from './app'; -export * from './context'; +export * from './context-provider'; export * from './environment'; export * from './runtime'; diff --git a/packages/@aws-cdk/cdk/lib/stack.ts b/packages/@aws-cdk/cdk/lib/stack.ts index bde986884950e..3672ddb3b0a4d 100644 --- a/packages/@aws-cdk/cdk/lib/stack.ts +++ b/packages/@aws-cdk/cdk/lib/stack.ts @@ -4,6 +4,7 @@ import fs = require('fs'); import path = require('path'); import { CLOUDFORMATION_TOKEN_RESOLVER, CloudFormationLang } from './cloudformation-lang'; import { Construct, ConstructNode, IConstruct, ISynthesisSession } from './construct'; +import { ContextProvider } from './context-provider'; import { Environment } from './environment'; import { LogicalIDs } from './logical-id'; import { resolve } from './private/resolve'; @@ -94,20 +95,46 @@ export class Stack extends Construct implements ITaggable { public readonly stackName: string; /** - * The region into which this stack will be deployed. + * The AWS region into which this stack will be deployed (e.g. `us-west-2`). * - * This will be a concrete value only if an account was specified in `env` - * when the stack was defined. Otherwise, it will be a string that resolves to - * `{ "Ref": "AWS::Region" }` + * This value is resolved according to the following rules: + * + * 1. The value provided to `env.region` when the stack is defined. This can + * either be a concerete region (e.g. `us-west-2`) or the `Aws.region` + * token. + * 3. `Aws.region`, which is represents the CloudFormation intrinsic reference + * `{ "Ref": "AWS::Region" }` encoded as a string token. + * + * Preferably, you should use the return value as an opaque string and not + * attempt to parse it to implement your logic. If you do, you must first + * check that it is a concerete value an not an unresolved token. If this + * value is an unresolved token (`Token.isUnresolved(stack.region)` returns + * `true`), this implies that the user wishes that this stack will synthesize + * into a **region-agnostic template**. In this case, your code should either + * fail (throw an error, emit a synth error using `node.addError`) or + * implement some other region-agnostic behavior. */ public readonly region: string; /** - * The account into which this stack will be deployed. + * The AWS account into which this stack will be deployed. + * + * This value is resolved according to the following rules: + * + * 1. The value provided to `env.account` when the stack is defined. This can + * either be a concerete account (e.g. `585695031111`) or the + * `Aws.accountId` token. + * 3. `Aws.accountId`, which represents the CloudFormation intrinsic reference + * `{ "Ref": "AWS::AccountId" }` encoded as a string token. * - * This will be a concrete value only if an account was specified in `env` - * when the stack was defined. Otherwise, it will be a string that resolves to - * `{ "Ref": "AWS::AccountId" }` + * Preferably, you should use the return value as an opaque string and not + * attempt to parse it to implement your logic. If you do, you must first + * check that it is a concerete value an not an unresolved token. If this + * value is an unresolved token (`Token.isUnresolved(stack.account)` returns + * `true`), this implies that the user wishes that this stack will synthesize + * into a **account-agnostic template**. In this case, your code should either + * fail (throw an error, emit a synth error using `node.addError`) or + * implement some other region-agnostic behavior. */ public readonly account: string; @@ -116,8 +143,13 @@ export class Stack extends Construct implements ITaggable { * `aws://account/region`. Use `stack.account` and `stack.region` to obtain * the specific values, no need to parse. * - * If either account or region are undefined, `unknown-account` or - * `unknown-region` will be used respectively. + * You can use this value to determine if two stacks are targeting the same + * environment. + * + * If either `stack.account` or `stack.region` are not concrete values (e.g. + * `Aws.account` or `Aws.region`) the special strings `unknown-account` and/or + * `unknown-region` will be used respectively to indicate this stack is + * region/account-agnostic. */ public readonly environment: string; @@ -303,6 +335,43 @@ export class Stack extends Construct implements ITaggable { return Arn.format(components, this); } + /** + * Returnst the list of AZs that are availability in the AWS environment + * (account/region) associated with this stack. + * + * If the stack is environment-agnostic (either account and/or region are + * tokens), this property will return an array with 2 tokens that will resolve + * at deploy-time to the first two availability zones returned from CloudFormation's + * `Fn::GetAZs` intrinsic function. + * + * If they are not available in the context, returns a set of dummy values and + * reports them as missing, and let the CLI resolve them by calling EC2 + * `DescribeAvailabilityZones` on the target environment. + */ + public get availabilityZones() { + // if account/region are tokens, we can't obtain AZs through the context + // provider, so we fallback to use Fn::GetAZs. the current lowest common + // denominator is 2 AZs across all AWS regions. + const agnostic = Token.isUnresolved(this.account) || Token.isUnresolved(this.region); + if (agnostic) { + return this.node.tryGetContext(cxapi.AVAILABILITY_ZONE_FALLBACK_CONTEXT_KEY) || [ + Fn.select(0, Fn.getAZs()), + Fn.select(1, Fn.getAZs()) + ]; + } + + const value = ContextProvider.getValue(this, { + provider: cxapi.AVAILABILITY_ZONE_PROVIDER, + dummyValue: ['dummy1a', 'dummy1b', 'dummy1c'], + }); + + if (!Array.isArray(value)) { + throw new Error(`Provider ${cxapi.AVAILABILITY_ZONE_PROVIDER} expects a list`); + } + + return value; + } + /** * Given an ARN, parses it and returns components. * @@ -505,23 +574,21 @@ export class Stack extends Construct implements ITaggable { */ private parseEnvironment(env: Environment = {}) { // if an environment property is explicitly specified when the stack is - // created, it will be used as concrete values for all intents. if not, use - // tokens for account and region but they do not need to be scoped, the only - // situation in which export/fn::importvalue would work if { Ref: - // "AWS::AccountId" } is the same for provider and consumer anyway. - const region = env.region || Aws.region; + // created, it will be used. if not, use tokens for account and region but + // they do not need to be scoped, the only situation in which + // export/fn::importvalue would work if { Ref: "AWS::AccountId" } is the + // same for provider and consumer anyway. const account = env.account || Aws.accountId; + const region = env.region || Aws.region; - // temporary fix for #2853, eventually behavior will be based on #2866. - // set the cloud assembly manifest environment spec of this stack to use the - // default account/region from the toolkit in case account/region are undefined or - // unresolved (i.e. tokens). - const envAccount = !Token.isUnresolved(account) ? account : Context.getDefaultAccount(this) || 'unknown-account'; - const envRegion = !Token.isUnresolved(region) ? region : Context.getDefaultRegion(this) || 'unknown-region'; + // this is the "aws://" env specification that will be written to the cloud assembly + // manifest. it will use "unknown-account" and "unknown-region" to indicate + // environment-agnosticness. + const envAccount = !Token.isUnresolved(account) ? account : cxapi.UNKNOWN_ACCOUNT; + const envRegion = !Token.isUnresolved(region) ? region : cxapi.UNKNOWN_REGION; return { - account: account || Aws.accountId, - region: region || Aws.region, + account, region, environment: EnvironmentUtils.format(envAccount, envRegion) }; } @@ -658,7 +725,7 @@ function cfnElements(node: IConstruct, into: CfnElement[] = []): CfnElement[] { import { Arn, ArnComponents } from './arn'; import { CfnElement } from './cfn-element'; import { CfnResource, TagType } from './cfn-resource'; -import { Context } from './context'; +import { Fn } from './fn'; import { CfnReference } from './private/cfn-reference'; import { Aws, ScopedAws } from './pseudo'; import { ITaggable, TagManager } from './tag-manager'; diff --git a/packages/@aws-cdk/cdk/test/test.construct.ts b/packages/@aws-cdk/cdk/test/test.construct.ts index bbe740acdc508..31c3a246852ea 100644 --- a/packages/@aws-cdk/cdk/test/test.construct.ts +++ b/packages/@aws-cdk/cdk/test/test.construct.ts @@ -1,6 +1,6 @@ import cxapi = require('@aws-cdk/cx-api'); import { Test } from 'nodeunit'; -import { App as Root, Construct, ConstructNode, ConstructOrder, IConstruct, Lazy, ValidationError } from '../lib'; +import { App as Root, Aws, Construct, ConstructNode, ConstructOrder, IConstruct, Lazy, ValidationError } from '../lib'; // tslint:disable:variable-name // tslint:disable:max-line-length @@ -185,6 +185,13 @@ export = { test.done(); }, + 'fails if context key contains unresolved tokens'(test: Test) { + const root = new Root(); + test.throws(() => root.node.setContext(`my-${Aws.region}`, 'foo'), /Invalid context key/); + test.throws(() => root.node.tryGetContext(Aws.region), /Invalid context key/); + test.done(); + }, + 'construct.pathParts returns an array of strings of all names from root to node'(test: Test) { const tree = createTree(); test.deepEqual(tree.root.node.path, ''); diff --git a/packages/@aws-cdk/cdk/test/test.context.ts b/packages/@aws-cdk/cdk/test/test.context.ts index 25df0b72b6b2f..7746f85529771 100644 --- a/packages/@aws-cdk/cdk/test/test.context.ts +++ b/packages/@aws-cdk/cdk/test/test.context.ts @@ -1,11 +1,11 @@ -import cxapi = require('@aws-cdk/cx-api'); import { Test } from 'nodeunit'; -import { App, Construct, ConstructNode, Context, ContextProvider, Stack } from '../lib'; +import { ConstructNode, Stack } from '../lib'; +import { ContextProvider } from '../lib/context-provider'; export = { 'AvailabilityZoneProvider returns a list with dummy values if the context is not available'(test: Test) { const stack = new Stack(undefined, 'TestStack', { env: { account: '12345', region: 'us-east-1' } }); - const azs = Context.getAvailabilityZones(stack); + const azs = stack.availabilityZones; test.deepEqual(azs, ['dummy1a', 'dummy1b', 'dummy1c']); test.done(); @@ -13,13 +13,13 @@ export = { 'AvailabilityZoneProvider will return context list if available'(test: Test) { const stack = new Stack(undefined, 'TestStack', { env: { account: '12345', region: 'us-east-1' } }); - const before = Context.getAvailabilityZones(stack); + const before = stack.availabilityZones; test.deepEqual(before, [ 'dummy1a', 'dummy1b', 'dummy1c' ]); const key = expectedContextKey(stack); stack.node.setContext(key, ['us-east-1a', 'us-east-1b']); - const azs = Context.getAvailabilityZones(stack); + const azs = stack.availabilityZones; test.deepEqual(azs, ['us-east-1a', 'us-east-1b']); test.done(); @@ -27,14 +27,14 @@ export = { 'AvailabilityZoneProvider will complain if not given a list'(test: Test) { const stack = new Stack(undefined, 'TestStack', { env: { account: '12345', region: 'us-east-1' } }); - const before = Context.getAvailabilityZones(stack); + const before = stack.availabilityZones; test.deepEqual(before, [ 'dummy1a', 'dummy1b', 'dummy1c' ]); const key = expectedContextKey(stack); stack.node.setContext(key, 'not-a-list'); test.throws( - () => Context.getAvailabilityZones(stack) + () => stack.availabilityZones ); test.done(); @@ -42,20 +42,42 @@ export = { 'ContextProvider consistently generates a key'(test: Test) { const stack = new Stack(undefined, 'TestStack', { env: { account: '12345', region: 'us-east-1' } }); - const provider = new ContextProvider(stack, 'ssm', { - parameterName: 'foo', - anyStringParam: 'bar', + const key = ContextProvider.getKey(stack, { + provider: 'ssm', + props: { + parameterName: 'foo', + anyStringParam: 'bar' + }, }); - const key = provider.key; - test.deepEqual(key, 'ssm:account=12345:anyStringParam=bar:parameterName=foo:region=us-east-1'); - const complex = new ContextProvider(stack, 'vpc', { - cidrBlock: '192.168.0.16', - tags: { Name: 'MyVPC', Env: 'Preprod' }, - igw: false, + + test.deepEqual(key, { + key: 'ssm:account=12345:anyStringParam=bar:parameterName=foo:region=us-east-1', + props: { + account: '12345', + region: 'us-east-1', + parameterName: 'foo', + anyStringParam: 'bar' + } + }); + + const complexKey = ContextProvider.getKey(stack, { + provider: 'vpc', + props: { + cidrBlock: '192.168.0.16', + tags: { Name: 'MyVPC', Env: 'Preprod' }, + igw: false, + } + }); + test.deepEqual(complexKey, { + key: 'vpc:account=12345:cidrBlock=192.168.0.16:igw=false:region=us-east-1:tags.Env=Preprod:tags.Name=MyVPC', + props: { + account: '12345', + region: 'us-east-1', + cidrBlock: '192.168.0.16', + tags: { Name: 'MyVPC', Env: 'Preprod' }, + igw: false, + } }); - const complexKey = complex.key; - test.deepEqual(complexKey, - 'vpc:account=12345:cidrBlock=192.168.0.16:igw=false:region=us-east-1:tags.Env=Preprod:tags.Name=MyVPC'); test.done(); }, @@ -64,65 +86,31 @@ export = { const stack = new Stack(undefined, 'TestStack', { env: { account: '12345', region: 'us-east-1' } }); // WHEN - const provider = new ContextProvider(stack, 'provider', { - list: [ - { key: 'key1', value: 'value1' }, - { key: 'key2', value: 'value2' }, - ], + const key = ContextProvider.getKey(stack, { + provider: 'provider', + props: { + list: [ + { key: 'key1', value: 'value1' }, + { key: 'key2', value: 'value2' }, + ], + } }); // THEN - test.equals(provider.key, 'provider:account=12345:list.0.key=key1:list.0.value=value1:list.1.key=key2:list.1.value=value2:region=us-east-1'); - - test.done(); - }, - - 'SSM parameter provider will return context values if available'(test: Test) { - const stack = new Stack(undefined, 'TestStack', { env: { account: '12345', region: 'us-east-1' } }); - Context.getSsmParameter(stack, 'test'); - const key = expectedContextKey(stack); - - stack.node.setContext(key, 'abc'); - - const ssmp = Context.getSsmParameter(stack, 'test'); - const azs = stack.resolve(ssmp); - test.deepEqual(azs, 'abc'); - - test.done(); - }, - - 'Return default values if "env" is undefined to facilitate unit tests, but also expect metadata to include "error" messages'(test: Test) { - const app = new App(); - const stack = new Stack(app, 'test-stack'); - - const child = new Construct(stack, 'ChildConstruct'); - - test.deepEqual(Context.getAvailabilityZones(stack), [ 'dummy1a', 'dummy1b', 'dummy1c' ]); - test.deepEqual(Context.getSsmParameter(child, 'foo'), 'dummy'); - - const assembly = app.synth(); - const output = assembly.getStack('test-stack'); - const metadata = output.manifest.metadata || {}; - const azError: cxapi.MetadataEntry | undefined = metadata['/test-stack'].find(x => x.type === cxapi.ERROR_METADATA_KEY); - const ssmError: cxapi.MetadataEntry | undefined = metadata['/test-stack/ChildConstruct'].find(x => x.type === cxapi.ERROR_METADATA_KEY); - - test.ok(azError && (azError.data as string).includes('Cannot determine scope for context provider availability-zones')); - test.ok(ssmError && (ssmError.data as string).includes('Cannot determine scope for context provider ssm')); + test.deepEqual(key, { + key: 'provider:account=12345:list.0.key=key1:list.0.value=value1:list.1.key=key2:list.1.value=value2:region=us-east-1', + props: { + account: '12345', + region: 'us-east-1', + list: [ + { key: 'key1', value: 'value1' }, + { key: 'key2', value: 'value2' }, + ], + } + }); test.done(); }, - - 'fails if region is not specified in CLI context'(test: Test) { - // GIVEN - const stack = new Stack(); - - // WHEN - stack.node.setContext(cxapi.DEFAULT_ACCOUNT_CONTEXT_KEY, '1111111111'); - - // THEN - test.throws(() => Context.getAvailabilityZones(stack), /A region must be specified in order to obtain environmental context: availability-zones/); - test.done(); - } }; /** diff --git a/packages/@aws-cdk/cdk/test/test.environment.ts b/packages/@aws-cdk/cdk/test/test.environment.ts index 47fac41072d0f..a7b65fe7b555a 100644 --- a/packages/@aws-cdk/cdk/test/test.environment.ts +++ b/packages/@aws-cdk/cdk/test/test.environment.ts @@ -1,5 +1,3 @@ -import { DEFAULT_ACCOUNT_CONTEXT_KEY, DEFAULT_REGION_CONTEXT_KEY } from '@aws-cdk/cx-api'; -import cxapi = require('@aws-cdk/cx-api'); import { Test } from 'nodeunit'; import { App, Aws, Stack, Token } from '../lib'; @@ -13,20 +11,6 @@ export = { test.done(); }, - 'Even if account and region are set in context, stack.account and region returns Refs)'(test: Test) { - const app = new App(); - - app.node.setContext(DEFAULT_ACCOUNT_CONTEXT_KEY, 'my-default-account'); - app.node.setContext(DEFAULT_REGION_CONTEXT_KEY, 'my-default-region'); - - const stack = new Stack(app, 'my-stack'); - - test.deepEqual(stack.resolve(stack.account), { Ref: 'AWS::AccountId' }); - test.deepEqual(stack.resolve(stack.region), { Ref: 'AWS::Region' }); - - test.done(); - }, - 'If only `env.region` or `env.account` are specified, Refs will be used for the other'(test: Test) { const app = new App(); @@ -43,52 +27,49 @@ export = { }, 'environment defaults': { - 'default-account-unknown-region'(test: Test) { + 'if "env" is not specified, it implies account/region agnostic'(test: Test) { // GIVEN const app = new App(); // WHEN - app.node.setContext(cxapi.DEFAULT_ACCOUNT_CONTEXT_KEY, 'my-default-account'); const stack = new Stack(app, 'stack'); // THEN - test.deepEqual(stack.resolve(stack.account), { Ref: 'AWS::AccountId' }); // TODO: after we implement #2866 this should be 'my-default-account' + test.deepEqual(stack.resolve(stack.account), { Ref: 'AWS::AccountId' }); test.deepEqual(stack.resolve(stack.region), { Ref: 'AWS::Region' }); test.deepEqual(app.synth().getStack(stack.stackName).environment, { - account: 'my-default-account', + account: 'unknown-account', region: 'unknown-region', - name: 'aws://my-default-account/unknown-region' + name: 'aws://unknown-account/unknown-region' }); test.done(); }, - 'default-account-explicit-region'(test: Test) { + 'only region is set'(test: Test) { // GIVEN const app = new App(); // WHEN - app.node.setContext(cxapi.DEFAULT_ACCOUNT_CONTEXT_KEY, 'my-default-account'); const stack = new Stack(app, 'stack', { env: { region: 'explicit-region' }}); // THEN - test.deepEqual(stack.resolve(stack.account), { Ref: 'AWS::AccountId' }); // TODO: after we implement #2866 this should be 'my-default-account' + test.deepEqual(stack.resolve(stack.account), { Ref: 'AWS::AccountId' }); test.deepEqual(stack.resolve(stack.region), 'explicit-region'); test.deepEqual(app.synth().getStack(stack.stackName).environment, { - account: 'my-default-account', + account: 'unknown-account', region: 'explicit-region', - name: 'aws://my-default-account/explicit-region' + name: 'aws://unknown-account/explicit-region' }); test.done(); }, - 'explicit-account-explicit-region'(test: Test) { + 'both "region" and "account" are set'(test: Test) { // GIVEN const app = new App(); // WHEN - app.node.setContext(cxapi.DEFAULT_ACCOUNT_CONTEXT_KEY, 'my-default-account'); const stack = new Stack(app, 'stack', { env: { account: 'explicit-account', region: 'explicit-region' @@ -106,34 +87,11 @@ export = { test.done(); }, - 'default-account-default-region'(test: Test) { + 'token-account and token-region'(test: Test) { // GIVEN const app = new App(); // WHEN - app.node.setContext(cxapi.DEFAULT_ACCOUNT_CONTEXT_KEY, 'my-default-account'); - app.node.setContext(cxapi.DEFAULT_REGION_CONTEXT_KEY, 'my-default-region'); - const stack = new Stack(app, 'stack'); - - // THEN - test.deepEqual(stack.resolve(stack.account), { Ref: 'AWS::AccountId' }); // TODO: after we implement #2866 this should be 'my-default-account' - test.deepEqual(stack.resolve(stack.region), { Ref: 'AWS::Region' }); // TODO: after we implement #2866 this should be 'my-default-region' - test.deepEqual(app.synth().getStack(stack.stackName).environment, { - account: 'my-default-account', - region: 'my-default-region', - name: 'aws://my-default-account/my-default-region' - }); - - test.done(); - }, - - 'token-account-token-region-no-defaults'(test: Test) { - // GIVEN - const app = new App(); - - // WHEN - app.node.setContext(cxapi.DEFAULT_ACCOUNT_CONTEXT_KEY, 'my-default-account'); - app.node.setContext(cxapi.DEFAULT_REGION_CONTEXT_KEY, 'my-default-region'); const stack = new Stack(app, 'stack', { env: { account: Aws.accountId, @@ -145,15 +103,15 @@ export = { test.deepEqual(stack.resolve(stack.account), { Ref: 'AWS::AccountId' }); test.deepEqual(stack.resolve(stack.region), { Ref: 'AWS::Region' }); test.deepEqual(app.synth().getStack(stack.stackName).environment, { - account: 'my-default-account', - region: 'my-default-region', - name: 'aws://my-default-account/my-default-region' + account: 'unknown-account', + region: 'unknown-region', + name: 'aws://unknown-account/unknown-region' }); test.done(); }, - 'token-account-token-region-with-defaults'(test: Test) { + 'token-account explicit region'(test: Test) { // GIVEN const app = new App(); @@ -161,17 +119,17 @@ export = { const stack = new Stack(app, 'stack', { env: { account: Aws.accountId, - region: Aws.region + region: 'us-east-2' } }); // THEN test.deepEqual(stack.resolve(stack.account), { Ref: 'AWS::AccountId' }); - test.deepEqual(stack.resolve(stack.region), { Ref: 'AWS::Region' }); + test.deepEqual(stack.resolve(stack.region), 'us-east-2'); test.deepEqual(app.synth().getStack(stack.stackName).environment, { account: 'unknown-account', - region: 'unknown-region', - name: 'aws://unknown-account/unknown-region' + region: 'us-east-2', + name: 'aws://unknown-account/us-east-2' }); test.done(); diff --git a/packages/@aws-cdk/cdk/test/test.stack.ts b/packages/@aws-cdk/cdk/test/test.stack.ts index 73e55363b5c98..7ea682d5d71b7 100644 --- a/packages/@aws-cdk/cdk/test/test.stack.ts +++ b/packages/@aws-cdk/cdk/test/test.stack.ts @@ -1,4 +1,3 @@ -import cxapi = require('@aws-cdk/cx-api'); import { Test } from 'nodeunit'; import { App, CfnCondition, CfnOutput, CfnParameter, CfnResource, Construct, ConstructNode, Include, Lazy, ScopedAws, Stack } from '../lib'; import { Intrinsic } from '../lib/private/intrinsic'; @@ -350,19 +349,6 @@ export = { test.done(); }, - 'stack with region supplied via context returns symbolic value'(test: Test) { - // GIVEN - const app = new App(); - - app.node.setContext(cxapi.DEFAULT_REGION_CONTEXT_KEY, 'es-norst-1'); - const stack = new Stack(app, 'Stack1'); - - // THEN - test.deepEqual(stack.resolve(stack.region), { Ref: 'AWS::Region' }); - - test.done(); - }, - 'overrideLogicalId(id) can be used to override the logical ID of a resource'(test: Test) { // GIVEN const stack = new Stack(); @@ -451,6 +437,22 @@ export = { test.throws(() => Stack.of(construct), /No stack could be identified for the construct at path/); test.done(); }, + + 'stack.availabilityZones falls back to Fn::GetAZ[0],[2] if region is not specified'(test: Test) { + // GIVEN + const app = new App(); + const stack = new Stack(app, 'MyStack'); + + // WHEN + const azs = stack.availabilityZones; + + // THEN + test.deepEqual(stack.resolve(azs), [ + { "Fn::Select": [ 0, { "Fn::GetAZs": "" } ] }, + { "Fn::Select": [ 1, { "Fn::GetAZs": "" } ] } + ]); + test.done(); + } }; class StackWithPostProcessor extends Stack { diff --git a/packages/@aws-cdk/cx-api/lib/context/availability-zones.ts b/packages/@aws-cdk/cx-api/lib/context/availability-zones.ts index fd66c220b9909..9fa2a2f9601d2 100644 --- a/packages/@aws-cdk/cx-api/lib/context/availability-zones.ts +++ b/packages/@aws-cdk/cx-api/lib/context/availability-zones.ts @@ -18,4 +18,13 @@ export interface AvailabilityZonesContextQuery { /** * Response of the AZ provider looks like this */ -export type AvailabilityZonesContextResponse = string[]; \ No newline at end of file +export type AvailabilityZonesContextResponse = string[]; + +/** + * This context key is used to determine the value of `stack.availabilityZones` + * when a stack is not associated with a specific account/region (env-agnostic). + * + * If this key is passed in the context, the values will be used. Otherwise, a + * system-fallback which uses `Fn::GetAZs` will be used. + */ +export const AVAILABILITY_ZONE_FALLBACK_CONTEXT_KEY = 'aws:cdk:availability-zones:fallback'; \ No newline at end of file diff --git a/packages/@aws-cdk/cx-api/lib/cxapi.ts b/packages/@aws-cdk/cx-api/lib/cxapi.ts index e918c087d9d17..ce3038dafdb78 100644 --- a/packages/@aws-cdk/cx-api/lib/cxapi.ts +++ b/packages/@aws-cdk/cx-api/lib/cxapi.ts @@ -5,14 +5,14 @@ export const OUTDIR_ENV = 'CDK_OUTDIR'; export const CONTEXT_ENV = 'CDK_CONTEXT_JSON'; /** - * Context parameter for the default AWS account to use if a stack's environment is not set. + * Environment variable set by the CDK CLI with the default AWS account ID. */ -export const DEFAULT_ACCOUNT_CONTEXT_KEY = 'aws:cdk:toolkit:default-account'; +export const DEFAULT_ACCOUNT_ENV = 'CDK_DEFAULT_ACCOUNT'; /** - * Context parameter for the default AWS region to use if a stack's environment is not set. + * Environment variable set by the CDK CLI with the default AWS region. */ -export const DEFAULT_REGION_CONTEXT_KEY = 'aws:cdk:toolkit:default-region'; +export const DEFAULT_REGION_ENV = 'CDK_DEFAULT_REGION'; /** * Enables the embedding of the "aws:cdk:path" in CloudFormation template metadata. diff --git a/packages/@aws-cdk/cx-api/lib/environment.ts b/packages/@aws-cdk/cx-api/lib/environment.ts index cb6845f058af9..42eb7697b8662 100644 --- a/packages/@aws-cdk/cx-api/lib/environment.ts +++ b/packages/@aws-cdk/cx-api/lib/environment.ts @@ -19,6 +19,9 @@ export interface Environment { readonly region: string; } +export const UNKNOWN_ACCOUNT = 'unknown-account'; +export const UNKNOWN_REGION = 'unknown-region'; + export class EnvironmentUtils { public static parse(environment: string): Environment { const env = AWS_ENV_REGEX.exec(environment); diff --git a/packages/aws-cdk/lib/api/cxapp/exec.ts b/packages/aws-cdk/lib/api/cxapp/exec.ts index c0d987a65d4f6..35fc1f67f106c 100644 --- a/packages/aws-cdk/lib/api/cxapp/exec.ts +++ b/packages/aws-cdk/lib/api/cxapp/exec.ts @@ -11,7 +11,7 @@ export async function execProgram(aws: SDK, config: Configuration): Promise { debug(`Reading existing template for stack ${stack.name}.`); - const cfn = await this.aws.cloudFormation(stack.environment, Mode.ForReading); + const cfn = await this.aws.cloudFormation(stack.environment.account, stack.environment.region, Mode.ForReading); try { const response = await cfn.getTemplate({ StackName: stack.name }).promise(); return (response.TemplateBody && deserializeStructure(response.TemplateBody)) || {}; diff --git a/packages/aws-cdk/lib/api/toolkit-info.ts b/packages/aws-cdk/lib/api/toolkit-info.ts index 81df8bb502462..25339696d58ab 100644 --- a/packages/aws-cdk/lib/api/toolkit-info.ts +++ b/packages/aws-cdk/lib/api/toolkit-info.ts @@ -52,7 +52,7 @@ export class ToolkitInfo { * already exists by this key. */ public async uploadIfChanged(data: string | Buffer | DataView, props: UploadProps): Promise { - const s3 = await this.props.sdk.s3(this.props.environment, Mode.ForWriting); + const s3 = await this.props.sdk.s3(this.props.environment.account, this.props.environment.region, Mode.ForWriting); const s3KeyPrefix = props.s3KeyPrefix || ''; const s3KeySuffix = props.s3KeySuffix || ''; @@ -101,7 +101,7 @@ export class ToolkitInfo { * Prepare an ECR repository for uploading to using Docker */ public async prepareEcrRepository(asset: cxapi.ContainerImageAssetMetadataEntry): Promise { - const ecr = await this.props.sdk.ecr(this.props.environment, Mode.ForWriting); + const ecr = await this.props.sdk.ecr(this.props.environment.account, this.props.environment.region, Mode.ForWriting); let repositoryName; if ( asset.repositoryName ) { // Repository name provided by user @@ -148,7 +148,7 @@ export class ToolkitInfo { * Get ECR credentials */ public async getEcrCredentials(): Promise { - const ecr = await this.props.sdk.ecr(this.props.environment, Mode.ForReading); + const ecr = await this.props.sdk.ecr(this.props.environment.account, this.props.environment.region, Mode.ForReading); debug(`Fetching ECR authorization token`); const authData = (await ecr.getAuthorizationToken({ }).promise()).authorizationData || []; @@ -169,7 +169,7 @@ export class ToolkitInfo { * Check if image already exists in ECR repository */ public async checkEcrImage(repositoryName: string, imageTag: string): Promise { - const ecr = await this.props.sdk.ecr(this.props.environment, Mode.ForReading); + const ecr = await this.props.sdk.ecr(this.props.environment.account, this.props.environment.region, Mode.ForReading); try { debug(`${repositoryName}: checking for image ${imageTag}`); @@ -210,7 +210,7 @@ async function objectExists(s3: aws.S3, bucket: string, key: string) { } export async function loadToolkitInfo(environment: cxapi.Environment, sdk: SDK, stackName: string): Promise { - const cfn = await sdk.cloudFormation(environment, Mode.ForReading); + const cfn = await sdk.cloudFormation(environment.account, environment.region, Mode.ForReading); const stack = await waitForStack(cfn, stackName); if (!stack) { debug('The environment %s doesn\'t have the CDK toolkit stack (%s) installed. Use %s to setup your environment for use with the toolkit.', diff --git a/packages/aws-cdk/lib/api/util/sdk.ts b/packages/aws-cdk/lib/api/util/sdk.ts index 659d0d25a0668..f1dd0d76e97d3 100644 --- a/packages/aws-cdk/lib/api/util/sdk.ts +++ b/packages/aws-cdk/lib/api/util/sdk.ts @@ -88,7 +88,8 @@ export class SDK { this.credentialsCache = new CredentialsCache(this.defaultAwsAccount, defaultCredentialProvider); } - public async cloudFormation(environment: cxapi.Environment, mode: Mode): Promise { + public async cloudFormation(account: string | undefined, region: string | undefined, mode: Mode): Promise { + const environment = await this.resolveEnvironment(account, region); return new AWS.CloudFormation({ ...this.retryOptions, region: environment.region, @@ -96,23 +97,26 @@ export class SDK { }); } - public async ec2(awsAccountId: string | undefined, region: string | undefined, mode: Mode): Promise { + public async ec2(account: string | undefined, region: string | undefined, mode: Mode): Promise { + const environment = await this.resolveEnvironment(account, region); return new AWS.EC2({ ...this.retryOptions, - region, - credentials: await this.credentialsCache.get(awsAccountId, mode) + region: environment.region, + credentials: await this.credentialsCache.get(environment.account, mode) }); } - public async ssm(awsAccountId: string | undefined, region: string | undefined, mode: Mode): Promise { + public async ssm(account: string | undefined, region: string | undefined, mode: Mode): Promise { + const environment = await this.resolveEnvironment(account, region); return new AWS.SSM({ ...this.retryOptions, - region, - credentials: await this.credentialsCache.get(awsAccountId, mode) + region: environment.account, + credentials: await this.credentialsCache.get(environment.region, mode) }); } - public async s3(environment: cxapi.Environment, mode: Mode): Promise { + public async s3(account: string | undefined, region: string | undefined, mode: Mode): Promise { + const environment = await this.resolveEnvironment(account, region); return new AWS.S3({ ...this.retryOptions, region: environment.region, @@ -120,15 +124,17 @@ export class SDK { }); } - public async route53(awsAccountId: string | undefined, region: string | undefined, mode: Mode): Promise { + public async route53(account: string | undefined, region: string | undefined, mode: Mode): Promise { + const environment = await this.resolveEnvironment(account, region); return new AWS.Route53({ ...this.retryOptions, - region, - credentials: await this.credentialsCache.get(awsAccountId, mode), + region: environment.region, + credentials: await this.credentialsCache.get(environment.account, mode), }); } - public async ecr(environment: cxapi.Environment, mode: Mode): Promise { + public async ecr(account: string | undefined, region: string | undefined, mode: Mode): Promise { + const environment = await this.resolveEnvironment(account, region); return new AWS.ECR({ ...this.retryOptions, region: environment.region, @@ -143,6 +149,31 @@ export class SDK { public defaultAccount(): Promise { return this.defaultAwsAccount.get(); } + + private async resolveEnvironment(account: string | undefined, region: string | undefined, ) { + if (region === cxapi.UNKNOWN_REGION) { + region = await this.defaultRegion(); + } + + if (account === cxapi.UNKNOWN_ACCOUNT) { + account = await this.defaultAccount(); + } + + if (!region) { + throw new Error(`AWS region must be configured either when you configure your CDK stack or through the environment`); + } + + if (!account) { + throw new Error(`Unable to resolve AWS account to use. It must be either configured when you define your CDK or through the environment`); + } + + const environment: cxapi.Environment = { + region, account, name: cxapi.EnvironmentUtils.format(account, region) + }; + + return environment; + } + } /** @@ -177,7 +208,7 @@ class CredentialsCache { // If requested account is undefined or equal to default account, use default credentials provider. // (Note that we ignore the mode in this case, if you preloaded credentials they better be correct!) const defaultAccount = await this.defaultAwsAccount.get(); - if (!awsAccountId || awsAccountId === defaultAccount) { + if (!awsAccountId || awsAccountId === defaultAccount || awsAccountId === cxapi.UNKNOWN_ACCOUNT) { debug(`Using default AWS SDK credentials for account ${awsAccountId}`); // CredentialProviderChain extends Credentials, but that is a lie. diff --git a/packages/decdk/test/__snapshots__/synth.test.js.snap b/packages/decdk/test/__snapshots__/synth.test.js.snap index cc7b17b3ea085..bf327752cb659 100644 --- a/packages/decdk/test/__snapshots__/synth.test.js.snap +++ b/packages/decdk/test/__snapshots__/synth.test.js.snap @@ -791,7 +791,14 @@ Object { }, "VPCPrivateSubnet1Subnet8BCA10E0": Object { "Properties": Object { - "AvailabilityZone": "dummy1a", + "AvailabilityZone": Object { + "Fn::Select": Array [ + 0, + Object { + "Fn::GetAZs": "", + }, + ], + }, "CidrBlock": "10.0.128.0/17", "MapPublicIpOnLaunch": false, "Tags": Array [ @@ -882,7 +889,14 @@ Object { }, "VPCPublicSubnet1SubnetB4246D30": Object { "Properties": Object { - "AvailabilityZone": "dummy1a", + "AvailabilityZone": Object { + "Fn::Select": Array [ + 0, + Object { + "Fn::GetAZs": "", + }, + ], + }, "CidrBlock": "10.0.0.0/17", "MapPublicIpOnLaunch": true, "Tags": Array [ @@ -2670,8 +2684,15 @@ Object { }, "VPCPrivateSubnet1Subnet8BCA10E0": Object { "Properties": Object { - "AvailabilityZone": "dummy1a", - "CidrBlock": "10.0.96.0/19", + "AvailabilityZone": Object { + "Fn::Select": Array [ + 0, + Object { + "Fn::GetAZs": "", + }, + ], + }, + "CidrBlock": "10.0.128.0/18", "MapPublicIpOnLaunch": false, "Tags": Array [ Object { @@ -2732,75 +2753,20 @@ Object { }, "VPCPrivateSubnet2SubnetCFCDAA7A": Object { "Properties": Object { - "AvailabilityZone": "dummy1b", - "CidrBlock": "10.0.128.0/19", - "MapPublicIpOnLaunch": false, - "Tags": Array [ - Object { - "Key": "Name", - "Value": "vpc/VPC/PrivateSubnet2", - }, - Object { - "Key": "aws-cdk:subnet-name", - "Value": "Private", - }, - Object { - "Key": "aws-cdk:subnet-type", - "Value": "Private", - }, - ], - "VpcId": Object { - "Ref": "VPCB9E5F0B4", - }, - }, - "Type": "AWS::EC2::Subnet", - }, - "VPCPrivateSubnet3DefaultRoute27F311AE": Object { - "Properties": Object { - "DestinationCidrBlock": "0.0.0.0/0", - "NatGatewayId": Object { - "Ref": "VPCPublicSubnet3NATGatewayD3048F5C", - }, - "RouteTableId": Object { - "Ref": "VPCPrivateSubnet3RouteTable192186F8", - }, - }, - "Type": "AWS::EC2::Route", - }, - "VPCPrivateSubnet3RouteTable192186F8": Object { - "Properties": Object { - "Tags": Array [ - Object { - "Key": "Name", - "Value": "vpc/VPC/PrivateSubnet3", - }, - ], - "VpcId": Object { - "Ref": "VPCB9E5F0B4", - }, - }, - "Type": "AWS::EC2::RouteTable", - }, - "VPCPrivateSubnet3RouteTableAssociationC28D144E": Object { - "Properties": Object { - "RouteTableId": Object { - "Ref": "VPCPrivateSubnet3RouteTable192186F8", - }, - "SubnetId": Object { - "Ref": "VPCPrivateSubnet3Subnet3EDCD457", + "AvailabilityZone": Object { + "Fn::Select": Array [ + 1, + Object { + "Fn::GetAZs": "", + }, + ], }, - }, - "Type": "AWS::EC2::SubnetRouteTableAssociation", - }, - "VPCPrivateSubnet3Subnet3EDCD457": Object { - "Properties": Object { - "AvailabilityZone": "dummy1c", - "CidrBlock": "10.0.160.0/19", + "CidrBlock": "10.0.192.0/18", "MapPublicIpOnLaunch": false, "Tags": Array [ Object { "Key": "Name", - "Value": "vpc/VPC/PrivateSubnet3", + "Value": "vpc/VPC/PrivateSubnet2", }, Object { "Key": "aws-cdk:subnet-name", @@ -2885,8 +2851,15 @@ Object { }, "VPCPublicSubnet1SubnetB4246D30": Object { "Properties": Object { - "AvailabilityZone": "dummy1a", - "CidrBlock": "10.0.0.0/19", + "AvailabilityZone": Object { + "Fn::Select": Array [ + 0, + Object { + "Fn::GetAZs": "", + }, + ], + }, + "CidrBlock": "10.0.0.0/18", "MapPublicIpOnLaunch": true, "Tags": Array [ Object { @@ -2976,104 +2949,20 @@ Object { }, "VPCPublicSubnet2Subnet74179F39": Object { "Properties": Object { - "AvailabilityZone": "dummy1b", - "CidrBlock": "10.0.32.0/19", - "MapPublicIpOnLaunch": true, - "Tags": Array [ - Object { - "Key": "Name", - "Value": "vpc/VPC/PublicSubnet2", - }, - Object { - "Key": "aws-cdk:subnet-name", - "Value": "Public", - }, - Object { - "Key": "aws-cdk:subnet-type", - "Value": "Public", - }, - ], - "VpcId": Object { - "Ref": "VPCB9E5F0B4", - }, - }, - "Type": "AWS::EC2::Subnet", - }, - "VPCPublicSubnet3DefaultRouteA0D29D46": Object { - "DependsOn": Array [ - "VPCVPCGW99B986DC", - ], - "Properties": Object { - "DestinationCidrBlock": "0.0.0.0/0", - "GatewayId": Object { - "Ref": "VPCIGWB7E252D3", - }, - "RouteTableId": Object { - "Ref": "VPCPublicSubnet3RouteTable98AE0E14", - }, - }, - "Type": "AWS::EC2::Route", - }, - "VPCPublicSubnet3EIPAD4BC883": Object { - "Properties": Object { - "Domain": "vpc", - }, - "Type": "AWS::EC2::EIP", - }, - "VPCPublicSubnet3NATGatewayD3048F5C": Object { - "Properties": Object { - "AllocationId": Object { - "Fn::GetAtt": Array [ - "VPCPublicSubnet3EIPAD4BC883", - "AllocationId", + "AvailabilityZone": Object { + "Fn::Select": Array [ + 1, + Object { + "Fn::GetAZs": "", + }, ], }, - "SubnetId": Object { - "Ref": "VPCPublicSubnet3Subnet631C5E25", - }, - "Tags": Array [ - Object { - "Key": "Name", - "Value": "vpc/VPC/PublicSubnet3", - }, - ], - }, - "Type": "AWS::EC2::NatGateway", - }, - "VPCPublicSubnet3RouteTable98AE0E14": Object { - "Properties": Object { - "Tags": Array [ - Object { - "Key": "Name", - "Value": "vpc/VPC/PublicSubnet3", - }, - ], - "VpcId": Object { - "Ref": "VPCB9E5F0B4", - }, - }, - "Type": "AWS::EC2::RouteTable", - }, - "VPCPublicSubnet3RouteTableAssociation427FE0C6": Object { - "Properties": Object { - "RouteTableId": Object { - "Ref": "VPCPublicSubnet3RouteTable98AE0E14", - }, - "SubnetId": Object { - "Ref": "VPCPublicSubnet3Subnet631C5E25", - }, - }, - "Type": "AWS::EC2::SubnetRouteTableAssociation", - }, - "VPCPublicSubnet3Subnet631C5E25": Object { - "Properties": Object { - "AvailabilityZone": "dummy1c", - "CidrBlock": "10.0.64.0/19", + "CidrBlock": "10.0.64.0/18", "MapPublicIpOnLaunch": true, "Tags": Array [ Object { "Key": "Name", - "Value": "vpc/VPC/PublicSubnet3", + "Value": "vpc/VPC/PublicSubnet2", }, Object { "Key": "aws-cdk:subnet-name", diff --git a/tools/cdk-integ-tools/bin/cdk-integ-assert.ts b/tools/cdk-integ-tools/bin/cdk-integ-assert.ts index c6d27bb0dbf39..0ca658490bc7d 100644 --- a/tools/cdk-integ-tools/bin/cdk-integ-assert.ts +++ b/tools/cdk-integ-tools/bin/cdk-integ-assert.ts @@ -1,7 +1,7 @@ #!/usr/bin/env node // Verify that all integration tests still match their expected output import { diffTemplate, formatDifferences } from '@aws-cdk/cloudformation-diff'; -import { IntegrationTests, STATIC_TEST_CONTEXT } from '../lib/integ-helpers'; +import { DEFAULT_SYNTH_OPTIONS, IntegrationTests } from '../lib/integ-helpers'; // tslint:disable:no-console @@ -10,7 +10,7 @@ async function main() { const failures: string[] = []; for (const test of tests) { - process.stdout.write(`Verifying ${test.name} against ${test.expectedFileName}... `); + process.stdout.write(`Verifying ${test.name} against ${test.expectedFileName} ... `); if (!test.hasExpected()) { throw new Error(`No such file: ${test.expectedFileName}. Run 'npm run integ'.`); @@ -23,8 +23,10 @@ async function main() { args.push('--no-path-metadata'); args.push('--no-asset-metadata'); args.push('--no-staging'); - - const actual = await test.invoke(['--json', ...args, 'synth', ...stackToDeploy], { json: true, context: STATIC_TEST_CONTEXT }); + const actual = await test.invoke(['--json', ...args, 'synth', ...stackToDeploy], { + json: true, + ...DEFAULT_SYNTH_OPTIONS + }); const diff = diffTemplate(expected, actual); diff --git a/tools/cdk-integ-tools/bin/cdk-integ.ts b/tools/cdk-integ-tools/bin/cdk-integ.ts index 691aa3fc4583e..78648f5429bdb 100644 --- a/tools/cdk-integ-tools/bin/cdk-integ.ts +++ b/tools/cdk-integ-tools/bin/cdk-integ.ts @@ -1,7 +1,7 @@ #!/usr/bin/env node // Exercise all integ stacks and if they deploy, update the expected synth files import yargs = require('yargs'); -import { IntegrationTests, STATIC_TEST_CONTEXT } from '../lib/integ-helpers'; +import { DEFAULT_SYNTH_OPTIONS, IntegrationTests } from '../lib/integ-helpers'; // tslint:disable:no-console @@ -40,22 +40,25 @@ async function main() { try { // tslint:disable-next-line:max-line-length - await test.invoke([ ...args, 'deploy', '--require-approval', 'never', ...stackToDeploy ], { verbose: argv.verbose }); // Note: no context, so use default user settings! + await test.invoke([ ...args, 'deploy', '--require-approval', 'never', ...stackToDeploy ], { + verbose: argv.verbose + // Note: no "context" and "env", so use default user settings! + }); console.error(`Success! Writing out reference synth.`); // If this all worked, write the new expectation file const actual = await test.invoke([ ...args, '--json', 'synth', ...stackToDeploy ], { json: true, - context: STATIC_TEST_CONTEXT, - verbose: argv.verbose + verbose: argv.verbose, + ...DEFAULT_SYNTH_OPTIONS }); await test.writeExpected(actual); } finally { if (argv.clean) { console.error(`Cleaning up.`); - await test.invoke(['destroy', '--force', ...stackToDeploy]); + await test.invoke(['destroy', '--force', ...stackToDeploy ]); } else { console.error('Skipping clean up (--no-clean).'); } diff --git a/tools/cdk-integ-tools/lib/integ-helpers.ts b/tools/cdk-integ-tools/lib/integ-helpers.ts index 199b75d07da61..a776839e40b5b 100644 --- a/tools/cdk-integ-tools/lib/integ-helpers.ts +++ b/tools/cdk-integ-tools/lib/integ-helpers.ts @@ -1,8 +1,8 @@ // Helper functions for integration tests -import { DEFAULT_ACCOUNT_CONTEXT_KEY, DEFAULT_REGION_CONTEXT_KEY } from '@aws-cdk/cx-api'; import { spawnSync } from 'child_process'; import fs = require('fs-extra'); import path = require('path'); +import { AVAILABILITY_ZONE_FALLBACK_CONTEXT_KEY } from '../../../packages/@aws-cdk/cx-api/lib'; const CDK_INTEG_STACK_PRAGMA = '/// !cdk-integ'; @@ -78,7 +78,7 @@ export class IntegrationTest { this.cdkContextPath = path.join(this.directory, 'cdk.context.json'); } - public async invoke(args: string[], options: { json?: boolean, context?: any, verbose?: boolean } = { }): Promise { + public async invoke(args: string[], options: { json?: boolean, context?: any, verbose?: boolean, env?: any } = { }): Promise { // Write context to cdk.json, afterwards delete. We need to do this because there is no way // to pass structured context data from the command-line, currently. if (options.context) { @@ -93,6 +93,7 @@ export class IntegrationTest { cwd: this.directory, json: options.json, verbose: options.verbose, + env: options.env }); } finally { this.deleteCdkContext(); @@ -120,7 +121,7 @@ export class IntegrationTest { return pragma; } - const stacks = (await this.invoke([ 'ls' ], { context: STATIC_TEST_CONTEXT })).split('\n'); + const stacks = (await this.invoke([ 'ls' ], { ...DEFAULT_SYNTH_OPTIONS })).split('\n'); if (stacks.length !== 1) { throw new Error(`"cdk-integ" can only operate on apps with a single stack.\n\n` + ` If your app has multiple stacks, specify which stack to select by adding this to your test source:\n\n` + @@ -175,30 +176,36 @@ export class IntegrationTest { // Default context we run all integ tests with, so they don't depend on the // account of the exercising user. -export const STATIC_TEST_CONTEXT = { - [DEFAULT_ACCOUNT_CONTEXT_KEY]: "12345678", - [DEFAULT_REGION_CONTEXT_KEY]: "test-region", - "availability-zones:account=12345678:region=test-region": [ "test-region-1a", "test-region-1b", "test-region-1c" ], - "ssm:account=12345678:parameterName=/aws/service/ami-amazon-linux-latest/amzn-ami-hvm-x86_64-gp2:region=test-region": "ami-1234", - "ssm:account=12345678:parameterName=/aws/service/ami-amazon-linux-latest/amzn2-ami-hvm-x86_64-gp2:region=test-region": "ami-1234", - "ssm:account=12345678:parameterName=/aws/service/ecs/optimized-ami/amazon-linux/recommended:region=test-region": "{\"image_id\": \"ami-1234\"}", - "vpc-provider:account=12345678:filter.isDefault=true:region=test-region": { - vpcId: "vpc-60900905", - availabilityZones: [ "us-east-1a", "us-east-1b", "us-east-1c" ], - publicSubnetIds: [ "subnet-e19455ca", "subnet-e0c24797", "subnet-ccd77395", ], - publicSubnetNames: [ "Public" ] +export const DEFAULT_SYNTH_OPTIONS = { + context: { + [AVAILABILITY_ZONE_FALLBACK_CONTEXT_KEY]: [ "test-region-1a", "test-region-1b", "test-region-1c" ], + "availability-zones:account=12345678:region=test-region": [ "test-region-1a", "test-region-1b", "test-region-1c" ], + "ssm:account=12345678:parameterName=/aws/service/ami-amazon-linux-latest/amzn-ami-hvm-x86_64-gp2:region=test-region": "ami-1234", + "ssm:account=12345678:parameterName=/aws/service/ami-amazon-linux-latest/amzn2-ami-hvm-x86_64-gp2:region=test-region": "ami-1234", + "ssm:account=12345678:parameterName=/aws/service/ecs/optimized-ami/amazon-linux/recommended:region=test-region": "{\"image_id\": \"ami-1234\"}", + "vpc-provider:account=12345678:filter.isDefault=true:region=test-region": { + vpcId: "vpc-60900905", + availabilityZones: [ "us-east-1a", "us-east-1b", "us-east-1c" ], + publicSubnetIds: [ "subnet-e19455ca", "subnet-e0c24797", "subnet-ccd77395", ], + publicSubnetNames: [ "Public" ] + } + }, + env: { + CDK_INTEG_ACCOUNT: "12345678", + CDK_INTEG_REGION: "test-region", } }; /** * Our own execute function which doesn't use shells and strings. */ -function exec(commandLine: string[], options: { cwd?: string, json?: boolean, verbose?: boolean} = { }): any { +function exec(commandLine: string[], options: { cwd?: string, json?: boolean, verbose?: boolean, env?: any } = { }): any { const proc = spawnSync(commandLine[0], commandLine.slice(1), { stdio: [ 'ignore', 'pipe', options.verbose ? 'inherit' : 'pipe' ], // inherit STDERR in verbose mode env: { ...process.env, - CDK_INTEG_MODE: '1' + CDK_INTEG_MODE: '1', + ...options.env, }, cwd: options.cwd });