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 });