Skip to content

Commit 678eede

Browse files
authored
fix(stepfunctions): task token integration cannot be used with API Gateway (#18595)
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. ---- *By submitting this pull request, I confirm that my contribution is made under the terms of the Apache-2.0 license*
1 parent 2465b51 commit 678eede

File tree

9 files changed

+688
-16
lines changed

9 files changed

+688
-16
lines changed

packages/@aws-cdk/aws-stepfunctions-tasks/README.md

+23-3
Original file line numberDiff line numberDiff line change
@@ -208,19 +208,20 @@ and invokes it asynchronously.
208208

209209
```ts
210210
declare const fn: lambda.Function;
211+
211212
const submitJob = new tasks.LambdaInvoke(this, 'Invoke Handler', {
212213
lambdaFunction: fn,
213214
payload: sfn.TaskInput.fromJsonPathAt('$.input'),
214215
invocationType: tasks.LambdaInvocationType.EVENT,
215216
});
216217
```
217218

218-
You can also use [intrinsic functions](https://docs.aws.amazon.com/step-functions/latest/dg/amazon-states-language-intrinsic-functions.html) with `JsonPath.stringAt()`.
219+
You can also use [intrinsic functions](https://docs.aws.amazon.com/step-functions/latest/dg/amazon-states-language-intrinsic-functions.html) available on `JsonPath`, for example `JsonPath.format()`.
219220
Here is an example of starting an Athena query that is dynamically created using the task input:
220221

221222
```ts
222223
const startQueryExecutionJob = new tasks.AthenaStartQueryExecution(this, 'Athena Start Query', {
223-
queryString: sfn.JsonPath.stringAt("States.Format('select contacts where year={};', $.year)"),
224+
queryString: sfn.JsonPath.format('select contacts where year={};', sfn.JsonPath.stringAt('$.year')),
224225
queryExecutionContext: {
225226
databaseName: 'interactions',
226227
},
@@ -305,6 +306,25 @@ const invokeTask = new tasks.CallApiGatewayRestApiEndpoint(this, 'Call REST API'
305306
});
306307
```
307308

309+
Be aware that the header values must be arrays. When passing the Task Token
310+
in the headers field `WAIT_FOR_TASK_TOKEN` integration, use
311+
`JsonPath.array()` to wrap the token in an array:
312+
313+
```ts
314+
import * as apigateway from '@aws-cdk/aws-apigateway';
315+
declare const api: apigateway.RestApi;
316+
317+
new tasks.CallApiGatewayRestApiEndpoint(this, 'Endpoint', {
318+
api,
319+
stageName: 'Stage',
320+
method: tasks.HttpMethod.PUT,
321+
integrationPattern: sfn.IntegrationPattern.WAIT_FOR_TASK_TOKEN,
322+
headers: sfn.TaskInput.fromObject({
323+
TaskToken: sfn.JsonPath.array(sfn.JsonPath.taskToken),
324+
}),
325+
});
326+
```
327+
308328
### Call HTTP API Endpoint
309329

310330
The `CallApiGatewayHttpApiEndpoint` calls the HTTP API endpoint.
@@ -798,7 +818,7 @@ The service integration APIs correspond to Amazon EMR on EKS APIs, but differ in
798818

799819
### Create Virtual Cluster
800820

801-
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.
821+
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.
802822

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

packages/@aws-cdk/aws-stepfunctions-tasks/lib/apigateway/call-rest-api.ts

+19
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,25 @@ export interface CallApiGatewayRestApiEndpointProps extends CallApiGatewayEndpoi
2424
/**
2525
* Call REST API endpoint as a Task
2626
*
27+
* Be aware that the header values must be arrays. When passing the Task Token
28+
* in the headers field `WAIT_FOR_TASK_TOKEN` integration, use
29+
* `JsonPath.array()` to wrap the token in an array:
30+
*
31+
* ```ts
32+
* import * as apigateway from '@aws-cdk/aws-apigateway';
33+
* declare const api: apigateway.RestApi;
34+
*
35+
* new tasks.CallApiGatewayRestApiEndpoint(this, 'Endpoint', {
36+
* api,
37+
* stageName: 'Stage',
38+
* method: tasks.HttpMethod.PUT,
39+
* integrationPattern: sfn.IntegrationPattern.WAIT_FOR_TASK_TOKEN,
40+
* headers: sfn.TaskInput.fromObject({
41+
* TaskToken: sfn.JsonPath.array(sfn.JsonPath.taskToken),
42+
* }),
43+
* });
44+
* ```
45+
*
2746
* @see https://docs.aws.amazon.com/step-functions/latest/dg/connect-api-gateway.html
2847
*/
2948
export class CallApiGatewayRestApiEndpoint extends CallApiGatewayEndpointBase {

packages/@aws-cdk/aws-stepfunctions-tasks/test/apigateway/call-rest-api.test.ts

+2-2
Original file line numberDiff line numberDiff line change
@@ -69,7 +69,7 @@ describe('CallApiGatewayRestApiEndpoint', () => {
6969
method: HttpMethod.GET,
7070
stageName: 'dev',
7171
integrationPattern: sfn.IntegrationPattern.WAIT_FOR_TASK_TOKEN,
72-
headers: sfn.TaskInput.fromObject({ TaskToken: sfn.JsonPath.taskToken }),
72+
headers: sfn.TaskInput.fromObject({ TaskToken: sfn.JsonPath.array(sfn.JsonPath.taskToken) }),
7373
});
7474

7575
// THEN
@@ -97,7 +97,7 @@ describe('CallApiGatewayRestApiEndpoint', () => {
9797
},
9898
AuthType: 'NO_AUTH',
9999
Headers: {
100-
'TaskToken.$': '$$.Task.Token',
100+
'TaskToken.$': 'States.Array($$.Task.Token)',
101101
},
102102
Method: 'GET',
103103
Stage: 'dev',

packages/@aws-cdk/aws-stepfunctions/README.md

+47-2
Original file line numberDiff line numberDiff line change
@@ -95,6 +95,51 @@ properly (for example, permissions to invoke any Lambda functions you add to
9595
your workflow). A role will be created by default, but you can supply an
9696
existing one as well.
9797

98+
## Accessing State (the JsonPath class)
99+
100+
Every State Machine execution has [State Machine
101+
Data](https://docs.aws.amazon.com/step-functions/latest/dg/concepts-state-machine-data.html):
102+
a JSON document containing keys and values that is fed into the state machine,
103+
gets modified as the state machine progresses, and finally is produced as output.
104+
105+
You can pass fragments of this State Machine Data into Tasks of the state machine.
106+
To do so, use the static methods on the `JsonPath` class. For example, to pass
107+
the value that's in the data key of `OrderId` to a Lambda function as you invoke
108+
it, use `JsonPath.stringAt('$.OrderId')`, like so:
109+
110+
```ts
111+
import * as lambda from '@aws-cdk/aws-lambda';
112+
113+
declare const orderFn: lambda.Function;
114+
115+
const submitJob = new tasks.LambdaInvoke(this, 'InvokeOrderProcessor', {
116+
lambdaFunction: orderFn,
117+
payload: sfn.TaskInput.fromObject({
118+
OrderId: sfn.JsonPath.stringAt('$.OrderId'),
119+
}),
120+
});
121+
```
122+
123+
The following methods are available:
124+
125+
| Method | Purpose |
126+
|--------|---------|
127+
| `JsonPath.stringAt('$.Field')` | reference a field, return the type as a `string`. |
128+
| `JsonPath.listAt('$.Field')` | reference a field, return the type as a list of strings. |
129+
| `JsonPath.numberAt('$.Field')` | reference a field, return the type as a number. Use this for functions that expect a number argument. |
130+
| `JsonPath.objectAt('$.Field')` | reference a field, return the type as an `IResolvable`. Use this for functions that expect an object argument. |
131+
| `JsonPath.entirePayload` | reference the entire data object (equivalent to a path of `$`). |
132+
| `JsonPath.taskToken` | reference the [Task Token](https://docs.aws.amazon.com/step-functions/latest/dg/connect-to-resource.html#connect-wait-token), used for integration patterns that need to run for a long time. |
133+
134+
You can also call [intrinsic functions](https://docs.aws.amazon.com/step-functions/latest/dg/amazon-states-language-intrinsic-functions.html) using the methods on `JsonPath`:
135+
136+
| Method | Purpose |
137+
|--------|---------|
138+
| `JsonPath.array(JsonPath.stringAt('$.Field'), ...)` | make an array from other elements. |
139+
| `JsonPath.format('The value is {}.', JsonPath.stringAt('$.Value'))` | insert elements into a format string. |
140+
| `JsonPath.stringToJson(JsonPath.stringAt('$.ObjStr'))` | parse a JSON string to an object |
141+
| `JsonPath.jsonToString(JsonPath.objectAt('$.Obj'))` | stringify an object to a JSON string |
142+
98143
## Amazon States Language
99144

100145
This library comes with a set of classes that model the [Amazon States
@@ -603,8 +648,8 @@ new cloudwatch.Alarm(this, 'ThrottledAlarm', {
603648

604649
## Error names
605650

606-
Step Functions identifies errors in the Amazon States Language using case-sensitive strings, known as error names.
607-
The Amazon States Language defines a set of built-in strings that name well-known errors, all beginning with the `States.` prefix.
651+
Step Functions identifies errors in the Amazon States Language using case-sensitive strings, known as error names.
652+
The Amazon States Language defines a set of built-in strings that name well-known errors, all beginning with the `States.` prefix.
608653

609654
* `States.ALL` - A wildcard that matches any known error name.
610655
* `States.Runtime` - An execution failed due to some exception that could not be processed. Often these are caused by errors at runtime, such as attempting to apply InputPath or OutputPath on a null JSON payload. A `States.Runtime` error is not retriable, and will always cause the execution to fail. A retry or catch on `States.ALL` will NOT catch States.Runtime errors.

packages/@aws-cdk/aws-stepfunctions/lib/fields.ts

+86-2
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
1-
import { Token } from '@aws-cdk/core';
2-
import { findReferencedPaths, jsonPathString, JsonPathToken, renderObject } from './json-path';
1+
import { Token, IResolvable } from '@aws-cdk/core';
2+
import { findReferencedPaths, jsonPathString, JsonPathToken, renderObject, renderInExpression, jsonPathFromAny } from './private/json-path';
33

44
/**
55
* Extract a field from the State Machine data or context
@@ -38,6 +38,14 @@ export class JsonPath {
3838
return Token.asNumber(new JsonPathToken(path));
3939
}
4040

41+
/**
42+
* Reference a complete (complex) object in a JSON path location
43+
*/
44+
public static objectAt(path: string): IResolvable {
45+
validateJsonPath(path);
46+
return new JsonPathToken(path);
47+
}
48+
4149
/**
4250
* Use the entire data structure
4351
*
@@ -78,6 +86,82 @@ export class JsonPath {
7886
return new JsonPathToken('$$').toString();
7987
}
8088

89+
/**
90+
* Make an intrinsic States.Array expression
91+
*
92+
* Combine any number of string literals or JsonPath expressions into an array.
93+
*
94+
* Use this function if the value of an array element directly has to come
95+
* from a JSON Path expression (either the State object or the Context object).
96+
*
97+
* If the array contains object literals whose values come from a JSON path
98+
* expression, you do not need to use this function.
99+
*
100+
* @see https://docs.aws.amazon.com/step-functions/latest/dg/amazon-states-language-intrinsic-functions.html
101+
*/
102+
public static array(...values: string[]): string {
103+
return new JsonPathToken(`States.Array(${values.map(renderInExpression).join(', ')})`).toString();
104+
}
105+
106+
/**
107+
* Make an intrinsic States.Format expression
108+
*
109+
* This can be used to embed JSON Path variables inside a format string.
110+
*
111+
* For example:
112+
*
113+
* ```ts
114+
* sfn.JsonPath.format('Hello, my name is {}.', sfn.JsonPath.stringAt('$.name'))
115+
* ```
116+
*
117+
* @see https://docs.aws.amazon.com/step-functions/latest/dg/amazon-states-language-intrinsic-functions.html
118+
*/
119+
public static format(formatString: string, ...values: string[]): string {
120+
const allArgs = [formatString, ...values];
121+
return new JsonPathToken(`States.Format(${allArgs.map(renderInExpression).join(', ')})`).toString();
122+
}
123+
124+
/**
125+
* Make an intrinsic States.StringToJson expression
126+
*
127+
* During the execution of the Step Functions state machine, parse the given
128+
* argument as JSON into its object form.
129+
*
130+
* For example:
131+
*
132+
* ```ts
133+
* sfn.JsonPath.stringToJson(sfn.JsonPath.stringAt('$.someJsonBody'))
134+
* ```
135+
*
136+
* @see https://docs.aws.amazon.com/step-functions/latest/dg/amazon-states-language-intrinsic-functions.html
137+
*/
138+
public static stringToJson(jsonString: string): IResolvable {
139+
return new JsonPathToken(`States.StringToJson(${renderInExpression(jsonString)})`);
140+
}
141+
142+
/**
143+
* Make an intrinsic States.JsonToString expression
144+
*
145+
* During the execution of the Step Functions state machine, encode the
146+
* given object into a JSON string.
147+
*
148+
* For example:
149+
*
150+
* ```ts
151+
* sfn.JsonPath.jsonToString(sfn.JsonPath.objectAt('$.someObject'))
152+
* ```
153+
*
154+
* @see https://docs.aws.amazon.com/step-functions/latest/dg/amazon-states-language-intrinsic-functions.html
155+
*/
156+
public static jsonToString(value: any): string {
157+
const path = jsonPathFromAny(value);
158+
if (!path) {
159+
throw new Error('Argument to JsonPath.jsonToString() must be a JsonPath object');
160+
}
161+
162+
return new JsonPathToken(`States.JsonToString(${path})`).toString();
163+
}
164+
81165
private constructor() {}
82166
}
83167

0 commit comments

Comments
 (0)