Skip to content

Commit

Permalink
fix(stepfunctions): task token integration cannot be used with API Ga…
Browse files Browse the repository at this point in the history
…teway

To pass the Task Token in headers to an API Gateway, the token must
be wrapped in an array (because that's the value type of headers).

Because JSONPath evaluation needs to happen to resolve the token,
we need to use the `States.Array()` function in a `JsonPathToken`
to properly resolve this. However, doing that makes the existing
validation code fail the validation checking that you are passing
the task token somewhere.

Add convenience methods for the intrinsics, and update the checker
to also find paths referenced inside intrinsic functions.

Fixes #14184, fixes #14181.
  • Loading branch information
rix0rrr committed Jan 21, 2022
1 parent 1393729 commit c64b700
Show file tree
Hide file tree
Showing 6 changed files with 454 additions and 11 deletions.
21 changes: 20 additions & 1 deletion packages/@aws-cdk/aws-stepfunctions-tasks/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -305,6 +305,25 @@ const invokeTask = new tasks.CallApiGatewayRestApiEndpoint(this, 'Call REST API'
});
```

Be aware that the header values must be arrays. When passing the Task Token
in the headers field `WAIT_FOR_TASK_TOKEN` integration, use
`JsonPath.array()` to wrap the token in an array:

```ts
import * as apigateway from '@aws-cdk/aws-apigateway';
declare const api: apigateway.RestApi;

new tasks.CallApiGatewayRestApiEndpoint(this, 'Endpoint', {
api,
stageName: 'Stage',
method: tasks.HttpMethod.PUT,
integrationPattern: sfn.IntegrationPattern.WAIT_FOR_TASK_TOKEN,
headers: sfn.TaskInput.fromObject({
TaskToken: sfn.JsonPath.array(sfn.JsonPath.taskToken),
}),
});
```

### Call HTTP API Endpoint

The `CallApiGatewayHttpApiEndpoint` calls the HTTP API endpoint.
Expand Down Expand Up @@ -798,7 +817,7 @@ The service integration APIs correspond to Amazon EMR on EKS APIs, but differ in

### Create Virtual Cluster

The [CreateVirtualCluster](https://docs.aws.amazon.com/emr-on-eks/latest/APIReference/API_CreateVirtualCluster.html) API creates a single virtual cluster that's mapped to a single Kubernetes namespace.
The [CreateVirtualCluster](https://docs.aws.amazon.com/emr-on-eks/latest/APIReference/API_CreateVirtualCluster.html) API creates a single virtual cluster that's mapped to a single Kubernetes namespace.

The EKS cluster containing the Kubernetes namespace where the virtual cluster will be mapped can be passed in from the task input.

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,25 @@ export interface CallApiGatewayRestApiEndpointProps extends CallApiGatewayEndpoi
/**
* Call REST API endpoint as a Task
*
* Be aware that the header values must be arrays. When passing the Task Token
* in the headers field `WAIT_FOR_TASK_TOKEN` integration, use
* `JsonPath.array()` to wrap the token in an array:
*
* ```ts
* import * as apigateway from '@aws-cdk/aws-apigateway';
* declare const api: apigateway.RestApi;
*
* new tasks.CallApiGatewayRestApiEndpoint(this, 'Endpoint', {
* api,
* stageName: 'Stage',
* method: tasks.HttpMethod.PUT,
* integrationPattern: sfn.IntegrationPattern.WAIT_FOR_TASK_TOKEN,
* headers: sfn.TaskInput.fromObject({
* TaskToken: sfn.JsonPath.array(sfn.JsonPath.taskToken),
* }),
* });
* ```
*
* @see https://docs.aws.amazon.com/step-functions/latest/dg/connect-api-gateway.html
*/
export class CallApiGatewayRestApiEndpoint extends CallApiGatewayEndpointBase {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -69,7 +69,7 @@ describe('CallApiGatewayRestApiEndpoint', () => {
method: HttpMethod.GET,
stageName: 'dev',
integrationPattern: sfn.IntegrationPattern.WAIT_FOR_TASK_TOKEN,
headers: sfn.TaskInput.fromObject({ TaskToken: sfn.JsonPath.taskToken }),
headers: sfn.TaskInput.fromObject({ TaskToken: sfn.JsonPath.array(sfn.JsonPath.taskToken) }),
});

// THEN
Expand Down Expand Up @@ -97,7 +97,7 @@ describe('CallApiGatewayRestApiEndpoint', () => {
},
AuthType: 'NO_AUTH',
Headers: {
'TaskToken.$': '$$.Task.Token',
'TaskToken.$': 'States.Array($$.Task.Token)',
},
Method: 'GET',
Stage: 'dev',
Expand Down
89 changes: 87 additions & 2 deletions packages/@aws-cdk/aws-stepfunctions/lib/fields.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { Token } from '@aws-cdk/core';
import { findReferencedPaths, jsonPathString, JsonPathToken, renderObject } from './json-path';
import { Token, IResolvable } from '@aws-cdk/core';
import { findReferencedPaths, jsonPathString, JsonPathToken, renderObject, renderInExpression, jsonPathFromAny } from './private/json-path';

/**
* Extract a field from the State Machine data or context
Expand Down Expand Up @@ -38,6 +38,15 @@ export class JsonPath {
return Token.asNumber(new JsonPathToken(path));
}

/**
* Reference a complete (complex) object in a JSON path location
*/
public static objectAt(path: string): IResolvable {
validateJsonPath(path);
return new JsonPathToken(path);
}


/**
* Use the entire data structure
*
Expand Down Expand Up @@ -78,6 +87,82 @@ export class JsonPath {
return new JsonPathToken('$$').toString();
}

/**
* Make an intrinsic States.Array expression
*
* Combine any number of string literals or JsonPath expressions into an array.
*
* Use this function if the value of an array element directly has to come
* from a JSON Path expression (either the State object or the Context object).
*
* If the array contains object literals whose values come from a JSON path
* expression, you do not need to use this function.
*
* @see https://docs.aws.amazon.com/step-functions/latest/dg/amazon-states-language-intrinsic-functions.html
*/
public static array(...values: string[]): string {
return new JsonPathToken(`States.Array(${values.map(renderInExpression).join(', ')})`).toString();
}

/**
* Make an intrinsic States.Format expression
*
* This can be used to embed JSON Path variables inside a format string.
*
* For example:
*
* ```ts
* sfn.JsonPath.format('Hello, my name is {}.', JsonPath.stringAt('$.name'))
* ```
*
* @see https://docs.aws.amazon.com/step-functions/latest/dg/amazon-states-language-intrinsic-functions.html
*/
public static format(formatString: string, ...values: string[]): string {
const allArgs = [formatString, ...values];
return new JsonPathToken(`States.Format(${allArgs.map(renderInExpression).join(', ')})`).toString();
}

/**
* Make an intrinsic States.StringToJson expression
*
* During the execution of the Step Functions state machine, parse the given
* argument as JSON into its object form.
*
* For example:
*
* ```ts
* sfn.JsonPath.stringToJson(JsonPath.stringAt('$.someJsonBody'))
* ```
*
* @see https://docs.aws.amazon.com/step-functions/latest/dg/amazon-states-language-intrinsic-functions.html
*/
public static stringToJson(jsonString: string): IResolvable {
return new JsonPathToken(`States.StringToJson(${renderInExpression(jsonString)})`);
}

/**
* Make an intrinsic States.JsonToString expression
*
* During the execution of the Step Functions state machine, encode the
* given object into a JSON string.
*
* For example:
*
* ```ts
* sfn.JsonPath.jsonToString(JsonPath.objectAt('$.someObject'))
* ```
*
* @see https://docs.aws.amazon.com/step-functions/latest/dg/amazon-states-language-intrinsic-functions.html
*/
public static jsonToString(value: any): string {
const path = jsonPathFromAny(value);
if (!path) {
throw new Error('Argument to JsonPath.jsonToString() must be a JsonPath object');
}

return new JsonPathToken(`States.JsonToString(${path})`).toString();
}

private constructor() {}
}

Expand Down
Loading

0 comments on commit c64b700

Please sign in to comment.