From 7766d0edad89caec7af3b4eb57897fcb94075243 Mon Sep 17 00:00:00 2001 From: Rico Huijbers Date: Sat, 27 Apr 2019 13:48:25 +0200 Subject: [PATCH] Adding tests --- packages/@aws-cdk/assert/jest.ts | 62 +- .../assert/lib/assertions/have-resource.ts | 24 +- .../aws-stepfunctions-tasks/.npmignore | 2 +- .../lib/base-run-task.ts | 107 +- .../aws-stepfunctions-tasks/lib/index.ts | 5 +- .../lib/invoke-activity.ts | 22 +- .../lib/invoke-function.ts | 22 +- .../lib/publish-to-topic.ts | 14 +- .../{ec2-run-task.ts => run-ecs-ec2-task.ts} | 33 +- ...te-run-task.ts => run-ecs-fargate-task.ts} | 26 +- .../lib/send-to-queue.ts | 18 +- .../aws-stepfunctions-tasks/package.json | 2 +- .../test/ecs-tasks.test.ts | 209 ++++ .../test/integ.ec2-task.expected.json | 937 ++++++++++++++++++ .../test/integ.ec2-task.ts | 51 + .../test/integ.fargate-task.expected.json | 619 ++++++++++++ .../test/integ.fargate-task.ts | 51 + .../aws-stepfunctions/lib/states/task.ts | 1 + tools/cdk-build-tools/bin/cdk-test.ts | 2 +- 19 files changed, 2058 insertions(+), 149 deletions(-) rename packages/@aws-cdk/aws-stepfunctions-tasks/lib/{ec2-run-task.ts => run-ecs-ec2-task.ts} (82%) rename packages/@aws-cdk/aws-stepfunctions-tasks/lib/{fargate-run-task.ts => run-ecs-fargate-task.ts} (64%) create mode 100644 packages/@aws-cdk/aws-stepfunctions-tasks/test/ecs-tasks.test.ts create mode 100644 packages/@aws-cdk/aws-stepfunctions-tasks/test/integ.ec2-task.expected.json create mode 100644 packages/@aws-cdk/aws-stepfunctions-tasks/test/integ.ec2-task.ts create mode 100644 packages/@aws-cdk/aws-stepfunctions-tasks/test/integ.fargate-task.expected.json create mode 100644 packages/@aws-cdk/aws-stepfunctions-tasks/test/integ.fargate-task.ts diff --git a/packages/@aws-cdk/assert/jest.ts b/packages/@aws-cdk/assert/jest.ts index d848d6fb70d4f..03c5778750ad3 100644 --- a/packages/@aws-cdk/assert/jest.ts +++ b/packages/@aws-cdk/assert/jest.ts @@ -1,6 +1,6 @@ import { Stack } from "@aws-cdk/cdk"; import { SynthesizedStack } from "@aws-cdk/cx-api"; -import { haveResource, ResourcePart } from "./lib/assertions/have-resource"; +import { HaveResourceAssertion, ResourcePart } from "./lib/assertions/have-resource"; import { expect as ourExpect } from './lib/expect'; declare global { @@ -8,32 +8,48 @@ declare global { interface Matchers { toHaveResource(resourceType: string, properties?: any, - comparison?: ResourcePart, - allowValueExtension?: boolean): R; + comparison?: ResourcePart): R; + + toHaveResourceLike(resourceType: string, + properties?: any, + comparison?: ResourcePart): R; } } } expect.extend({ - toHaveResource(actual: SynthesizedStack | Stack, - resourceType: string, - properties?: any, - comparison?: ResourcePart, - allowValueExtension: boolean = false) { + toHaveResource( + actual: SynthesizedStack | Stack, + resourceType: string, + properties?: any, + comparison?: ResourcePart) { - const assertion = haveResource(resourceType, properties, comparison, allowValueExtension); - const inspector = ourExpect(actual); - const pass = assertion.assertUsing(inspector); - if (pass) { - return { - pass, - message: `Expected ${JSON.stringify(inspector.value, null, 2)} not to match ${assertion.description}`, - }; - } else { - return { - pass, - message: `Expected ${JSON.stringify(inspector.value, null, 2)} to match ${assertion.description}`, - }; - } + const assertion = new HaveResourceAssertion(resourceType, properties, comparison, false); + return assertHaveResource(assertion, actual); + }, + toHaveResourceLike( + actual: SynthesizedStack | Stack, + resourceType: string, + properties?: any, + comparison?: ResourcePart) { + + const assertion = new HaveResourceAssertion(resourceType, properties, comparison, true); + return assertHaveResource(assertion, actual); + } +}); + +function assertHaveResource(assertion: HaveResourceAssertion, actual: SynthesizedStack | Stack) { + const inspector = ourExpect(actual); + const pass = assertion.assertUsing(inspector); + if (pass) { + return { + pass, + message: () => `Not ` + assertion.generateErrorMessage(), + }; + } else { + return { + pass, + message: () => assertion.generateErrorMessage(), + }; } -}); \ No newline at end of file +} \ No newline at end of file diff --git a/packages/@aws-cdk/assert/lib/assertions/have-resource.ts b/packages/@aws-cdk/assert/lib/assertions/have-resource.ts index 6b4c6d2ad294a..d6653f3b64ce6 100644 --- a/packages/@aws-cdk/assert/lib/assertions/have-resource.ts +++ b/packages/@aws-cdk/assert/lib/assertions/have-resource.ts @@ -30,7 +30,7 @@ export function haveResourceLike(resourceType: string, type PropertyPredicate = (props: any, inspection: InspectionFailure) => boolean; -class HaveResourceAssertion extends Assertion { +export class HaveResourceAssertion extends Assertion { private inspected: InspectionFailure[] = []; private readonly part: ResourcePart; private readonly predicate: PropertyPredicate; @@ -66,17 +66,21 @@ class HaveResourceAssertion extends Assertion { return false; } - public assertOrThrow(inspector: StackInspector) { - if (!this.assertUsing(inspector)) { - const lines: string[] = []; - lines.push(`None of ${this.inspected.length} resources matches ${this.description}.`); + public generateErrorMessage() { + const lines: string[] = []; + lines.push(`None of ${this.inspected.length} resources matches ${this.description}.`); - for (const inspected of this.inspected) { - lines.push(`- ${inspected.failureReason} in:`); - lines.push(indent(4, JSON.stringify(inspected.resource, null, 2))); - } + for (const inspected of this.inspected) { + lines.push(`- ${inspected.failureReason} in:`); + lines.push(indent(4, JSON.stringify(inspected.resource, null, 2))); + } - throw new Error(lines.join('\n')); + return lines.join('\n'); + } + + public assertOrThrow(inspector: StackInspector) { + if (!this.assertUsing(inspector)) { + throw new Error(this.generateErrorMessage()); } } diff --git a/packages/@aws-cdk/aws-stepfunctions-tasks/.npmignore b/packages/@aws-cdk/aws-stepfunctions-tasks/.npmignore index 3f1f9c81eee9b..323f9e5fb74ed 100644 --- a/packages/@aws-cdk/aws-stepfunctions-tasks/.npmignore +++ b/packages/@aws-cdk/aws-stepfunctions-tasks/.npmignore @@ -14,4 +14,4 @@ dist !.jsii *.snk -tsconfig.* \ No newline at end of file +*.tsbuildinfo diff --git a/packages/@aws-cdk/aws-stepfunctions-tasks/lib/base-run-task.ts b/packages/@aws-cdk/aws-stepfunctions-tasks/lib/base-run-task.ts index bb8e4692c8d01..58d2ff76c4ab4 100644 --- a/packages/@aws-cdk/aws-stepfunctions-tasks/lib/base-run-task.ts +++ b/packages/@aws-cdk/aws-stepfunctions-tasks/lib/base-run-task.ts @@ -7,16 +7,16 @@ import cdk = require('@aws-cdk/cdk'); /** * Properties for SendMessageTask */ -export interface BaseRunTaskProps extends stepfunctions.BasicTaskProps { +export interface CommonRunTaskProps extends stepfunctions.BasicTaskProps { /** * The topic to run the task on */ - cluster: ecs.ICluster; + readonly cluster: ecs.ICluster; /** * Task Definition used for running tasks in the service */ - taskDefinition: ecs.TaskDefinition; + readonly taskDefinition: ecs.TaskDefinition; /** * Container setting overrides @@ -24,14 +24,24 @@ export interface BaseRunTaskProps extends stepfunctions.BasicTaskProps { * Key is the name of the container to override, value is the * values you want to override. */ - containerOverrides?: ContainerOverride[]; + readonly containerOverrides?: ContainerOverride[]; /** * Whether to wait for the task to complete and return the response * * @default true */ - synchronous?: boolean; + readonly synchronous?: boolean; +} + +/** + * Construction properties for the BaseRunTaskProps + */ +export interface BaseRunTaskProps extends CommonRunTaskProps { + /** + * Additional parameters to pass to the base task + */ + readonly parameters?: {[key: string]: any}; } export interface ContainerOverride { @@ -40,75 +50,75 @@ export interface ContainerOverride { * * Exactly one of `containerName` and `containerNamePath` is required. */ - containerName?: string; + readonly containerName?: string; /** * JSONPath expression for the name of the container inside the task definition * * Exactly one of `containerName` and `containerNamePath` is required. */ - containerNamePath?: string; + readonly containerNamePath?: string; /** * Command to run inside the container * * @default Default command */ - command?: string[]; + readonly command?: string[]; /** * JSON expression for command to run inside the container * * @default Default command */ - commandPath?: string; + readonly commandPath?: string; /** * Variables to set in the container's environment */ - environment?: TaskEnvironmentVariable[]; + readonly environment?: TaskEnvironmentVariable[]; /** * The number of cpu units reserved for the container * * @Default The default value from the task definition. */ - cpu?: number; + readonly cpu?: number; /** * JSON expression for the number of CPU units * * @Default The default value from the task definition. */ - cpuPath?: string; + readonly cpuPath?: string; /** * Hard memory limit on the container * * @Default The default value from the task definition. */ - memoryLimit?: number; + readonly memoryLimit?: number; /** * JSON expression path for the hard memory limit * * @Default The default value from the task definition. */ - memoryLimitPath?: string; + readonly memoryLimitPath?: string; /** * Soft memory limit on the container * * @Default The default value from the task definition. */ - memoryReservation?: number; + readonly memoryReservation?: number; /** * JSONExpr path for memory limit on the container * * @Default The default value from the task definition. */ - memoryReservationPath?: number; + readonly memoryReservationPath?: number; } /** @@ -120,28 +130,28 @@ export interface TaskEnvironmentVariable { * * Exactly one of `name` and `namePath` must be specified. */ - name?: string; + readonly name?: string; /** * JSONExpr for the name of the variable * * Exactly one of `name` and `namePath` must be specified. */ - namePath?: string; + readonly namePath?: string; /** * Value of the environment variable * * Exactly one of `value` and `valuePath` must be specified. */ - value?: string; + readonly value?: string; /** * JSONPath expr for the environment variable * * Exactly one of `value` and `valuePath` must be specified. */ - valuePath?: string; + readonly valuePath?: string; } /** @@ -154,7 +164,6 @@ export class BaseRunTask extends stepfunctions.Task implements ec2.IConnectable public readonly connections: ec2.Connections = new ec2.Connections(); protected networkConfiguration?: any; - protected readonly _parameters: {[key: string]: any} = {}; protected readonly taskDefinition: ecs.TaskDefinition; private readonly sync: boolean; @@ -162,26 +171,26 @@ export class BaseRunTask extends stepfunctions.Task implements ec2.IConnectable super(scope, id, { ...props, resourceArn: 'arn:aws:states:::ecs:runTask' + (props.synchronous !== false ? '.sync' : ''), - parameters: new cdk.Token(() => ({ + parameters: { Cluster: props.cluster.clusterArn, TaskDefinition: props.taskDefinition.taskDefinitionArn, - NetworkConfiguration: this.networkConfiguration, - ...this._parameters - })) + NetworkConfiguration: new cdk.Token(() => this.networkConfiguration), + Overrides: renderOverrides(props.containerOverrides), + ...props.parameters, + } }); this.sync = props.synchronous !== false; - this._parameters.Overrides = this.renderOverrides(props.containerOverrides); this.taskDefinition = props.taskDefinition; } protected configureAwsVpcNetworking( vpc: ec2.IVpcNetwork, assignPublicIp?: boolean, - subnetSelection?: ec2.VpcSubnetSelection, + subnetSelection?: ec2.SubnetSelection, securityGroup?: ec2.ISecurityGroup) { if (subnetSelection === undefined) { - subnetSelection = { subnetsToUse: assignPublicIp ? ec2.SubnetType.Public : ec2.SubnetType.Private }; + subnetSelection = { subnetType: assignPublicIp ? ec2.SubnetType.Public : ec2.SubnetType.Private }; } if (securityGroup === undefined) { securityGroup = new ec2.SecurityGroup(this, 'SecurityGroup', { vpc }); @@ -225,27 +234,10 @@ export class BaseRunTask extends stepfunctions.Task implements ec2.IConnectable resourceName: 'StepFunctionsGetEventsForECSTaskRule' }))); } - } - private renderOverrides(containerOverrides?: ContainerOverride[]) { - if (!containerOverrides) { return undefined; } - - const ret = new Array(); - for (const override of containerOverrides) { - ret.push({ - ...extractRequired(override, 'containerName', 'Name'), - ...extractOptional(override, 'command', 'Command'), - ...extractOptional(override, 'cpu', 'Cpu'), - ...extractOptional(override, 'memoryLimit', 'Memory'), - ...extractOptional(override, 'memoryReservation', 'MemoryReservation'), - Environment: override.environment && override.environment.map(e => ({ - ...extractRequired(e, 'name', 'Name'), - ...extractRequired(e, 'value', 'Value'), - })) - }); + for (const policyStatement of policyStatements) { + graph.registerPolicyStatement(policyStatement); } - - return { ContainerOverrides: ret }; } private taskExecutionRoles(): iam.IRole[] { @@ -266,6 +258,27 @@ function extractRequired(obj: any, srcKey: string, dstKey: string) { return mapValue(obj, srcKey, dstKey); } +function renderOverrides(containerOverrides?: ContainerOverride[]) { + if (!containerOverrides) { return undefined; } + + const ret = new Array(); + for (const override of containerOverrides) { + ret.push({ + ...extractRequired(override, 'containerName', 'Name'), + ...extractOptional(override, 'command', 'Command'), + ...extractOptional(override, 'cpu', 'Cpu'), + ...extractOptional(override, 'memoryLimit', 'Memory'), + ...extractOptional(override, 'memoryReservation', 'MemoryReservation'), + Environment: override.environment && override.environment.map(e => ({ + ...extractRequired(e, 'name', 'Name'), + ...extractRequired(e, 'value', 'Value'), + })) + }); + } + + return { ContainerOverrides: ret }; +} + function extractOptional(obj: any, srcKey: string, dstKey: string) { if ((obj[srcKey] !== undefined) && (obj[srcKey + 'Path'] !== undefined)) { throw new Error(`Supply only one of '${srcKey}' or '${srcKey}Path'`); diff --git a/packages/@aws-cdk/aws-stepfunctions-tasks/lib/index.ts b/packages/@aws-cdk/aws-stepfunctions-tasks/lib/index.ts index 665ddf6f26f11..02d3b691fc0aa 100644 --- a/packages/@aws-cdk/aws-stepfunctions-tasks/lib/index.ts +++ b/packages/@aws-cdk/aws-stepfunctions-tasks/lib/index.ts @@ -1,7 +1,8 @@ export * from './invoke-function'; export * from './invoke-activity'; export * from './nested-state-machine'; +export * from './base-run-task'; export * from './publish-to-topic'; export * from './send-to-queue'; -export * from './ec2-run-task'; -export * from './fargate-run-task'; +export * from './run-ecs-ec2-task'; +export * from './run-ecs-fargate-task'; diff --git a/packages/@aws-cdk/aws-stepfunctions-tasks/lib/invoke-activity.ts b/packages/@aws-cdk/aws-stepfunctions-tasks/lib/invoke-activity.ts index 0594463680d2d..4fbe4671691ee 100644 --- a/packages/@aws-cdk/aws-stepfunctions-tasks/lib/invoke-activity.ts +++ b/packages/@aws-cdk/aws-stepfunctions-tasks/lib/invoke-activity.ts @@ -12,7 +12,7 @@ export interface InvokeActivityProps extends stepfunctions.BasicTaskProps { /** * The activity to invoke */ - activity: stepfunctions.IActivity; + readonly activity: stepfunctions.IActivity; /** * Maximum time between heart beats @@ -49,7 +49,7 @@ export class InvokeActivity extends stepfunctions.Task { * * @default sum over 5 minutes */ - public metric(metricName: string, props?: cloudwatch.MetricCustomization): cloudwatch.Metric { + public metric(metricName: string, props?: cloudwatch.MetricOptions): cloudwatch.Metric { return new cloudwatch.Metric({ namespace: 'AWS/States', metricName, @@ -64,7 +64,7 @@ export class InvokeActivity extends stepfunctions.Task { * * @default average over 5 minutes */ - public metricRunTime(props?: cloudwatch.MetricCustomization): cloudwatch.Metric { + public metricRunTime(props?: cloudwatch.MetricOptions): cloudwatch.Metric { return this.metric(METRIC_PREFIX_SINGULAR + 'RunTime', { statistic: 'avg', ...props }); } @@ -73,7 +73,7 @@ export class InvokeActivity extends stepfunctions.Task { * * @default average over 5 minutes */ - public metricScheduleTime(props?: cloudwatch.MetricCustomization): cloudwatch.Metric { + public metricScheduleTime(props?: cloudwatch.MetricOptions): cloudwatch.Metric { return this.metric(METRIC_PREFIX_SINGULAR + 'ScheduleTime', { statistic: 'avg', ...props }); } @@ -82,7 +82,7 @@ export class InvokeActivity extends stepfunctions.Task { * * @default average over 5 minutes */ - public metricTime(props?: cloudwatch.MetricCustomization): cloudwatch.Metric { + public metricTime(props?: cloudwatch.MetricOptions): cloudwatch.Metric { return this.metric(METRIC_PREFIX_SINGULAR + 'Time', { statistic: 'avg', ...props }); } @@ -91,7 +91,7 @@ export class InvokeActivity extends stepfunctions.Task { * * @default sum over 5 minutes */ - public metricScheduled(props?: cloudwatch.MetricCustomization): cloudwatch.Metric { + public metricScheduled(props?: cloudwatch.MetricOptions): cloudwatch.Metric { return this.metric(METRIC_PREFIX_PLURAL + 'Scheduled', props); } @@ -100,7 +100,7 @@ export class InvokeActivity extends stepfunctions.Task { * * @default sum over 5 minutes */ - public metricTimedOut(props?: cloudwatch.MetricCustomization): cloudwatch.Metric { + public metricTimedOut(props?: cloudwatch.MetricOptions): cloudwatch.Metric { return this.metric(METRIC_PREFIX_PLURAL + 'TimedOut', props); } @@ -109,7 +109,7 @@ export class InvokeActivity extends stepfunctions.Task { * * @default sum over 5 minutes */ - public metricStarted(props?: cloudwatch.MetricCustomization): cloudwatch.Metric { + public metricStarted(props?: cloudwatch.MetricOptions): cloudwatch.Metric { return this.metric(METRIC_PREFIX_PLURAL + 'Started', props); } @@ -118,7 +118,7 @@ export class InvokeActivity extends stepfunctions.Task { * * @default sum over 5 minutes */ - public metricSucceeded(props?: cloudwatch.MetricCustomization): cloudwatch.Metric { + public metricSucceeded(props?: cloudwatch.MetricOptions): cloudwatch.Metric { return this.metric(METRIC_PREFIX_PLURAL + 'Succeeded', props); } @@ -127,7 +127,7 @@ export class InvokeActivity extends stepfunctions.Task { * * @default sum over 5 minutes */ - public metricFailed(props?: cloudwatch.MetricCustomization): cloudwatch.Metric { + public metricFailed(props?: cloudwatch.MetricOptions): cloudwatch.Metric { return this.metric(METRIC_PREFIX_PLURAL + 'Failed', props); } @@ -136,7 +136,7 @@ export class InvokeActivity extends stepfunctions.Task { * * @default sum over 5 minutes */ - public metricHeartbeatTimedOut(props?: cloudwatch.MetricCustomization): cloudwatch.Metric { + public metricHeartbeatTimedOut(props?: cloudwatch.MetricOptions): cloudwatch.Metric { return this.metric(METRIC_PREFIX_PLURAL + 'HeartbeatTimedOut', props); } } diff --git a/packages/@aws-cdk/aws-stepfunctions-tasks/lib/invoke-function.ts b/packages/@aws-cdk/aws-stepfunctions-tasks/lib/invoke-function.ts index 77e990b81dbe6..cc7f9a71da7ef 100644 --- a/packages/@aws-cdk/aws-stepfunctions-tasks/lib/invoke-function.ts +++ b/packages/@aws-cdk/aws-stepfunctions-tasks/lib/invoke-function.ts @@ -14,7 +14,7 @@ export interface InvokeFunctionProps extends stepfunctions.BasicTaskProps { /** * The function to run */ - function: lambda.IFunction; + readonly function: lambda.IFunction; } /** @@ -44,7 +44,7 @@ export class InvokeFunction extends stepfunctions.Task { * * @default sum over 5 minutes */ - public metric(metricName: string, props?: cloudwatch.MetricCustomization): cloudwatch.Metric { + public metric(metricName: string, props?: cloudwatch.MetricOptions): cloudwatch.Metric { return new cloudwatch.Metric({ namespace: 'AWS/States', metricName, @@ -59,7 +59,7 @@ export class InvokeFunction extends stepfunctions.Task { * * @default average over 5 minutes */ - public metricRunTime(props?: cloudwatch.MetricCustomization): cloudwatch.Metric { + public metricRunTime(props?: cloudwatch.MetricOptions): cloudwatch.Metric { return this.metric(METRIC_PREFIX_SINGULAR + 'RunTime', { statistic: 'avg', ...props }); } @@ -68,7 +68,7 @@ export class InvokeFunction extends stepfunctions.Task { * * @default average over 5 minutes */ - public metricScheduleTime(props?: cloudwatch.MetricCustomization): cloudwatch.Metric { + public metricScheduleTime(props?: cloudwatch.MetricOptions): cloudwatch.Metric { return this.metric(METRIC_PREFIX_SINGULAR + 'ScheduleTime', { statistic: 'avg', ...props }); } @@ -77,7 +77,7 @@ export class InvokeFunction extends stepfunctions.Task { * * @default average over 5 minutes */ - public metricTime(props?: cloudwatch.MetricCustomization): cloudwatch.Metric { + public metricTime(props?: cloudwatch.MetricOptions): cloudwatch.Metric { return this.metric(METRIC_PREFIX_SINGULAR + 'Time', { statistic: 'avg', ...props }); } @@ -86,7 +86,7 @@ export class InvokeFunction extends stepfunctions.Task { * * @default sum over 5 minutes */ - public metricScheduled(props?: cloudwatch.MetricCustomization): cloudwatch.Metric { + public metricScheduled(props?: cloudwatch.MetricOptions): cloudwatch.Metric { return this.metric(METRIC_PREFIX_PLURAL + 'Scheduled', props); } @@ -95,7 +95,7 @@ export class InvokeFunction extends stepfunctions.Task { * * @default sum over 5 minutes */ - public metricTimedOut(props?: cloudwatch.MetricCustomization): cloudwatch.Metric { + public metricTimedOut(props?: cloudwatch.MetricOptions): cloudwatch.Metric { return this.metric(METRIC_PREFIX_PLURAL + 'TimedOut', props); } @@ -104,7 +104,7 @@ export class InvokeFunction extends stepfunctions.Task { * * @default sum over 5 minutes */ - public metricStarted(props?: cloudwatch.MetricCustomization): cloudwatch.Metric { + public metricStarted(props?: cloudwatch.MetricOptions): cloudwatch.Metric { return this.metric(METRIC_PREFIX_PLURAL + 'Started', props); } @@ -113,7 +113,7 @@ export class InvokeFunction extends stepfunctions.Task { * * @default sum over 5 minutes */ - public metricSucceeded(props?: cloudwatch.MetricCustomization): cloudwatch.Metric { + public metricSucceeded(props?: cloudwatch.MetricOptions): cloudwatch.Metric { return this.metric(METRIC_PREFIX_PLURAL + 'Succeeded', props); } @@ -122,7 +122,7 @@ export class InvokeFunction extends stepfunctions.Task { * * @default sum over 5 minutes */ - public metricFailed(props?: cloudwatch.MetricCustomization): cloudwatch.Metric { + public metricFailed(props?: cloudwatch.MetricOptions): cloudwatch.Metric { return this.metric(METRIC_PREFIX_PLURAL + 'Failed', props); } @@ -131,7 +131,7 @@ export class InvokeFunction extends stepfunctions.Task { * * @default sum over 5 minutes */ - public metricHeartbeatTimedOut(props?: cloudwatch.MetricCustomization): cloudwatch.Metric { + public metricHeartbeatTimedOut(props?: cloudwatch.MetricOptions): cloudwatch.Metric { return this.metric(METRIC_PREFIX_PLURAL + 'HeartbeatTimedOut', props); } } \ No newline at end of file diff --git a/packages/@aws-cdk/aws-stepfunctions-tasks/lib/publish-to-topic.ts b/packages/@aws-cdk/aws-stepfunctions-tasks/lib/publish-to-topic.ts index 195e0659d1cd8..2d1c0ef6655fd 100644 --- a/packages/@aws-cdk/aws-stepfunctions-tasks/lib/publish-to-topic.ts +++ b/packages/@aws-cdk/aws-stepfunctions-tasks/lib/publish-to-topic.ts @@ -10,28 +10,28 @@ export interface PublishTaskProps extends stepfunctions.BasicTaskProps { /** * The topic to publish to */ - topic: sns.ITopic; + readonly topic: sns.ITopic; /** * The text message to send to the queue. * * Exactly one of `message`, `messageObject` and `messagePath` is required. */ - message?: string; + readonly message?: string; /** * JSONPath expression of the message to send to the queue * * Exactly one of `message`, `messageObject` and `messagePath` is required. */ - messagePath?: string; + readonly messagePath?: string; /** * Object to be JSON-encoded and used as message * * Exactly one of `message`, `messageObject` and `messagePath` is required. */ - messageObject?: string; + readonly messageObject?: string; /** * If true, send a different message to every subscription type @@ -43,17 +43,17 @@ export interface PublishTaskProps extends stepfunctions.BasicTaskProps { * * @see https://docs.aws.amazon.com/sns/latest/api/API_Publish.html#API_Publish_RequestParameters */ - messagePerSubscriptionType?: boolean; + readonly messagePerSubscriptionType?: boolean; /** * Message subject */ - subject?: string; + readonly subject?: string; /** * JSONPath expression of subject */ - subjectPath?: string; + readonly subjectPath?: string; } /** diff --git a/packages/@aws-cdk/aws-stepfunctions-tasks/lib/ec2-run-task.ts b/packages/@aws-cdk/aws-stepfunctions-tasks/lib/run-ecs-ec2-task.ts similarity index 82% rename from packages/@aws-cdk/aws-stepfunctions-tasks/lib/ec2-run-task.ts rename to packages/@aws-cdk/aws-stepfunctions-tasks/lib/run-ecs-ec2-task.ts index 8cd04b38ce8aa..ab1ec827be63d 100644 --- a/packages/@aws-cdk/aws-stepfunctions-tasks/lib/ec2-run-task.ts +++ b/packages/@aws-cdk/aws-stepfunctions-tasks/lib/run-ecs-ec2-task.ts @@ -1,12 +1,12 @@ import ec2 = require('@aws-cdk/aws-ec2'); import ecs = require('@aws-cdk/aws-ecs'); import cdk = require('@aws-cdk/cdk'); -import { BaseRunTask, BaseRunTaskProps } from './base-run-task'; +import { BaseRunTask, CommonRunTaskProps } from './base-run-task'; /** * Properties to run an ECS task on EC2 in StepFunctionsan ECS */ -export interface Ec2RunTaskProps extends BaseRunTaskProps { +export interface RunEcsEc2TaskProps extends CommonRunTaskProps { /** * In what subnets to place the task's ENIs * @@ -14,7 +14,7 @@ export interface Ec2RunTaskProps extends BaseRunTaskProps { * * @default Private subnets */ - vpcPlacement?: ec2.VpcPlacementStrategy; + readonly subnets?: ec2.SubnetSelection; /** * Existing security group to use for the task's ENIs @@ -23,25 +23,25 @@ export interface Ec2RunTaskProps extends BaseRunTaskProps { * * @default A new security group is created */ - securityGroup?: ec2.ISecurityGroup; + readonly securityGroup?: ec2.ISecurityGroup; /** * Whether to start services on distinct instances * * @default false */ - placeOnDistinctInstances?: boolean; + readonly placeOnDistinctInstances?: boolean; } /** * Run an ECS/EC2 Task in a StepFunctions workflow */ -export class Ec2RunTask extends BaseRunTask { +export class RunEcsEc2Task extends BaseRunTask { private readonly constraints: any[]; private readonly strategies: any[]; private readonly cluster: ecs.ICluster; - constructor(scope: cdk.Construct, id: string, props: Ec2RunTaskProps) { + constructor(scope: cdk.Construct, id: string, props: RunEcsEc2TaskProps) { if (!props.taskDefinition.isEc2Compatible) { throw new Error('Supplied TaskDefinition is not configured for compatibility with EC2'); } @@ -50,18 +50,21 @@ export class Ec2RunTask extends BaseRunTask { throw new Error('A TaskDefinition must have at least one essential container'); } - super(scope, id, props); + super(scope, id, { + ...props, + parameters: { + LaunchType: 'EC2', + PlacementConstraints: new cdk.Token(() => this.constraints.length > 0 ? this.constraints : undefined), + PlacementStrategy: new cdk.Token(() => this.constraints.length > 0 ? this.strategies : undefined), + } + }); this.cluster = props.cluster; this.constraints = []; this.strategies = []; - this._parameters.LaunchType = 'EC2'; - this._parameters.PlacementConstraints = new cdk.Token(() => this.constraints.length > 0 ? this.constraints : undefined); - this._parameters.PlacementStrategy = new cdk.Token(() => this.constraints.length > 0 ? this.strategies : undefined); - if (props.taskDefinition.networkMode === ecs.NetworkMode.AwsVpc) { - this.configureAwsVpcNetworking(props.cluster.vpc, false, props.vpcPlacement, props.securityGroup); + this.configureAwsVpcNetworking(props.cluster.vpc, false, props.subnets, props.securityGroup); } else { // Either None, Bridge or Host networking. Copy SecurityGroup from ASG. validateNoNetworkingProps(props); @@ -143,8 +146,8 @@ export class Ec2RunTask extends BaseRunTask { /** * Validate combinations of networking arguments */ -function validateNoNetworkingProps(props: Ec2RunTaskProps) { - if (props.vpcPlacement !== undefined || props.securityGroup !== undefined) { +function validateNoNetworkingProps(props: RunEcsEc2TaskProps) { + if (props.subnets !== undefined || props.securityGroup !== undefined) { throw new Error('vpcPlacement and securityGroup can only be used in AwsVpc networking mode'); } } \ No newline at end of file diff --git a/packages/@aws-cdk/aws-stepfunctions-tasks/lib/fargate-run-task.ts b/packages/@aws-cdk/aws-stepfunctions-tasks/lib/run-ecs-fargate-task.ts similarity index 64% rename from packages/@aws-cdk/aws-stepfunctions-tasks/lib/fargate-run-task.ts rename to packages/@aws-cdk/aws-stepfunctions-tasks/lib/run-ecs-fargate-task.ts index 45126cd65bdc0..8a23870a2939d 100644 --- a/packages/@aws-cdk/aws-stepfunctions-tasks/lib/fargate-run-task.ts +++ b/packages/@aws-cdk/aws-stepfunctions-tasks/lib/run-ecs-fargate-task.ts @@ -1,32 +1,32 @@ import ec2 = require('@aws-cdk/aws-ec2'); import ecs = require('@aws-cdk/aws-ecs'); import cdk = require('@aws-cdk/cdk'); -import { BaseRunTask, BaseRunTaskProps } from './base-run-task'; +import { BaseRunTask, CommonRunTaskProps } from './base-run-task'; /** * Properties to define an ECS service */ -export interface FargateRunTaskProps extends BaseRunTaskProps { +export interface RunEcsFargateTaskProps extends CommonRunTaskProps { /** * Assign public IP addresses to each task * * @default false */ - assignPublicIp?: boolean; + readonly assignPublicIp?: boolean; /** * In what subnets to place the task's ENIs * * @default Private subnet if assignPublicIp, public subnets otherwise */ - vpcPlacement?: ec2.VpcPlacementStrategy; + readonly subnets?: ec2.SubnetSelection; /** * Existing security group to use for the tasks * * @default A new security group is created */ - securityGroup?: ec2.ISecurityGroup; + readonly securityGroup?: ec2.ISecurityGroup; /** * Fargate platform version to run this service on @@ -36,14 +36,14 @@ export interface FargateRunTaskProps extends BaseRunTaskProps { * * @default Latest */ - platformVersion?: ecs.FargatePlatformVersion; + readonly platformVersion?: ecs.FargatePlatformVersion; } /** * Start a service on an ECS cluster */ -export class FargateRunTask extends BaseRunTask { - constructor(scope: cdk.Construct, id: string, props: FargateRunTaskProps) { +export class RunEcsFargateTask extends BaseRunTask { + constructor(scope: cdk.Construct, id: string, props: RunEcsFargateTaskProps) { if (!props.taskDefinition.isFargateCompatible) { throw new Error('Supplied TaskDefinition is not configured for compatibility with EC2'); } @@ -52,9 +52,13 @@ export class FargateRunTask extends BaseRunTask { throw new Error('A TaskDefinition must have at least one essential container'); } - super(scope, id, props); + super(scope, id, { + ...props, + parameters: { + LaunchType: 'FARGATE', + }, + }); - this._parameters.LaunchType = 'FARGATE'; - this.configureAwsVpcNetworking(props.cluster.vpc, props.assignPublicIp, props.vpcPlacement, props.securityGroup); + this.configureAwsVpcNetworking(props.cluster.vpc, props.assignPublicIp, props.subnets, props.securityGroup); } } diff --git a/packages/@aws-cdk/aws-stepfunctions-tasks/lib/send-to-queue.ts b/packages/@aws-cdk/aws-stepfunctions-tasks/lib/send-to-queue.ts index ccb00e0f464fd..0a8168c1d9286 100644 --- a/packages/@aws-cdk/aws-stepfunctions-tasks/lib/send-to-queue.ts +++ b/packages/@aws-cdk/aws-stepfunctions-tasks/lib/send-to-queue.ts @@ -10,21 +10,21 @@ export interface SendMessageTaskProps extends stepfunctions.BasicTaskProps { /** * The topic to send a message to to */ - queue: sqs.IQueue; + readonly queue: sqs.IQueue; /** * The message body to send to the queue. * * Exactly one of `messageBody` and `messageBodyPath` is required. */ - messageBody?: string; + readonly messageBody?: string; /** * JSONPath for the message body to send to the queue. * * Exactly one of `messageBody` and `messageBodyPath` is required. */ - messageBodyPath?: string; + readonly messageBodyPath?: string; /** * The length of time, in seconds, for which to delay a specific message. @@ -33,28 +33,28 @@ export interface SendMessageTaskProps extends stepfunctions.BasicTaskProps { * * @default Default value of the queue is used */ - delaySeconds?: number; + readonly delaySeconds?: number; /** * JSONPath expression for delaySeconds setting * * @default Default value of the queue is used */ - delaySecondsPath?: string; + readonly delaySecondsPath?: string; /** * The token used for deduplication of sent messages. * * @default Use content-based deduplication */ - messageDeduplicationId?: string; + readonly messageDeduplicationId?: string; /** * JSONPath expression for deduplication ID * * @default Use content-based deduplication */ - messageDeduplicationIdPath?: string; + readonly messageDeduplicationIdPath?: string; /** * The tag that specifies that a message belongs to a specific message group. @@ -64,14 +64,14 @@ export interface SendMessageTaskProps extends stepfunctions.BasicTaskProps { * * @default No group ID */ - messageGroupId?: string; + readonly messageGroupId?: string; /** * JSONPath expression for message group ID * * @default No group ID */ - messageGroupIdPath?: string; + readonly messageGroupIdPath?: string; } /** diff --git a/packages/@aws-cdk/aws-stepfunctions-tasks/package.json b/packages/@aws-cdk/aws-stepfunctions-tasks/package.json index 9c75bed10733c..183f26bf1bd36 100644 --- a/packages/@aws-cdk/aws-stepfunctions-tasks/package.json +++ b/packages/@aws-cdk/aws-stepfunctions-tasks/package.json @@ -23,7 +23,7 @@ "sphinx": {}, "python": { "distName": "aws-cdk.aws-stepfunctions-tasks", - "module": "aws_cdk.aws_stepfunctions.tasks" + "module": "aws_cdk.aws_stepfunctions_tasks" } } }, diff --git a/packages/@aws-cdk/aws-stepfunctions-tasks/test/ecs-tasks.test.ts b/packages/@aws-cdk/aws-stepfunctions-tasks/test/ecs-tasks.test.ts new file mode 100644 index 0000000000000..1ad5ccec506d6 --- /dev/null +++ b/packages/@aws-cdk/aws-stepfunctions-tasks/test/ecs-tasks.test.ts @@ -0,0 +1,209 @@ +import '@aws-cdk/assert/jest'; +import ec2 = require('@aws-cdk/aws-ec2'); +import ecs = require('@aws-cdk/aws-ecs'); +import sfn = require('@aws-cdk/aws-stepfunctions'); +import { Stack } from '@aws-cdk/cdk'; +import tasks = require('../lib'); + +test('Running a Fargate Task', () => { + // GIVEN + const stack = new Stack(); + + const vpc = new ec2.VpcNetwork(stack, 'Vpc'); + const cluster = new ecs.Cluster(stack, 'Cluster', { vpc }); + const taskDefinition = new ecs.TaskDefinition(stack, 'TD', { + memoryMiB: '512', + cpu: '256', + compatibility: ecs.Compatibility.Fargate + }); + taskDefinition.addContainer('henk', { + image: ecs.ContainerImage.fromRegistry('foo/bar'), + memoryLimitMiB: 256, + }); + + // WHEN + const runTask = new tasks.RunEcsFargateTask(stack, 'Run', { + cluster, + taskDefinition, + containerOverrides: [ + { + containerName: 'TheContainer', + environment: [ + {name: 'SOME_KEY', valuePath: '$.SomeKey'} + ] + } + ] + }); + + new sfn.StateMachine(stack, 'SM', { + definition: runTask + }); + + // THEN + expect(stack.node.resolve(runTask.toStateJson())).toEqual({ + End: true, + Parameters: { + Cluster: {"Fn::GetAtt": ["ClusterEB0386A7", "Arn"]}, + LaunchType: "FARGATE", + NetworkConfiguration: { + AwsvpcConfiguration: { + AssignPublicIp: "DISABLED", + SecurityGroups: [{"Fn::GetAtt": ["RunSecurityGroupFBA8EDA8", "GroupId"]}], + Subnets: [ + {Ref: "VpcPrivateSubnet1Subnet536B997A"}, + {Ref: "VpcPrivateSubnet2Subnet3788AAA1"}, + {Ref: "VpcPrivateSubnet3SubnetF258B56E"}, + ] + }, + }, + TaskDefinition: {Ref: "TD49C78F36"}, + Overrides: { + ContainerOverrides: [ + { + Environment: [ + { + "Name": "SOME_KEY", + "Value.$": "$.SomeKey", + }, + ], + Name: "TheContainer", + }, + ], + }, + }, + Resource: "arn:aws:states:::ecs:runTask.sync", + Type: "Task", + }); + + expect(stack).toHaveResourceLike('AWS::IAM::Policy', { + PolicyDocument: { + Statement: [ + { + Action: "ecs:RunTask", + Effect: "Allow", + Resource: {Ref: "TD49C78F36"} + }, + { + Action: ["ecs:StopTask", "ecs:DescribeTasks"], + Effect: "Allow", + Resource: "*" + }, + { + Action: "iam:PassRole", + Effect: "Allow", + Resource: [{"Fn::GetAtt": ["TDTaskRoleC497AFFC", "Arn"]}] + }, + { + Action: ["events:PutTargets", "events:PutRule", "events:DescribeRule"], + Effect: "Allow", + Resource: {"Fn::Join": ["", [ + "arn:", + {Ref: "AWS::Partition"}, + ":events:", + {Ref: "AWS::Region"}, + ":", + {Ref: "AWS::AccountId"}, + ":rule/StepFunctionsGetEventsForECSTaskRule" + ]]} + } + ], + }, + }); +}); + +test('Running an EC2 Task with bridge network', () => { + // GIVEN + const stack = new Stack(); + + const vpc = new ec2.VpcNetwork(stack, 'Vpc'); + const cluster = new ecs.Cluster(stack, 'Cluster', { vpc }); + cluster.addCapacity('Capacity', { + instanceType: new ec2.InstanceType('t3.medium') + }); + + const taskDefinition = new ecs.TaskDefinition(stack, 'TD', { + compatibility: ecs.Compatibility.Ec2 + }); + taskDefinition.addContainer('henk', { + image: ecs.ContainerImage.fromRegistry('foo/bar'), + memoryLimitMiB: 256, + }); + + // WHEN + const runTask = new tasks.RunEcsEc2Task(stack, 'Run', { + cluster, + taskDefinition, + containerOverrides: [ + { + containerName: 'TheContainer', + environment: [ + {name: 'SOME_KEY', valuePath: '$.SomeKey'} + ] + } + ] + }); + + new sfn.StateMachine(stack, 'SM', { + definition: runTask + }); + + // THEN + expect(stack.node.resolve(runTask.toStateJson())).toEqual({ + End: true, + Parameters: { + Cluster: {"Fn::GetAtt": ["ClusterEB0386A7", "Arn"]}, + LaunchType: "EC2", + TaskDefinition: {Ref: "TD49C78F36"}, + Overrides: { + ContainerOverrides: [ + { + Environment: [ + { + "Name": "SOME_KEY", + "Value.$": "$.SomeKey", + }, + ], + Name: "TheContainer", + }, + ], + }, + }, + Resource: "arn:aws:states:::ecs:runTask.sync", + Type: "Task", + }); + + expect(stack).toHaveResourceLike('AWS::IAM::Policy', { + PolicyDocument: { + Statement: [ + { + Action: "ecs:RunTask", + Effect: "Allow", + Resource: {Ref: "TD49C78F36"} + }, + { + Action: ["ecs:StopTask", "ecs:DescribeTasks"], + Effect: "Allow", + Resource: "*" + }, + { + Action: "iam:PassRole", + Effect: "Allow", + Resource: [{"Fn::GetAtt": ["TDTaskRoleC497AFFC", "Arn"]}] + }, + { + Action: ["events:PutTargets", "events:PutRule", "events:DescribeRule"], + Effect: "Allow", + Resource: {"Fn::Join": ["", [ + "arn:", + {Ref: "AWS::Partition"}, + ":events:", + {Ref: "AWS::Region"}, + ":", + {Ref: "AWS::AccountId"}, + ":rule/StepFunctionsGetEventsForECSTaskRule" + ]]} + } + ], + }, + }); +}); \ No newline at end of file diff --git a/packages/@aws-cdk/aws-stepfunctions-tasks/test/integ.ec2-task.expected.json b/packages/@aws-cdk/aws-stepfunctions-tasks/test/integ.ec2-task.expected.json new file mode 100644 index 0000000000000..630b75193813f --- /dev/null +++ b/packages/@aws-cdk/aws-stepfunctions-tasks/test/integ.ec2-task.expected.json @@ -0,0 +1,937 @@ +{ + "Resources": { + "FargateCluster7CCD5F93": { + "Type": "AWS::ECS::Cluster" + }, + "FargateClusterDefaultAutoScalingGroupInstanceSecurityGroup42AF8A40": { + "Type": "AWS::EC2::SecurityGroup", + "Properties": { + "GroupDescription": "aws-ecs-integ2/FargateCluster/DefaultAutoScalingGroup/InstanceSecurityGroup", + "SecurityGroupEgress": [ + { + "CidrIp": "0.0.0.0/0", + "Description": "Allow all outbound traffic by default", + "IpProtocol": "-1" + } + ], + "SecurityGroupIngress": [], + "Tags": [ + { + "Key": "Name", + "Value": "aws-ecs-integ2/FargateCluster/DefaultAutoScalingGroup" + } + ], + "VpcId": "vpc-60900905" + } + }, + "FargateClusterDefaultAutoScalingGroupInstanceRole0C1F7FF7": { + "Type": "AWS::IAM::Role", + "Properties": { + "AssumeRolePolicyDocument": { + "Statement": [ + { + "Action": "sts:AssumeRole", + "Effect": "Allow", + "Principal": { + "Service": "ec2.amazonaws.com" + } + } + ], + "Version": "2012-10-17" + } + } + }, + "FargateClusterDefaultAutoScalingGroupInstanceRoleDefaultPolicy3BD78F3E": { + "Type": "AWS::IAM::Policy", + "Properties": { + "PolicyDocument": { + "Statement": [ + { + "Action": [ + "ecs:CreateCluster", + "ecs:DeregisterContainerInstance", + "ecs:DiscoverPollEndpoint", + "ecs:Poll", + "ecs:RegisterContainerInstance", + "ecs:StartTelemetrySession", + "ecs:Submit*", + "ecr:GetAuthorizationToken", + "logs:CreateLogStream", + "logs:PutLogEvents" + ], + "Effect": "Allow", + "Resource": "*" + } + ], + "Version": "2012-10-17" + }, + "PolicyName": "FargateClusterDefaultAutoScalingGroupInstanceRoleDefaultPolicy3BD78F3E", + "Roles": [ + { + "Ref": "FargateClusterDefaultAutoScalingGroupInstanceRole0C1F7FF7" + } + ] + } + }, + "FargateClusterDefaultAutoScalingGroupInstanceProfile2C0FEF3B": { + "Type": "AWS::IAM::InstanceProfile", + "Properties": { + "Roles": [ + { + "Ref": "FargateClusterDefaultAutoScalingGroupInstanceRole0C1F7FF7" + } + ] + } + }, + "FargateClusterDefaultAutoScalingGroupLaunchConfig57306899": { + "Type": "AWS::AutoScaling::LaunchConfiguration", + "Properties": { + "ImageId": "ami-1234", + "InstanceType": "t2.micro", + "IamInstanceProfile": { + "Ref": "FargateClusterDefaultAutoScalingGroupInstanceProfile2C0FEF3B" + }, + "SecurityGroups": [ + { + "Fn::GetAtt": [ + "FargateClusterDefaultAutoScalingGroupInstanceSecurityGroup42AF8A40", + "GroupId" + ] + } + ], + "UserData": { + "Fn::Base64": { + "Fn::Join": [ + "", + [ + "#!/bin/bash\necho ECS_CLUSTER=", + { + "Ref": "FargateCluster7CCD5F93" + }, + " >> /etc/ecs/ecs.config\nsudo iptables --insert FORWARD 1 --in-interface docker+ --destination 169.254.169.254/32 --jump DROP\nsudo service iptables save\necho ECS_AWSVPC_BLOCK_IMDS=true >> /etc/ecs/ecs.config" + ] + ] + } + } + }, + "DependsOn": [ + "FargateClusterDefaultAutoScalingGroupInstanceRole0C1F7FF7", + "FargateClusterDefaultAutoScalingGroupInstanceRoleDefaultPolicy3BD78F3E" + ] + }, + "FargateClusterDefaultAutoScalingGroupASG36A4948F": { + "Type": "AWS::AutoScaling::AutoScalingGroup", + "Properties": { + "MaxSize": "1", + "MinSize": "1", + "DesiredCapacity": "1", + "LaunchConfigurationName": { + "Ref": "FargateClusterDefaultAutoScalingGroupLaunchConfig57306899" + }, + "Tags": [ + { + "Key": "Name", + "PropagateAtLaunch": true, + "Value": "aws-ecs-integ2/FargateCluster/DefaultAutoScalingGroup" + } + ], + "VPCZoneIdentifier": [ + "subnet-e19455ca", + "subnet-e0c24797", + "subnet-ccd77395" + ] + }, + "UpdatePolicy": { + "AutoScalingReplacingUpdate": { + "WillReplace": true + }, + "AutoScalingScheduledAction": { + "IgnoreUnmodifiedGroupSizeProperties": true + } + } + }, + "FargateClusterDefaultAutoScalingGroupDrainECSHookTopic92C2F1DA": { + "Type": "AWS::SNS::Topic" + }, + "FargateClusterDefaultAutoScalingGroupDrainECSHookTopicFunctionSubscription129830E9": { + "Type": "AWS::SNS::Subscription", + "Properties": { + "Endpoint": { + "Fn::GetAtt": [ + "FargateClusterDefaultAutoScalingGroupDrainECSHookFunctionE3D5BEE8", + "Arn" + ] + }, + "Protocol": "lambda", + "TopicArn": { + "Ref": "FargateClusterDefaultAutoScalingGroupDrainECSHookTopic92C2F1DA" + } + } + }, + "FargateClusterDefaultAutoScalingGroupDrainECSHookFunctionServiceRole7FEDCD32": { + "Type": "AWS::IAM::Role", + "Properties": { + "AssumeRolePolicyDocument": { + "Statement": [ + { + "Action": "sts:AssumeRole", + "Effect": "Allow", + "Principal": { + "Service": "lambda.amazonaws.com" + } + } + ], + "Version": "2012-10-17" + }, + "ManagedPolicyArns": [ + { + "Fn::Join": [ + "", + [ + "arn:", + { + "Ref": "AWS::Partition" + }, + ":iam::aws:policy/service-role/AWSLambdaBasicExecutionRole" + ] + ] + } + ] + } + }, + "FargateClusterDefaultAutoScalingGroupDrainECSHookFunctionServiceRoleDefaultPolicyB91C5343": { + "Type": "AWS::IAM::Policy", + "Properties": { + "PolicyDocument": { + "Statement": [ + { + "Action": [ + "autoscaling:CompleteLifecycleAction", + "ec2:DescribeInstances", + "ec2:DescribeInstanceAttribute", + "ec2:DescribeInstanceStatus", + "ec2:DescribeHosts" + ], + "Effect": "Allow", + "Resource": "*" + }, + { + "Action": [ + "ecs:ListContainerInstances", + "ecs:SubmitContainerStateChange", + "ecs:SubmitTaskStateChange", + "ecs:DescribeContainerInstances", + "ecs:UpdateContainerInstancesState", + "ecs:ListTasks", + "ecs:DescribeTasks" + ], + "Effect": "Allow", + "Resource": "*" + } + ], + "Version": "2012-10-17" + }, + "PolicyName": "FargateClusterDefaultAutoScalingGroupDrainECSHookFunctionServiceRoleDefaultPolicyB91C5343", + "Roles": [ + { + "Ref": "FargateClusterDefaultAutoScalingGroupDrainECSHookFunctionServiceRole7FEDCD32" + } + ] + } + }, + "FargateClusterDefaultAutoScalingGroupDrainECSHookFunctionE3D5BEE8": { + "Type": "AWS::Lambda::Function", + "Properties": { + "Code": { + "ZipFile": "import boto3, json, os, time\n\necs = boto3.client('ecs')\nautoscaling = boto3.client('autoscaling')\n\n\ndef lambda_handler(event, context):\n print(json.dumps(event))\n cluster = os.environ['CLUSTER']\n snsTopicArn = event['Records'][0]['Sns']['TopicArn']\n lifecycle_event = json.loads(event['Records'][0]['Sns']['Message'])\n instance_id = lifecycle_event.get('EC2InstanceId')\n if not instance_id:\n print('Got event without EC2InstanceId: %s', json.dumps(event))\n return\n\n instance_arn = container_instance_arn(cluster, instance_id)\n print('Instance %s has container instance ARN %s' % (lifecycle_event['EC2InstanceId'], instance_arn))\n\n if not instance_arn:\n return\n\n while has_tasks(cluster, instance_arn):\n time.sleep(10)\n\n try:\n print('Terminating instance %s' % instance_id)\n autoscaling.complete_lifecycle_action(\n LifecycleActionResult='CONTINUE',\n **pick(lifecycle_event, 'LifecycleHookName', 'LifecycleActionToken', 'AutoScalingGroupName'))\n except Exception as e:\n # Lifecycle action may have already completed.\n print(str(e))\n\n\ndef container_instance_arn(cluster, instance_id):\n \"\"\"Turn an instance ID into a container instance ARN.\"\"\"\n arns = ecs.list_container_instances(cluster=cluster, filter='ec2InstanceId==' + instance_id)['containerInstanceArns']\n if not arns:\n return None\n return arns[0]\n\n\ndef has_tasks(cluster, instance_arn):\n \"\"\"Return True if the instance is running tasks for the given cluster.\"\"\"\n instances = ecs.describe_container_instances(cluster=cluster, containerInstances=[instance_arn])['containerInstances']\n if not instances:\n return False\n instance = instances[0]\n\n if instance['status'] == 'ACTIVE':\n # Start draining, then try again later\n set_container_instance_to_draining(cluster, instance_arn)\n return True\n\n tasks = instance['runningTasksCount'] + instance['pendingTasksCount']\n print('Instance %s has %s tasks' % (instance_arn, tasks))\n\n return tasks > 0\n\n\ndef set_container_instance_to_draining(cluster, instance_arn):\n ecs.update_container_instances_state(\n cluster=cluster,\n containerInstances=[instance_arn], status='DRAINING')\n\n\ndef pick(dct, *keys):\n \"\"\"Pick a subset of a dict.\"\"\"\n return {k: v for k, v in dct.items() if k in keys}\n" + }, + "Handler": "index.lambda_handler", + "Role": { + "Fn::GetAtt": [ + "FargateClusterDefaultAutoScalingGroupDrainECSHookFunctionServiceRole7FEDCD32", + "Arn" + ] + }, + "Runtime": "python3.6", + "Environment": { + "Variables": { + "CLUSTER": { + "Ref": "FargateCluster7CCD5F93" + } + } + }, + "Timeout": 310 + }, + "DependsOn": [ + "FargateClusterDefaultAutoScalingGroupDrainECSHookFunctionServiceRole7FEDCD32", + "FargateClusterDefaultAutoScalingGroupDrainECSHookFunctionServiceRoleDefaultPolicyB91C5343" + ] + }, + "FargateClusterDefaultAutoScalingGroupDrainECSHookFunctionTopicA1F1F9E9": { + "Type": "AWS::Lambda::Permission", + "Properties": { + "Action": "lambda:InvokeFunction", + "FunctionName": { + "Ref": "FargateClusterDefaultAutoScalingGroupDrainECSHookFunctionE3D5BEE8" + }, + "Principal": "sns.amazonaws.com", + "SourceArn": { + "Ref": "FargateClusterDefaultAutoScalingGroupDrainECSHookTopic92C2F1DA" + } + } + }, + "FargateClusterDefaultAutoScalingGroupLifecycleHookDrainHookRole410D556D": { + "Type": "AWS::IAM::Role", + "Properties": { + "AssumeRolePolicyDocument": { + "Statement": [ + { + "Action": "sts:AssumeRole", + "Effect": "Allow", + "Principal": { + "Service": "autoscaling.amazonaws.com" + } + } + ], + "Version": "2012-10-17" + } + } + }, + "FargateClusterDefaultAutoScalingGroupLifecycleHookDrainHookRoleDefaultPolicy4958D19D": { + "Type": "AWS::IAM::Policy", + "Properties": { + "PolicyDocument": { + "Statement": [ + { + "Action": "sns:Publish", + "Effect": "Allow", + "Resource": { + "Ref": "FargateClusterDefaultAutoScalingGroupDrainECSHookTopic92C2F1DA" + } + } + ], + "Version": "2012-10-17" + }, + "PolicyName": "FargateClusterDefaultAutoScalingGroupLifecycleHookDrainHookRoleDefaultPolicy4958D19D", + "Roles": [ + { + "Ref": "FargateClusterDefaultAutoScalingGroupLifecycleHookDrainHookRole410D556D" + } + ] + } + }, + "FargateClusterDefaultAutoScalingGroupLifecycleHookDrainHook2AE13680": { + "Type": "AWS::AutoScaling::LifecycleHook", + "Properties": { + "AutoScalingGroupName": { + "Ref": "FargateClusterDefaultAutoScalingGroupASG36A4948F" + }, + "LifecycleTransition": "autoscaling:EC2_INSTANCE_TERMINATING", + "DefaultResult": "CONTINUE", + "HeartbeatTimeout": 300, + "NotificationTargetARN": { + "Ref": "FargateClusterDefaultAutoScalingGroupDrainECSHookTopic92C2F1DA" + }, + "RoleARN": { + "Fn::GetAtt": [ + "FargateClusterDefaultAutoScalingGroupLifecycleHookDrainHookRole410D556D", + "Arn" + ] + } + }, + "DependsOn": [ + "FargateClusterDefaultAutoScalingGroupLifecycleHookDrainHookRole410D556D", + "FargateClusterDefaultAutoScalingGroupLifecycleHookDrainHookRoleDefaultPolicy4958D19D" + ] + }, + "TaskDefTaskRole1EDB4A67": { + "Type": "AWS::IAM::Role", + "Properties": { + "AssumeRolePolicyDocument": { + "Statement": [ + { + "Action": "sts:AssumeRole", + "Effect": "Allow", + "Principal": { + "Service": "ecs-tasks.amazonaws.com" + } + } + ], + "Version": "2012-10-17" + } + } + }, + "TaskDef54694570": { + "Type": "AWS::ECS::TaskDefinition", + "Properties": { + "ContainerDefinitions": [ + { + "Essential": true, + "Image": { + "Fn::Join": [ + "", + [ + { + "Fn::Select": [ + 4, + { + "Fn::Split": [ + ":", + { + "Fn::Join": [ + "", + [ + "arn:", + { + "Ref": "AWS::Partition" + }, + ":ecr:", + { + "Ref": "AWS::Region" + }, + ":", + { + "Ref": "AWS::AccountId" + }, + ":repository/", + { + "Fn::GetAtt": [ + "EventImageAdoptRepositoryDFAAC242", + "RepositoryName" + ] + } + ] + ] + } + ] + } + ] + }, + ".dkr.ecr.", + { + "Fn::Select": [ + 3, + { + "Fn::Split": [ + ":", + { + "Fn::Join": [ + "", + [ + "arn:", + { + "Ref": "AWS::Partition" + }, + ":ecr:", + { + "Ref": "AWS::Region" + }, + ":", + { + "Ref": "AWS::AccountId" + }, + ":repository/", + { + "Fn::GetAtt": [ + "EventImageAdoptRepositoryDFAAC242", + "RepositoryName" + ] + } + ] + ] + } + ] + } + ] + }, + ".amazonaws.com/", + { + "Fn::GetAtt": [ + "EventImageAdoptRepositoryDFAAC242", + "RepositoryName" + ] + }, + ":", + { + "Fn::Select": [ + 1, + { + "Fn::Split": [ + ":", + { + "Ref": "EventImageImageNameE972A8B1" + } + ] + } + ] + } + ] + ] + }, + "Links": [], + "LinuxParameters": { + "Capabilities": { + "Add": [], + "Drop": [] + }, + "Devices": [], + "Tmpfs": [] + }, + "LogConfiguration": { + "LogDriver": "awslogs", + "Options": { + "awslogs-group": { + "Ref": "TaskLoggingLogGroupC7E938D4" + }, + "awslogs-stream-prefix": "EventDemo", + "awslogs-region": { + "Ref": "AWS::Region" + } + } + }, + "Memory": 256, + "MountPoints": [], + "Name": "TheContainer", + "PortMappings": [], + "Ulimits": [], + "VolumesFrom": [] + } + ], + "ExecutionRoleArn": { + "Fn::GetAtt": [ + "TaskDefExecutionRoleB4775C97", + "Arn" + ] + }, + "Family": "awsecsinteg2TaskDef1F38909D", + "NetworkMode": "bridge", + "PlacementConstraints": [], + "RequiresCompatibilities": [ + "EC2" + ], + "TaskRoleArn": { + "Fn::GetAtt": [ + "TaskDefTaskRole1EDB4A67", + "Arn" + ] + }, + "Volumes": [] + } + }, + "TaskDefExecutionRoleB4775C97": { + "Type": "AWS::IAM::Role", + "Properties": { + "AssumeRolePolicyDocument": { + "Statement": [ + { + "Action": "sts:AssumeRole", + "Effect": "Allow", + "Principal": { + "Service": "ecs-tasks.amazonaws.com" + } + } + ], + "Version": "2012-10-17" + } + } + }, + "TaskDefExecutionRoleDefaultPolicy0DBB737A": { + "Type": "AWS::IAM::Policy", + "Properties": { + "PolicyDocument": { + "Statement": [ + { + "Action": [ + "ecr:BatchCheckLayerAvailability", + "ecr:GetDownloadUrlForLayer", + "ecr:BatchGetImage" + ], + "Effect": "Allow", + "Resource": { + "Fn::Join": [ + "", + [ + "arn:", + { + "Ref": "AWS::Partition" + }, + ":ecr:", + { + "Ref": "AWS::Region" + }, + ":", + { + "Ref": "AWS::AccountId" + }, + ":repository/", + { + "Fn::GetAtt": [ + "EventImageAdoptRepositoryDFAAC242", + "RepositoryName" + ] + } + ] + ] + } + }, + { + "Action": [ + "ecr:GetAuthorizationToken", + "logs:CreateLogStream", + "logs:PutLogEvents" + ], + "Effect": "Allow", + "Resource": "*" + }, + { + "Action": [ + "logs:CreateLogStream", + "logs:PutLogEvents" + ], + "Effect": "Allow", + "Resource": { + "Fn::GetAtt": [ + "TaskLoggingLogGroupC7E938D4", + "Arn" + ] + } + } + ], + "Version": "2012-10-17" + }, + "PolicyName": "TaskDefExecutionRoleDefaultPolicy0DBB737A", + "Roles": [ + { + "Ref": "TaskDefExecutionRoleB4775C97" + } + ] + } + }, + "EventImageAdoptRepositoryDFAAC242": { + "Type": "Custom::ECRAdoptedRepository", + "Properties": { + "ServiceToken": { + "Fn::GetAtt": [ + "AdoptEcrRepositorydbc60defc59544bcaa5c28c95d68f62c52BE89E9", + "Arn" + ] + }, + "RepositoryName": { + "Fn::Select": [ + 0, + { + "Fn::Split": [ + ":", + { + "Ref": "EventImageImageNameE972A8B1" + } + ] + } + ] + } + } + }, + "AdoptEcrRepositorydbc60defc59544bcaa5c28c95d68f62cServiceRoleD788AA17": { + "Type": "AWS::IAM::Role", + "Properties": { + "AssumeRolePolicyDocument": { + "Statement": [ + { + "Action": "sts:AssumeRole", + "Effect": "Allow", + "Principal": { + "Service": "lambda.amazonaws.com" + } + } + ], + "Version": "2012-10-17" + }, + "ManagedPolicyArns": [ + { + "Fn::Join": [ + "", + [ + "arn:", + { + "Ref": "AWS::Partition" + }, + ":iam::aws:policy/service-role/AWSLambdaBasicExecutionRole" + ] + ] + } + ] + } + }, + "AdoptEcrRepositorydbc60defc59544bcaa5c28c95d68f62cServiceRoleDefaultPolicy6BC8737C": { + "Type": "AWS::IAM::Policy", + "Properties": { + "PolicyDocument": { + "Statement": [ + { + "Action": [ + "ecr:GetRepositoryPolicy", + "ecr:SetRepositoryPolicy", + "ecr:DeleteRepository", + "ecr:ListImages", + "ecr:BatchDeleteImage" + ], + "Effect": "Allow", + "Resource": { + "Fn::Join": [ + "", + [ + "arn:", + { + "Ref": "AWS::Partition" + }, + ":ecr:", + { + "Ref": "AWS::Region" + }, + ":", + { + "Ref": "AWS::AccountId" + }, + ":repository/", + { + "Fn::Select": [ + 0, + { + "Fn::Split": [ + ":", + { + "Ref": "EventImageImageNameE972A8B1" + } + ] + } + ] + } + ] + ] + } + } + ], + "Version": "2012-10-17" + }, + "PolicyName": "AdoptEcrRepositorydbc60defc59544bcaa5c28c95d68f62cServiceRoleDefaultPolicy6BC8737C", + "Roles": [ + { + "Ref": "AdoptEcrRepositorydbc60defc59544bcaa5c28c95d68f62cServiceRoleD788AA17" + } + ] + } + }, + "AdoptEcrRepositorydbc60defc59544bcaa5c28c95d68f62c52BE89E9": { + "Type": "AWS::Lambda::Function", + "Properties": { + "Code": { + "S3Bucket": { + "Ref": "AdoptEcrRepositorydbc60defc59544bcaa5c28c95d68f62cCodeS3Bucket92AB06B6" + }, + "S3Key": { + "Fn::Join": [ + "", + [ + { + "Fn::Select": [ + 0, + { + "Fn::Split": [ + "||", + { + "Ref": "AdoptEcrRepositorydbc60defc59544bcaa5c28c95d68f62cCodeS3VersionKey393B7276" + } + ] + } + ] + }, + { + "Fn::Select": [ + 1, + { + "Fn::Split": [ + "||", + { + "Ref": "AdoptEcrRepositorydbc60defc59544bcaa5c28c95d68f62cCodeS3VersionKey393B7276" + } + ] + } + ] + } + ] + ] + } + }, + "Handler": "handler.handler", + "Role": { + "Fn::GetAtt": [ + "AdoptEcrRepositorydbc60defc59544bcaa5c28c95d68f62cServiceRoleD788AA17", + "Arn" + ] + }, + "Runtime": "nodejs8.10", + "Timeout": 300 + }, + "DependsOn": [ + "AdoptEcrRepositorydbc60defc59544bcaa5c28c95d68f62cServiceRoleD788AA17", + "AdoptEcrRepositorydbc60defc59544bcaa5c28c95d68f62cServiceRoleDefaultPolicy6BC8737C" + ] + }, + "TaskLoggingLogGroupC7E938D4": { + "Type": "AWS::Logs::LogGroup", + "Properties": { + "RetentionInDays": 365 + }, + "DeletionPolicy": "Retain" + }, + "StateMachineRoleB840431D": { + "Type": "AWS::IAM::Role", + "Properties": { + "AssumeRolePolicyDocument": { + "Statement": [ + { + "Action": "sts:AssumeRole", + "Effect": "Allow", + "Principal": { + "Service": { + "Fn::Join": [ + "", + [ + "states.", + { + "Ref": "AWS::Region" + }, + ".amazonaws.com" + ] + ] + } + } + } + ], + "Version": "2012-10-17" + } + } + }, + "StateMachineRoleDefaultPolicyDF1E6607": { + "Type": "AWS::IAM::Policy", + "Properties": { + "PolicyDocument": { + "Statement": [ + { + "Action": "ecs:RunTask", + "Effect": "Allow", + "Resource": { + "Ref": "TaskDef54694570" + } + }, + { + "Action": [ + "ecs:StopTask", + "ecs:DescribeTasks" + ], + "Effect": "Allow", + "Resource": "*" + }, + { + "Action": "iam:PassRole", + "Effect": "Allow", + "Resource": [ + { + "Fn::GetAtt": [ + "TaskDefTaskRole1EDB4A67", + "Arn" + ] + }, + { + "Fn::GetAtt": [ + "TaskDefExecutionRoleB4775C97", + "Arn" + ] + } + ] + }, + { + "Action": [ + "events:PutTargets", + "events:PutRule", + "events:DescribeRule" + ], + "Effect": "Allow", + "Resource": { + "Fn::Join": [ + "", + [ + "arn:", + { + "Ref": "AWS::Partition" + }, + ":events:", + { + "Ref": "AWS::Region" + }, + ":", + { + "Ref": "AWS::AccountId" + }, + ":rule/StepFunctionsGetEventsForECSTaskRule" + ] + ] + } + } + ], + "Version": "2012-10-17" + }, + "PolicyName": "StateMachineRoleDefaultPolicyDF1E6607", + "Roles": [ + { + "Ref": "StateMachineRoleB840431D" + } + ] + } + }, + "StateMachine2E01A3A5": { + "Type": "AWS::StepFunctions::StateMachine", + "Properties": { + "DefinitionString": { + "Fn::Join": [ + "", + [ + "{\"StartAt\":\"Start\",\"States\":{\"Start\":{\"Type\":\"Pass\",\"Result\":{\"SomeKey\":\"SomeValue\"},\"Next\":\"RunEc2\"},\"RunEc2\":{\"End\":true,\"Parameters\":{\"Cluster\":\"", + { + "Fn::GetAtt": [ + "FargateCluster7CCD5F93", + "Arn" + ] + }, + "\",\"TaskDefinition\":\"", + { + "Ref": "TaskDef54694570" + }, + "\",\"Overrides\":{\"ContainerOverrides\":[{\"Name\":\"TheContainer\",\"Environment\":[{\"Name\":\"SOME_KEY\",\"Value.$\":\"$.SomeKey\"}]}]},\"LaunchType\":\"EC2\"},\"Type\":\"Task\",\"Resource\":\"arn:aws:states:::ecs:runTask.sync\"}}}" + ] + ] + }, + "RoleArn": { + "Fn::GetAtt": [ + "StateMachineRoleB840431D", + "Arn" + ] + } + } + } + }, + "Parameters": { + "EventImageImageNameE972A8B1": { + "Type": "String", + "Description": "ECR repository name and tag asset \"aws-ecs-integ2/EventImage\"" + }, + "AdoptEcrRepositorydbc60defc59544bcaa5c28c95d68f62cCodeS3Bucket92AB06B6": { + "Type": "String", + "Description": "S3 bucket for asset \"aws-ecs-integ2/AdoptEcrRepositorydbc60defc59544bcaa5c28c95d68f62c/Code\"" + }, + "AdoptEcrRepositorydbc60defc59544bcaa5c28c95d68f62cCodeS3VersionKey393B7276": { + "Type": "String", + "Description": "S3 key for asset version \"aws-ecs-integ2/AdoptEcrRepositorydbc60defc59544bcaa5c28c95d68f62c/Code\"" + } + } +} \ No newline at end of file diff --git a/packages/@aws-cdk/aws-stepfunctions-tasks/test/integ.ec2-task.ts b/packages/@aws-cdk/aws-stepfunctions-tasks/test/integ.ec2-task.ts new file mode 100644 index 0000000000000..2502c7ae307df --- /dev/null +++ b/packages/@aws-cdk/aws-stepfunctions-tasks/test/integ.ec2-task.ts @@ -0,0 +1,51 @@ +import ec2 = require('@aws-cdk/aws-ec2'); +import ecs = require('@aws-cdk/aws-ecs'); +import sfn = require('@aws-cdk/aws-stepfunctions'); +import cdk = require('@aws-cdk/cdk'); +import path = require('path'); +import tasks = require('../lib'); + +const app = new cdk.App(); +const stack = new cdk.Stack(app, 'aws-ecs-integ2'); + +const vpc = ec2.VpcNetwork.importFromContext(stack, 'Vpc', { + isDefault: true +}); + +const cluster = new ecs.Cluster(stack, 'FargateCluster', { vpc }); +cluster.addCapacity('Capacity', { + instanceType: new ec2.InstanceType('t2.micro'), + vpcSubnets: { subnetType: ec2.SubnetType.Public }, +}); + +// Build task definition +const taskDefinition = new ecs.Ec2TaskDefinition(stack, 'TaskDef'); +taskDefinition.addContainer('TheContainer', { + image: ecs.ContainerImage.fromAsset(stack, 'EventImage', { directory: path.resolve(__dirname, '..', 'eventhandler-image') }), + memoryLimitMiB: 256, + logging: new ecs.AwsLogDriver(stack, 'TaskLogging', { streamPrefix: 'EventDemo' }) +}); + +// Build state machine +const definition = new sfn.Pass(stack, 'Start', { + result: { SomeKey: 'SomeValue' } +}).next(new tasks.RunEcsEc2Task(stack, 'RunEc2', { + cluster, taskDefinition, + containerOverrides: [ + { + containerName: 'TheContainer', + environment: [ + { + name: 'SOME_KEY', + valuePath: '$.SomeKey' + } + ] + } + ] +})); + +new sfn.StateMachine(stack, 'StateMachine', { + definition, +}); + +app.run(); \ No newline at end of file diff --git a/packages/@aws-cdk/aws-stepfunctions-tasks/test/integ.fargate-task.expected.json b/packages/@aws-cdk/aws-stepfunctions-tasks/test/integ.fargate-task.expected.json new file mode 100644 index 0000000000000..61ca415d2f552 --- /dev/null +++ b/packages/@aws-cdk/aws-stepfunctions-tasks/test/integ.fargate-task.expected.json @@ -0,0 +1,619 @@ +{ + "Resources": { + "FargateCluster7CCD5F93": { + "Type": "AWS::ECS::Cluster" + }, + "TaskDefTaskRole1EDB4A67": { + "Type": "AWS::IAM::Role", + "Properties": { + "AssumeRolePolicyDocument": { + "Statement": [ + { + "Action": "sts:AssumeRole", + "Effect": "Allow", + "Principal": { + "Service": "ecs-tasks.amazonaws.com" + } + } + ], + "Version": "2012-10-17" + } + } + }, + "TaskDef54694570": { + "Type": "AWS::ECS::TaskDefinition", + "Properties": { + "ContainerDefinitions": [ + { + "Essential": true, + "Image": { + "Fn::Join": [ + "", + [ + { + "Fn::Select": [ + 4, + { + "Fn::Split": [ + ":", + { + "Fn::Join": [ + "", + [ + "arn:", + { + "Ref": "AWS::Partition" + }, + ":ecr:", + { + "Ref": "AWS::Region" + }, + ":", + { + "Ref": "AWS::AccountId" + }, + ":repository/", + { + "Fn::GetAtt": [ + "EventImageAdoptRepositoryDFAAC242", + "RepositoryName" + ] + } + ] + ] + } + ] + } + ] + }, + ".dkr.ecr.", + { + "Fn::Select": [ + 3, + { + "Fn::Split": [ + ":", + { + "Fn::Join": [ + "", + [ + "arn:", + { + "Ref": "AWS::Partition" + }, + ":ecr:", + { + "Ref": "AWS::Region" + }, + ":", + { + "Ref": "AWS::AccountId" + }, + ":repository/", + { + "Fn::GetAtt": [ + "EventImageAdoptRepositoryDFAAC242", + "RepositoryName" + ] + } + ] + ] + } + ] + } + ] + }, + ".amazonaws.com/", + { + "Fn::GetAtt": [ + "EventImageAdoptRepositoryDFAAC242", + "RepositoryName" + ] + }, + ":", + { + "Fn::Select": [ + 1, + { + "Fn::Split": [ + ":", + { + "Ref": "EventImageImageNameE972A8B1" + } + ] + } + ] + } + ] + ] + }, + "Links": [], + "LinuxParameters": { + "Capabilities": { + "Add": [], + "Drop": [] + }, + "Devices": [], + "Tmpfs": [] + }, + "LogConfiguration": { + "LogDriver": "awslogs", + "Options": { + "awslogs-group": { + "Ref": "TaskLoggingLogGroupC7E938D4" + }, + "awslogs-stream-prefix": "EventDemo", + "awslogs-region": { + "Ref": "AWS::Region" + } + } + }, + "Memory": 256, + "MountPoints": [], + "Name": "TheContainer", + "PortMappings": [], + "Ulimits": [], + "VolumesFrom": [] + } + ], + "Cpu": "256", + "ExecutionRoleArn": { + "Fn::GetAtt": [ + "TaskDefExecutionRoleB4775C97", + "Arn" + ] + }, + "Family": "awsecsinteg2TaskDef1F38909D", + "Memory": "512", + "NetworkMode": "awsvpc", + "RequiresCompatibilities": [ + "FARGATE" + ], + "TaskRoleArn": { + "Fn::GetAtt": [ + "TaskDefTaskRole1EDB4A67", + "Arn" + ] + }, + "Volumes": [] + } + }, + "TaskDefExecutionRoleB4775C97": { + "Type": "AWS::IAM::Role", + "Properties": { + "AssumeRolePolicyDocument": { + "Statement": [ + { + "Action": "sts:AssumeRole", + "Effect": "Allow", + "Principal": { + "Service": "ecs-tasks.amazonaws.com" + } + } + ], + "Version": "2012-10-17" + } + } + }, + "TaskDefExecutionRoleDefaultPolicy0DBB737A": { + "Type": "AWS::IAM::Policy", + "Properties": { + "PolicyDocument": { + "Statement": [ + { + "Action": [ + "ecr:BatchCheckLayerAvailability", + "ecr:GetDownloadUrlForLayer", + "ecr:BatchGetImage" + ], + "Effect": "Allow", + "Resource": { + "Fn::Join": [ + "", + [ + "arn:", + { + "Ref": "AWS::Partition" + }, + ":ecr:", + { + "Ref": "AWS::Region" + }, + ":", + { + "Ref": "AWS::AccountId" + }, + ":repository/", + { + "Fn::GetAtt": [ + "EventImageAdoptRepositoryDFAAC242", + "RepositoryName" + ] + } + ] + ] + } + }, + { + "Action": [ + "ecr:GetAuthorizationToken", + "logs:CreateLogStream", + "logs:PutLogEvents" + ], + "Effect": "Allow", + "Resource": "*" + }, + { + "Action": [ + "logs:CreateLogStream", + "logs:PutLogEvents" + ], + "Effect": "Allow", + "Resource": { + "Fn::GetAtt": [ + "TaskLoggingLogGroupC7E938D4", + "Arn" + ] + } + } + ], + "Version": "2012-10-17" + }, + "PolicyName": "TaskDefExecutionRoleDefaultPolicy0DBB737A", + "Roles": [ + { + "Ref": "TaskDefExecutionRoleB4775C97" + } + ] + } + }, + "EventImageAdoptRepositoryDFAAC242": { + "Type": "Custom::ECRAdoptedRepository", + "Properties": { + "ServiceToken": { + "Fn::GetAtt": [ + "AdoptEcrRepositorydbc60defc59544bcaa5c28c95d68f62c52BE89E9", + "Arn" + ] + }, + "RepositoryName": { + "Fn::Select": [ + 0, + { + "Fn::Split": [ + ":", + { + "Ref": "EventImageImageNameE972A8B1" + } + ] + } + ] + } + } + }, + "AdoptEcrRepositorydbc60defc59544bcaa5c28c95d68f62cServiceRoleD788AA17": { + "Type": "AWS::IAM::Role", + "Properties": { + "AssumeRolePolicyDocument": { + "Statement": [ + { + "Action": "sts:AssumeRole", + "Effect": "Allow", + "Principal": { + "Service": "lambda.amazonaws.com" + } + } + ], + "Version": "2012-10-17" + }, + "ManagedPolicyArns": [ + { + "Fn::Join": [ + "", + [ + "arn:", + { + "Ref": "AWS::Partition" + }, + ":iam::aws:policy/service-role/AWSLambdaBasicExecutionRole" + ] + ] + } + ] + } + }, + "AdoptEcrRepositorydbc60defc59544bcaa5c28c95d68f62cServiceRoleDefaultPolicy6BC8737C": { + "Type": "AWS::IAM::Policy", + "Properties": { + "PolicyDocument": { + "Statement": [ + { + "Action": [ + "ecr:GetRepositoryPolicy", + "ecr:SetRepositoryPolicy", + "ecr:DeleteRepository", + "ecr:ListImages", + "ecr:BatchDeleteImage" + ], + "Effect": "Allow", + "Resource": { + "Fn::Join": [ + "", + [ + "arn:", + { + "Ref": "AWS::Partition" + }, + ":ecr:", + { + "Ref": "AWS::Region" + }, + ":", + { + "Ref": "AWS::AccountId" + }, + ":repository/", + { + "Fn::Select": [ + 0, + { + "Fn::Split": [ + ":", + { + "Ref": "EventImageImageNameE972A8B1" + } + ] + } + ] + } + ] + ] + } + } + ], + "Version": "2012-10-17" + }, + "PolicyName": "AdoptEcrRepositorydbc60defc59544bcaa5c28c95d68f62cServiceRoleDefaultPolicy6BC8737C", + "Roles": [ + { + "Ref": "AdoptEcrRepositorydbc60defc59544bcaa5c28c95d68f62cServiceRoleD788AA17" + } + ] + } + }, + "AdoptEcrRepositorydbc60defc59544bcaa5c28c95d68f62c52BE89E9": { + "Type": "AWS::Lambda::Function", + "Properties": { + "Code": { + "S3Bucket": { + "Ref": "AdoptEcrRepositorydbc60defc59544bcaa5c28c95d68f62cCodeS3Bucket92AB06B6" + }, + "S3Key": { + "Fn::Join": [ + "", + [ + { + "Fn::Select": [ + 0, + { + "Fn::Split": [ + "||", + { + "Ref": "AdoptEcrRepositorydbc60defc59544bcaa5c28c95d68f62cCodeS3VersionKey393B7276" + } + ] + } + ] + }, + { + "Fn::Select": [ + 1, + { + "Fn::Split": [ + "||", + { + "Ref": "AdoptEcrRepositorydbc60defc59544bcaa5c28c95d68f62cCodeS3VersionKey393B7276" + } + ] + } + ] + } + ] + ] + } + }, + "Handler": "handler.handler", + "Role": { + "Fn::GetAtt": [ + "AdoptEcrRepositorydbc60defc59544bcaa5c28c95d68f62cServiceRoleD788AA17", + "Arn" + ] + }, + "Runtime": "nodejs8.10", + "Timeout": 300 + }, + "DependsOn": [ + "AdoptEcrRepositorydbc60defc59544bcaa5c28c95d68f62cServiceRoleD788AA17", + "AdoptEcrRepositorydbc60defc59544bcaa5c28c95d68f62cServiceRoleDefaultPolicy6BC8737C" + ] + }, + "TaskLoggingLogGroupC7E938D4": { + "Type": "AWS::Logs::LogGroup", + "Properties": { + "RetentionInDays": 365 + }, + "DeletionPolicy": "Retain" + }, + "RunFargateSecurityGroup709740F2": { + "Type": "AWS::EC2::SecurityGroup", + "Properties": { + "GroupDescription": "aws-ecs-integ2/RunFargate/SecurityGroup", + "SecurityGroupEgress": [ + { + "CidrIp": "0.0.0.0/0", + "Description": "Allow all outbound traffic by default", + "IpProtocol": "-1" + } + ], + "SecurityGroupIngress": [], + "VpcId": "vpc-60900905" + } + }, + "StateMachineRoleB840431D": { + "Type": "AWS::IAM::Role", + "Properties": { + "AssumeRolePolicyDocument": { + "Statement": [ + { + "Action": "sts:AssumeRole", + "Effect": "Allow", + "Principal": { + "Service": { + "Fn::Join": [ + "", + [ + "states.", + { + "Ref": "AWS::Region" + }, + ".amazonaws.com" + ] + ] + } + } + } + ], + "Version": "2012-10-17" + } + } + }, + "StateMachineRoleDefaultPolicyDF1E6607": { + "Type": "AWS::IAM::Policy", + "Properties": { + "PolicyDocument": { + "Statement": [ + { + "Action": "ecs:RunTask", + "Effect": "Allow", + "Resource": { + "Ref": "TaskDef54694570" + } + }, + { + "Action": [ + "ecs:StopTask", + "ecs:DescribeTasks" + ], + "Effect": "Allow", + "Resource": "*" + }, + { + "Action": "iam:PassRole", + "Effect": "Allow", + "Resource": [ + { + "Fn::GetAtt": [ + "TaskDefTaskRole1EDB4A67", + "Arn" + ] + }, + { + "Fn::GetAtt": [ + "TaskDefExecutionRoleB4775C97", + "Arn" + ] + } + ] + }, + { + "Action": [ + "events:PutTargets", + "events:PutRule", + "events:DescribeRule" + ], + "Effect": "Allow", + "Resource": { + "Fn::Join": [ + "", + [ + "arn:", + { + "Ref": "AWS::Partition" + }, + ":events:", + { + "Ref": "AWS::Region" + }, + ":", + { + "Ref": "AWS::AccountId" + }, + ":rule/StepFunctionsGetEventsForECSTaskRule" + ] + ] + } + } + ], + "Version": "2012-10-17" + }, + "PolicyName": "StateMachineRoleDefaultPolicyDF1E6607", + "Roles": [ + { + "Ref": "StateMachineRoleB840431D" + } + ] + } + }, + "StateMachine2E01A3A5": { + "Type": "AWS::StepFunctions::StateMachine", + "Properties": { + "DefinitionString": { + "Fn::Join": [ + "", + [ + "{\"StartAt\":\"Start\",\"States\":{\"Start\":{\"Type\":\"Pass\",\"Result\":{\"SomeKey\":\"SomeValue\"},\"Next\":\"RunFargate\"},\"RunFargate\":{\"End\":true,\"Parameters\":{\"Cluster\":\"", + { + "Fn::GetAtt": [ + "FargateCluster7CCD5F93", + "Arn" + ] + }, + "\",\"TaskDefinition\":\"", + { + "Ref": "TaskDef54694570" + }, + "\",\"NetworkConfiguration\":{\"AwsvpcConfiguration\":{\"AssignPublicIp\":\"ENABLED\",\"Subnets\":[\"subnet-e19455ca\",\"subnet-e0c24797\",\"subnet-ccd77395\"],\"SecurityGroups\":[\"", + { + "Fn::GetAtt": [ + "RunFargateSecurityGroup709740F2", + "GroupId" + ] + }, + "\"]}},\"Overrides\":{\"ContainerOverrides\":[{\"Name\":\"TheContainer\",\"Environment\":[{\"Name\":\"SOME_KEY\",\"Value.$\":\"$.SomeKey\"}]}]},\"LaunchType\":\"FARGATE\"},\"Type\":\"Task\",\"Resource\":\"arn:aws:states:::ecs:runTask.sync\"}}}" + ] + ] + }, + "RoleArn": { + "Fn::GetAtt": [ + "StateMachineRoleB840431D", + "Arn" + ] + } + } + } + }, + "Parameters": { + "EventImageImageNameE972A8B1": { + "Type": "String", + "Description": "ECR repository name and tag asset \"aws-ecs-integ2/EventImage\"" + }, + "AdoptEcrRepositorydbc60defc59544bcaa5c28c95d68f62cCodeS3Bucket92AB06B6": { + "Type": "String", + "Description": "S3 bucket for asset \"aws-ecs-integ2/AdoptEcrRepositorydbc60defc59544bcaa5c28c95d68f62c/Code\"" + }, + "AdoptEcrRepositorydbc60defc59544bcaa5c28c95d68f62cCodeS3VersionKey393B7276": { + "Type": "String", + "Description": "S3 key for asset version \"aws-ecs-integ2/AdoptEcrRepositorydbc60defc59544bcaa5c28c95d68f62c/Code\"" + } + } +} \ No newline at end of file diff --git a/packages/@aws-cdk/aws-stepfunctions-tasks/test/integ.fargate-task.ts b/packages/@aws-cdk/aws-stepfunctions-tasks/test/integ.fargate-task.ts new file mode 100644 index 0000000000000..747d977f7aee1 --- /dev/null +++ b/packages/@aws-cdk/aws-stepfunctions-tasks/test/integ.fargate-task.ts @@ -0,0 +1,51 @@ +import ec2 = require('@aws-cdk/aws-ec2'); +import ecs = require('@aws-cdk/aws-ecs'); +import sfn = require('@aws-cdk/aws-stepfunctions'); +import cdk = require('@aws-cdk/cdk'); +import path = require('path'); +import tasks = require('../lib'); + +const app = new cdk.App(); +const stack = new cdk.Stack(app, 'aws-ecs-integ2'); + +const vpc = ec2.VpcNetwork.importFromContext(stack, 'Vpc', { + isDefault: true +}); + +const cluster = new ecs.Cluster(stack, 'FargateCluster', { vpc }); + +// Build task definition +const taskDefinition = new ecs.FargateTaskDefinition(stack, 'TaskDef', { + memoryMiB: '512', + cpu: '256' +}); +taskDefinition.addContainer('TheContainer', { + image: ecs.ContainerImage.fromAsset(stack, 'EventImage', { directory: path.resolve(__dirname, '..', 'eventhandler-image') }), + memoryLimitMiB: 256, + logging: new ecs.AwsLogDriver(stack, 'TaskLogging', { streamPrefix: 'EventDemo' }) +}); + +// Build state machine +const definition = new sfn.Pass(stack, 'Start', { + result: { SomeKey: 'SomeValue' } +}).next(new tasks.RunEcsFargateTask(stack, 'RunFargate', { + cluster, taskDefinition, + assignPublicIp: true, + containerOverrides: [ + { + containerName: 'TheContainer', + environment: [ + { + name: 'SOME_KEY', + valuePath: '$.SomeKey' + } + ] + } + ] +})); + +new sfn.StateMachine(stack, 'StateMachine', { + definition, +}); + +app.run(); \ No newline at end of file diff --git a/packages/@aws-cdk/aws-stepfunctions/lib/states/task.ts b/packages/@aws-cdk/aws-stepfunctions/lib/states/task.ts index 5a6fa4f2868ee..a6c00e2dc8369 100644 --- a/packages/@aws-cdk/aws-stepfunctions/lib/states/task.ts +++ b/packages/@aws-cdk/aws-stepfunctions/lib/states/task.ts @@ -169,6 +169,7 @@ export class Task extends State implements INextable { Type: StateType.Task, Comment: this.comment, Resource: this.resourceArn, + Parameters: this.parameters, ResultPath: renderJsonPath(this.resultPath), TimeoutSeconds: this.timeoutSeconds, HeartbeatSeconds: this.heartbeatSeconds, diff --git a/tools/cdk-build-tools/bin/cdk-test.ts b/tools/cdk-build-tools/bin/cdk-test.ts index d9068029aa9f7..6bb08d668ff53 100644 --- a/tools/cdk-build-tools/bin/cdk-test.ts +++ b/tools/cdk-build-tools/bin/cdk-test.ts @@ -49,7 +49,7 @@ async function main() { if (testFiles.length > 0) { throw new Error(`Jest is enabled, but ${testFiles.length} nodeunit tests were found!`); } - await shell([args.jest, '--testEnvironment=node', '--coverage'], { timers }); + await shell([args.jest, '--testEnvironment=node', '--coverage', '--coverageReporters', 'html', 'lcov', 'text-summary'], { timers }); } else if (testFiles.length > 0) { const testCommand: string[] = [];