From 6f7cebdf61073cc1fb358fcac5f5b2156389cb81 Mon Sep 17 00:00:00 2001 From: rli1994hi Date: Wed, 3 Mar 2021 09:42:58 -0800 Subject: [PATCH] fix(events): imported ECS Task Definition cannot be used as target (#13293) ### Note Fixes #12811 This change allows creating `EcsTask` from an imported task definition. It does so by changing the `taskDefinition` type in `EcsTaskProps` from concrete class `TaskDefinition` to interface `ITaskDefinition`. ### Testing `buildup` aws-events-targets ---- *By submitting this pull request, I confirm that my contribution is made under the terms of the Apache-2.0 license* --- .../lib/base/_imported-task-definition.ts | 108 ++++++++ .../aws-ecs/lib/base/task-definition.ts | 73 +++++- .../aws-ecs/lib/ec2/ec2-task-definition.ts | 46 +++- .../lib/fargate/fargate-task-definition.ts | 42 ++- .../test/ec2/ec2-task-definition.test.ts | 81 +++++- .../fargate/fargate-task-definition.test.ts | 88 ++++++- .../aws-ecs/test/task-definition.test.ts | 131 ++++++++-- .../aws-events-targets/lib/ecs-task.ts | 11 +- .../test/ecs/event-rule-target.test.ts | 243 ++++++++++++++++++ 9 files changed, 774 insertions(+), 49 deletions(-) create mode 100644 packages/@aws-cdk/aws-ecs/lib/base/_imported-task-definition.ts diff --git a/packages/@aws-cdk/aws-ecs/lib/base/_imported-task-definition.ts b/packages/@aws-cdk/aws-ecs/lib/base/_imported-task-definition.ts new file mode 100644 index 0000000000000..3c9c583b96de0 --- /dev/null +++ b/packages/@aws-cdk/aws-ecs/lib/base/_imported-task-definition.ts @@ -0,0 +1,108 @@ +import { IRole } from '@aws-cdk/aws-iam'; +import { Construct } from 'constructs'; +import { IEc2TaskDefinition } from '../ec2/ec2-task-definition'; +import { IFargateTaskDefinition } from '../fargate/fargate-task-definition'; +import { Compatibility, NetworkMode, isEc2Compatible, isFargateCompatible } from './task-definition'; +import { Resource } from '@aws-cdk/core'; + +/** + * The properties of ImportedTaskDefinition + */ +export interface ImportedTaskDefinitionProps { + /** + * The arn of the task definition + */ + readonly taskDefinitionArn: string; + + /** + * What launch types this task definition should be compatible with. + * + * @default Compatibility.EC2_AND_FARGATE + */ + readonly compatibility?: Compatibility; + + /** + * The networking mode to use for the containers in the task. + * + * @default Network mode cannot be provided to the imported task. + */ + readonly networkMode?: NetworkMode; + + /** + * The name of the IAM role that grants containers in the task permission to call AWS APIs on your behalf. + * + * @default Permissions cannot be granted to the imported task. + */ + readonly taskRole?: IRole; +} + +/** + * Task definition reference of an imported task + */ +export class ImportedTaskDefinition extends Resource implements IEc2TaskDefinition, IFargateTaskDefinition { + /** + * What launch types this task definition should be compatible with. + */ + readonly compatibility: Compatibility; + + /** + * ARN of this task definition + */ + readonly taskDefinitionArn: string; + + /** + * Execution role for this task definition + */ + readonly executionRole?: IRole = undefined; + + /** + * The networking mode to use for the containers in the task. + */ + readonly _networkMode?: NetworkMode; + + /** + * The name of the IAM role that grants containers in the task permission to call AWS APIs on your behalf. + */ + readonly _taskRole?: IRole; + + constructor(scope: Construct, id: string, props: ImportedTaskDefinitionProps) { + super(scope, id); + + this.compatibility = props.compatibility ?? Compatibility.EC2_AND_FARGATE; + this.taskDefinitionArn = props.taskDefinitionArn; + this._taskRole = props.taskRole; + this._networkMode = props.networkMode; + } + + public get networkMode(): NetworkMode { + if (this._networkMode == undefined) { + throw new Error('This operation requires the networkMode in ImportedTaskDefinition to be defined. ' + + 'Add the \'networkMode\' in ImportedTaskDefinitionProps to instantiate ImportedTaskDefinition'); + } else { + return this._networkMode; + } + } + + public get taskRole(): IRole { + if (this._taskRole == undefined) { + throw new Error('This operation requires the taskRole in ImportedTaskDefinition to be defined. ' + + 'Add the \'taskRole\' in ImportedTaskDefinitionProps to instantiate ImportedTaskDefinition'); + } else { + return this._taskRole; + } + } + + /** + * Return true if the task definition can be run on an EC2 cluster + */ + public get isEc2Compatible(): boolean { + return isEc2Compatible(this.compatibility); + } + + /** + * Return true if the task definition can be run on a Fargate cluster + */ + public get isFargateCompatible(): boolean { + return isFargateCompatible(this.compatibility); + } +} diff --git a/packages/@aws-cdk/aws-ecs/lib/base/task-definition.ts b/packages/@aws-cdk/aws-ecs/lib/base/task-definition.ts index f7b0f05c92800..338fd59b39ea7 100644 --- a/packages/@aws-cdk/aws-ecs/lib/base/task-definition.ts +++ b/packages/@aws-cdk/aws-ecs/lib/base/task-definition.ts @@ -8,6 +8,7 @@ import { FirelensLogRouter, FirelensLogRouterDefinitionOptions, FirelensLogRoute import { AwsLogDriver } from '../log-drivers/aws-log-driver'; import { PlacementConstraint } from '../placement'; import { ProxyConfiguration } from '../proxy-configuration/proxy-configuration'; +import { ImportedTaskDefinition } from './_imported-task-definition'; /** * The interface for all task definitions. @@ -38,6 +39,16 @@ export interface ITaskDefinition extends IResource { * Return true if the task definition can be run on a Fargate cluster */ readonly isFargateCompatible: boolean; + + /** + * The networking mode to use for the containers in the task. + */ + readonly networkMode: NetworkMode; + + /** + * The name of the IAM role that grants containers in the task permission to call AWS APIs on your behalf. + */ + readonly taskRole: iam.IRole; } /** @@ -175,10 +186,48 @@ export interface TaskDefinitionProps extends CommonTaskDefinitionProps { readonly pidMode?: PidMode; } +/** + * The common task definition attributes used across all types of task definitions. + */ +export interface CommonTaskDefinitionAttributes { + /** + * The arn of the task definition + */ + readonly taskDefinitionArn: string; + + /** + * The networking mode to use for the containers in the task. + * + * @default Network mode cannot be provided to the imported task. + */ + readonly networkMode?: NetworkMode; + + /** + * The name of the IAM role that grants containers in the task permission to call AWS APIs on your behalf. + * + * @default Permissions cannot be granted to the imported task. + */ + readonly taskRole?: iam.IRole; +} + +/** + * A reference to an existing task definition + */ +export interface TaskDefinitionAttributes extends CommonTaskDefinitionAttributes { + /** + * What launch types this task definition should be compatible with. + * + * @default Compatibility.EC2_AND_FARGATE + */ + readonly compatibility?: Compatibility; +} + abstract class TaskDefinitionBase extends Resource implements ITaskDefinition { public abstract readonly compatibility: Compatibility; + public abstract readonly networkMode: NetworkMode; public abstract readonly taskDefinitionArn: string; + public abstract readonly taskRole: iam.IRole; public abstract readonly executionRole?: iam.IRole; /** @@ -207,13 +256,19 @@ export class TaskDefinition extends TaskDefinitionBase { * The task will have a compatibility of EC2+Fargate. */ public static fromTaskDefinitionArn(scope: Construct, id: string, taskDefinitionArn: string): ITaskDefinition { - class Import extends TaskDefinitionBase { - public readonly taskDefinitionArn = taskDefinitionArn; - public readonly compatibility = Compatibility.EC2_AND_FARGATE; - public readonly executionRole?: iam.IRole = undefined; - } + return new ImportedTaskDefinition(scope, id, { taskDefinitionArn: taskDefinitionArn }); + } - return new Import(scope, id); + /** + * Create a task definition from a task definition reference + */ + public static fromTaskDefinitionAttributes(scope: Construct, id: string, attrs: TaskDefinitionAttributes): ITaskDefinition { + return new ImportedTaskDefinition(scope, id, { + taskDefinitionArn: attrs.taskDefinitionArn, + compatibility: attrs.compatibility, + networkMode: attrs.networkMode, + taskRole: attrs.taskRole, + }); } /** @@ -248,7 +303,7 @@ export class TaskDefinition extends TaskDefinitionBase { public defaultContainer?: ContainerDefinition; /** - * The task launch type compatiblity requirement. + * The task launch type compatibility requirement. */ public readonly compatibility: Compatibility; @@ -890,13 +945,13 @@ export interface ITaskDefinitionExtension { /** * Return true if the given task definition can be run on an EC2 cluster */ -function isEc2Compatible(compatibility: Compatibility): boolean { +export function isEc2Compatible(compatibility: Compatibility): boolean { return [Compatibility.EC2, Compatibility.EC2_AND_FARGATE].includes(compatibility); } /** * Return true if the given task definition can be run on a Fargate cluster */ -function isFargateCompatible(compatibility: Compatibility): boolean { +export function isFargateCompatible(compatibility: Compatibility): boolean { return [Compatibility.FARGATE, Compatibility.EC2_AND_FARGATE].includes(compatibility); } diff --git a/packages/@aws-cdk/aws-ecs/lib/ec2/ec2-task-definition.ts b/packages/@aws-cdk/aws-ecs/lib/ec2/ec2-task-definition.ts index 67d096830b9c7..ff571c884b73e 100644 --- a/packages/@aws-cdk/aws-ecs/lib/ec2/ec2-task-definition.ts +++ b/packages/@aws-cdk/aws-ecs/lib/ec2/ec2-task-definition.ts @@ -1,7 +1,16 @@ -import { Resource } from '@aws-cdk/core'; import { Construct } from 'constructs'; -import { CommonTaskDefinitionProps, Compatibility, IpcMode, ITaskDefinition, NetworkMode, PidMode, TaskDefinition } from '../base/task-definition'; +import { + CommonTaskDefinitionAttributes, + CommonTaskDefinitionProps, + Compatibility, + IpcMode, + ITaskDefinition, + NetworkMode, + PidMode, + TaskDefinition, +} from '../base/task-definition'; import { PlacementConstraint } from '../placement'; +import { ImportedTaskDefinition } from '../base/_imported-task-definition'; /** * The properties for a task definition run on an EC2 cluster. @@ -51,6 +60,13 @@ export interface IEc2TaskDefinition extends ITaskDefinition { } +/** + * Attributes used to import an existing EC2 task definition + */ +export interface Ec2TaskDefinitionAttributes extends CommonTaskDefinitionAttributes { + +} + /** * The details of a task definition run on an EC2 cluster. * @@ -62,13 +78,25 @@ export class Ec2TaskDefinition extends TaskDefinition implements IEc2TaskDefinit * Imports a task definition from the specified task definition ARN. */ public static fromEc2TaskDefinitionArn(scope: Construct, id: string, ec2TaskDefinitionArn: string): IEc2TaskDefinition { - class Import extends Resource implements IEc2TaskDefinition { - public readonly taskDefinitionArn = ec2TaskDefinitionArn; - public readonly compatibility = Compatibility.EC2; - public readonly isEc2Compatible = true; - public readonly isFargateCompatible = false; - } - return new Import(scope, id); + return new ImportedTaskDefinition(scope, id, { + taskDefinitionArn: ec2TaskDefinitionArn, + }); + } + + /** + * Imports an existing Ec2 task definition from its attributes + */ + public static fromEc2TaskDefinitionAttributes( + scope: Construct, + id: string, + attrs: Ec2TaskDefinitionAttributes, + ): IEc2TaskDefinition { + return new ImportedTaskDefinition(scope, id, { + taskDefinitionArn: attrs.taskDefinitionArn, + compatibility: Compatibility.EC2, + networkMode: attrs.networkMode, + taskRole: attrs.taskRole, + }); } /** diff --git a/packages/@aws-cdk/aws-ecs/lib/fargate/fargate-task-definition.ts b/packages/@aws-cdk/aws-ecs/lib/fargate/fargate-task-definition.ts index eba4ac4371ee8..3d8d113886709 100644 --- a/packages/@aws-cdk/aws-ecs/lib/fargate/fargate-task-definition.ts +++ b/packages/@aws-cdk/aws-ecs/lib/fargate/fargate-task-definition.ts @@ -1,6 +1,14 @@ -import { Resource, Tokenization } from '@aws-cdk/core'; +import { Tokenization } from '@aws-cdk/core'; import { Construct } from 'constructs'; -import { CommonTaskDefinitionProps, Compatibility, ITaskDefinition, NetworkMode, TaskDefinition } from '../base/task-definition'; +import { + CommonTaskDefinitionAttributes, + CommonTaskDefinitionProps, + Compatibility, + ITaskDefinition, + NetworkMode, + TaskDefinition, +} from '../base/task-definition'; +import { ImportedTaskDefinition } from '../base/_imported-task-definition'; /** * The properties for a task definition. @@ -51,6 +59,13 @@ export interface IFargateTaskDefinition extends ITaskDefinition { } +/** + * Attributes used to import an existing Fargate task definition + */ +export interface FargateTaskDefinitionAttributes extends CommonTaskDefinitionAttributes { + +} + /** * The details of a task definition run on a Fargate cluster. * @@ -62,14 +77,23 @@ export class FargateTaskDefinition extends TaskDefinition implements IFargateTas * Imports a task definition from the specified task definition ARN. */ public static fromFargateTaskDefinitionArn(scope: Construct, id: string, fargateTaskDefinitionArn: string): IFargateTaskDefinition { - class Import extends Resource implements IFargateTaskDefinition { - public readonly taskDefinitionArn = fargateTaskDefinitionArn; - public readonly compatibility = Compatibility.FARGATE; - public readonly isEc2Compatible = false; - public readonly isFargateCompatible = true; - } + return new ImportedTaskDefinition(scope, id, { taskDefinitionArn: fargateTaskDefinitionArn }); + } - return new Import(scope, id); + /** + * Import an existing Fargate task definition from its attributes + */ + public static fromFargateTaskDefinitionAttributes( + scope: Construct, + id: string, + attrs: FargateTaskDefinitionAttributes, + ): IFargateTaskDefinition { + return new ImportedTaskDefinition(scope, id, { + taskDefinitionArn: attrs.taskDefinitionArn, + compatibility: Compatibility.FARGATE, + networkMode: attrs.networkMode, + taskRole: attrs.taskRole, + }); } /** diff --git a/packages/@aws-cdk/aws-ecs/test/ec2/ec2-task-definition.test.ts b/packages/@aws-cdk/aws-ecs/test/ec2/ec2-task-definition.test.ts index bd13e8b83b524..4f713b06d2d71 100644 --- a/packages/@aws-cdk/aws-ecs/test/ec2/ec2-task-definition.test.ts +++ b/packages/@aws-cdk/aws-ecs/test/ec2/ec2-task-definition.test.ts @@ -1199,6 +1199,85 @@ describe('ec2 task definition', () => { }); }); + describe('When importing from an existing Ec2 TaskDefinition', () => { + test('can succeed using TaskDefinition Arn', () => { + // GIVEN + const stack = new cdk.Stack(); + const expectTaskDefinitionArn = 'TD_ARN'; + + // WHEN + const taskDefinition = ecs.Ec2TaskDefinition.fromEc2TaskDefinitionArn(stack, 'EC2_TD_ID', expectTaskDefinitionArn); + + // THEN + expect(taskDefinition.taskDefinitionArn).toBe(expectTaskDefinitionArn); + }); + }); + + describe('When importing from an existing Ec2 TaskDefinition using attributes', () => { + test('can set the imported task attribuets successfully', () => { + // GIVEN + const stack = new cdk.Stack(); + const expectTaskDefinitionArn = 'TD_ARN'; + const expectNetworkMode = ecs.NetworkMode.AWS_VPC; + const expectTaskRole = new iam.Role(stack, 'TaskRole', { + assumedBy: new iam.ServicePrincipal('ecs-tasks.amazonaws.com'), + }); + + // WHEN + const taskDefinition = ecs.Ec2TaskDefinition.fromEc2TaskDefinitionAttributes(stack, 'TD_ID', { + taskDefinitionArn: expectTaskDefinitionArn, + networkMode: expectNetworkMode, + taskRole: expectTaskRole, + }); + + // THEN + expect(taskDefinition.taskDefinitionArn).toBe(expectTaskDefinitionArn); + expect(taskDefinition.compatibility).toBe(ecs.Compatibility.EC2); + expect(taskDefinition.isEc2Compatible).toBeTruthy(); + expect(taskDefinition.isFargateCompatible).toBeFalsy(); + expect(taskDefinition.networkMode).toBe(expectNetworkMode); + expect(taskDefinition.taskRole).toBe(expectTaskRole); + }); + + test('returns an Ec2 TaskDefinition that will throw an error when trying to access its yet to defined networkMode', () => { + // GIVEN + const stack = new cdk.Stack(); + const expectTaskDefinitionArn = 'TD_ARN'; + const expectTaskRole = new iam.Role(stack, 'TaskRole', { + assumedBy: new iam.ServicePrincipal('ecs-tasks.amazonaws.com'), + }); + + // WHEN + const taskDefinition = ecs.Ec2TaskDefinition.fromEc2TaskDefinitionAttributes(stack, 'TD_ID', { + taskDefinitionArn: expectTaskDefinitionArn, + taskRole: expectTaskRole, + }); + + // THEN + expect(() => taskDefinition.networkMode).toThrow( + 'This operation requires the networkMode in ImportedTaskDefinition to be defined. ' + + 'Add the \'networkMode\' in ImportedTaskDefinitionProps to instantiate ImportedTaskDefinition'); + }); + + test('returns an Ec2 TaskDefinition that will throw an error when trying to access its yet to defined taskRole', () => { + // GIVEN + const stack = new cdk.Stack(); + const expectTaskDefinitionArn = 'TD_ARN'; + const expectNetworkMode = ecs.NetworkMode.AWS_VPC; + + // WHEN + const taskDefinition = ecs.Ec2TaskDefinition.fromEc2TaskDefinitionAttributes(stack, 'TD_ID', { + taskDefinitionArn: expectTaskDefinitionArn, + networkMode: expectNetworkMode, + }); + + // THEN + expect(() => { taskDefinition.taskRole; }).toThrow( + 'This operation requires the taskRole in ImportedTaskDefinition to be defined. ' + + 'Add the \'taskRole\' in ImportedTaskDefinitionProps to instantiate ImportedTaskDefinition'); + }); + }); + test('throws when setting proxyConfiguration without networkMode AWS_VPC', () => { // GIVEN const stack = new cdk.Stack(); @@ -1218,7 +1297,5 @@ describe('ec2 task definition', () => { expect(() => { new ecs.Ec2TaskDefinition(stack, 'TaskDef', { networkMode: ecs.NetworkMode.BRIDGE, proxyConfiguration }); }).toThrow(/ProxyConfiguration can only be used with AwsVpc network mode, got: bridge/); - - }); }); diff --git a/packages/@aws-cdk/aws-ecs/test/fargate/fargate-task-definition.test.ts b/packages/@aws-cdk/aws-ecs/test/fargate/fargate-task-definition.test.ts index 00ec0028ef1a3..b8fad1bc9316f 100644 --- a/packages/@aws-cdk/aws-ecs/test/fargate/fargate-task-definition.test.ts +++ b/packages/@aws-cdk/aws-ecs/test/fargate/fargate-task-definition.test.ts @@ -5,7 +5,7 @@ import { nodeunitShim, Test } from 'nodeunit-shim'; import * as ecs from '../../lib'; nodeunitShim({ - 'When creating an Fargate TaskDefinition': { + 'When creating a Fargate TaskDefinition': { 'with only required properties set, it correctly sets default properties'(test: Test) { // GIVEN const stack = new cdk.Stack(); @@ -114,4 +114,90 @@ nodeunitShim({ test.done(); }, }, + + 'When importing from an existing Fargate TaskDefinition': { + 'can succeed using TaskDefinition Arn'(test: Test) { + // GIVEN + const stack = new cdk.Stack(); + const expectTaskDefinitionArn = 'TD_ARN'; + + // WHEN + const taskDefinition = ecs.FargateTaskDefinition.fromFargateTaskDefinitionArn(stack, 'FARGATE_TD_ID', expectTaskDefinitionArn); + + // THEN + test.equal(taskDefinition.taskDefinitionArn, expectTaskDefinitionArn); + test.done(); + }, + + 'can succeed using attributes'(test: Test) { + // GIVEN + const stack = new cdk.Stack(); + const expectTaskDefinitionArn = 'TD_ARN'; + const expectNetworkMode = ecs.NetworkMode.AWS_VPC; + const expectTaskRole = new iam.Role(stack, 'TaskRole', { + assumedBy: new iam.ServicePrincipal('ecs-tasks.amazonaws.com'), + }); + + // WHEN + const taskDefinition = ecs.FargateTaskDefinition.fromFargateTaskDefinitionAttributes(stack, 'TD_ID', { + taskDefinitionArn: expectTaskDefinitionArn, + networkMode: expectNetworkMode, + taskRole: expectTaskRole, + }); + + // THEN + test.equal(taskDefinition.taskDefinitionArn, expectTaskDefinitionArn); + test.equal(taskDefinition.compatibility, ecs.Compatibility.FARGATE); + test.ok(taskDefinition.isFargateCompatible); + test.equal(taskDefinition.isEc2Compatible, false); + test.equal(taskDefinition.networkMode, expectNetworkMode); + test.equal(taskDefinition.taskRole, expectTaskRole); + + test.done(); + }, + + 'returns a Fargate TaskDefinition that will throw an error when trying to access its networkMode but its networkMode is undefined'(test: Test) { + // GIVEN + const stack = new cdk.Stack(); + const expectTaskDefinitionArn = 'TD_ARN'; + const expectTaskRole = new iam.Role(stack, 'TaskRole', { + assumedBy: new iam.ServicePrincipal('ecs-tasks.amazonaws.com'), + }); + + // WHEN + const taskDefinition = ecs.FargateTaskDefinition.fromFargateTaskDefinitionAttributes(stack, 'TD_ID', { + taskDefinitionArn: expectTaskDefinitionArn, + taskRole: expectTaskRole, + }); + + // THEN + test.throws(() => { + taskDefinition.networkMode; + }, 'This operation requires the networkMode in ImportedTaskDefinition to be defined. ' + + 'Add the \'networkMode\' in ImportedTaskDefinitionProps to instantiate ImportedTaskDefinition'); + + test.done(); + }, + + 'returns a Fargate TaskDefinition that will throw an error when trying to access its taskRole but its taskRole is undefined'(test: Test) { + // GIVEN + const stack = new cdk.Stack(); + const expectTaskDefinitionArn = 'TD_ARN'; + const expectNetworkMode = ecs.NetworkMode.AWS_VPC; + + // WHEN + const taskDefinition = ecs.FargateTaskDefinition.fromFargateTaskDefinitionAttributes(stack, 'TD_ID', { + taskDefinitionArn: expectTaskDefinitionArn, + networkMode: expectNetworkMode, + }); + + // THEN + test.throws(() => { + taskDefinition.taskRole; + }, 'This operation requires the taskRole in ImportedTaskDefinition to be defined. ' + + 'Add the \'taskRole\' in ImportedTaskDefinitionProps to instantiate ImportedTaskDefinition'); + + test.done(); + }, + }, }); diff --git a/packages/@aws-cdk/aws-ecs/test/task-definition.test.ts b/packages/@aws-cdk/aws-ecs/test/task-definition.test.ts index 34a7306106d66..709d3abd86026 100644 --- a/packages/@aws-cdk/aws-ecs/test/task-definition.test.ts +++ b/packages/@aws-cdk/aws-ecs/test/task-definition.test.ts @@ -2,24 +2,121 @@ import { expect, haveResource } from '@aws-cdk/assert'; import * as cdk from '@aws-cdk/core'; import { nodeunitShim, Test } from 'nodeunit-shim'; import * as ecs from '../lib'; +import * as iam from '@aws-cdk/aws-iam'; nodeunitShim({ - 'A task definition with both compatibilities defaults to networkmode AwsVpc'(test: Test) { - // GIVEN - const stack = new cdk.Stack(); - - // WHEN - new ecs.TaskDefinition(stack, 'TD', { - cpu: '512', - memoryMiB: '512', - compatibility: ecs.Compatibility.EC2_AND_FARGATE, - }); - - // THEN - expect(stack).to(haveResource('AWS::ECS::TaskDefinition', { - NetworkMode: 'awsvpc', - })); - - test.done(); + 'When creating a new TaskDefinition': { + 'A task definition with both compatibilities defaults to networkmode AwsVpc'(test: Test) { + // GIVEN + const stack = new cdk.Stack(); + + // WHEN + new ecs.TaskDefinition(stack, 'TD', { + cpu: '512', + memoryMiB: '512', + compatibility: ecs.Compatibility.EC2_AND_FARGATE, + }); + + // THEN + expect(stack).to(haveResource('AWS::ECS::TaskDefinition', { + NetworkMode: 'awsvpc', + })); + + test.done(); + }, + }, + + 'When importing from an existing Task definition': { + 'can import using a task definition arn'(test: Test) { + // GIVEN + const stack = new cdk.Stack(); + const taskDefinitionArn = 'TDArn'; + + // WHEN + const taskDefinition = ecs.TaskDefinition.fromTaskDefinitionArn(stack, 'TD_ID', taskDefinitionArn); + + // THEN + test.equal(taskDefinition.taskDefinitionArn, taskDefinitionArn); + test.equal(taskDefinition.compatibility, ecs.Compatibility.EC2_AND_FARGATE); + test.equal(taskDefinition.executionRole, undefined); + + test.done(); + }, + + 'can import a Task Definition using attributes'(test: Test) { + // GIVEN + const stack = new cdk.Stack(); + const expectTaskDefinitionArn = 'TD_ARN'; + const expectCompatibility = ecs.Compatibility.EC2; + const expectNetworkMode = ecs.NetworkMode.AWS_VPC; + const expectTaskRole = new iam.Role(stack, 'TaskRole', { + assumedBy: new iam.ServicePrincipal('ecs-tasks.amazonaws.com'), + }); + + // WHEN + const taskDefinition = ecs.TaskDefinition.fromTaskDefinitionAttributes(stack, 'TD_ID', { + taskDefinitionArn: expectTaskDefinitionArn, + compatibility: expectCompatibility, + networkMode: expectNetworkMode, + taskRole: expectTaskRole, + }); + + // THEN + test.equal(taskDefinition.taskDefinitionArn, expectTaskDefinitionArn); + test.equal(taskDefinition.compatibility, expectCompatibility); + test.equal(taskDefinition.executionRole, undefined); + test.equal(taskDefinition.networkMode, expectNetworkMode); + test.equal(taskDefinition.taskRole, expectTaskRole); + + test.done(); + }, + + 'returns an imported TaskDefinition that will throw an error when trying to access its yet to defined networkMode'(test: Test) { + // GIVEN + const stack = new cdk.Stack(); + const expectTaskDefinitionArn = 'TD_ARN'; + const expectCompatibility = ecs.Compatibility.EC2; + const expectTaskRole = new iam.Role(stack, 'TaskRole', { + assumedBy: new iam.ServicePrincipal('ecs-tasks.amazonaws.com'), + }); + + // WHEN + const taskDefinition = ecs.TaskDefinition.fromTaskDefinitionAttributes(stack, 'TD_ID', { + taskDefinitionArn: expectTaskDefinitionArn, + compatibility: expectCompatibility, + taskRole: expectTaskRole, + }); + + // THEN + test.throws(() => { + taskDefinition.networkMode; + }, 'This operation requires the networkMode in ImportedTaskDefinition to be defined. ' + + 'Add the \'networkMode\' in ImportedTaskDefinitionProps to instantiate ImportedTaskDefinition'); + + test.done(); + }, + + 'returns an imported TaskDefinition that will throw an error when trying to access its yet to defined taskRole'(test: Test) { + // GIVEN + const stack = new cdk.Stack(); + const expectTaskDefinitionArn = 'TD_ARN'; + const expectCompatibility = ecs.Compatibility.EC2; + const expectNetworkMode = ecs.NetworkMode.AWS_VPC; + + // WHEN + const taskDefinition = ecs.TaskDefinition.fromTaskDefinitionAttributes(stack, 'TD_ID', { + taskDefinitionArn: expectTaskDefinitionArn, + compatibility: expectCompatibility, + networkMode: expectNetworkMode, + }); + + // THEN + test.throws(() => { + taskDefinition.taskRole; + }, 'This operation requires the taskRole in ImportedTaskDefinition to be defined. ' + + 'Add the \'taskRole\' in ImportedTaskDefinitionProps to instantiate ImportedTaskDefinition'); + + test.done(); + }, }, }); diff --git a/packages/@aws-cdk/aws-events-targets/lib/ecs-task.ts b/packages/@aws-cdk/aws-events-targets/lib/ecs-task.ts index baff4c76cde66..a31fc0b68ca5c 100644 --- a/packages/@aws-cdk/aws-events-targets/lib/ecs-task.ts +++ b/packages/@aws-cdk/aws-events-targets/lib/ecs-task.ts @@ -18,7 +18,7 @@ export interface EcsTaskProps { /** * Task Definition of the task that should be started */ - readonly taskDefinition: ecs.TaskDefinition; + readonly taskDefinition: ecs.ITaskDefinition; /** * How many tasks should be started when this event is triggered @@ -103,7 +103,7 @@ export class EcsTask implements events.IRuleTarget { */ public readonly securityGroups?: ec2.ISecurityGroup[]; private readonly cluster: ecs.ICluster; - private readonly taskDefinition: ecs.TaskDefinition; + private readonly taskDefinition: ecs.ITaskDefinition; private readonly taskCount: number; private readonly role: iam.IRole; private readonly platformVersion?: ecs.FargatePlatformVersion; @@ -137,6 +137,13 @@ export class EcsTask implements events.IRuleTarget { this.securityGroups = props.securityGroups; return; } + + if (!cdk.Construct.isConstruct(this.taskDefinition)) { + throw new Error('Cannot create a security group for ECS task. ' + + 'The task definition in ECS task is not a Construct. ' + + 'Please pass a taskDefinition as a Construct in EcsTaskProps.'); + } + let securityGroup = props.securityGroup || this.taskDefinition.node.tryFindChild('SecurityGroup') as ec2.ISecurityGroup; securityGroup = securityGroup || new ec2.SecurityGroup(this.taskDefinition, 'SecurityGroup', { vpc: this.props.cluster.vpc }); this.securityGroup = securityGroup; // Maintain backwards-compatibility for customers that read the generated security group. diff --git a/packages/@aws-cdk/aws-events-targets/test/ecs/event-rule-target.test.ts b/packages/@aws-cdk/aws-events-targets/test/ecs/event-rule-target.test.ts index 4ac8347d02ec3..f0ab7770fadbd 100644 --- a/packages/@aws-cdk/aws-events-targets/test/ecs/event-rule-target.test.ts +++ b/packages/@aws-cdk/aws-events-targets/test/ecs/event-rule-target.test.ts @@ -58,6 +58,249 @@ test('Can use EC2 taskdef as EventRule target', () => { }); }); +test('Throws error for lacking of taskRole ' + + 'when importing from an EC2 task definition just from a task definition arn as EventRule target', () => { + // GIVEN + const stack = new cdk.Stack(); + const vpc = new ec2.Vpc(stack, 'Vpc', { maxAzs: 1 }); + const cluster = new ecs.Cluster(stack, 'EcsCluster', { vpc }); + cluster.addCapacity('DefaultAutoScalingGroup', { + instanceType: new ec2.InstanceType('t2.micro'), + }); + + const taskDefinition = ecs.Ec2TaskDefinition.fromEc2TaskDefinitionArn(stack, 'TaskDef', 'importedTaskDefArn'); + + const rule = new events.Rule(stack, 'Rule', { + schedule: events.Schedule.expression('rate(1 min)'), + }); + + // THEN + expect(() => { + rule.addTarget(new targets.EcsTask({ + cluster, + taskDefinition, + taskCount: 1, + containerOverrides: [{ + containerName: 'TheContainer', + command: ['echo', events.EventField.fromPath('$.detail.event')], + }], + })); + }).toThrow('This operation requires the taskRole in ImportedTaskDefinition to be defined. ' + + 'Add the \'taskRole\' in ImportedTaskDefinitionProps to instantiate ImportedTaskDefinition'); +}); + +test('Can import an EC2 task definition from task definition attributes as EventRule target', () => { + // GIVEN + const stack = new cdk.Stack(); + const vpc = new ec2.Vpc(stack, 'Vpc', { maxAzs: 1 }); + const cluster = new ecs.Cluster(stack, 'EcsCluster', { vpc }); + cluster.addCapacity('DefaultAutoScalingGroup', { + instanceType: new ec2.InstanceType('t2.micro'), + }); + + const taskDefinition = ecs.Ec2TaskDefinition.fromEc2TaskDefinitionAttributes(stack, 'TaskDef', { + taskDefinitionArn: 'importedTaskDefArn', + networkMode: ecs.NetworkMode.BRIDGE, + taskRole: new iam.Role(stack, 'TaskRole', { + assumedBy: new iam.ServicePrincipal('ecs-tasks.amazonaws.com'), + }), + }); + + const rule = new events.Rule(stack, 'Rule', { + schedule: events.Schedule.expression('rate(1 min)'), + }); + + // WHEN + rule.addTarget(new targets.EcsTask({ + cluster, + taskDefinition, + taskCount: 1, + containerOverrides: [{ + containerName: 'TheContainer', + command: ['echo', events.EventField.fromPath('$.detail.event')], + }], + })); + + // THEN + expect(stack).toHaveResourceLike('AWS::Events::Rule', { + Targets: [ + { + Arn: { 'Fn::GetAtt': ['EcsCluster97242B84', 'Arn'] }, + EcsParameters: { + TaskCount: 1, + TaskDefinitionArn: 'importedTaskDefArn', + }, + InputTransformer: { + InputPathsMap: { + 'detail-event': '$.detail.event', + }, + InputTemplate: '{"containerOverrides":[{"name":"TheContainer","command":["echo",]}]}', + }, + RoleArn: { 'Fn::GetAtt': ['TaskDefEventsRoleFB3B67B8', 'Arn'] }, + Id: 'Target0', + }, + ], + }); +}); + +test('Throws error for lacking of taskRole ' + + 'when importing from a Fargate task definition just from a task definition arn as EventRule target', () => { + // GIVEN + const stack = new cdk.Stack(); + const vpc = new ec2.Vpc(stack, 'Vpc', { maxAzs: 1 }); + const cluster = new ecs.Cluster(stack, 'EcsCluster', { vpc }); + + const taskDefinition = ecs.FargateTaskDefinition.fromFargateTaskDefinitionArn(stack, 'TaskDef', 'ImportedTaskDefArn'); + + const rule = new events.Rule(stack, 'Rule', { + schedule: events.Schedule.expression('rate(1 min)'), + }); + + // THEN + expect(() => { + rule.addTarget(new targets.EcsTask({ + cluster, + taskDefinition, + taskCount: 1, + containerOverrides: [{ + containerName: 'TheContainer', + command: ['echo', events.EventField.fromPath('$.detail.event')], + }], + })); + }).toThrow('This operation requires the taskRole in ImportedTaskDefinition to be defined. ' + + 'Add the \'taskRole\' in ImportedTaskDefinitionProps to instantiate ImportedTaskDefinition'); +}); + +test('Can import a Fargate task definition from task definition attributes as EventRule target', () => { + // GIVEN + const stack = new cdk.Stack(); + const vpc = new ec2.Vpc(stack, 'Vpc', { maxAzs: 1 }); + const cluster = new ecs.Cluster(stack, 'EcsCluster', { vpc }); + + const taskDefinition = ecs.FargateTaskDefinition.fromFargateTaskDefinitionAttributes(stack, 'TaskDef', { + taskDefinitionArn: 'importedTaskDefArn', + networkMode: ecs.NetworkMode.AWS_VPC, + taskRole: new iam.Role(stack, 'TaskRole', { + assumedBy: new iam.ServicePrincipal('ecs-tasks.amazonaws.com'), + }), + }); + + const rule = new events.Rule(stack, 'Rule', { + schedule: events.Schedule.expression('rate(1 min)'), + }); + + // WHEN + rule.addTarget(new targets.EcsTask({ + cluster, + taskDefinition, + taskCount: 1, + containerOverrides: [{ + containerName: 'TheContainer', + command: ['echo', events.EventField.fromPath('$.detail.event')], + }], + })); + + // THEN + expect(stack).toHaveResourceLike('AWS::Events::Rule', { + Targets: [ + { + Arn: { 'Fn::GetAtt': ['EcsCluster97242B84', 'Arn'] }, + EcsParameters: { + TaskCount: 1, + TaskDefinitionArn: 'importedTaskDefArn', + }, + InputTransformer: { + InputPathsMap: { + 'detail-event': '$.detail.event', + }, + InputTemplate: '{"containerOverrides":[{"name":"TheContainer","command":["echo",]}]}', + }, + RoleArn: { 'Fn::GetAtt': ['TaskDefEventsRoleFB3B67B8', 'Arn'] }, + Id: 'Target0', + }, + ], + }); +}); + +test('Throws error for lacking of taskRole ' + + 'when importing from a task definition just from a task definition arn as EventRule target', () => { + // GIVEN + const stack = new cdk.Stack(); + const vpc = new ec2.Vpc(stack, 'Vpc', { maxAzs: 1 }); + const cluster = new ecs.Cluster(stack, 'EcsCluster', { vpc }); + + const taskDefinition = ecs.TaskDefinition.fromTaskDefinitionArn(stack, 'TaskDef', 'ImportedTaskDefArn'); + + const rule = new events.Rule(stack, 'Rule', { + schedule: events.Schedule.expression('rate(1 min)'), + }); + + // THEN + expect(() => { + rule.addTarget(new targets.EcsTask({ + cluster, + taskDefinition, + taskCount: 1, + containerOverrides: [{ + containerName: 'TheContainer', + command: ['echo', events.EventField.fromPath('$.detail.event')], + }], + })); + }).toThrow('This operation requires the taskRole in ImportedTaskDefinition to be defined. ' + + 'Add the \'taskRole\' in ImportedTaskDefinitionProps to instantiate ImportedTaskDefinition'); +}); + +test('Can import a Task definition from task definition attributes as EventRule target', () => { + // GIVEN + const stack = new cdk.Stack(); + const vpc = new ec2.Vpc(stack, 'Vpc', { maxAzs: 1 }); + const cluster = new ecs.Cluster(stack, 'EcsCluster', { vpc }); + + const taskDefinition = ecs.FargateTaskDefinition.fromFargateTaskDefinitionAttributes(stack, 'TaskDef', { + taskDefinitionArn: 'importedTaskDefArn', + networkMode: ecs.NetworkMode.AWS_VPC, + taskRole: new iam.Role(stack, 'TaskRole', { + assumedBy: new iam.ServicePrincipal('ecs-tasks.amazonaws.com'), + }), + }); + + const rule = new events.Rule(stack, 'Rule', { + schedule: events.Schedule.expression('rate(1 min)'), + }); + + // WHEN + rule.addTarget(new targets.EcsTask({ + cluster, + taskDefinition, + taskCount: 1, + containerOverrides: [{ + containerName: 'TheContainer', + command: ['echo', events.EventField.fromPath('$.detail.event')], + }], + })); + + // THEN + expect(stack).toHaveResourceLike('AWS::Events::Rule', { + Targets: [ + { + Arn: { 'Fn::GetAtt': ['EcsCluster97242B84', 'Arn'] }, + EcsParameters: { + TaskCount: 1, + TaskDefinitionArn: 'importedTaskDefArn', + }, + InputTransformer: { + InputPathsMap: { + 'detail-event': '$.detail.event', + }, + InputTemplate: '{"containerOverrides":[{"name":"TheContainer","command":["echo",]}]}', + }, + RoleArn: { 'Fn::GetAtt': ['TaskDefEventsRoleFB3B67B8', 'Arn'] }, + Id: 'Target0', + }, + ], + }); +}); + test('Can use Fargate taskdef as EventRule target', () => { // GIVEN const stack = new cdk.Stack();