From 0e04945a5a731f58b76a4119fa7faa143573df54 Mon Sep 17 00:00:00 2001 From: Shiv Lakshminarayan Date: Sun, 13 Dec 2020 23:25:15 -0800 Subject: [PATCH 1/9] docs(efs): README and doc string refresh (#11992) ---- *By submitting this pull request, I confirm that my contribution is made under the terms of the Apache-2.0 license* --- packages/@aws-cdk/aws-efs/README.md | 134 ++++++++++-------- packages/@aws-cdk/aws-efs/lib/access-point.ts | 14 +- .../@aws-cdk/aws-efs/lib/efs-file-system.ts | 42 +++--- .../aws-efs/rosetta/default.ts-fixture | 13 ++ .../with-filesystem-instance.ts-fixture | 30 ++++ 5 files changed, 144 insertions(+), 89 deletions(-) create mode 100644 packages/@aws-cdk/aws-efs/rosetta/default.ts-fixture create mode 100644 packages/@aws-cdk/aws-efs/rosetta/with-filesystem-instance.ts-fixture diff --git a/packages/@aws-cdk/aws-efs/README.md b/packages/@aws-cdk/aws-efs/README.md index 16e04aa27462b..002ba355e51eb 100644 --- a/packages/@aws-cdk/aws-efs/README.md +++ b/packages/@aws-cdk/aws-efs/README.md @@ -21,48 +21,57 @@ -This construct library allows you to set up AWS Elastic File System (EFS). +[Amazon Elastic File System](https://docs.aws.amazon.com/efs/latest/ug/whatisefs.html) (Amazon EFS) provides a simple, scalable, +fully managed elastic NFS file system for use with AWS Cloud services and on-premises resources. +Amazon EFS provides file storage in the AWS Cloud. With Amazon EFS, you can create a file system, +mount the file system on an Amazon EC2 instance, and then read and write data to and from your file system. -```ts -import * as efs from '@aws-cdk/aws-efs'; +This module is part of the [AWS Cloud Development Kit](https://github.com/aws/aws-cdk) project. -const myVpc = new ec2.Vpc(this, 'VPC'); -const fileSystem = new efs.FileSystem(this, 'MyEfsFileSystem', { - vpc: myVpc, - encrypted: true, - lifecyclePolicy: efs.LifecyclePolicy.AFTER_14_DAYS, - performanceMode: efs.PerformanceMode.GENERAL_PURPOSE, - throughputMode: efs.ThroughputMode.BURSTING -}); -``` +## File Systems + +Amazon EFS provides elastic, shared file storage that is POSIX-compliant. The file system you create +supports concurrent read and write access from multiple Amazon EC2 instances and is accessible from +all of the Availability Zones in the AWS Region where it is created. Learn more about [EFS file systems](https://docs.aws.amazon.com/efs/latest/ug/creating-using.html) -A file system can set `RemovalPolicy`. Default policy is `RETAIN`. +### Create an Amazon EFS file system + +A Virtual Private Cloud (VPC) is required to create an Amazon EFS file system. +The following example creates a file system that is encrypted at rest, running in `General Purpose` +performance mode, and `Bursting` throughput mode and does not transition files to the Infrequent +Access (IA) storage class. ```ts -const fileSystem = new FileSystem(this, 'EfsFileSystem', { - vpc, - removalPolicy: RemovalPolicy.DESTROY +const fileSystem = new efs.FileSystem(this, 'MyEfsFileSystem', { + vpc: new ec2.Vpc(this, 'VPC'), + encrypted: true, // file system is not encrypted by default + lifecyclePolicy: efs.LifecyclePolicy.AFTER_14_DAYS, // files are not transitioned to infrequent access (IA) storage by default + performanceMode: efs.PerformanceMode.GENERAL_PURPOSE, // default }); ``` -## Access Point +⚠️ An Amazon EFS file system's performance mode can't be changed after the file system has been created. +Updating this property will replace the file system. -An access point is an application-specific view into an EFS file system that applies an operating system user and -group, and a file system path, to any file system request made through the access point. The operating system user -and group override any identity information provided by the NFS client. The file system path is exposed as the -access point's root directory. Applications using the access point can only access data in its own directory and -below. To learn more, see [Mounting a File System Using EFS Access Points](https://docs.aws.amazon.com/efs/latest/ug/efs-access-points.html). +### Access Point -Use `addAccessPoint` to create an access point from a fileSystem: +An access point is an application-specific view into an EFS file system that applies an operating +system user and group, and a file system path, to any file system request made through the access +point. The operating system user and group override any identity information provided by the NFS +client. The file system path is exposed as the access point's root directory. Applications using +the access point can only access data in its own directory and below. To learn more, see [Mounting a File System Using EFS Access Points](https://docs.aws.amazon.com/efs/latest/ug/efs-access-points.html). -```ts +Use the `addAccessPoint` API to create an access point from a fileSystem. + +```ts fixture=with-filesystem-instance fileSystem.addAccessPoint('AccessPoint'); ``` -By default, when you create an access point, the root(`/`) directory is exposed to the client connecting to -the access point. You may specify custom path with the `path` property. If `path` does not exist, it will be -created with the settings defined in the `creationInfo`. See -[Creating Access Points](https://docs.aws.amazon.com/efs/latest/ug/create-access-point.html) for more details. +By default, when you create an access point, the root(`/`) directory is exposed to the client +connecting to the access point. You can specify a custom path with the `path` property. + +If `path` does not exist, it will be created with the settings defined in the `creationInfo`. +See [Creating Access Points](https://docs.aws.amazon.com/efs/latest/ug/create-access-point.html) for more details. Any access point that has been created outside the stack can be imported into your CDK app. @@ -70,59 +79,44 @@ Use the `fromAccessPointAttributes()` API to import an existing access point. ```ts efs.AccessPoint.fromAccessPointAttributes(this, 'ap', { - accessPointArn: 'fsap-1293c4d9832fo0912', + accessPointId: 'fsap-1293c4d9832fo0912', fileSystem: efs.FileSystem.fromFileSystemAttributes(this, 'efs', { fileSystemId: 'fs-099d3e2f', - securityGroup: SecurityGroup.fromSecurityGroupId(this, 'sg', 'sg-51530134'), + securityGroup: ec2.SecurityGroup.fromSecurityGroupId(this, 'sg', 'sg-51530134'), }), }); ``` -⚠️ Notice: When importing an Access Point using `fromAccessPointAttributes()`, you must make sure the mount targets are deployed and their lifecycle state is `available`. Otherwise, you may encounter the following error when deploying: +⚠️ Notice: When importing an Access Point using `fromAccessPointAttributes()`, you must make sure +the mount targets are deployed and their lifecycle state is `available`. Otherwise, you may encounter +the following error when deploying: > EFS file system referenced by access point has -mount targets created in all availability zones the function will execute in, but not all are in the available life cycle -state yet. Please wait for them to become available and try the request again. +> mount targets created in all availability zones the function will execute in, but not all +>are in the available life cycle state yet. Please wait for them to become available and +> try the request again. -## Connecting +### Connecting To control who can access the EFS, use the `.connections` attribute. EFS has a fixed default port, so you don't need to specify the port: -```ts +```ts fixture=with-filesystem-instance fileSystem.connections.allowDefaultPortFrom(instance); ``` -## Mounting the file system using User Data +Learn more about [managing file system network accessibility](https://docs.aws.amazon.com/efs/latest/ug/manage-fs-access.html) -In order to automatically mount this file system during instance launch, -following code can be used as reference: +### Mounting the file system using User Data -```ts -const vpc = new ec2.Vpc(this, 'VPC'); +After you create a file system, you can create mount targets. Then you can mount the file system on +EC2 instances, containers, and Lambda functions in your virtual private cloud (VPC). -const fileSystem = new efs.FileSystem(this, 'MyEfsFileSystem', { - vpc, - encrypted: true, - lifecyclePolicy: efs.LifecyclePolicy.AFTER_14_DAYS, - performanceMode: efs.PerformanceMode.GENERAL_PURPOSE, - throughputMode: efs.ThroughputMode.BURSTING, - enableAutomaticBackups: true -}); - -const inst = new Instance(this, 'inst', { - instanceType: InstanceType.of(InstanceClass.T2, InstanceSize.LARGE), - machineImage: new AmazonLinuxImage({ - generation: AmazonLinuxGeneration.AMAZON_LINUX_2 - }), - vpc, - vpcSubnets: { - subnetType: SubnetType.PUBLIC, - } -}); +The following example automatically mounts a file system during instance launch. -fileSystem.connections.allowDefaultPortFrom(inst); +```ts fixture=with-filesystem-instance +fileSystem.connections.allowDefaultPortFrom(instance); -inst.userData.addCommands("yum check-update -y", // Ubuntu: apt-get -y update +instance.userData.addCommands("yum check-update -y", // Ubuntu: apt-get -y update "yum upgrade -y", // Ubuntu: apt-get -y upgrade "yum install -y amazon-efs-utils", // Ubuntu: apt-get -y install amazon-efs-utils "yum install -y nfs-utils", // Ubuntu: apt-get -y install nfs-common @@ -130,8 +124,22 @@ inst.userData.addCommands("yum check-update -y", // Ubuntu: apt-get -y update "efs_mount_point_1=/mnt/efs/fs1", "mkdir -p \"${efs_mount_point_1}\"", "test -f \"/sbin/mount.efs\" && echo \"${file_system_id_1}:/ ${efs_mount_point_1} efs defaults,_netdev\" >> /etc/fstab || " + - "echo \"${file_system_id_1}.efs." + cdk.Stack.of(this).region + ".amazonaws.com:/ ${efs_mount_point_1} nfs4 nfsvers=4.1,rsize=1048576,wsize=1048576,hard,timeo=600,retrans=2,noresvport,_netdev 0 0\" >> /etc/fstab", + "echo \"${file_system_id_1}.efs." + Stack.of(this).region + ".amazonaws.com:/ ${efs_mount_point_1} nfs4 nfsvers=4.1,rsize=1048576,wsize=1048576,hard,timeo=600,retrans=2,noresvport,_netdev 0 0\" >> /etc/fstab", "mount -a -t efs,nfs4 defaults"); ``` -This module is part of the [AWS Cloud Development Kit](https://github.com/aws/aws-cdk) project. +Learn more about [mounting EFS file systems](https://docs.aws.amazon.com/efs/latest/ug/mounting-fs.html) + +### Deleting + +Since file systems are stateful resources, by default the file system will not be deleted when your +stack is deleted. + +You can configure the file system to be destroyed on stack deletion by setting a `removalPolicy` + +```ts +const fileSystem = new efs.FileSystem(this, 'EfsFileSystem', { + vpc: new ec2.Vpc(this, 'VPC'), + removalPolicy: RemovalPolicy.DESTROY +}); +``` diff --git a/packages/@aws-cdk/aws-efs/lib/access-point.ts b/packages/@aws-cdk/aws-efs/lib/access-point.ts index 29d22a5ec6032..6a0e6973cc6c2 100644 --- a/packages/@aws-cdk/aws-efs/lib/access-point.ts +++ b/packages/@aws-cdk/aws-efs/lib/access-point.ts @@ -22,7 +22,7 @@ export interface IAccessPoint extends IResource { readonly accessPointArn: string; /** - * The efs filesystem + * The EFS file system */ readonly fileSystem: IFileSystem; } @@ -120,7 +120,7 @@ export interface AccessPointProps extends AccessPointOptions { export interface AccessPointAttributes { /** * The ID of the AccessPoint - * One of this, of {@link accessPointArn} is required + * One of this, or {@link accessPointArn} is required * * @default - determined based on accessPointArn */ @@ -128,16 +128,16 @@ export interface AccessPointAttributes { /** * The ARN of the AccessPoint - * One of this, of {@link accessPointId} is required + * One of this, or {@link accessPointId} is required * * @default - determined based on accessPointId */ readonly accessPointArn?: string; /** - * The EFS filesystem + * The EFS file system * - * @default - no EFS filesystem + * @default - no EFS file system */ readonly fileSystem?: IFileSystem; } @@ -156,7 +156,7 @@ abstract class AccessPointBase extends Resource implements IAccessPoint { public abstract readonly accessPointId: string; /** - * The filesystem of the access point + * The file system of the access point */ public abstract readonly fileSystem: IFileSystem; } @@ -194,7 +194,7 @@ export class AccessPoint extends AccessPointBase { public readonly accessPointId: string; /** - * The filesystem of the access point + * The file system of the access point */ public readonly fileSystem: IFileSystem; diff --git a/packages/@aws-cdk/aws-efs/lib/efs-file-system.ts b/packages/@aws-cdk/aws-efs/lib/efs-file-system.ts index e9ba1852a4006..a93ea1b76d5a4 100644 --- a/packages/@aws-cdk/aws-efs/lib/efs-file-system.ts +++ b/packages/@aws-cdk/aws-efs/lib/efs-file-system.ts @@ -40,17 +40,22 @@ export enum LifecyclePolicy { /** * EFS Performance mode. * - * @see http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-resource-efs-filesystem.html#cfn-efs-filesystem-performancemode + * @see https://docs.aws.amazon.com/efs/latest/ug/performance.html#performancemodes */ export enum PerformanceMode { /** - * This is the general purpose performance mode for most file systems. + * General Purpose is ideal for latency-sensitive use cases, like web serving + * environments, content management systems, home directories, and general file serving. + * Recommended for the majority of Amazon EFS file systems. */ GENERAL_PURPOSE = 'generalPurpose', /** - * This performance mode can scale to higher levels of aggregate throughput and operations per second with a - * tradeoff of slightly higher latencies. + * File systems in the Max I/O mode can scale to higher levels of aggregate + * throughput and operations per second. This scaling is done with a tradeoff + * of slightly higher latencies for file metadata operations. + * Highly parallelized applications and workloads, such as big data analysis, + * media processing, and genomics analysis, can benefit from this mode. */ MAX_IO = 'maxIO' } @@ -58,11 +63,11 @@ export enum PerformanceMode { /** * EFS Throughput mode. * - * @see http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-resource-efs-filesystem.html#cfn-elasticfilesystem-filesystem-throughputmode + * @see https://docs.aws.amazon.com/efs/latest/ug/performance.html#throughput-modes */ export enum ThroughputMode { /** - * This mode on Amazon EFS scales as the size of the file system in the standard storage class grows. + * This mode on Amazon EFS scales as the size of the file system in the standard storage class grows. */ BURSTING = 'bursting', @@ -73,7 +78,7 @@ export enum ThroughputMode { } /** - * Interface to implement AWS File Systems. + * Represents an Amazon EFS file system */ export interface IFileSystem extends ec2.IConnectable, IResource { /** @@ -103,7 +108,7 @@ export interface FileSystemProps { /** * Security Group to assign to this file system. * - * @default - creates new security group which allow all out bound traffic + * @default - creates new security group which allows all outbound traffic */ readonly securityGroup?: ec2.ISecurityGroup; @@ -117,12 +122,12 @@ export interface FileSystemProps { /** * Defines if the data at rest in the file system is encrypted or not. * - * @default - false + * @default false */ readonly encrypted?: boolean; /** - * The filesystem's name. + * The file system's name. * * @default - CDK generated name */ @@ -131,28 +136,30 @@ export interface FileSystemProps { /** * The KMS key used for encryption. This is required to encrypt the data at rest if @encrypted is set to true. * - * @default - if @encrypted is true, the default key for EFS (/aws/elasticfilesystem) is used + * @default - if 'encrypted' is true, the default key for EFS (/aws/elasticfilesystem) is used */ readonly kmsKey?: kms.IKey; /** * A policy used by EFS lifecycle management to transition files to the Infrequent Access (IA) storage class. * - * @default - none + * @default - None. EFS will not transition files to the IA storage class. */ readonly lifecyclePolicy?: LifecyclePolicy; /** - * Enum to mention the performance mode of the file system. + * The performance mode that the file system will operate under. + * An Amazon EFS file system's performance mode can't be changed after the file system has been created. + * Updating this property will replace the file system. * - * @default - GENERAL_PURPOSE + * @default PerformanceMode.GENERAL_PURPOSE */ readonly performanceMode?: PerformanceMode; /** * Enum to mention the throughput mode of the file system. * - * @default - BURSTING + * @default ThroughputMode.BURSTING */ readonly throughputMode?: ThroughputMode; @@ -294,7 +301,6 @@ export class FileSystem extends Resource implements IFileSystem { } } - class ImportedFileSystem extends Resource implements IFileSystem { /** * The security groups/rules used to allow network connections to the file system. @@ -323,6 +329,4 @@ class ImportedFileSystem extends Resource implements IFileSystem { this.mountTargetsAvailable = new ConcreteDependable(); } - - -} +} \ No newline at end of file diff --git a/packages/@aws-cdk/aws-efs/rosetta/default.ts-fixture b/packages/@aws-cdk/aws-efs/rosetta/default.ts-fixture new file mode 100644 index 0000000000000..d3667dedefca0 --- /dev/null +++ b/packages/@aws-cdk/aws-efs/rosetta/default.ts-fixture @@ -0,0 +1,13 @@ +// Fixture with packages imported, but nothing else +import { Duration, RemovalPolicy, Stack } from '@aws-cdk/core'; +import { Construct } from 'constructs'; +import * as efs from '@aws-cdk/aws-efs'; +import * as ec2 from '@aws-cdk/aws-ec2'; + +class Fixture extends Stack { + constructor(scope: Construct, id: string) { + super(scope, id); + + /// here + } +} diff --git a/packages/@aws-cdk/aws-efs/rosetta/with-filesystem-instance.ts-fixture b/packages/@aws-cdk/aws-efs/rosetta/with-filesystem-instance.ts-fixture new file mode 100644 index 0000000000000..092b572afa726 --- /dev/null +++ b/packages/@aws-cdk/aws-efs/rosetta/with-filesystem-instance.ts-fixture @@ -0,0 +1,30 @@ +// Fixture with file system and an EC2 instance created in a VPC +import { Stack } from '@aws-cdk/core'; +import { Construct } from 'constructs'; +import * as efs from '@aws-cdk/aws-efs'; +import * as ec2 from '@aws-cdk/aws-ec2'; + +class Fixture extends Stack { + constructor(scope: Construct, id: string) { + super(scope, id); + + const vpc = new ec2.Vpc(this, 'VPC'); + + const fileSystem = new efs.FileSystem(this, 'FileSystem', { + vpc, + }); + + const instance = new ec2.Instance(this, 'instance', { + instanceType: ec2.InstanceType.of(ec2.InstanceClass.T2, ec2.InstanceSize.LARGE), + machineImage: new ec2.AmazonLinuxImage({ + generation: ec2.AmazonLinuxGeneration.AMAZON_LINUX_2 + }), + vpc, + vpcSubnets: { + subnetType: ec2.SubnetType.PUBLIC, + } + }); + + /// here + } +} From 0690da925144c821a73bfab4ae8d678a8c074357 Mon Sep 17 00:00:00 2001 From: Rico Huijbers Date: Mon, 14 Dec 2020 10:07:37 +0100 Subject: [PATCH 2/9] fix(ec2): 'encoded list token' error using Vpc imported from deploy-time lists (#12040) Even though using `Vpc.fromVpcAttributes()` using deploy-time lists like from `Fn.importValue()`s and `CfnParameters` was never really supposed to work, it accidentally did. The reason for that is: ```ts // Encoded list token const subnetIds = Token.asList(Fn.importValue('someValue')) // [ '#{Token[1234]}' ] // Gets parsed to a singleton `Subnet` list: const subnets = subnetIds.map(s => Subnet.fromSubnetAttributes({ subnetId: s })); // [ Subnet({ subnetId: '#{Token[1234]}' }) ] // This 'subnetId' is illegal by itself, and if yould try to use it for, // say, an ec2.Instance it would fail. However, if you treat this single // subnet as a GROUP of subnets: new CfnAutoScalingGroup({ subnetIds: subnets.map(s => s.subnetId) }) // [ '#{Token[1234]}' ] // And this resolves back to: resolve(cfnSubnetIds) // SubnetIds: { Fn::ImportValue: 'someValue' } ``` -------- We introduced an additional check in #11899 to make sure that the list-element token that represents an encoded list (`'#{Token[1234]}'`) never occurs in a non-list context, because it's illegal there. However, because: * `Subnet.fromSubnetAttributes()` logs the subnetId as a *warning* to its own metadata (which will log a string like `"there's something wrong with '#{Token[1234]}' ..."`). * All metadata is resolved just the same as the template expressions are. The `resolve()` function encounters that orphaned list token in the metadata and throws. -------- The *proper* solution would be to handle unparseable list tokens specially to never get into this situation, but doing that requires introducing classes and new concepts that will be a large effort and not be backwards compatible. Tracking in #4118. Another possible solution is to stop resolving metadata. I don't know if we usefully use this feature; I think we don't. However, we have tests enforcing that it is being done, and I'm scared to break something there. The quick solution around this for now is to have `Subnet.fromSubnetAttributes()` recognize when it's about to log a problematic identifier to metadata, and don't do it. Fixes #11945. ---- *By submitting this pull request, I confirm that my contribution is made under the terms of the Apache-2.0 license* --- .../test/auto-scaling-group.test.ts | 39 +++++++++++++++++++ packages/@aws-cdk/aws-ec2/lib/vpc.ts | 27 ++++++++++++- packages/@aws-cdk/aws-ec2/test/vpc.test.ts | 32 ++++++++++++++- 3 files changed, 95 insertions(+), 3 deletions(-) diff --git a/packages/@aws-cdk/aws-autoscaling/test/auto-scaling-group.test.ts b/packages/@aws-cdk/aws-autoscaling/test/auto-scaling-group.test.ts index 737566c369b76..ae3be33401f71 100644 --- a/packages/@aws-cdk/aws-autoscaling/test/auto-scaling-group.test.ts +++ b/packages/@aws-cdk/aws-autoscaling/test/auto-scaling-group.test.ts @@ -1351,6 +1351,45 @@ test('Can set autoScalingGroupName', () => { })); }); +test('can use Vpc imported from unparseable list tokens', () => { + // GIVEN + const stack = new cdk.Stack(); + + const vpcId = cdk.Fn.importValue('myVpcId'); + const availabilityZones = cdk.Fn.split(',', cdk.Fn.importValue('myAvailabilityZones')); + const publicSubnetIds = cdk.Fn.split(',', cdk.Fn.importValue('myPublicSubnetIds')); + const privateSubnetIds = cdk.Fn.split(',', cdk.Fn.importValue('myPrivateSubnetIds')); + const isolatedSubnetIds = cdk.Fn.split(',', cdk.Fn.importValue('myIsolatedSubnetIds')); + + const vpc = ec2.Vpc.fromVpcAttributes(stack, 'importedVpc', { + vpcId, + availabilityZones, + publicSubnetIds, + privateSubnetIds, + isolatedSubnetIds, + }); + + // WHEN + new autoscaling.AutoScalingGroup(stack, 'ecs-ec2-asg', { + instanceType: new ec2.InstanceType('t2.micro'), + machineImage: new ec2.AmazonLinuxImage(), + minCapacity: 1, + maxCapacity: 1, + desiredCapacity: 1, + vpc, + allowAllOutbound: false, + associatePublicIpAddress: false, + vpcSubnets: { subnetType: ec2.SubnetType.PRIVATE }, + }); + + // THEN + expect(stack).to(haveResourceLike('AWS::AutoScaling::AutoScalingGroup', { + VPCZoneIdentifier: { + 'Fn::Split': [',', { 'Fn::ImportValue': 'myPrivateSubnetIds' }], + }, + })); +}); + function mockSecurityGroup(stack: cdk.Stack) { return ec2.SecurityGroup.fromSecurityGroupId(stack, 'MySG', 'most-secure'); } diff --git a/packages/@aws-cdk/aws-ec2/lib/vpc.ts b/packages/@aws-cdk/aws-ec2/lib/vpc.ts index f6ce634b18a75..f642f131d5a70 100644 --- a/packages/@aws-cdk/aws-ec2/lib/vpc.ts +++ b/packages/@aws-cdk/aws-ec2/lib/vpc.ts @@ -1010,7 +1010,16 @@ export class Vpc extends VpcBase { ]; /** - * Import an exported VPC + * Import a VPC by supplying all attributes directly + * + * NOTE: using `fromVpcAttributes()` with deploy-time parameters (like a `Fn.importValue()` or + * `CfnParameter` to represent a list of subnet IDs) sometimes accidentally works. It happens + * to work for constructs that need a list of subnets (like `AutoScalingGroup` and `eks.Cluster`) + * but it does not work for constructs that need individual subnets (like + * `Instance`). See https://github.com/aws/aws-cdk/issues/4118 for more + * information. + * + * Prefer to use `Vpc.fromLookup()` instead. */ public static fromVpcAttributes(scope: Construct, id: string, attrs: VpcAttributes): IVpc { return new ImportedVpc(scope, id, attrs, false); @@ -1927,7 +1936,21 @@ class ImportedSubnet extends Resource implements ISubnet, IPublicSubnet, IPrivat super(scope, id); if (!attrs.routeTableId) { - const ref = Token.isUnresolved(attrs.subnetId) + // The following looks a little weird, but comes down to: + // + // * Is the subnetId itself unresolved ({ Ref: Subnet }); or + // * Was it the accidentally extracted first element of a list-encoded + // token? ({ Fn::ImportValue: Subnets } => ['#{Token[1234]}'] => + // '#{Token[1234]}' + // + // There's no other API to test for the second case than to the string back into + // a list and see if the combination is Unresolved. + // + // In both cases we can't output the subnetId literally into the metadata (because it'll + // be useless). In the 2nd case even, if we output it to metadata, the `resolve()` call + // that gets done on the metadata will even `throw`, because the '#{Token}' value will + // occur in an illegal position (not in a list context). + const ref = Token.isUnresolved(attrs.subnetId) || Token.isUnresolved([attrs.subnetId]) ? `at '${Node.of(scope).path}/${id}'` : `'${attrs.subnetId}'`; // eslint-disable-next-line max-len diff --git a/packages/@aws-cdk/aws-ec2/test/vpc.test.ts b/packages/@aws-cdk/aws-ec2/test/vpc.test.ts index 9ea01086411dd..0b9401f289119 100644 --- a/packages/@aws-cdk/aws-ec2/test/vpc.test.ts +++ b/packages/@aws-cdk/aws-ec2/test/vpc.test.ts @@ -1,5 +1,5 @@ import { countResources, expect, haveResource, haveResourceLike, isSuperObject, MatchStyle } from '@aws-cdk/assert'; -import { CfnOutput, Lazy, Stack, Tags } from '@aws-cdk/core'; +import { CfnOutput, CfnResource, Fn, Lazy, Stack, Tags } from '@aws-cdk/core'; import { nodeunitShim, Test } from 'nodeunit-shim'; import { AclCidr, AclTraffic, BastionHostLinux, CfnSubnet, CfnVPC, SubnetFilter, DefaultInstanceTenancy, GenericLinuxImage, @@ -1227,6 +1227,36 @@ nodeunitShim({ test.done(); }, + 'fromVpcAttributes using imported refs'(test: Test) { + // GIVEN + const stack = getTestStack(); + + const vpcId = Fn.importValue('myVpcId'); + const availabilityZones = Fn.split(',', Fn.importValue('myAvailabilityZones')); + const publicSubnetIds = Fn.split(',', Fn.importValue('myPublicSubnetIds')); + + // WHEN + const vpc = Vpc.fromVpcAttributes(stack, 'VPC', { + vpcId, + availabilityZones, + publicSubnetIds, + }); + + new CfnResource(stack, 'Resource', { + type: 'Some::Resource', + properties: { + subnetIds: vpc.selectSubnets().subnetIds, + }, + }); + + // THEN - No exception + expect(stack).to(haveResource('Some::Resource', { + subnetIds: { 'Fn::Split': [',', { 'Fn::ImportValue': 'myPublicSubnetIds' }] }, + })); + + test.done(); + }, + 'select explicitly defined subnets'(test: Test) { // GIVEN const stack = getTestStack(); From f813bff2da4792cfa7bfce6f572a7d2bb5c4759d Mon Sep 17 00:00:00 2001 From: Iain Cole Date: Mon, 14 Dec 2020 02:12:47 -0800 Subject: [PATCH 3/9] feat(ivs): add IVS L2 Constructs (#11454) Adds IVS L2 constructs and closes https://github.com/aws/aws-cdk/issues/11434 --- packages/@aws-cdk/aws-ivs/README.md | 79 +++++- packages/@aws-cdk/aws-ivs/lib/channel.ts | 173 +++++++++++++ packages/@aws-cdk/aws-ivs/lib/index.ts | 4 + .../@aws-cdk/aws-ivs/lib/playback-key-pair.ts | 71 ++++++ packages/@aws-cdk/aws-ivs/lib/stream-key.ts | 51 ++++ packages/@aws-cdk/aws-ivs/package.json | 19 +- .../aws-ivs/rosetta/default.ts-fixture | 19 ++ .../aws-ivs/rosetta/with-channel.ts-fixture | 15 ++ .../aws-ivs/test/integ.ivs.expected.json | 57 +++++ packages/@aws-cdk/aws-ivs/test/integ.ivs.ts | 44 ++++ packages/@aws-cdk/aws-ivs/test/ivs.test.ts | 232 +++++++++++++++++- 11 files changed, 757 insertions(+), 7 deletions(-) create mode 100644 packages/@aws-cdk/aws-ivs/lib/channel.ts create mode 100644 packages/@aws-cdk/aws-ivs/lib/playback-key-pair.ts create mode 100644 packages/@aws-cdk/aws-ivs/lib/stream-key.ts create mode 100644 packages/@aws-cdk/aws-ivs/rosetta/default.ts-fixture create mode 100644 packages/@aws-cdk/aws-ivs/rosetta/with-channel.ts-fixture create mode 100644 packages/@aws-cdk/aws-ivs/test/integ.ivs.expected.json create mode 100644 packages/@aws-cdk/aws-ivs/test/integ.ivs.ts diff --git a/packages/@aws-cdk/aws-ivs/README.md b/packages/@aws-cdk/aws-ivs/README.md index 8ebb729640cf4..6217a89abd0ca 100644 --- a/packages/@aws-cdk/aws-ivs/README.md +++ b/packages/@aws-cdk/aws-ivs/README.md @@ -9,12 +9,89 @@ > > [CFN Resources]: https://docs.aws.amazon.com/cdk/latest/guide/constructs.html#constructs_lib +![cdk-constructs: Experimental](https://img.shields.io/badge/cdk--constructs-experimental-important.svg?style=for-the-badge) + +> The APIs of higher level constructs in this module are experimental and under active development. +> They are subject to non-backward compatible changes or removal in any future version. These are +> not subject to the [Semantic Versioning](https://semver.org/) model and breaking changes will be +> announced in the release notes. This means that while you may use them, you may need to update +> your source code when upgrading to a newer version of this package. + --- +Amazon Interactive Video Service (Amazon IVS) is a managed live streaming +solution that is quick and easy to set up, and ideal for creating interactive +video experiences. Send your live streams to Amazon IVS using streaming software +and the service does everything you need to make low-latency live video +available to any viewer around the world, letting you focus on building +interactive experiences alongside the live video. You can easily customize and +enhance the audience experience through the Amazon IVS player SDK and timed +metadata APIs, allowing you to build a more valuable relationship with your +viewers on your own websites and applications. + This module is part of the [AWS Cloud Development Kit](https://github.com/aws/aws-cdk) project. +## Channels + +An Amazon IVS channel stores configuration information related to your live +stream. You first create a channel and then contribute video to it using the +channel’s stream key to start your live stream. + +You can create a channel + ```ts -import ivs = require('@aws-cdk/aws-ivs'); +const myChannel = new ivs.Channel(this, 'Channel'); ``` + +### Importing an existing channel + +You can reference an existing channel, for example, if you need to create a +stream key for an existing channel + +```ts +const myChannel = ivs.Channel.fromChannelArn(this, 'Channel', myChannelArn); +``` + +## Stream Keys + +A Stream Key is used by a broadcast encoder to initiate a stream and identify +to Amazon IVS which customer and channel the stream is for. If you are +storing this value, it should be treated as if it were a password. + +You can create a stream key for a given channel + +```ts fixture=with-channel +const myStreamKey = myChannel.addStreamKey('StreamKey'); +``` + +## Private Channels + +Amazon IVS offers the ability to create private channels, allowing +you to restrict your streams by channel or viewer. You control access +to video playback by enabling playback authorization on channels and +generating signed JSON Web Tokens (JWTs) for authorized playback requests. + +A playback token is a JWT that you sign (with a playback authorization key) +and include with every playback request for a channel that has playback +authorization enabled. + +In order for Amazon IVS to validate the token, you need to upload +the public key that corresponds to the private key you use to sign the token. + +```ts +const keyPair = new ivs.PlaybackKeyPair(this, 'PlaybackKeyPair', { + publicKeyMaterial: myPublicKeyPemString, +}); +``` + +Then, when creating a channel, specify the authorized property + +```ts +const myChannel = new ivs.Channel(this, 'Channel', { + authorized: true, // default value is false +}); +``` + + diff --git a/packages/@aws-cdk/aws-ivs/lib/channel.ts b/packages/@aws-cdk/aws-ivs/lib/channel.ts new file mode 100644 index 0000000000000..34d9228bc787c --- /dev/null +++ b/packages/@aws-cdk/aws-ivs/lib/channel.ts @@ -0,0 +1,173 @@ +import * as core from '@aws-cdk/core'; +import { Construct } from 'constructs'; +import { CfnChannel } from './ivs.generated'; +import { StreamKey } from './stream-key'; + +/** + * Represents an IVS Channel + */ +export interface IChannel extends core.IResource { + /** + * The channel ARN. For example: arn:aws:ivs:us-west-2:123456789012:channel/abcdABCDefgh + * + * @attribute + */ + readonly channelArn: string; + + /** + * Adds a stream key for this IVS Channel. + * @param id construct ID + */ + addStreamKey(id: string): StreamKey; +} + +/** + * Reference to a new or existing IVS Channel + */ +abstract class ChannelBase extends core.Resource implements IChannel { + public abstract readonly channelArn: string; + + public addStreamKey(id: string): StreamKey { + return new StreamKey(this, id, { + channel: this, + }); + } +} + +/** + Channel latency mode +*/ +export enum LatencyMode { + /** + * Use LOW to minimize broadcaster-to-viewer latency for interactive broadcasts. + */ + LOW = 'LOW', + + /** + * Use NORMAL for broadcasts that do not require viewer interaction. + */ + NORMAL = 'NORMAL', +} + +/** + * The channel type, which determines the allowable resolution and bitrate. + * If you exceed the allowable resolution or bitrate, the stream probably will disconnect immediately. +*/ +export enum ChannelType { + /** + * Multiple qualities are generated from the original input, to automatically give viewers the best experience for + * their devices and network conditions. + * + * @see https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-resource-ivs-channel.html + */ + STANDARD = 'STANDARD', + + /** + * delivers the original input to viewers. The viewer’s video-quality choice is limited to the original input. + * + * @see https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-resource-ivs-channel.html + */ + BASIC = 'BASIC', +} + +/** + * Properties for creating a new Channel + */ +export interface ChannelProps { + /** + * Whether the channel is authorized. + * + * If you wish to make an authorized channel, you will need to ensure that + * a PlaybackKeyPair has been uploaded to your account as this is used to + * validate the signed JWT that is required for authorization + * + * @default false + */ + readonly authorized?: boolean; + + /** + * Channel latency mode. + * + * @default LatencyMode.LOW + */ + readonly latencyMode?: LatencyMode; + + /** + * Channel name. + * + * @default - None + */ + readonly name?: string; + + /** + * The channel type, which determines the allowable resolution and bitrate. + * If you exceed the allowable resolution or bitrate, the stream will disconnect immediately + * + * @default ChannelType.STANDARD + */ + readonly type?: ChannelType; +} + +/** + A new IVS channel +*/ +export class Channel extends ChannelBase { + /** + * Import an existing channel + */ + public static fromChannelArn(scope: Construct, id: string, channelArn: string): IChannel { + // This will throw an error if the arn cannot be parsed + let arnComponents = core.Arn.parse(channelArn); + + if (!core.Token.isUnresolved(arnComponents.service) && arnComponents.service !== 'ivs') { + throw new Error(`Invalid service, expected 'ivs', got '${arnComponents.service}'`); + } + + if (!core.Token.isUnresolved(arnComponents.resource) && arnComponents.resource !== 'channel') { + throw new Error(`Invalid resource, expected 'channel', got '${arnComponents.resource}'`); + } + + class Import extends ChannelBase { + public readonly channelArn = channelArn; + } + + return new Import(scope, id); + } + + public readonly channelArn: string; + + /** + * Channel ingest endpoint, part of the definition of an ingest server, used when you set up streaming software. + * For example: a1b2c3d4e5f6.global-contribute.live-video.net + * @attribute + */ + public readonly channelIngestEndpoint: string; + + /** + * Channel playback URL. For example: + * https://a1b2c3d4e5f6.us-west-2.playback.live-video.net/api/video/v1/us-west-2.123456789012.channel.abcdEFGH.m3u8 + * @attribute + */ + public readonly channelPlaybackUrl: string; + + constructor(scope: Construct, id: string, props: ChannelProps = {}) { + super(scope, id, { + physicalName: props.name, + }); + + if (props.name && !core.Token.isUnresolved(props.name) && !/^[a-zA-Z0-9-_]*$/.test(props.name)) { + throw new Error(`name must contain only numbers, letters, hyphens and underscores, got: '${props.name}'`); + } + + const resource = new CfnChannel(this, 'Resource', { + authorized: props.authorized, + latencyMode: props.latencyMode, + name: props.name, + type: props.type, + }); + + this.channelArn = resource.attrArn; + this.channelIngestEndpoint = resource.attrIngestEndpoint; + this.channelPlaybackUrl = resource.attrPlaybackUrl; + } +} diff --git a/packages/@aws-cdk/aws-ivs/lib/index.ts b/packages/@aws-cdk/aws-ivs/lib/index.ts index 418b7c6157e85..f9fc9b7df42c0 100644 --- a/packages/@aws-cdk/aws-ivs/lib/index.ts +++ b/packages/@aws-cdk/aws-ivs/lib/index.ts @@ -1,2 +1,6 @@ +export * from './channel'; +export * from './playback-key-pair'; +export * from './stream-key'; + // AWS::IVS CloudFormation Resources: export * from './ivs.generated'; diff --git a/packages/@aws-cdk/aws-ivs/lib/playback-key-pair.ts b/packages/@aws-cdk/aws-ivs/lib/playback-key-pair.ts new file mode 100644 index 0000000000000..4a74f6dbd3215 --- /dev/null +++ b/packages/@aws-cdk/aws-ivs/lib/playback-key-pair.ts @@ -0,0 +1,71 @@ +import * as core from '@aws-cdk/core'; +import { Construct } from 'constructs'; +import { CfnPlaybackKeyPair } from './ivs.generated'; + +/** + * Represents an IVS Playback Key Pair + */ +export interface IPlaybackKeyPair extends core.IResource { + /** + * Key-pair ARN. For example: arn:aws:ivs:us-west-2:693991300569:playback-key/f99cde61-c2b0-4df3-8941-ca7d38acca1a + * + * @attribute + */ + readonly playbackKeyPairArn: string; +} + +/** + * Reference to a new or existing IVS Playback Key Pair + */ +abstract class PlaybackKeyPairBase extends core.Resource implements IPlaybackKeyPair { + // these stay abstract at this level + public abstract readonly playbackKeyPairArn: string; +} + +/** + * Properties for creating a new Playback Key Pair + */ +export interface PlaybackKeyPairProps { + /** + * The public portion of a customer-generated key pair. + */ + readonly publicKeyMaterial: string; + + /** + * An arbitrary string (a nickname) assigned to a playback key pair that helps the customer identify that resource. + * The value does not need to be unique. + * + * @default None + */ + readonly name?: string; +} + +/** + A new IVS Playback Key Pair +*/ +export class PlaybackKeyPair extends PlaybackKeyPairBase { + public readonly playbackKeyPairArn: string; + + /** + * Key-pair identifier. For example: 98:0d:1a:a0:19:96:1e:ea:0a:0a:2c:9a:42:19:2b:e7 + * + * @attribute + */ + public readonly playbackKeyPairFingerprint: string; + + constructor(scope: Construct, id: string, props: PlaybackKeyPairProps) { + super(scope, id, {}); + + if (props.name && !core.Token.isUnresolved(props.name) && !/^[a-zA-Z0-9-_]*$/.test(props.name)) { + throw new Error(`name must contain only numbers, letters, hyphens and underscores, got: '${props.name}'`); + } + + const resource = new CfnPlaybackKeyPair(this, 'Resource', { + publicKeyMaterial: props.publicKeyMaterial, + name: props.name, + }); + + this.playbackKeyPairArn = resource.attrArn; + this.playbackKeyPairFingerprint = resource.attrFingerprint; + } +} \ No newline at end of file diff --git a/packages/@aws-cdk/aws-ivs/lib/stream-key.ts b/packages/@aws-cdk/aws-ivs/lib/stream-key.ts new file mode 100644 index 0000000000000..5dadc8fbef556 --- /dev/null +++ b/packages/@aws-cdk/aws-ivs/lib/stream-key.ts @@ -0,0 +1,51 @@ +import * as core from '@aws-cdk/core'; +import { Construct } from 'constructs'; +import { IChannel } from './channel'; +import { CfnStreamKey } from './ivs.generated'; + +/** + * Represents an IVS Stream Key + */ +export interface IStreamKey extends core.IResource { + /** + * The stream-key ARN. For example: arn:aws:ivs:us-west-2:123456789012:stream-key/g1H2I3j4k5L6 + * + * @attribute + */ + readonly streamKeyArn: string; +} + +/** + * Properties for creating a new Stream Key + */ +export interface StreamKeyProps { + /** + * Channel ARN for the stream. + */ + readonly channel: IChannel; +} + +/** + A new IVS Stream Key +*/ +export class StreamKey extends core.Resource implements IStreamKey { + public readonly streamKeyArn: string; + + /** + * The stream-key value. For example: sk_us-west-2_abcdABCDefgh_567890abcdef + * + * @attribute + */ + public readonly streamKeyValue: string; + + constructor(scope: Construct, id: string, props: StreamKeyProps) { + super(scope, id, {}); + + const resource = new CfnStreamKey(this, 'Resource', { + channelArn: props.channel.channelArn, + }); + + this.streamKeyArn = resource.attrArn; + this.streamKeyValue = resource.attrValue; + } +} \ No newline at end of file diff --git a/packages/@aws-cdk/aws-ivs/package.json b/packages/@aws-cdk/aws-ivs/package.json index e45a537ce9e02..1e631e4d46db0 100644 --- a/packages/@aws-cdk/aws-ivs/package.json +++ b/packages/@aws-cdk/aws-ivs/package.json @@ -32,6 +32,15 @@ } } }, + "awslint": { + "exclude": [ + "props-physical-name:@aws-cdk/aws-ivs.ChannelProps", + "props-physical-name:@aws-cdk/aws-ivs.StreamKeyProps", + "props-physical-name:@aws-cdk/aws-ivs.PlaybackKeyPairProps", + "from-method:@aws-cdk/aws-ivs.StreamKey", + "from-method:@aws-cdk/aws-ivs.PlaybackKeyPair" + ] + }, "repository": { "type": "git", "url": "https://github.com/aws/aws-cdk.git", @@ -64,6 +73,7 @@ "keywords": [ "aws", "cdk", + "ivs", "constructs", "AWS::IVS", "aws-ivs" @@ -77,20 +87,23 @@ "devDependencies": { "@aws-cdk/assert": "0.0.0", "cdk-build-tools": "0.0.0", + "cdk-integ-tools": "0.0.0", "cfn2ts": "0.0.0", "pkglint": "0.0.0" }, "dependencies": { - "@aws-cdk/core": "0.0.0" + "@aws-cdk/core": "0.0.0", + "constructs": "^3.2.0" }, "peerDependencies": { - "@aws-cdk/core": "0.0.0" + "@aws-cdk/core": "0.0.0", + "constructs": "^3.2.0" }, "engines": { "node": ">= 10.13.0 <13 || >=13.7.0" }, "stability": "experimental", - "maturity": "cfn-only", + "maturity": "experimental", "awscdkio": { "announce": false } diff --git a/packages/@aws-cdk/aws-ivs/rosetta/default.ts-fixture b/packages/@aws-cdk/aws-ivs/rosetta/default.ts-fixture new file mode 100644 index 0000000000000..c1975240634c8 --- /dev/null +++ b/packages/@aws-cdk/aws-ivs/rosetta/default.ts-fixture @@ -0,0 +1,19 @@ +// Fixture with packages imported, but nothing else +import { Duration, Stack } from '@aws-cdk/core'; +import { Construct } from 'constructs'; +import * as ivs from '@aws-cdk/aws-ivs'; + +class Fixture extends Stack { + constructor(scope: Construct, id: string) { + super(scope, id); + + const myChannelArn = 'arn:aws:ivs:us-west-2:123456789012:channel/abcdABCDefgh'; + const myPublicKeyPemString = `-----BEGIN PUBLIC KEY----- +MHYwEAYHKoZIzj0CAQYFK4EEACIDYgAEmL8CBtm6KLUegSd5IJexniN8LItJiDwg +zlYiti/ZTP/JPoUMb0fjqBDKZRhGbA6gSHdcm5YkdbGzIsQRnIqfhYy52kO13miR +d2/EL+sn3x1/ziRhvZ2elvpaZAN68QiM +-----END PUBLIC KEY-----`; + + /// here + } +} \ No newline at end of file diff --git a/packages/@aws-cdk/aws-ivs/rosetta/with-channel.ts-fixture b/packages/@aws-cdk/aws-ivs/rosetta/with-channel.ts-fixture new file mode 100644 index 0000000000000..44da118b81afa --- /dev/null +++ b/packages/@aws-cdk/aws-ivs/rosetta/with-channel.ts-fixture @@ -0,0 +1,15 @@ +// Fixture with packages imported, but nothing else +import { Duration, Stack } from '@aws-cdk/core'; +import { Construct } from 'constructs'; +import * as ivs from '@aws-cdk/aws-ivs'; + +class Fixture extends Stack { + constructor(scope: Construct, id: string) { + super(scope, id); + + const myChannelArn = 'arn:aws:ivs:us-west-2:123456789012:channel/abcdABCDefgh'; + const myChannel = ivs.Channel.fromChannelArn(this, 'Channel', myChannelArn); + + /// here + } +} \ No newline at end of file diff --git a/packages/@aws-cdk/aws-ivs/test/integ.ivs.expected.json b/packages/@aws-cdk/aws-ivs/test/integ.ivs.expected.json new file mode 100644 index 0000000000000..f681d0e581c83 --- /dev/null +++ b/packages/@aws-cdk/aws-ivs/test/integ.ivs.expected.json @@ -0,0 +1,57 @@ +{ + "Resources": { + "PlaybackKeyPairBE17315B": { + "Type": "AWS::IVS::PlaybackKeyPair", + "Properties": { + "PublicKeyMaterial": "-----BEGIN PUBLIC KEY-----\nMHYwEAYHKoZIzj0CAQYFK4EEACIDYgAEs6k8Xf6WyFq3yZXoup8G/gH6DntSATqD\nYfo83eX0GJCKxJ8fr09h9LP9HDGof8/bo66P+SGHeAARGF/O9WPAQVUgSlm/KMFX\nEPtPtOm1s0GR9k1ydU5hkI++f9CoZ5lM\n-----END PUBLIC KEY-----", + "Name": "IVSIntegrationTestPlaybackKeyPair" + } + }, + "Channel4048F119": { + "Type": "AWS::IVS::Channel", + "Properties": { + "Authorized": true, + "LatencyMode": "NORMAL", + "Name": "IVSIntegrationTestChannel", + "Type": "BASIC" + } + }, + "StreamKey9F296F4F": { + "Type": "AWS::IVS::StreamKey", + "Properties": { + "ChannelArn": { + "Fn::GetAtt": [ + "Channel4048F119", + "Arn" + ] + } + } + } + }, + "Outputs": { + "PlaybackKeyPairArn": { + "Value": { + "Fn::GetAtt": [ + "PlaybackKeyPairBE17315B", + "Arn" + ] + } + }, + "ChannelArn": { + "Value": { + "Fn::GetAtt": [ + "Channel4048F119", + "Arn" + ] + } + }, + "StreamKeyArn": { + "Value": { + "Fn::GetAtt": [ + "StreamKey9F296F4F", + "Arn" + ] + } + } + } +} \ No newline at end of file diff --git a/packages/@aws-cdk/aws-ivs/test/integ.ivs.ts b/packages/@aws-cdk/aws-ivs/test/integ.ivs.ts new file mode 100644 index 0000000000000..49845ea221287 --- /dev/null +++ b/packages/@aws-cdk/aws-ivs/test/integ.ivs.ts @@ -0,0 +1,44 @@ +import { App, CfnOutput, Stack } from '@aws-cdk/core'; +import * as ivs from '../lib'; + +/* + * Creates a channel, playback key pair and stream key + * + * Stack verification steps: + * Check to make sure the resources exist + * + * -- aws ivs get-channel --arn provides channel with name IVSIntegrationTestChannel + * -- aws ivs get-stream-key --arn provides stream key for the channel with the arn from output + * -- aws ivs get-playback-key-pair --arn provides playback key pair with name IVSIntegrationTestPlaybackKeyPair + */ +const app = new App(); + +const stack = new Stack(app, 'aws-cdk-ivs'); + +const publicKey = `-----BEGIN PUBLIC KEY----- +MHYwEAYHKoZIzj0CAQYFK4EEACIDYgAEs6k8Xf6WyFq3yZXoup8G/gH6DntSATqD +Yfo83eX0GJCKxJ8fr09h9LP9HDGof8/bo66P+SGHeAARGF/O9WPAQVUgSlm/KMFX +EPtPtOm1s0GR9k1ydU5hkI++f9CoZ5lM +-----END PUBLIC KEY-----`; + +const keypair = new ivs.PlaybackKeyPair(stack, 'PlaybackKeyPair', { + publicKeyMaterial: publicKey, + name: 'IVSIntegrationTestPlaybackKeyPair', +}); + +const channel = new ivs.Channel(stack, 'Channel', { + name: 'IVSIntegrationTestChannel', + authorized: true, + type: ivs.ChannelType.BASIC, + latencyMode: ivs.LatencyMode.NORMAL, +}); + +const streamKey = new ivs.StreamKey(stack, 'StreamKey', { + channel: channel, +}); + +new CfnOutput(stack, 'PlaybackKeyPairArn', { value: keypair.playbackKeyPairArn }); +new CfnOutput(stack, 'ChannelArn', { value: channel.channelArn }); +new CfnOutput(stack, 'StreamKeyArn', { value: streamKey.streamKeyArn }); + +app.synth(); diff --git a/packages/@aws-cdk/aws-ivs/test/ivs.test.ts b/packages/@aws-cdk/aws-ivs/test/ivs.test.ts index e394ef336bfb4..0ed2c6712e0d5 100644 --- a/packages/@aws-cdk/aws-ivs/test/ivs.test.ts +++ b/packages/@aws-cdk/aws-ivs/test/ivs.test.ts @@ -1,6 +1,232 @@ import '@aws-cdk/assert/jest'; -import {} from '../lib'; +import { expect as expectStack } from '@aws-cdk/assert'; +import { Stack } from '@aws-cdk/core'; +import * as ivs from '../lib'; -test('No tests are specified for this package', () => { - expect(true).toBe(true); +const publicKey = `-----BEGIN PUBLIC KEY----- +MHYwEAYHKoZIzj0CAQYFK4EEACIDYgAEs6k8Xf6WyFq3yZXoup8G/gH6DntSATqD +Yfo83eX0GJCKxJ8fr09h9LP9HDGof8/bo66P+SGHeAARGF/O9WPAQVUgSlm/KMFX +EPtPtOm1s0GR9k1ydU5hkI++f9CoZ5lM +-----END PUBLIC KEY-----`; + +test('channel default properties', () => { + const stack = new Stack(); + new ivs.Channel(stack, 'Channel'); + + expectStack(stack).toMatch({ + Resources: { + Channel4048F119: { + Type: 'AWS::IVS::Channel', + }, + }, + }); +}); + +test('channel name', () => { + const stack = new Stack(); + new ivs.Channel(stack, 'Channel', { + name: 'CarrotsAreTasty', + }); + + expectStack(stack).toMatch({ + Resources: { + Channel4048F119: { + Type: 'AWS::IVS::Channel', + Properties: { + Name: 'CarrotsAreTasty', + }, + }, + }, + }); +}); + +test('channel is authorized', () => { + const stack = new Stack(); + new ivs.Channel(stack, 'Channel', { + authorized: true, + }); + + expectStack(stack).toMatch({ + Resources: { + Channel4048F119: { + Type: 'AWS::IVS::Channel', + Properties: { + Authorized: 'true', + }, + }, + }, + }); +}); + +test('channel type', () => { + const stack = new Stack(); + new ivs.Channel(stack, 'Channel', { + type: ivs.ChannelType.BASIC, + }); + + expectStack(stack).toMatch({ + Resources: { + Channel4048F119: { + Type: 'AWS::IVS::Channel', + Properties: { + Type: 'BASIC', + }, + }, + }, + }); +}); + +test('channel latency mode', () => { + const stack = new Stack(); + new ivs.Channel(stack, 'Channel', { + latencyMode: ivs.LatencyMode.NORMAL, + }); + + expectStack(stack).toMatch({ + Resources: { + Channel4048F119: { + Type: 'AWS::IVS::Channel', + Properties: { + LatencyMode: 'NORMAL', + }, + }, + }, + }); +}); + +test('channel from arn', () => { + const stack = new Stack(); + const channel = ivs.Channel.fromChannelArn(stack, 'Channel', 'arn:aws:ivs:us-west-2:123456789012:channel/abcdABCDefgh'); + + expect(stack.resolve(channel.channelArn)).toBe('arn:aws:ivs:us-west-2:123456789012:channel/abcdABCDefgh'); +}); + +test('channel invalid name throws validation error', () => { + const stack = new Stack(); + + expect(() => new ivs.Channel(stack, 'Channel', { + name: 'Would you like a carrot?', + })).toThrow('name must contain only numbers, letters, hyphens and underscores, got: \'Would you like a carrot?\''); +}); + +test('playback key pair mandatory properties', () => { + const stack = new Stack(); + new ivs.PlaybackKeyPair(stack, 'PlaybackKeyPair', { + publicKeyMaterial: publicKey, + }); + + expectStack(stack).toMatch({ + Resources: { + PlaybackKeyPairBE17315B: { + Type: 'AWS::IVS::PlaybackKeyPair', + Properties: { + PublicKeyMaterial: publicKey, + }, + }, + }, + }); +}); + +test('playback key pair name', () => { + const stack = new Stack(); + new ivs.PlaybackKeyPair(stack, 'PlaybackKeyPair', { + publicKeyMaterial: publicKey, + name: 'CarrotsAreNutritious', + }); + + expectStack(stack).toMatch({ + Resources: { + PlaybackKeyPairBE17315B: { + Type: 'AWS::IVS::PlaybackKeyPair', + Properties: { + PublicKeyMaterial: publicKey, + Name: 'CarrotsAreNutritious', + }, + }, + }, + }); +}); + +test('playback key pair invalid name throws validation error', () => { + const stack = new Stack(); + + expect(() => new ivs.PlaybackKeyPair(stack, 'PlaybackKeyPair', { + publicKeyMaterial: 'Carrots Are Orange', + name: 'Would you like a carrot?', + })).toThrow('name must contain only numbers, letters, hyphens and underscores, got: \'Would you like a carrot?\''); +}); + +test('stream key mandatory properties', () => { + const stack = new Stack(); + new ivs.StreamKey(stack, 'StreamKey', { + channel: ivs.Channel.fromChannelArn(stack, 'ChannelRef', 'arn:aws:ivs:us-west-2:123456789012:channel/abcdABCDefgh'), + }); + + expectStack(stack).toMatch({ + Resources: { + StreamKey9F296F4F: { + Type: 'AWS::IVS::StreamKey', + Properties: { + ChannelArn: 'arn:aws:ivs:us-west-2:123456789012:channel/abcdABCDefgh', + }, + }, + }, + }); +}); + +test('channel and stream key.. at the same time', () => { + const stack = new Stack(); + const channel = new ivs.Channel(stack, 'Channel'); + channel.addStreamKey('StreamKey'); + + expectStack(stack).toMatch({ + Resources: { + Channel4048F119: { + Type: 'AWS::IVS::Channel', + }, + ChannelStreamKey60BDC2BE: { + Type: 'AWS::IVS::StreamKey', + Properties: { + ChannelArn: { 'Fn::GetAtt': ['Channel4048F119', 'Arn'] }, + }, + }, + }, + }); +}); + +test('stream key from channel reference', () => { + const stack = new Stack(); + const channel = ivs.Channel.fromChannelArn(stack, 'Channel', 'arn:aws:ivs:us-west-2:123456789012:channel/abcdABCDefgh'); + channel.addStreamKey('StreamKey'); + + expectStack(stack).toMatch({ + Resources: { + ChannelStreamKey60BDC2BE: { + Type: 'AWS::IVS::StreamKey', + Properties: { + ChannelArn: 'arn:aws:ivs:us-west-2:123456789012:channel/abcdABCDefgh', + }, + }, + }, + }); +}); + +test('channel from invalid channel arn throws error', () => { + const stack = new Stack(); + expect(() => ivs.Channel.fromChannelArn(stack, 'ChannelRef', 'this is an invalid arn, in fact, it is a carrot 🥕')) + .toThrow('ARNs must start with \"arn:\" and have at least 6 components: this is an invalid arn, in fact, it is a carrot 🥕'); +}); + +test('channel from invalid channel arn service throws error', () => { + const stack = new Stack(); + expect( + () => ivs.Channel.fromChannelArn(stack, 'ChannelRef', 'arn:aws:ec2:us-west-2:123456789012:instance/abcdABCDefgh')) + .toThrow('Invalid service, expected \'ivs\', got \'ec2\''); +}); + +test('channel from invalid channel arn resource throws error', () => { + const stack = new Stack(); + expect( + () => ivs.Channel.fromChannelArn(stack, 'ChannelRef', 'arn:aws:ivs:us-west-2:123456789012:stream-key/abcdABCDefgh')) + .toThrow('Invalid resource, expected \'channel\', got \'stream-key\''); }); From 9417b0211eb2939f5a853751333f7941d9dd99f8 Mon Sep 17 00:00:00 2001 From: Jonathan Goldwasser Date: Mon, 14 Dec 2020 12:10:34 +0100 Subject: [PATCH 4/9] chore(core): show Docker build output for BundlingDockerImage.fromAsset() (#12001) Remove the `-q` option and use a known tag derived from the path to the Dockerfile and the build options. ---- *By submitting this pull request, I confirm that my contribution is made under the terms of the Apache-2.0 license* --- packages/@aws-cdk/core/lib/bundling.ts | 36 +++++++++--------- packages/@aws-cdk/core/test/bundling.test.ts | 39 +++++++++----------- 2 files changed, 37 insertions(+), 38 deletions(-) diff --git a/packages/@aws-cdk/core/lib/bundling.ts b/packages/@aws-cdk/core/lib/bundling.ts index 234b3ce969b42..c9a9c07e77f34 100644 --- a/packages/@aws-cdk/core/lib/bundling.ts +++ b/packages/@aws-cdk/core/lib/bundling.ts @@ -1,4 +1,5 @@ import { spawnSync, SpawnSyncOptions } from 'child_process'; +import * as crypto from 'crypto'; import { FileSystem } from './fs'; /** @@ -108,20 +109,21 @@ export class BundlingDockerImage { public static fromAsset(path: string, options: DockerBuildOptions = {}) { const buildArgs = options.buildArgs || {}; + // Image tag derived from path and build options + const tagHash = crypto.createHash('sha256').update(JSON.stringify({ + path, + ...options, + })).digest('hex'); + const tag = `cdk-${tagHash}`; + const dockerArgs: string[] = [ - 'build', '-q', + 'build', '-t', tag, ...(options.file ? ['-f', options.file] : []), ...flatten(Object.entries(buildArgs).map(([k, v]) => ['--build-arg', `${k}=${v}`])), path, ]; - const docker = dockerExec(dockerArgs); - - const match = docker.stdout.toString().match(/sha256:[a-z0-9]+/); - - if (!match) { - throw new Error('Failed to extract image ID from Docker build output'); - } + dockerExec(dockerArgs); // Fingerprints the directory containing the Dockerfile we're building and // differentiates the fingerprint based on build arguments. We do this so @@ -129,7 +131,7 @@ export class BundlingDockerImage { // different every time the Docker layer cache is cleared, due primarily to // timestamps. const hash = FileSystem.fingerprint(path, { extraHash: JSON.stringify(options) }); - return new BundlingDockerImage(match[0], hash); + return new BundlingDockerImage(tag, hash); } /** @param image The Docker image */ @@ -166,13 +168,7 @@ export class BundlingDockerImage { ...command, ]; - dockerExec(dockerArgs, { - stdio: [ // show Docker output - 'ignore', // ignore stdio - process.stderr, // redirect stdout to stderr - 'inherit', // inherit stderr - ], - }); + dockerExec(dockerArgs); } /** @@ -303,7 +299,13 @@ function flatten(x: string[][]) { function dockerExec(args: string[], options?: SpawnSyncOptions) { const prog = process.env.CDK_DOCKER ?? 'docker'; - const proc = spawnSync(prog, args, options); + const proc = spawnSync(prog, args, options ?? { + stdio: [ // show Docker output + 'ignore', // ignore stdio + process.stderr, // redirect stdout to stderr + 'inherit', // inherit stderr + ], + }); if (proc.error) { throw proc.error; diff --git a/packages/@aws-cdk/core/test/bundling.test.ts b/packages/@aws-cdk/core/test/bundling.test.ts index 5bc4c0d55e1be..e2b0d6b43b98b 100644 --- a/packages/@aws-cdk/core/test/bundling.test.ts +++ b/packages/@aws-cdk/core/test/bundling.test.ts @@ -1,4 +1,5 @@ import * as child_process from 'child_process'; +import * as crypto from 'crypto'; import * as path from 'path'; import { nodeunitShim, Test } from 'nodeunit-shim'; import * as sinon from 'sinon'; @@ -46,11 +47,10 @@ nodeunitShim({ }, 'bundling with image from asset'(test: Test) { - const imageId = 'sha256:abcdef123456'; const spawnSyncStub = sinon.stub(child_process, 'spawnSync').returns({ status: 0, stderr: Buffer.from('stderr'), - stdout: Buffer.from(imageId), + stdout: Buffer.from('stdout'), pid: 123, output: ['stdout', 'stderr'], signal: null, @@ -67,33 +67,27 @@ nodeunitShim({ }); image.run(); + const tagHash = crypto.createHash('sha256').update(JSON.stringify({ + path: 'docker-path', + buildArgs: { + TEST_ARG: 'cdk-test', + }, + })).digest('hex'); + const tag = `cdk-${tagHash}`; + test.ok(spawnSyncStub.firstCall.calledWith('docker', [ - 'build', '-q', + 'build', '-t', tag, '--build-arg', 'TEST_ARG=cdk-test', 'docker-path', ])); test.ok(spawnSyncStub.secondCall.calledWith('docker', [ 'run', '--rm', - imageId, + tag, ])); test.done(); }, - 'throws if image id cannot be extracted from build output'(test: Test) { - sinon.stub(child_process, 'spawnSync').returns({ - status: 0, - stderr: Buffer.from('stderr'), - stdout: Buffer.from('stdout'), - pid: 123, - output: ['stdout', 'stderr'], - signal: null, - }); - - test.throws(() => BundlingDockerImage.fromAsset('docker-path'), /Failed to extract image ID from Docker build output/); - test.done(); - }, - 'throws in case of spawnSync error'(test: Test) { sinon.stub(child_process, 'spawnSync').returns({ status: 0, @@ -133,11 +127,10 @@ nodeunitShim({ }, 'BundlerDockerImage json is the bundler image if building an image'(test: Test) { - const imageId = 'sha256:abcdef123456'; sinon.stub(child_process, 'spawnSync').returns({ status: 0, stderr: Buffer.from('stderr'), - stdout: Buffer.from(imageId), + stdout: Buffer.from('stdout'), pid: 123, output: ['stdout', 'stderr'], signal: null, @@ -148,7 +141,11 @@ nodeunitShim({ const image = BundlingDockerImage.fromAsset('docker-path'); - test.equals(image.image, imageId); + const tagHash = crypto.createHash('sha256').update(JSON.stringify({ + path: 'docker-path', + })).digest('hex'); + + test.equals(image.image, `cdk-${tagHash}`); test.equals(image.toJSON(), imageHash); test.ok(fingerprintStub.calledWith('docker-path', sinon.match({ extraHash: JSON.stringify({}) }))); test.done(); From 06f26d390707b0e2a4e05e36405a4751c907a234 Mon Sep 17 00:00:00 2001 From: Jonathan Goldwasser Date: Mon, 14 Dec 2020 12:41:00 +0100 Subject: [PATCH 5/9] feat(core): expose custom resource provider's role (#11923) The role ARN can then be used in resource-based IAM policies. See https://github.com/aws/aws-cdk/pull/9751#issuecomment-723554595 ---- *By submitting this pull request, I confirm that my contribution is made under the terms of the Apache-2.0 license* --- packages/@aws-cdk/core/README.md | 14 +++++++++++ .../custom-resource-provider.ts | 23 ++++++++++++++++++- .../custom-resource-provider.test.ts | 20 ++++++++++++++++ 3 files changed, 56 insertions(+), 1 deletion(-) diff --git a/packages/@aws-cdk/core/README.md b/packages/@aws-cdk/core/README.md index ffa333cd0634f..3b013f3ff990e 100644 --- a/packages/@aws-cdk/core/README.md +++ b/packages/@aws-cdk/core/README.md @@ -486,6 +486,20 @@ const sum = new Sum(this, 'MySum', { lhs: 40, rhs: 2 }); new CfnOutput(this, 'Result', { value: Token.asString(sum.result) }); ``` +To access the ARN of the provider's AWS Lambda function role, use the `getOrCreateProvider()` +built-in singleton method: + +```ts +const provider = CustomResourceProvider.getOrCreateProvider(this, 'Custom::MyCustomResourceType', { + codeDirectory: `${__dirname}/my-handler`, + runtime: CustomResourceProviderRuntime.NODEJS_12, // currently the only supported runtime +}); + +const roleArn = provider.roleArn; +``` + +This role ARN can then be used in resource-based IAM policies. + #### The Custom Resource Provider Framework The [`@aws-cdk/custom-resources`] module includes an advanced framework for diff --git a/packages/@aws-cdk/core/lib/custom-resource-provider/custom-resource-provider.ts b/packages/@aws-cdk/core/lib/custom-resource-provider/custom-resource-provider.ts index ea5b0882dbb44..f7905fc51447b 100644 --- a/packages/@aws-cdk/core/lib/custom-resource-provider/custom-resource-provider.ts +++ b/packages/@aws-cdk/core/lib/custom-resource-provider/custom-resource-provider.ts @@ -100,12 +100,27 @@ export class CustomResourceProvider extends CoreConstruct { * used when defining a `CustomResource`. */ public static getOrCreate(scope: Construct, uniqueid: string, props: CustomResourceProviderProps) { + return this.getOrCreateProvider(scope, uniqueid, props).serviceToken; + } + + /** + * Returns a stack-level singleton for the custom resource provider. + * + * @param scope Construct scope + * @param uniqueid A globally unique id that will be used for the stack-level + * construct. + * @param props Provider properties which will only be applied when the + * provider is first created. + * @returns the service token of the custom resource provider, which should be + * used when defining a `CustomResource`. + */ + public static getOrCreateProvider(scope: Construct, uniqueid: string, props: CustomResourceProviderProps) { const id = `${uniqueid}CustomResourceProvider`; const stack = Stack.of(scope); const provider = stack.node.tryFindChild(id) as CustomResourceProvider ?? new CustomResourceProvider(stack, id, props); - return provider.serviceToken; + return provider; } /** @@ -121,6 +136,11 @@ export class CustomResourceProvider extends CoreConstruct { */ public readonly serviceToken: string; + /** + * The ARN of the provider's AWS Lambda function role. + */ + public readonly roleArn: string; + protected constructor(scope: Construct, id: string, props: CustomResourceProviderProps) { super(scope, id); @@ -167,6 +187,7 @@ export class CustomResourceProvider extends CoreConstruct { Policies: policies, }, }); + this.roleArn = Token.asString(role.getAtt('Arn')); const timeout = props.timeout ?? Duration.minutes(15); const memory = props.memorySize ?? Size.mebibytes(128); diff --git a/packages/@aws-cdk/core/test/custom-resource-provider/custom-resource-provider.test.ts b/packages/@aws-cdk/core/test/custom-resource-provider/custom-resource-provider.test.ts index c56a991298574..b6c1e608e2f59 100644 --- a/packages/@aws-cdk/core/test/custom-resource-provider/custom-resource-provider.test.ts +++ b/packages/@aws-cdk/core/test/custom-resource-provider/custom-resource-provider.test.ts @@ -232,5 +232,25 @@ nodeunitShim({ }); test.done(); }, + + 'roleArn'(test: Test) { + // GIVEN + const stack = new Stack(); + + // WHEN + const cr = CustomResourceProvider.getOrCreateProvider(stack, 'Custom:MyResourceType', { + codeDirectory: TEST_HANDLER, + runtime: CustomResourceProviderRuntime.NODEJS_12, + }); + + // THEN + test.deepEqual(stack.resolve(cr.roleArn), { + 'Fn::GetAtt': [ + 'CustomMyResourceTypeCustomResourceProviderRoleBD5E655F', + 'Arn', + ], + }); + test.done(); + }, }); From 86ae5d6ec5291f7a8da37bbf021c31f88e66d283 Mon Sep 17 00:00:00 2001 From: Thomas Poignant Date: Mon, 14 Dec 2020 13:11:29 +0100 Subject: [PATCH 6/9] fix(ec2): fromInterfaceVpcEndpointAttributes: Security Groups should not be required (#11857) fix: This pull request removes the fact that a security group is mandatory while using `fromInterfaceVpcEndpointAttributes`. resolves #11050 *By submitting this pull request, I confirm that my contribution is made under the terms of the Apache-2.0 license* --- packages/@aws-cdk/aws-ec2/lib/vpc-endpoint.ts | 4 ---- .../@aws-cdk/aws-ec2/test/vpc-endpoint.test.ts | 18 ++++++++++++++++++ 2 files changed, 18 insertions(+), 4 deletions(-) diff --git a/packages/@aws-cdk/aws-ec2/lib/vpc-endpoint.ts b/packages/@aws-cdk/aws-ec2/lib/vpc-endpoint.ts index e17632ac2acb0..2efcfbc69b68a 100644 --- a/packages/@aws-cdk/aws-ec2/lib/vpc-endpoint.ts +++ b/packages/@aws-cdk/aws-ec2/lib/vpc-endpoint.ts @@ -405,10 +405,6 @@ export class InterfaceVpcEndpoint extends VpcEndpoint implements IInterfaceVpcEn * Imports an existing interface VPC endpoint. */ public static fromInterfaceVpcEndpointAttributes(scope: Construct, id: string, attrs: InterfaceVpcEndpointAttributes): IInterfaceVpcEndpoint { - if (!attrs.securityGroups && !attrs.securityGroupId) { - throw new Error('Either `securityGroups` or `securityGroupId` must be specified.'); - } - const securityGroups = attrs.securityGroupId ? [SecurityGroup.fromSecurityGroupId(scope, 'SecurityGroup', attrs.securityGroupId)] : attrs.securityGroups; diff --git a/packages/@aws-cdk/aws-ec2/test/vpc-endpoint.test.ts b/packages/@aws-cdk/aws-ec2/test/vpc-endpoint.test.ts index cc6f116bd1807..68ff81c7f8b86 100644 --- a/packages/@aws-cdk/aws-ec2/test/vpc-endpoint.test.ts +++ b/packages/@aws-cdk/aws-ec2/test/vpc-endpoint.test.ts @@ -290,6 +290,24 @@ nodeunitShim({ test.done(); }, + 'import/export without security group'(test: Test) { + // GIVEN + const stack2 = new Stack(); + + // WHEN + const importedEndpoint = InterfaceVpcEndpoint.fromInterfaceVpcEndpointAttributes(stack2, 'ImportedEndpoint', { + vpcEndpointId: 'vpc-endpoint-id', + port: 80, + }); + importedEndpoint.connections.allowDefaultPortFromAnyIpv4(); + + // THEN + test.deepEqual(importedEndpoint.vpcEndpointId, 'vpc-endpoint-id'); + test.deepEqual(importedEndpoint.connections.securityGroups.length, 0); + + test.done(); + }, + 'with existing security groups'(test: Test) { // GIVEN const stack = new Stack(); From ccbaf8399c3a9f3ff6e60758e0b713d82f37420b Mon Sep 17 00:00:00 2001 From: Alvyn Duy-Khoi Le Date: Mon, 14 Dec 2020 07:42:04 -0500 Subject: [PATCH 7/9] feat(lambda): encryption key for environment variables (#11893) - ARN for KMS key used to encrypt lambda env vars can now be specified in L2 `Function` construct - fixes #10837 #### Notes Could we please decorate this PR with the `pr-linter/exempt-readme` label? I don't think this is a big enough feature to add README content. ---- *By submitting this pull request, I confirm that my contribution is made under the terms of the Apache-2.0 license* --- packages/@aws-cdk/aws-lambda/lib/function.ts | 9 +++++ packages/@aws-cdk/aws-lambda/package.json | 2 + .../@aws-cdk/aws-lambda/test/function.test.ts | 37 ++++++++++++++++++- 3 files changed, 47 insertions(+), 1 deletion(-) diff --git a/packages/@aws-cdk/aws-lambda/lib/function.ts b/packages/@aws-cdk/aws-lambda/lib/function.ts index d69a76c0639a5..71f91b9fd76d3 100644 --- a/packages/@aws-cdk/aws-lambda/lib/function.ts +++ b/packages/@aws-cdk/aws-lambda/lib/function.ts @@ -2,6 +2,7 @@ import * as cloudwatch from '@aws-cdk/aws-cloudwatch'; import { IProfilingGroup, ProfilingGroup, ComputePlatform } from '@aws-cdk/aws-codeguruprofiler'; import * as ec2 from '@aws-cdk/aws-ec2'; import * as iam from '@aws-cdk/aws-iam'; +import * as kms from '@aws-cdk/aws-kms'; import * as logs from '@aws-cdk/aws-logs'; import * as sqs from '@aws-cdk/aws-sqs'; import { Annotations, CfnResource, Duration, Fn, Lazy, Names, Stack } from '@aws-cdk/core'; @@ -282,6 +283,13 @@ export interface FunctionOptions extends EventInvokeConfigOptions { * @default false */ readonly allowPublicSubnet?: boolean; + + /** + * The AWS KMS key that's used to encrypt your function's environment variables. + * + * @default - AWS Lambda creates and uses an AWS managed customer master key (CMK). + */ + readonly environmentEncryption?: kms.IKey; } export interface FunctionProps extends FunctionOptions { @@ -625,6 +633,7 @@ export class Function extends FunctionBase { command: code.image?.cmd, entryPoint: code.image?.entrypoint, }), + kmsKeyArn: props.environmentEncryption?.keyArn, }); // since patching the CFN spec to make Runtime and Handler optional causes a diff --git a/packages/@aws-cdk/aws-lambda/package.json b/packages/@aws-cdk/aws-lambda/package.json index 85cb64f0e1a17..4414239146e74 100644 --- a/packages/@aws-cdk/aws-lambda/package.json +++ b/packages/@aws-cdk/aws-lambda/package.json @@ -97,6 +97,7 @@ "@aws-cdk/aws-efs": "0.0.0", "@aws-cdk/aws-events": "0.0.0", "@aws-cdk/aws-iam": "0.0.0", + "@aws-cdk/aws-kms": "0.0.0", "@aws-cdk/aws-logs": "0.0.0", "@aws-cdk/aws-s3": "0.0.0", "@aws-cdk/aws-s3-assets": "0.0.0", @@ -116,6 +117,7 @@ "@aws-cdk/aws-efs": "0.0.0", "@aws-cdk/aws-events": "0.0.0", "@aws-cdk/aws-iam": "0.0.0", + "@aws-cdk/aws-kms": "0.0.0", "@aws-cdk/aws-logs": "0.0.0", "@aws-cdk/aws-s3": "0.0.0", "@aws-cdk/aws-s3-assets": "0.0.0", diff --git a/packages/@aws-cdk/aws-lambda/test/function.test.ts b/packages/@aws-cdk/aws-lambda/test/function.test.ts index 3a10ebab4247b..707b32428912b 100644 --- a/packages/@aws-cdk/aws-lambda/test/function.test.ts +++ b/packages/@aws-cdk/aws-lambda/test/function.test.ts @@ -5,6 +5,7 @@ import { ProfilingGroup } from '@aws-cdk/aws-codeguruprofiler'; import * as ec2 from '@aws-cdk/aws-ec2'; import * as efs from '@aws-cdk/aws-efs'; import * as iam from '@aws-cdk/aws-iam'; +import * as kms from '@aws-cdk/aws-kms'; import * as logs from '@aws-cdk/aws-logs'; import * as s3 from '@aws-cdk/aws-s3'; import * as sqs from '@aws-cdk/aws-sqs'; @@ -1569,6 +1570,40 @@ describe('function', () => { expect(stack.resolve(version2.functionArn)).toEqual(expectedArn); }); + test('default function with kmsKeyArn, environmentEncryption passed as props', () => { + // GIVEN + const stack = new cdk.Stack(); + const key: kms.IKey = new kms.Key(stack, 'EnvVarEncryptKey', { + description: 'sample key', + }); + + // WHEN + new lambda.Function(stack, 'MyLambda', { + code: new lambda.InlineCode('foo'), + handler: 'index.handler', + runtime: lambda.Runtime.NODEJS_10_X, + environment: { + SOME: 'Variable', + }, + environmentEncryption: key, + }); + + // THEN + expect(stack).toHaveResource('AWS::Lambda::Function', { + Environment: { + Variables: { + SOME: 'Variable', + }, + }, + KmsKeyArn: { + 'Fn::GetAtt': [ + 'EnvVarEncryptKey1A7CABDB', + 'Arn', + ], + }, + }); + }); + describe('profiling group', () => { test('default function with CDK created Profiling Group', () => { const stack = new cdk.Stack(); @@ -1962,4 +1997,4 @@ function newTestLambda(scope: constructs.Construct) { handler: 'bar', runtime: lambda.Runtime.PYTHON_2_7, }); -} \ No newline at end of file +} From 1cf6713b778c789af7a420ad890910a9516473f0 Mon Sep 17 00:00:00 2001 From: Shiv Lakshminarayan Date: Mon, 14 Dec 2020 05:14:20 -0800 Subject: [PATCH 8/9] fix(stepfunctions-tasks): policies created for EMR tasks have ARNs that are not partition-aware (#11553) closes #11503 ---- *By submitting this pull request, I confirm that my contribution is made under the terms of the Apache-2.0 license* --- .../lib/emr/emr-add-step.ts | 10 ++- .../lib/emr/emr-cancel-step.ts | 10 ++- .../emr/emr-modify-instance-fleet-by-name.ts | 10 ++- .../emr/emr-modify-instance-group-by-name.ts | 19 +++-- .../emr-set-cluster-termination-protection.ts | 10 ++- .../lib/emr/emr-terminate-cluster.ts | 10 ++- .../test/emr/emr-add-step.test.ts | 76 +++++++++++++++++++ .../test/emr/emr-cancel-step.test.ts | 44 +++++++++++ .../emr-modify-instance-fleet-by-name.test.ts | 49 ++++++++++++ .../emr-modify-instance-group-by-name.test.ts | 55 ++++++++++++++ ...set-cluster-termination-protection.test.ts | 44 +++++++++++ .../test/emr/emr-terminate-cluster.test.ts | 75 ++++++++++++++++++ 12 files changed, 397 insertions(+), 15 deletions(-) diff --git a/packages/@aws-cdk/aws-stepfunctions-tasks/lib/emr/emr-add-step.ts b/packages/@aws-cdk/aws-stepfunctions-tasks/lib/emr/emr-add-step.ts index 9df7234bd2a01..83fe924866e95 100644 --- a/packages/@aws-cdk/aws-stepfunctions-tasks/lib/emr/emr-add-step.ts +++ b/packages/@aws-cdk/aws-stepfunctions-tasks/lib/emr/emr-add-step.ts @@ -1,6 +1,6 @@ import * as iam from '@aws-cdk/aws-iam'; import * as sfn from '@aws-cdk/aws-stepfunctions'; -import { Aws, Stack } from '@aws-cdk/core'; +import { Stack } from '@aws-cdk/core'; import { Construct } from 'constructs'; import { integrationResourceArn, validatePatternSupported } from '../private/task-utils'; @@ -165,7 +165,13 @@ export class EmrAddStep extends sfn.TaskStateBase { 'elasticmapreduce:DescribeStep', 'elasticmapreduce:CancelSteps', ], - resources: [`arn:aws:elasticmapreduce:${Aws.REGION}:${Aws.ACCOUNT_ID}:cluster/*`], + resources: [ + stack.formatArn({ + service: 'elasticmapreduce', + resource: 'cluster', + resourceName: '*', + }), + ], }), ]; diff --git a/packages/@aws-cdk/aws-stepfunctions-tasks/lib/emr/emr-cancel-step.ts b/packages/@aws-cdk/aws-stepfunctions-tasks/lib/emr/emr-cancel-step.ts index 3a4215ab61bac..ee15fb4f44e68 100644 --- a/packages/@aws-cdk/aws-stepfunctions-tasks/lib/emr/emr-cancel-step.ts +++ b/packages/@aws-cdk/aws-stepfunctions-tasks/lib/emr/emr-cancel-step.ts @@ -1,6 +1,6 @@ import * as iam from '@aws-cdk/aws-iam'; import * as sfn from '@aws-cdk/aws-stepfunctions'; -import { Aws } from '@aws-cdk/core'; +import { Stack } from '@aws-cdk/core'; import { Construct } from 'constructs'; import { integrationResourceArn } from '../private/task-utils'; @@ -36,7 +36,13 @@ export class EmrCancelStep extends sfn.TaskStateBase { this.taskPolicies = [ new iam.PolicyStatement({ actions: ['elasticmapreduce:CancelSteps'], - resources: [`arn:aws:elasticmapreduce:${Aws.REGION}:${Aws.ACCOUNT_ID}:cluster/*`], + resources: [ + Stack.of(this).formatArn({ + service: 'elasticmapreduce', + resource: 'cluster', + resourceName: '*', + }), + ], }), ]; } diff --git a/packages/@aws-cdk/aws-stepfunctions-tasks/lib/emr/emr-modify-instance-fleet-by-name.ts b/packages/@aws-cdk/aws-stepfunctions-tasks/lib/emr/emr-modify-instance-fleet-by-name.ts index 0e5e5c54c0b5f..9c0300abef845 100644 --- a/packages/@aws-cdk/aws-stepfunctions-tasks/lib/emr/emr-modify-instance-fleet-by-name.ts +++ b/packages/@aws-cdk/aws-stepfunctions-tasks/lib/emr/emr-modify-instance-fleet-by-name.ts @@ -1,6 +1,6 @@ import * as iam from '@aws-cdk/aws-iam'; import * as sfn from '@aws-cdk/aws-stepfunctions'; -import { Aws } from '@aws-cdk/core'; +import { Stack } from '@aws-cdk/core'; import { Construct } from 'constructs'; import { integrationResourceArn } from '../private/task-utils'; @@ -58,7 +58,13 @@ export class EmrModifyInstanceFleetByName extends sfn.TaskStateBase { 'elasticmapreduce:ModifyInstanceFleet', 'elasticmapreduce:ListInstanceFleets', ], - resources: [`arn:aws:elasticmapreduce:${Aws.REGION}:${Aws.ACCOUNT_ID}:cluster/*`], + resources: [ + Stack.of(this).formatArn({ + service: 'elasticmapreduce', + resource: 'cluster', + resourceName: '*', + }), + ], }), ]; } diff --git a/packages/@aws-cdk/aws-stepfunctions-tasks/lib/emr/emr-modify-instance-group-by-name.ts b/packages/@aws-cdk/aws-stepfunctions-tasks/lib/emr/emr-modify-instance-group-by-name.ts index 2b680f145c426..a131f8b2e7f19 100644 --- a/packages/@aws-cdk/aws-stepfunctions-tasks/lib/emr/emr-modify-instance-group-by-name.ts +++ b/packages/@aws-cdk/aws-stepfunctions-tasks/lib/emr/emr-modify-instance-group-by-name.ts @@ -1,6 +1,6 @@ import * as iam from '@aws-cdk/aws-iam'; import * as sfn from '@aws-cdk/aws-stepfunctions'; -import * as cdk from '@aws-cdk/core'; +import { Duration, Stack } from '@aws-cdk/core'; import { Construct } from 'constructs'; import { integrationResourceArn } from '../private/task-utils'; import { EmrCreateCluster } from './emr-create-cluster'; @@ -45,8 +45,17 @@ export class EmrModifyInstanceGroupByName extends sfn.TaskStateBase { super(scope, id, props); this.taskPolicies = [ new iam.PolicyStatement({ - actions: ['elasticmapreduce:ModifyInstanceGroups', 'elasticmapreduce:ListInstanceGroups'], - resources: [`arn:aws:elasticmapreduce:${cdk.Aws.REGION}:${cdk.Aws.ACCOUNT_ID}:cluster/*`], + actions: [ + 'elasticmapreduce:ModifyInstanceGroups', + 'elasticmapreduce:ListInstanceGroups', + ], + resources: [ + Stack.of(this).formatArn({ + service: 'elasticmapreduce', + resource: 'cluster', + resourceName: '*', + }), + ], }), ]; } @@ -94,7 +103,7 @@ export namespace EmrModifyInstanceGroupByName { * * @default cdk.Duration.seconds */ - readonly instanceTerminationTimeout?: cdk.Duration; + readonly instanceTerminationTimeout?: Duration; } /** @@ -110,7 +119,7 @@ export namespace EmrModifyInstanceGroupByName { * * @default - EMR selected default */ - readonly decommissionTimeout?: cdk.Duration; + readonly decommissionTimeout?: Duration; /** * Custom policy for requesting termination protection or termination of specific instances when shrinking an instance group. diff --git a/packages/@aws-cdk/aws-stepfunctions-tasks/lib/emr/emr-set-cluster-termination-protection.ts b/packages/@aws-cdk/aws-stepfunctions-tasks/lib/emr/emr-set-cluster-termination-protection.ts index 67d10bea4415d..78a5f305b54d2 100644 --- a/packages/@aws-cdk/aws-stepfunctions-tasks/lib/emr/emr-set-cluster-termination-protection.ts +++ b/packages/@aws-cdk/aws-stepfunctions-tasks/lib/emr/emr-set-cluster-termination-protection.ts @@ -1,6 +1,6 @@ import * as iam from '@aws-cdk/aws-iam'; import * as sfn from '@aws-cdk/aws-stepfunctions'; -import { Aws } from '@aws-cdk/core'; +import { Stack } from '@aws-cdk/core'; import { Construct } from 'constructs'; import { integrationResourceArn } from '../private/task-utils'; @@ -37,7 +37,13 @@ export class EmrSetClusterTerminationProtection extends sfn.TaskStateBase { this.taskPolicies = [ new iam.PolicyStatement({ actions: ['elasticmapreduce:SetTerminationProtection'], - resources: [`arn:aws:elasticmapreduce:${Aws.REGION}:${Aws.ACCOUNT_ID}:cluster/*`], + resources: [ + Stack.of(this).formatArn({ + service: 'elasticmapreduce', + resource: 'cluster', + resourceName: '*', + }), + ], }), ]; } diff --git a/packages/@aws-cdk/aws-stepfunctions-tasks/lib/emr/emr-terminate-cluster.ts b/packages/@aws-cdk/aws-stepfunctions-tasks/lib/emr/emr-terminate-cluster.ts index 290aaeb19a597..9783da1a3c9d3 100644 --- a/packages/@aws-cdk/aws-stepfunctions-tasks/lib/emr/emr-terminate-cluster.ts +++ b/packages/@aws-cdk/aws-stepfunctions-tasks/lib/emr/emr-terminate-cluster.ts @@ -1,6 +1,6 @@ import * as iam from '@aws-cdk/aws-iam'; import * as sfn from '@aws-cdk/aws-stepfunctions'; -import { Aws, Stack } from '@aws-cdk/core'; +import { Stack } from '@aws-cdk/core'; import { Construct } from 'constructs'; import { integrationResourceArn, validatePatternSupported } from '../private/task-utils'; @@ -64,7 +64,13 @@ export class EmrTerminateCluster extends sfn.TaskStateBase { 'elasticmapreduce:DescribeCluster', 'elasticmapreduce:TerminateJobFlows', ], - resources: [`arn:aws:elasticmapreduce:${Aws.REGION}:${Aws.ACCOUNT_ID}:cluster/*`], + resources: [ + Stack.of(this).formatArn({ + service: 'elasticmapreduce', + resource: 'cluster', + resourceName: '*', + }), + ], }), ]; diff --git a/packages/@aws-cdk/aws-stepfunctions-tasks/test/emr/emr-add-step.test.ts b/packages/@aws-cdk/aws-stepfunctions-tasks/test/emr/emr-add-step.test.ts index 489c5612b5f2f..3060184a57746 100644 --- a/packages/@aws-cdk/aws-stepfunctions-tasks/test/emr/emr-add-step.test.ts +++ b/packages/@aws-cdk/aws-stepfunctions-tasks/test/emr/emr-add-step.test.ts @@ -1,3 +1,4 @@ +import '@aws-cdk/assert/jest'; import * as sfn from '@aws-cdk/aws-stepfunctions'; import * as cdk from '@aws-cdk/core'; import * as tasks from '../../lib'; @@ -291,6 +292,81 @@ test('Add Step with static ClusterId and Step configuration with Properties', () }); }); +test('task policies are generated', () => { + // WHEN + const task = new tasks.EmrAddStep(stack, 'Task', { + clusterId: 'ClusterId', + name: 'StepName', + jar: 'Jar', + integrationPattern: sfn.IntegrationPattern.RUN_JOB, + }); + new sfn.StateMachine(stack, 'SM', { + definition: task, + }); + + // THEN + expect(stack).toHaveResourceLike('AWS::IAM::Policy', { + PolicyDocument: { + Statement: [{ + Action: [ + 'elasticmapreduce:AddJobFlowSteps', + 'elasticmapreduce:DescribeStep', + 'elasticmapreduce:CancelSteps', + ], + Effect: 'Allow', + Resource: { + 'Fn::Join': [ + '', + [ + 'arn:', + { + Ref: 'AWS::Partition', + }, + ':elasticmapreduce:', + { + Ref: 'AWS::Region', + }, + ':', + { + Ref: 'AWS::AccountId', + }, + ':cluster/*', + ], + ], + }, + }, + { + Action: [ + 'events:PutTargets', + 'events:PutRule', + 'events:DescribeRule', + ], + Effect: 'Allow', + Resource: { + 'Fn::Join': [ + '', + [ + 'arn:', + { + Ref: 'AWS::Partition', + }, + ':events:', + { + Ref: 'AWS::Region', + }, + ':', + { + Ref: 'AWS::AccountId', + }, + ':rule/StepFunctionsGetEventForEMRAddJobFlowStepsRule', + ], + ], + }, + }], + }, + }); +}); + test('Task throws if WAIT_FOR_TASK_TOKEN is supplied as service integration pattern', () => { expect(() => { new tasks.EmrAddStep(stack, 'Task', { diff --git a/packages/@aws-cdk/aws-stepfunctions-tasks/test/emr/emr-cancel-step.test.ts b/packages/@aws-cdk/aws-stepfunctions-tasks/test/emr/emr-cancel-step.test.ts index e7a1cf2194708..f4abcfbf66fdd 100644 --- a/packages/@aws-cdk/aws-stepfunctions-tasks/test/emr/emr-cancel-step.test.ts +++ b/packages/@aws-cdk/aws-stepfunctions-tasks/test/emr/emr-cancel-step.test.ts @@ -1,3 +1,4 @@ +import '@aws-cdk/assert/jest'; import * as sfn from '@aws-cdk/aws-stepfunctions'; import * as cdk from '@aws-cdk/core'; import * as tasks from '../../lib'; @@ -39,6 +40,49 @@ test('Cancel a Step with static ClusterId and StepId', () => { }); }); +test('task policies are generated', () => { + // WHEN + const task = new tasks.EmrCancelStep(stack, 'Task', { + clusterId: 'ClusterId', + stepId: 'StepId', + }); + new sfn.StateMachine(stack, 'SM', { + definition: task, + }); + + // THEN + expect(stack).toHaveResourceLike('AWS::IAM::Policy', { + PolicyDocument: { + Statement: [ + { + Action: 'elasticmapreduce:CancelSteps', + Effect: 'Allow', + Resource: { + 'Fn::Join': [ + '', + [ + 'arn:', + { + Ref: 'AWS::Partition', + }, + ':elasticmapreduce:', + { + Ref: 'AWS::Region', + }, + ':', + { + Ref: 'AWS::AccountId', + }, + ':cluster/*', + ], + ], + }, + }, + ], + }, + }); +}); + test('Cancel a Step with static ClusterId and StepId from payload', () => { // WHEN const task = new tasks.EmrCancelStep(stack, 'Task', { diff --git a/packages/@aws-cdk/aws-stepfunctions-tasks/test/emr/emr-modify-instance-fleet-by-name.test.ts b/packages/@aws-cdk/aws-stepfunctions-tasks/test/emr/emr-modify-instance-fleet-by-name.test.ts index bd8fe53e7ff51..d1349bafb9e65 100644 --- a/packages/@aws-cdk/aws-stepfunctions-tasks/test/emr/emr-modify-instance-fleet-by-name.test.ts +++ b/packages/@aws-cdk/aws-stepfunctions-tasks/test/emr/emr-modify-instance-fleet-by-name.test.ts @@ -1,3 +1,4 @@ +import '@aws-cdk/assert/jest'; import * as sfn from '@aws-cdk/aws-stepfunctions'; import * as cdk from '@aws-cdk/core'; import * as tasks from '../../lib'; @@ -45,6 +46,54 @@ test('Modify an InstanceFleet with static ClusterId, InstanceFleetName, and Inst }); }); +test('task policies are generated', () => { + // WHEN + const task = new tasks.EmrModifyInstanceFleetByName(stack, 'Task', { + clusterId: 'ClusterId', + instanceFleetName: 'InstanceFleetName', + targetOnDemandCapacity: 2, + targetSpotCapacity: 0, + }); + new sfn.StateMachine(stack, 'SM', { + definition: task, + }); + + // THEN + expect(stack).toHaveResourceLike('AWS::IAM::Policy', { + PolicyDocument: { + Statement: [ + { + Action: [ + 'elasticmapreduce:ModifyInstanceFleet', + 'elasticmapreduce:ListInstanceFleets', + ], + Effect: 'Allow', + Resource: { + 'Fn::Join': [ + '', + [ + 'arn:', + { + Ref: 'AWS::Partition', + }, + ':elasticmapreduce:', + { + Ref: 'AWS::Region', + }, + ':', + { + Ref: 'AWS::AccountId', + }, + ':cluster/*', + ], + ], + }, + }, + ], + }, + }); +}); + test('Modify an InstanceFleet with ClusterId from payload and static InstanceFleetName and InstanceFleetConfiguration', () => { // WHEN const task = new tasks.EmrModifyInstanceFleetByName(stack, 'Task', { diff --git a/packages/@aws-cdk/aws-stepfunctions-tasks/test/emr/emr-modify-instance-group-by-name.test.ts b/packages/@aws-cdk/aws-stepfunctions-tasks/test/emr/emr-modify-instance-group-by-name.test.ts index 51bd3600147c1..197e92e6d526d 100644 --- a/packages/@aws-cdk/aws-stepfunctions-tasks/test/emr/emr-modify-instance-group-by-name.test.ts +++ b/packages/@aws-cdk/aws-stepfunctions-tasks/test/emr/emr-modify-instance-group-by-name.test.ts @@ -1,3 +1,4 @@ +import '@aws-cdk/assert/jest'; import * as sfn from '@aws-cdk/aws-stepfunctions'; import * as cdk from '@aws-cdk/core'; import * as tasks from '../../lib'; @@ -75,6 +76,60 @@ test('Modify an InstanceGroup with static ClusterId, InstanceGroupName, and Inst }); }); +test('task policies are generated', () => { + // WHEN + const task = new tasks.EmrModifyInstanceGroupByName(stack, 'Task', { + clusterId: 'ClusterId', + instanceGroupName: 'InstanceGroupName', + instanceGroup: { + configurations: [{ + classification: 'Classification', + properties: { + Key: 'Value', + }, + }], + }, + }); + new sfn.StateMachine(stack, 'SM', { + definition: task, + }); + + // THEN + expect(stack).toHaveResourceLike('AWS::IAM::Policy', { + PolicyDocument: { + Statement: [ + { + Action: [ + 'elasticmapreduce:ModifyInstanceGroups', + 'elasticmapreduce:ListInstanceGroups', + ], + Effect: 'Allow', + Resource: { + 'Fn::Join': [ + '', + [ + 'arn:', + { + Ref: 'AWS::Partition', + }, + ':elasticmapreduce:', + { + Ref: 'AWS::Region', + }, + ':', + { + Ref: 'AWS::AccountId', + }, + ':cluster/*', + ], + ], + }, + }, + ], + }, + }); +}); + test('Modify an InstanceGroup with ClusterId from payload and static InstanceGroupName and InstanceGroupConfiguration', () => { // WHEN const task = new tasks.EmrModifyInstanceGroupByName(stack, 'Task', { diff --git a/packages/@aws-cdk/aws-stepfunctions-tasks/test/emr/emr-set-cluster-termination-protection.test.ts b/packages/@aws-cdk/aws-stepfunctions-tasks/test/emr/emr-set-cluster-termination-protection.test.ts index 6b68837837b7b..4373702a617ad 100644 --- a/packages/@aws-cdk/aws-stepfunctions-tasks/test/emr/emr-set-cluster-termination-protection.test.ts +++ b/packages/@aws-cdk/aws-stepfunctions-tasks/test/emr/emr-set-cluster-termination-protection.test.ts @@ -1,3 +1,4 @@ +import '@aws-cdk/assert/jest'; import * as sfn from '@aws-cdk/aws-stepfunctions'; import * as cdk from '@aws-cdk/core'; import * as tasks from '../../lib'; @@ -39,6 +40,49 @@ test('Set termination protection with static ClusterId and TerminationProtected' }); }); +test('task policies are generated', () => { + // WHEN + const task = new tasks.EmrSetClusterTerminationProtection(stack, 'Task', { + clusterId: 'ClusterId', + terminationProtected: false, + }); + new sfn.StateMachine(stack, 'SM', { + definition: task, + }); + + // THEN + expect(stack).toHaveResourceLike('AWS::IAM::Policy', { + PolicyDocument: { + Statement: [ + { + Action: 'elasticmapreduce:SetTerminationProtection', + Effect: 'Allow', + Resource: { + 'Fn::Join': [ + '', + [ + 'arn:', + { + Ref: 'AWS::Partition', + }, + ':elasticmapreduce:', + { + Ref: 'AWS::Region', + }, + ':', + { + Ref: 'AWS::AccountId', + }, + ':cluster/*', + ], + ], + }, + }, + ], + }, + }); +}); + test('Set termination protection with static ClusterId and TerminationProtected from payload', () => { // WHEN const task = new tasks.EmrSetClusterTerminationProtection(stack, 'Task', { diff --git a/packages/@aws-cdk/aws-stepfunctions-tasks/test/emr/emr-terminate-cluster.test.ts b/packages/@aws-cdk/aws-stepfunctions-tasks/test/emr/emr-terminate-cluster.test.ts index 8df767949a7b5..3e10f073fe09b 100644 --- a/packages/@aws-cdk/aws-stepfunctions-tasks/test/emr/emr-terminate-cluster.test.ts +++ b/packages/@aws-cdk/aws-stepfunctions-tasks/test/emr/emr-terminate-cluster.test.ts @@ -1,3 +1,4 @@ +import '@aws-cdk/assert/jest'; import * as sfn from '@aws-cdk/aws-stepfunctions'; import * as cdk from '@aws-cdk/core'; import * as tasks from '../../lib'; @@ -38,6 +39,80 @@ test('Terminate cluster with static ClusterId', () => { }); }); +test('task policies are generated', () => { + // WHEN + const task = new tasks.EmrTerminateCluster(stack, 'Task', { + clusterId: 'ClusterId', + integrationPattern: sfn.IntegrationPattern.RUN_JOB, + }); + new sfn.StateMachine(stack, 'SM', { + definition: task, + }); + + // THEN + expect(stack).toHaveResourceLike('AWS::IAM::Policy', { + PolicyDocument: { + Statement: [ + { + Action: [ + 'elasticmapreduce:DescribeCluster', + 'elasticmapreduce:TerminateJobFlows', + ], + Effect: 'Allow', + Resource: { + 'Fn::Join': [ + '', + [ + 'arn:', + { + Ref: 'AWS::Partition', + }, + ':elasticmapreduce:', + { + Ref: 'AWS::Region', + }, + ':', + { + Ref: 'AWS::AccountId', + }, + ':cluster/*', + ], + ], + }, + }, + { + Action: [ + 'events:PutTargets', + 'events:PutRule', + 'events:DescribeRule', + ], + Effect: 'Allow', + Resource: { + 'Fn::Join': [ + '', + [ + 'arn:', + { + Ref: 'AWS::Partition', + }, + ':events:', + { + Ref: 'AWS::Region', + }, + ':', + { + Ref: 'AWS::AccountId', + }, + ':rule/StepFunctionsGetEventForEMRTerminateJobFlowsRule', + ], + ], + }, + }, + ], + }, + }); +}); + test('Terminate cluster with ClusterId from payload', () => { // WHEN const task = new tasks.EmrTerminateCluster(stack, 'Task', { From dfb54053a60699e8304e1210ab38eeed0114f3a8 Mon Sep 17 00:00:00 2001 From: Romain Marcadier Date: Mon, 14 Dec 2020 14:45:40 +0100 Subject: [PATCH 9/9] docs: add code example recommendations (#12065) Edited `CONTRIBUTING.md` to add information about the `rosetta:extract` script and some recommendations around code examples in order to incentize a coherent sample code style across the project. ---- *By submitting this pull request, I confirm that my contribution is made under the terms of the Apache-2.0 license* --- CONTRIBUTING.md | 81 ++++++++++++++++++++++++++++++++++++++++++++----- 1 file changed, 74 insertions(+), 7 deletions(-) diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 3ccfe335daddf..1d113183c9a67 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -382,6 +382,31 @@ Here are a few useful commands: evaluate only the rule specified [awslint README](./packages/awslint/README.md) for details on include/exclude rule patterns. + +#### jsii-rosetta + +**jsii-rosetta** can be used to verify that all code examples included in documentation for a package (including those +in `README.md`) successfully compile against the library they document. It is recommended to run it to ensure all +examples are still accurate. Successfully building examples is also necessary to ensure the best possible translation to +other supported languages (`C#`, `Java`, `Python`, ...). + +> Note that examples may use libraries that are not part of the `dependencies` or `devDependencies` of the documented +> package. For example `@aws-cdk/core` contains mainy examples that leverage libraries built *on top of it* (such as +> `@aws-cdk/aws-sns`). Such libraries must be built (using `yarn build`) before **jsii-rosetta** can verify that +> examples are correct. + +To run **jsii-rosetta** in *strict* mode (so that it always fails when encountering examples that fail to compile), use +the following command: + +```console +$ yarn rosetta:extract --strict +``` + +For more information on how you can address examples that fail compiling due to missing fixtures (declarations that are +necessary for the example to compile, but which would distract the reader away from what is being demonstrated), you +might need to introduce [rosetta fixtures](https://github.com/aws/jsii/tree/main/packages/jsii-rosetta#fixtures). Refer +to the [Examples](#examples) section. + ### cfn2ts This tool is used to generate our low-level CloudFormation resources @@ -685,6 +710,8 @@ can be used in these cases. ### Examples +#### Fixture Files + Examples typed in fenced code blocks (looking like `'''ts`, but then with backticks instead of regular quotes) will be automatically extrated, compiled and translated to other languages when the bindings are generated. @@ -694,7 +721,7 @@ a *fixture*, which looks like this: ``` '''ts fixture=with-bucket -bucket.addLifecycleTransition({ ... }); +bucket.addLifecycleTransition({ ...props }); ''' ``` @@ -717,8 +744,8 @@ contain three slashes to achieve the same effect: ``` /** * @example - * /// fixture=with-bucket - * bucket.addLifecycleTransition({ ... }); + * /// fixture=with-bucket + * bucket.addLifecycleTransition({ ...props }); */ ``` @@ -732,12 +759,52 @@ the current package. For a practical example of how making sample code compilable works, see the `aws-ec2` package. +#### Recommendations + +In order to offer a consistent documentation style throughout the AWS CDK +codebase, example code should follow the following recommendations (there may be +cases where some of those do not apply - good judgement is to be applied): + +- Types from the documented module should be **un-qualified** + + ```ts + // An example in the @aws-cdk/core library, which defines Duration + Duration.minutes(15); + ``` + +- Types from other modules should be **qualified** + + ```ts + // An example in the @aws-cdk/core library, using something from @aws-cdk/aws-s3 + const bucket = new s3.Bucket(this, 'Bucket'); + // ...rest of the example... + ``` + +- Within `.ts-fixture` files, make use of `declare` statements instead of + writing a compatible value (this will make your fixtures more durable): + + ```ts + // An hypothetical 'rosetta/default.ts-fixture' file in `@aws-cdk/core` + import * as kms from '@aws-cdk/aws-kms'; + import * as s3 from '@aws-cdk/aws-s3'; + import { StackProps } from '@aws-cdk/core'; + + declare const kmsKey: kms.IKey; + declare const bucket: s3.Bucket; + + declare const props: StackProps; + ``` + +> Those recommendations are not verified or enforced by automated tooling. Pull +> request reviewers may however request that new sample code is edited to meet +> those requirements as needed. + +#### Checking a single package + Examples of all packages are extracted and compiled as part of the packaging step. If you are working on getting rid of example compilation errors of a -single package, you can run `scripts/compile-samples` on the package by itself. - -For now, non-compiling examples will not yet block the build, but at some point -in the future they will. +single package, you can run `yarn rosetta:extract --strict` in the package's +directory (see the [**jsii-rosetta**](#jsii-rosetta) section). ### Feature Flags