From 01ff2415dcc490f50cd19d1febc43452df8b8d84 Mon Sep 17 00:00:00 2001 From: Matthias Gubler Date: Thu, 28 Mar 2024 07:43:05 -0600 Subject: [PATCH] Add enableExecuteCommand to sfn ECSRunTask Fixes aws/aws-cdk#29637 --- .../aws-stepfunctions-tasks/README.md | 1 + .../lib/ecs/run-ecs-task-base.ts | 64 +++--- .../lib/ecs/run-task.ts | 199 ++++++++++-------- .../test/ecs/run-tasks.test.ts | 109 ++++++++-- 4 files changed, 238 insertions(+), 135 deletions(-) diff --git a/packages/aws-cdk-lib/aws-stepfunctions-tasks/README.md b/packages/aws-cdk-lib/aws-stepfunctions-tasks/README.md index 789529547b6d6..ac5f25779e112 100644 --- a/packages/aws-cdk-lib/aws-stepfunctions-tasks/README.md +++ b/packages/aws-cdk-lib/aws-stepfunctions-tasks/README.md @@ -555,6 +555,7 @@ const runTask = new tasks.EcsRunTask(this, 'Run', { ], }), propagatedTagSource: ecs.PropagatedTagSource.TASK_DEFINITION, + enableExecuteCommand: false, }); ``` diff --git a/packages/aws-cdk-lib/aws-stepfunctions-tasks/lib/ecs/run-ecs-task-base.ts b/packages/aws-cdk-lib/aws-stepfunctions-tasks/lib/ecs/run-ecs-task-base.ts index a23b5c26eff6e..8e98bfbe2a12e 100644 --- a/packages/aws-cdk-lib/aws-stepfunctions-tasks/lib/ecs/run-ecs-task-base.ts +++ b/packages/aws-cdk-lib/aws-stepfunctions-tasks/lib/ecs/run-ecs-task-base.ts @@ -11,37 +11,37 @@ import { getResourceArn } from '../resource-arn-suffix'; */ export interface CommonEcsRunTaskProps { /** - * The topic to run the task on - */ + * The topic to run the task on + */ readonly cluster: ecs.ICluster; /** - * Task Definition used for running tasks in the service. - * - * Note: this must be TaskDefinition, and not ITaskDefinition, - * as it requires properties that are not known for imported task definitions - * If you want to run a RunTask with an imported task definition, - * consider using CustomState - */ + * Task Definition used for running tasks in the service. + * + * Note: this must be TaskDefinition, and not ITaskDefinition, + * as it requires properties that are not known for imported task definitions + * If you want to run a RunTask with an imported task definition, + * consider using CustomState + */ readonly taskDefinition: ecs.TaskDefinition; /** - * Container setting overrides - * - * Key is the name of the container to override, value is the - * values you want to override. - * - * @default - No overrides - */ + * Container setting overrides + * + * Key is the name of the container to override, value is the + * values you want to override. + * + * @default - No overrides + */ readonly containerOverrides?: ContainerOverride[]; /** - * The service integration pattern indicates different ways to call RunTask in ECS. - * - * The valid value for Lambda is FIRE_AND_FORGET, SYNC and WAIT_FOR_TASK_TOKEN. - * - * @default FIRE_AND_FORGET - */ + * The service integration pattern indicates different ways to call RunTask in ECS. + * + * The valid value for Lambda is FIRE_AND_FORGET, SYNC and WAIT_FOR_TASK_TOKEN. + * + * @default FIRE_AND_FORGET + */ readonly integrationPattern?: sfn.ServiceIntegrationPattern; } @@ -51,11 +51,11 @@ export interface CommonEcsRunTaskProps { */ export interface EcsRunTaskBaseProps extends CommonEcsRunTaskProps { /** - * Additional parameters to pass to the base task - * - * @default - No additional parameters passed - */ - readonly parameters?: {[key: string]: any}; + * Additional parameters to pass to the base task + * + * @default - No additional parameters passed + */ + readonly parameters?: { [key: string]: any }; } /** @@ -64,8 +64,8 @@ export interface EcsRunTaskBaseProps extends CommonEcsRunTaskProps { */ export class EcsRunTaskBase implements ec2.IConnectable, sfn.IStepFunctionsTask { /** - * Manage allowed network traffic for this service - */ + * Manage allowed network traffic for this service + */ public readonly connections: ec2.Connections = new ec2.Connections(); private securityGroup?: ec2.ISecurityGroup; @@ -86,7 +86,7 @@ export class EcsRunTaskBase implements ec2.IConnectable, sfn.IStepFunctionsTask } if (this.integrationPattern === sfn.ServiceIntegrationPattern.WAIT_FOR_TASK_TOKEN - && !sfn.FieldUtils.containsTaskToken(props.containerOverrides?.map(override => override.environment))) { + && !sfn.FieldUtils.containsTaskToken(props.containerOverrides?.map(override => override.environment))) { throw new Error('Task Token is required in at least one `containerOverrides.environment` for callback. Use JsonPath.taskToken to set the token.'); } @@ -190,7 +190,9 @@ export class EcsRunTaskBase implements ec2.IConnectable, sfn.IStepFunctionsTask } function renderOverrides(containerOverrides?: ContainerOverride[]) { - if (!containerOverrides) { return undefined; } + if (!containerOverrides) { + return undefined; + } const ret = new Array(); for (const override of containerOverrides) { diff --git a/packages/aws-cdk-lib/aws-stepfunctions-tasks/lib/ecs/run-task.ts b/packages/aws-cdk-lib/aws-stepfunctions-tasks/lib/ecs/run-task.ts index 303468707ea54..1c44256bcbcc7 100644 --- a/packages/aws-cdk-lib/aws-stepfunctions-tasks/lib/ecs/run-task.ts +++ b/packages/aws-cdk-lib/aws-stepfunctions-tasks/lib/ecs/run-task.ts @@ -12,75 +12,82 @@ import { integrationResourceArn, validatePatternSupported } from '../private/tas */ export interface EcsRunTaskProps extends sfn.TaskStateBaseProps { /** - * The ECS cluster to run the task on - */ + * The ECS cluster to run the task on + */ readonly cluster: ecs.ICluster; /** - * [disable-awslint:ref-via-interface] - * Task Definition used for running tasks in the service. - * - * Note: this must be TaskDefinition, and not ITaskDefinition, - * as it requires properties that are not known for imported task definitions - * If you want to run a RunTask with an imported task definition, - * consider using CustomState - */ + * [disable-awslint:ref-via-interface] + * Task Definition used for running tasks in the service. + * + * Note: this must be TaskDefinition, and not ITaskDefinition, + * as it requires properties that are not known for imported task definitions + * If you want to run a RunTask with an imported task definition, + * consider using CustomState + */ readonly taskDefinition: ecs.TaskDefinition; /** - * The revision number of ECS task definiton family - * - * @default - '$latest' - */ + * The revision number of ECS task definition family + * + * @default - '$latest' + */ readonly revisionNumber?: number; /** - * Container setting overrides - * - * Specify the container to use and the overrides to apply. - * - * @default - No overrides - */ + * Container setting overrides + * + * Specify the container to use and the overrides to apply. + * + * @default - No overrides + */ readonly containerOverrides?: ContainerOverride[]; /** - * Subnets to place the task's ENIs - * - * @default - Public subnets if assignPublicIp is set. Private subnets otherwise. - */ + * Subnets to place the task's ENIs + * + * @default - Public subnets if assignPublicIp is set. Private subnets otherwise. + */ readonly subnets?: ec2.SubnetSelection; /** - * Existing security groups to use for the tasks - * - * @default - A new security group is created - */ + * Existing security groups to use for the tasks + * + * @default - A new security group is created + */ readonly securityGroups?: ec2.ISecurityGroup[]; /** - * Assign public IP addresses to each task - * - * @default false - */ + * Assign public IP addresses to each task + * + * @default false + */ readonly assignPublicIp?: boolean; /** - * An Amazon ECS launch type determines the type of infrastructure on which your - * tasks and services are hosted. - * - * @see https://docs.aws.amazon.com/AmazonECS/latest/developerguide/launch_types.html - */ + * An Amazon ECS launch type determines the type of infrastructure on which your + * tasks and services are hosted. + * + * @see https://docs.aws.amazon.com/AmazonECS/latest/developerguide/launch_types.html + */ readonly launchTarget: IEcsLaunchTarget; /** - * Specifies whether to propagate the tags from the task definition to the task. - * An error will be received if you specify the SERVICE option when running a task. - * - * @see https://docs.aws.amazon.com/AmazonECS/latest/APIReference/API_RunTask.html#ECS-RunTask-request-propagateTags - * - * @default - No tags are propagated. - */ + * Specifies whether to propagate the tags from the task definition to the task. + * An error will be received if you specify the SERVICE option when running a task. + * + * @see https://docs.aws.amazon.com/AmazonECS/latest/APIReference/API_RunTask.html#ECS-RunTask-request-propagateTags + * + * @default - No tags are propagated. + */ readonly propagatedTagSource?: ecs.PropagatedTagSource; + + /** + * Whether ECS Exec should be enabled + * + * @default - false + */ + readonly enableExecuteCommand?: boolean; } /** @@ -89,8 +96,8 @@ export interface EcsRunTaskProps extends sfn.TaskStateBaseProps { */ export interface IEcsLaunchTarget { /** - * called when the ECS launch target is configured on RunTask - */ + * called when the ECS launch target is configured on RunTask + */ bind(task: EcsRunTask, launchTargetOptions: LaunchTargetBindOptions): EcsLaunchTargetConfig; } @@ -99,16 +106,16 @@ export interface IEcsLaunchTarget { */ export interface LaunchTargetBindOptions { /** - * Task definition to run Docker containers in Amazon ECS - */ + * Task definition to run Docker containers in Amazon ECS + */ readonly taskDefinition: ecs.ITaskDefinition; /** - * A regional grouping of one or more container instances on which you can run - * tasks and services. - * - * @default - No cluster - */ + * A regional grouping of one or more container instances on which you can run + * tasks and services. + * + * @default - No cluster + */ readonly cluster?: ecs.ICluster; } @@ -117,10 +124,10 @@ export interface LaunchTargetBindOptions { */ export interface EcsLaunchTargetConfig { /** - * Additional parameters to pass to the base task - * - * @default - No additional parameters passed - */ + * Additional parameters to pass to the base task + * + * @default - No additional parameters passed + */ readonly parameters?: { [key: string]: any }; } @@ -129,11 +136,11 @@ export interface EcsLaunchTargetConfig { */ export interface EcsFargateLaunchTargetOptions { /** - * Refers to a specific runtime environment for Fargate task infrastructure. - * Fargate platform version is a combination of the kernel and container runtime versions. - * - * @see https://docs.aws.amazon.com/AmazonECS/latest/developerguide/platform_versions.html - */ + * Refers to a specific runtime environment for Fargate task infrastructure. + * Fargate platform version is a combination of the kernel and container runtime versions. + * + * @see https://docs.aws.amazon.com/AmazonECS/latest/developerguide/platform_versions.html + */ readonly platformVersion: ecs.FargatePlatformVersion; } @@ -142,17 +149,17 @@ export interface EcsFargateLaunchTargetOptions { */ export interface EcsEc2LaunchTargetOptions { /** - * Placement constraints - * - * @default - None - */ + * Placement constraints + * + * @default - None + */ readonly placementConstraints?: ecs.PlacementConstraint[]; /** - * Placement strategies - * - * @default - None - */ + * Placement strategies + * + * @default - None + */ readonly placementStrategies?: ecs.PlacementStrategy[]; } @@ -162,11 +169,12 @@ export interface EcsEc2LaunchTargetOptions { * @see https://docs.aws.amazon.com/AmazonECS/latest/userguide/launch_types.html#launch-type-fargate */ export class EcsFargateLaunchTarget implements IEcsLaunchTarget { - constructor(private readonly options?: EcsFargateLaunchTargetOptions) {} + constructor(private readonly options?: EcsFargateLaunchTargetOptions) { + } /** - * Called when the Fargate launch type configured on RunTask - */ + * Called when the Fargate launch type configured on RunTask + */ public bind(_task: EcsRunTask, launchTargetOptions: LaunchTargetBindOptions): EcsLaunchTargetConfig { if (!launchTargetOptions.taskDefinition.isFargateCompatible) { throw new Error('Supplied TaskDefinition is not compatible with Fargate'); @@ -187,10 +195,12 @@ export class EcsFargateLaunchTarget implements IEcsLaunchTarget { * @see https://docs.aws.amazon.com/AmazonECS/latest/userguide/launch_types.html#launch-type-ec2 */ export class EcsEc2LaunchTarget implements IEcsLaunchTarget { - constructor(private readonly options?: EcsEc2LaunchTargetOptions) {} + constructor(private readonly options?: EcsEc2LaunchTargetOptions) { + } + /** - * Called when the EC2 launch type is configured on RunTask - */ + * Called when the EC2 launch type is configured on RunTask + */ public bind(_task: EcsRunTask, launchTargetOptions: LaunchTargetBindOptions): EcsLaunchTargetConfig { if (!launchTargetOptions.taskDefinition.isEc2Compatible) { throw new Error('Supplied TaskDefinition is not compatible with EC2'); @@ -244,8 +254,8 @@ export class EcsRunTask extends sfn.TaskStateBase implements ec2.IConnectable { ]; /** - * Manage allowed network traffic for this service - */ + * Manage allowed network traffic for this service + */ public readonly connections: ec2.Connections = new ec2.Connections(); protected readonly taskMetrics?: sfn.TaskMetricsConfig; @@ -262,7 +272,7 @@ export class EcsRunTask extends sfn.TaskStateBase implements ec2.IConnectable { validatePatternSupported(this.integrationPattern, EcsRunTask.SUPPORTED_INTEGRATION_PATTERNS); if (this.integrationPattern === sfn.IntegrationPattern.WAIT_FOR_TASK_TOKEN - && !sfn.FieldUtils.containsTaskToken(props.containerOverrides?.map(override => override.environment))) { + && !sfn.FieldUtils.containsTaskToken(props.containerOverrides?.map(override => override.environment))) { throw new Error('Task Token is required in at least one `containerOverrides.environment` for callback. Use JsonPath.taskToken to set the token.'); } @@ -292,8 +302,8 @@ export class EcsRunTask extends sfn.TaskStateBase implements ec2.IConnectable { } /** - * @internal - */ + * @internal + */ protected _renderTask(): any { return { Resource: integrationResourceArn('ecs', 'runTask', this.integrationPattern), @@ -303,14 +313,19 @@ export class EcsRunTask extends sfn.TaskStateBase implements ec2.IConnectable { NetworkConfiguration: this.networkConfiguration, Overrides: renderOverrides(this.props.containerOverrides), PropagateTags: this.props.propagatedTagSource, - ...this.props.launchTarget.bind(this, { taskDefinition: this.props.taskDefinition, cluster: this.props.cluster }).parameters, + ...this.props.launchTarget.bind(this, { + taskDefinition: this.props.taskDefinition, + cluster: this.props.cluster, + }, + ).parameters, + EnableExecuteCommand: this.props.enableExecuteCommand, }), }; } private configureAwsVpcNetworking() { const subnetSelection = this.props.subnets ?? - { subnetType: this.props.assignPublicIp ? ec2.SubnetType.PUBLIC : ec2.SubnetType.PRIVATE_WITH_EGRESS }; + { subnetType: this.props.assignPublicIp ? ec2.SubnetType.PUBLIC : ec2.SubnetType.PRIVATE_WITH_EGRESS }; this.networkConfiguration = { AwsvpcConfiguration: { @@ -375,11 +390,11 @@ export class EcsRunTask extends sfn.TaskStateBase implements ec2.IConnectable { } /** - * Returns the ARN of the task definition family by removing the - * revision from the task definition ARN - * Before - arn:aws:ecs:us-west-2:123456789012:task-definition/hello_world:8 - * After - arn:aws:ecs:us-west-2:123456789012:task-definition/hello_world - */ + * Returns the ARN of the task definition family by removing the + * revision from the task definition ARN + * Before - arn:aws:ecs:us-west-2:123456789012:task-definition/hello_world:8 + * After - arn:aws:ecs:us-west-2:123456789012:task-definition/hello_world + */ private getTaskDefinitionFamilyArn(): string { const arnComponents = cdk.Stack.of(this).splitArn(this.props.taskDefinition.taskDefinitionArn, cdk.ArnFormat.SLASH_RESOURCE_NAME); let { resourceName } = arnComponents; @@ -424,10 +439,10 @@ function renderOverrides(containerOverrides?: ContainerOverride[]) { Memory: override.memoryLimit, MemoryReservation: override.memoryReservation, Environment: - override.environment?.map((e) => ({ - Name: e.name, - Value: e.value, - })), + override.environment?.map((e) => ({ + Name: e.name, + Value: e.value, + })), }); } diff --git a/packages/aws-cdk-lib/aws-stepfunctions-tasks/test/ecs/run-tasks.test.ts b/packages/aws-cdk-lib/aws-stepfunctions-tasks/test/ecs/run-tasks.test.ts index 928e7486eb02f..aea9845175ae0 100644 --- a/packages/aws-cdk-lib/aws-stepfunctions-tasks/test/ecs/run-tasks.test.ts +++ b/packages/aws-cdk-lib/aws-stepfunctions-tasks/test/ecs/run-tasks.test.ts @@ -38,7 +38,11 @@ test('Cannot create a Fargate task with a fargate-incompatible task definition', }); expect(() => - new tasks.EcsRunTask(stack, 'task', { cluster, taskDefinition, launchTarget: new tasks.EcsFargateLaunchTarget() }).toStateJson(), + new tasks.EcsRunTask(stack, 'task', { + cluster, + taskDefinition, + launchTarget: new tasks.EcsFargateLaunchTarget(), + }).toStateJson(), ).toThrowError(/Supplied TaskDefinition is not compatible with Fargate/); }); @@ -49,7 +53,11 @@ test('Cannot create a Fargate task without a default container', () => { compatibility: ecs.Compatibility.FARGATE, }); expect(() => - new tasks.EcsRunTask(stack, 'task', { cluster, taskDefinition, launchTarget: new tasks.EcsFargateLaunchTarget() }).toStateJson(), + new tasks.EcsRunTask(stack, 'task', { + cluster, + taskDefinition, + launchTarget: new tasks.EcsFargateLaunchTarget(), + }).toStateJson(), ).toThrowError(/must have at least one essential container/); }); @@ -81,7 +89,10 @@ test('Cannot override container definitions when container is not in task defini containerOverrides: [ { containerDefinition: containerDefinitionB, - environment: [{ name: 'SOME_KEY', value: sfn.JsonPath.stringAt('$.SomeKey') }], + environment: [{ + name: 'SOME_KEY', + value: sfn.JsonPath.stringAt('$.SomeKey'), + }], }, ], launchTarget: new tasks.EcsFargateLaunchTarget(), @@ -108,7 +119,10 @@ test('Running a task with container override and container has explicitly set a containerOverrides: [ { containerDefinition, - environment: [{ name: 'SOME_KEY', value: sfn.JsonPath.stringAt('$.SomeKey') }], + environment: [{ + name: 'SOME_KEY', + value: sfn.JsonPath.stringAt('$.SomeKey'), + }], }, ], launchTarget: new tasks.EcsFargateLaunchTarget(), @@ -148,7 +162,10 @@ test('Running a task without propagated tag source', () => { containerOverrides: [ { containerDefinition, - environment: [{ name: 'SOME_KEY', value: sfn.JsonPath.stringAt('$.SomeKey') }], + environment: [{ + name: 'SOME_KEY', + value: sfn.JsonPath.stringAt('$.SomeKey'), + }], }, ], launchTarget: new tasks.EcsFargateLaunchTarget(), @@ -174,7 +191,10 @@ test('Running a task with TASK_DEFINITION as propagated tag source', () => { containerOverrides: [ { containerDefinition, - environment: [{ name: 'SOME_KEY', value: sfn.JsonPath.stringAt('$.SomeKey') }], + environment: [{ + name: 'SOME_KEY', + value: sfn.JsonPath.stringAt('$.SomeKey'), + }], }, ], launchTarget: new tasks.EcsFargateLaunchTarget(), @@ -201,7 +221,10 @@ test('Running a task with NONE as propagated tag source', () => { containerOverrides: [ { containerDefinition, - environment: [{ name: 'SOME_KEY', value: sfn.JsonPath.stringAt('$.SomeKey') }], + environment: [{ + name: 'SOME_KEY', + value: sfn.JsonPath.stringAt('$.SomeKey'), + }], }, ], launchTarget: new tasks.EcsFargateLaunchTarget(), @@ -228,7 +251,10 @@ test('Running a Fargate Task', () => { containerOverrides: [ { containerDefinition, - environment: [{ name: 'SOME_KEY', value: sfn.JsonPath.stringAt('$.SomeKey') }], + environment: [{ + name: 'SOME_KEY', + value: sfn.JsonPath.stringAt('$.SomeKey'), + }], }, ], launchTarget: new tasks.EcsFargateLaunchTarget({ @@ -379,7 +405,10 @@ test('Running an EC2 Task with bridge network', () => { containerOverrides: [ { containerDefinition, - environment: [{ name: 'SOME_KEY', value: sfn.JsonPath.stringAt('$.SomeKey') }], + environment: [{ + name: 'SOME_KEY', + value: sfn.JsonPath.stringAt('$.SomeKey'), + }], }, ], launchTarget: new tasks.EcsEc2LaunchTarget(), @@ -535,8 +564,17 @@ test('Running an EC2 Task with placement strategies', () => { Cluster: { 'Fn::GetAtt': ['ClusterEB0386A7', 'Arn'] }, LaunchType: 'EC2', TaskDefinition: 'TD', - PlacementConstraints: [{ Type: 'memberOf', Expression: 'blieptuut' }], - PlacementStrategy: [{ Field: 'instanceId', Type: 'spread' }, { Field: 'CPU', Type: 'binpack' }, { Type: 'random' }], + PlacementConstraints: [{ + Type: 'memberOf', + Expression: 'blieptuut', + }], + PlacementStrategy: [{ + Field: 'instanceId', + Type: 'spread', + }, { + Field: 'CPU', + Type: 'binpack', + }, { Type: 'random' }], }, Resource: { 'Fn::Join': [ @@ -686,7 +724,7 @@ test('Running a task with WAIT_FOR_TASK_TOKEN and task token in environment', () })).not.toThrow(); }); -test('Set revision number of ECS task denition family', () => { +test('Set revision number of ECS task definition family', () => { // When const taskDefinition = new ecs.TaskDefinition(stack, 'TD', { memoryMiB: '512', @@ -754,3 +792,50 @@ test('Set revision number of ECS task denition family', () => { }, ); }); + +test('set enableExecuteCommand', () => { + const taskDefinition = new ecs.TaskDefinition(stack, 'TD', { + compatibility: ecs.Compatibility.EC2, + }); + taskDefinition.addContainer('TheContainer', { + image: ecs.ContainerImage.fromRegistry('foo/bar'), + memoryLimitMiB: 256, + }); + + // WHEN + const runTask = new tasks.EcsRunTask(stack, 'Run', { + integrationPattern: sfn.IntegrationPattern.RUN_JOB, + launchTarget: new tasks.EcsEc2LaunchTarget(), + cluster, + taskDefinition, + enableExecuteCommand: true, + }); + + new sfn.StateMachine(stack, 'SM', { + definitionBody: sfn.DefinitionBody.fromChainable(runTask), + }); + + // THEN + expect(stack.resolve(runTask.toStateJson())).toEqual({ + End: true, + Parameters: { + Cluster: { 'Fn::GetAtt': ['ClusterEB0386A7', 'Arn'] }, + LaunchType: 'EC2', + TaskDefinition: 'TD', + EnableExecuteCommand: true, + }, + Resource: { + 'Fn::Join': [ + '', + [ + 'arn:', + { + Ref: 'AWS::Partition', + }, + ':states:::ecs:runTask.sync', + ], + ], + }, + Type: 'Task', + }); +});