diff --git a/packages/@aws-cdk/aws-ec2/README.md b/packages/@aws-cdk/aws-ec2/README.md index 1a442e104e4b0..3b902e5cb8c75 100644 --- a/packages/@aws-cdk/aws-ec2/README.md +++ b/packages/@aws-cdk/aws-ec2/README.md @@ -538,6 +538,9 @@ examples of things you might want to use: > `cdk.context.json`, or use the `cdk context` command. For more information, see > [Runtime Context](https://docs.aws.amazon.com/cdk/latest/guide/context.html) in the CDK > developer guide. +> +> `MachineImage.genericLinux()`, `MachineImage.genericWindows()` will use `CfnMapping` in +> an agnostic stack. ## Special VPC configurations diff --git a/packages/@aws-cdk/aws-ec2/lib/machine-image.ts b/packages/@aws-cdk/aws-ec2/lib/machine-image.ts index 1871fc1e75ac5..df4a1eece07e0 100644 --- a/packages/@aws-cdk/aws-ec2/lib/machine-image.ts +++ b/packages/@aws-cdk/aws-ec2/lib/machine-image.ts @@ -1,6 +1,6 @@ import * as ssm from '@aws-cdk/aws-ssm'; import * as cxschema from '@aws-cdk/cloud-assembly-schema'; -import { ContextProvider, Stack, Token } from '@aws-cdk/core'; +import { ContextProvider, CfnMapping, Aws, Stack, Token } from '@aws-cdk/core'; import * as cxapi from '@aws-cdk/cx-api'; import { UserData } from './user-data'; import { WindowsVersion } from './windows-versions'; @@ -367,24 +367,33 @@ export interface GenericWindowsImageProps { * manually specify an AMI map. */ export class GenericLinuxImage implements IMachineImage { - constructor(private readonly amiMap: {[region: string]: string}, private readonly props: GenericLinuxImageProps = {}) { + constructor(private readonly amiMap: { [region: string]: string }, private readonly props: GenericLinuxImageProps = {}) { } public getImage(scope: Construct): MachineImageConfig { + const userData = this.props.userData ?? UserData.forLinux(); + const osType = OperatingSystemType.LINUX; const region = Stack.of(scope).region; if (Token.isUnresolved(region)) { - throw new Error('Unable to determine AMI from AMI map since stack is region-agnostic'); + const mapping: { [k1: string]: { [k2: string]: any } } = {}; + for (const [rgn, ami] of Object.entries(this.amiMap)) { + mapping[rgn] = { ami }; + } + const amiMap = new CfnMapping(scope, 'AmiMap', { mapping }); + return { + imageId: amiMap.findInMap(Aws.REGION, 'ami'), + userData, + osType, + }; } - - const ami = region !== 'test-region' ? this.amiMap[region] : 'ami-12345'; - if (!ami) { + const imageId = region !== 'test-region' ? this.amiMap[region] : 'ami-12345'; + if (!imageId) { throw new Error(`Unable to find AMI in AMI map: no AMI specified for region '${region}'`); } - return { - imageId: ami, - userData: this.props.userData ?? UserData.forLinux(), - osType: OperatingSystemType.LINUX, + imageId, + userData, + osType, }; } } @@ -399,20 +408,29 @@ export class GenericWindowsImage implements IMachineImage { } public getImage(scope: Construct): MachineImageConfig { + const userData = this.props.userData ?? UserData.forWindows(); + const osType = OperatingSystemType.WINDOWS; const region = Stack.of(scope).region; if (Token.isUnresolved(region)) { - throw new Error('Unable to determine AMI from AMI map since stack is region-agnostic'); + const mapping: { [k1: string]: { [k2: string]: any } } = {}; + for (const [rgn, ami] of Object.entries(this.amiMap)) { + mapping[rgn] = { ami }; + } + const amiMap = new CfnMapping(scope, 'AmiMap', { mapping }); + return { + imageId: amiMap.findInMap(Aws.REGION, 'ami'), + userData, + osType, + }; } - - const ami = region !== 'test-region' ? this.amiMap[region] : 'ami-12345'; - if (!ami) { + const imageId = region !== 'test-region' ? this.amiMap[region] : 'ami-12345'; + if (!imageId) { throw new Error(`Unable to find AMI in AMI map: no AMI specified for region '${region}'`); } - return { - imageId: ami, - userData: this.props.userData ?? UserData.forWindows(), - osType: OperatingSystemType.WINDOWS, + imageId, + userData, + osType, }; } } diff --git a/packages/@aws-cdk/aws-ec2/lib/vpc.ts b/packages/@aws-cdk/aws-ec2/lib/vpc.ts index 4e3f90780db4c..2a4e524fc5150 100644 --- a/packages/@aws-cdk/aws-ec2/lib/vpc.ts +++ b/packages/@aws-cdk/aws-ec2/lib/vpc.ts @@ -1886,6 +1886,7 @@ class LookedUpVpc extends VpcBase { availabilityZone: vpcSubnet.availabilityZone, subnetId: vpcSubnet.subnetId, routeTableId: vpcSubnet.routeTableId, + ipv4CidrBlock: vpcSubnet.cidr, })); } return ret; diff --git a/packages/@aws-cdk/aws-ec2/lib/vpn.ts b/packages/@aws-cdk/aws-ec2/lib/vpn.ts index 4683180d2af84..19fb4f963acd3 100644 --- a/packages/@aws-cdk/aws-ec2/lib/vpn.ts +++ b/packages/@aws-cdk/aws-ec2/lib/vpn.ts @@ -224,7 +224,7 @@ export class VpnConnection extends Resource implements IVpnConnection { }); } - if (!net.isIPv4(props.ip)) { + if (!Token.isUnresolved(props.ip) && !net.isIPv4(props.ip)) { throw new Error(`The \`ip\` ${props.ip} is not a valid IPv4 address.`); } diff --git a/packages/@aws-cdk/aws-ec2/test/machine-image.test.ts b/packages/@aws-cdk/aws-ec2/test/machine-image.test.ts index 2fcf7e2980be9..43e6dbdcd88cf 100644 --- a/packages/@aws-cdk/aws-ec2/test/machine-image.test.ts +++ b/packages/@aws-cdk/aws-ec2/test/machine-image.test.ts @@ -1,3 +1,4 @@ +import { expect as cdkExpect, matchTemplate, MatchStyle } from '@aws-cdk/assert'; import { App, Stack } from '@aws-cdk/core'; import * as ec2 from '../lib'; @@ -11,6 +12,43 @@ beforeEach(() => { }); }); +test('can make and use a Linux image', () => { + // WHEN + const image = new ec2.GenericLinuxImage({ + testregion: 'ami-1234', + }); + + // THEN + const details = image.getImage(stack); + expect(details.imageId).toEqual('ami-1234'); + expect(details.osType).toEqual(ec2.OperatingSystemType.LINUX); +}); + +test('can make and use a Linux image in agnostic stack', () => { + // WHEN + app = new App(); + stack = new Stack(app, 'Stack'); + const image = new ec2.GenericLinuxImage({ + testregion: 'ami-1234', + }); + + // THEN + const details = image.getImage(stack); + const expected = { + Mappings: { + AmiMap: { + testregion: { + ami: 'ami-1234', + }, + }, + }, + }; + + cdkExpect(stack).to(matchTemplate(expected, MatchStyle.EXACT)); + expect(stack.resolve(details.imageId)).toEqual({ 'Fn::FindInMap': ['AmiMap', { Ref: 'AWS::Region' }, 'ami'] }); + expect(details.osType).toEqual(ec2.OperatingSystemType.LINUX); +}); + test('can make and use a Windows image', () => { // WHEN const image = new ec2.GenericWindowsImage({ @@ -23,6 +61,31 @@ test('can make and use a Windows image', () => { expect(details.osType).toEqual(ec2.OperatingSystemType.WINDOWS); }); +test('can make and use a Windows image in agnostic stack', () => { + // WHEN + app = new App(); + stack = new Stack(app, 'Stack'); + const image = new ec2.GenericWindowsImage({ + testregion: 'ami-1234', + }); + + // THEN + const details = image.getImage(stack); + const expected = { + Mappings: { + AmiMap: { + testregion: { + ami: 'ami-1234', + }, + }, + }, + }; + + cdkExpect(stack).to(matchTemplate(expected, MatchStyle.EXACT)); + expect(stack.resolve(details.imageId)).toEqual({ 'Fn::FindInMap': ['AmiMap', { Ref: 'AWS::Region' }, 'ami'] }); + expect(details.osType).toEqual(ec2.OperatingSystemType.WINDOWS); +}); + test('can make and use a Generic SSM image', () => { // WHEN const image = new ec2.GenericSSMParameterImage('testParam', ec2.OperatingSystemType.LINUX); diff --git a/packages/@aws-cdk/aws-ec2/test/vpc.from-lookup.test.ts b/packages/@aws-cdk/aws-ec2/test/vpc.from-lookup.test.ts index 5555dc3fa9ed7..12ed7d05329d5 100644 --- a/packages/@aws-cdk/aws-ec2/test/vpc.from-lookup.test.ts +++ b/packages/@aws-cdk/aws-ec2/test/vpc.from-lookup.test.ts @@ -210,6 +210,47 @@ nodeunitShim({ test.done(); }, + 'subnets in imported VPC has all expected attributes'(test: Test) { + const previous = mockVpcContextProviderWith(test, { + vpcId: 'vpc-1234', + subnetGroups: [ + { + name: 'Public', + type: cxapi.VpcSubnetGroupType.PUBLIC, + subnets: [ + { + subnetId: 'pub-sub-in-us-east-1a', + availabilityZone: 'us-east-1a', + routeTableId: 'rt-123', + cidr: '10.100.0.0/24', + }, + ], + }, + ], + }, options => { + test.deepEqual(options.filter, { + isDefault: 'true', + }); + + test.equal(options.subnetGroupNameTag, undefined); + }); + + const stack = new Stack(); + const vpc = Vpc.fromLookup(stack, 'Vpc', { + isDefault: true, + }); + + let subnet = vpc.publicSubnets[0]; + + test.equal(subnet.availabilityZone, 'us-east-1a'); + test.equal(subnet.subnetId, 'pub-sub-in-us-east-1a'); + test.equal(subnet.routeTable.routeTableId, 'rt-123'); + test.equal(subnet.ipv4CidrBlock, '10.100.0.0/24'); + + + restoreContextProvider(previous); + test.done(); + }, }, }); diff --git a/packages/@aws-cdk/aws-ec2/test/vpn.test.ts b/packages/@aws-cdk/aws-ec2/test/vpn.test.ts index 72be541bf45e2..4cf8880c3b0ab 100644 --- a/packages/@aws-cdk/aws-ec2/test/vpn.test.ts +++ b/packages/@aws-cdk/aws-ec2/test/vpn.test.ts @@ -1,5 +1,5 @@ import { expect, haveResource } from '@aws-cdk/assert'; -import { Duration, Stack } from '@aws-cdk/core'; +import { Duration, Stack, Token } from '@aws-cdk/core'; import { nodeunitShim, Test } from 'nodeunit-shim'; import { PublicSubnet, Vpc, VpnConnection } from '../lib'; @@ -322,4 +322,24 @@ nodeunitShim({ })); test.done(); }, + 'can add a vpn connection with a Token as customer gateway ip'(test:Test) { + // GIVEN + const stack = new Stack(); + const token = Token.asAny('192.0.2.1'); + + // WHEN + new Vpc(stack, 'VpcNetwork', { + vpnConnections: { + VpnConnection: { + ip: token as any, + }, + }, + }); + + // THEN + expect(stack).to(haveResource('AWS::EC2::CustomerGateway', { + IpAddress: '192.0.2.1', + })); + test.done(); + }, }); diff --git a/packages/@aws-cdk/aws-kms/lib/key.ts b/packages/@aws-cdk/aws-kms/lib/key.ts index d8de804e61a12..4e18d228fe38e 100644 --- a/packages/@aws-cdk/aws-kms/lib/key.ts +++ b/packages/@aws-cdk/aws-kms/lib/key.ts @@ -156,7 +156,7 @@ abstract class KeyBase extends Resource implements IKey { resourceArns: [this.keyArn], resourceSelfArns: crossEnvironment ? undefined : ['*'], }; - if (this.trustAccountIdentities) { + if (this.trustAccountIdentities && !crossEnvironment) { return iam.Grant.addToPrincipalOrResource(grantOptions); } else { return iam.Grant.addToPrincipalAndResource({ diff --git a/packages/@aws-cdk/aws-kms/test/key.test.ts b/packages/@aws-cdk/aws-kms/test/key.test.ts index 42461faece6f4..b2d37496d00ec 100644 --- a/packages/@aws-cdk/aws-kms/test/key.test.ts +++ b/packages/@aws-cdk/aws-kms/test/key.test.ts @@ -245,6 +245,103 @@ describe('key policies', () => { }); }); + testFutureBehavior('grant for a principal in a different region', flags, cdk.App, (app) => { + const principalStack = new cdk.Stack(app, 'PrincipalStack', { env: { region: 'testregion1' } }); + const principal = new iam.Role(principalStack, 'Role', { + assumedBy: new iam.AnyPrincipal(), + roleName: 'MyRolePhysicalName', + }); + + const keyStack = new cdk.Stack(app, 'KeyStack', { env: { region: 'testregion2' } }); + const key = new kms.Key(keyStack, 'Key'); + + key.grantEncrypt(principal); + + expect(keyStack).toHaveResourceLike('AWS::KMS::Key', { + KeyPolicy: { + Statement: arrayWith( + { + Action: [ + 'kms:Encrypt', + 'kms:ReEncrypt*', + 'kms:GenerateDataKey*', + ], + Effect: 'Allow', + Principal: { AWS: { 'Fn::Join': ['', ['arn:', { Ref: 'AWS::Partition' }, ':iam::', { Ref: 'AWS::AccountId' }, ':role/MyRolePhysicalName']] } }, + Resource: '*', + }, + ), + Version: '2012-10-17', + }, + }); + expect(principalStack).toHaveResourceLike('AWS::IAM::Policy', { + PolicyDocument: { + Statement: [ + { + Action: [ + 'kms:Encrypt', + 'kms:ReEncrypt*', + 'kms:GenerateDataKey*', + ], + Effect: 'Allow', + Resource: '*', + }, + ], + Version: '2012-10-17', + }, + }); + }); + + testFutureBehavior('grant for a principal in a different account', flags, cdk.App, (app) => { + const principalStack = new cdk.Stack(app, 'PrincipalStack', { env: { account: '0123456789012' } }); + const principal = new iam.Role(principalStack, 'Role', { + assumedBy: new iam.AnyPrincipal(), + roleName: 'MyRolePhysicalName', + }); + + const keyStack = new cdk.Stack(app, 'KeyStack', { env: { account: '111111111111' } }); + const key = new kms.Key(keyStack, 'Key'); + + key.grantEncrypt(principal); + + expect(keyStack).toHaveResourceLike('AWS::KMS::Key', { + KeyPolicy: { + Statement: [ + { + // Default policy, unmodified + }, + { + Action: [ + 'kms:Encrypt', + 'kms:ReEncrypt*', + 'kms:GenerateDataKey*', + ], + Effect: 'Allow', + Principal: { AWS: { 'Fn::Join': ['', ['arn:', { Ref: 'AWS::Partition' }, ':iam::0123456789012:role/MyRolePhysicalName']] } }, + Resource: '*', + }, + ], + Version: '2012-10-17', + }, + }); + expect(principalStack).toHaveResourceLike('AWS::IAM::Policy', { + PolicyDocument: { + Statement: [ + { + Action: [ + 'kms:Encrypt', + 'kms:ReEncrypt*', + 'kms:GenerateDataKey*', + ], + Effect: 'Allow', + Resource: '*', + }, + ], + Version: '2012-10-17', + }, + }); + }); + testFutureBehavior('additional key admins can be specified (with imported/immutable principal)', flags, cdk.App, (app) => { const stack = new cdk.Stack(app); const adminRole = iam.Role.fromRoleArn(stack, 'Admin', 'arn:aws:iam::123456789012:role/TrustedAdmin'); diff --git a/packages/@aws-cdk/aws-s3-assets/README.md b/packages/@aws-cdk/aws-s3-assets/README.md index 3d508070d6a50..aab4c46d9c44d 100644 --- a/packages/@aws-cdk/aws-s3-assets/README.md +++ b/packages/@aws-cdk/aws-s3-assets/README.md @@ -115,6 +115,7 @@ new assets.Asset(this, 'BundledAsset', { }, // Docker bundling fallback image: BundlingDockerImage.fromRegistry('alpine'), + entrypoint: ['/bin/sh', '-c'], command: ['bundle'], }, }); diff --git a/packages/@aws-cdk/core/lib/bundling.ts b/packages/@aws-cdk/core/lib/bundling.ts index c9a9c07e77f34..b1247fd913ea0 100644 --- a/packages/@aws-cdk/core/lib/bundling.ts +++ b/packages/@aws-cdk/core/lib/bundling.ts @@ -13,6 +13,17 @@ export interface BundlingOptions { */ readonly image: BundlingDockerImage; + /** + * The entrypoint to run in the Docker container. + * + * @example ['/bin/sh', '-c'] + * + * @see https://docs.docker.com/engine/reference/builder/#entrypoint + * + * @default - run the entrypoint defined in the image + */ + readonly entrypoint?: string[]; + /** * The command to run in the Docker container. * @@ -152,7 +163,15 @@ export class BundlingDockerImage { public run(options: DockerRunOptions = {}) { const volumes = options.volumes || []; const environment = options.environment || {}; - const command = options.command || []; + const entrypoint = options.entrypoint?.[0] || null; + const command = [ + ...options.entrypoint?.[1] + ? [...options.entrypoint.slice(1)] + : [], + ...options.command + ? [...options.command] + : [], + ]; const dockerArgs: string[] = [ 'run', '--rm', @@ -164,6 +183,9 @@ export class BundlingDockerImage { ...options.workingDirectory ? ['-w', options.workingDirectory] : [], + ...entrypoint + ? ['--entrypoint', entrypoint] + : [], this.image, ...command, ]; @@ -238,6 +260,13 @@ export enum DockerVolumeConsistency { * Docker run options */ export interface DockerRunOptions { + /** + * The entrypoint to run in the container. + * + * @default - run the entrypoint defined in the image + */ + readonly entrypoint?: string[]; + /** * The command to run in the container. * diff --git a/packages/@aws-cdk/core/test/bundling.test.ts b/packages/@aws-cdk/core/test/bundling.test.ts index e2b0d6b43b98b..258860d65585c 100644 --- a/packages/@aws-cdk/core/test/bundling.test.ts +++ b/packages/@aws-cdk/core/test/bundling.test.ts @@ -171,6 +171,44 @@ nodeunitShim({ test.done(); }, + 'custom entrypoint is passed through to docker exec'(test: Test) { + const spawnSyncStub = sinon.stub(child_process, 'spawnSync').returns({ + status: 0, + stderr: Buffer.from('stderr'), + stdout: Buffer.from('stdout'), + pid: 123, + output: ['stdout', 'stderr'], + signal: null, + }); + + const image = BundlingDockerImage.fromRegistry('alpine'); + image.run({ + entrypoint: ['/cool/entrypoint', '--cool-entrypoint-arg'], + command: ['cool', 'command'], + environment: { + VAR1: 'value1', + VAR2: 'value2', + }, + volumes: [{ hostPath: '/host-path', containerPath: '/container-path' }], + workingDirectory: '/working-directory', + user: 'user:group', + }); + + test.ok(spawnSyncStub.calledWith('docker', [ + 'run', '--rm', + '-u', 'user:group', + '-v', '/host-path:/container-path:delegated', + '--env', 'VAR1=value1', + '--env', 'VAR2=value2', + '-w', '/working-directory', + '--entrypoint', '/cool/entrypoint', + 'alpine', + '--cool-entrypoint-arg', + 'cool', 'command', + ], { stdio: ['ignore', process.stderr, 'inherit'] })); + test.done(); + }, + 'cp utility copies from an image'(test: Test) { // GIVEN const containerId = '1234567890abcdef1234567890abcdef';