-
Notifications
You must be signed in to change notification settings - Fork 4k
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
feat(stepfunctions-tasks): support for APIGW API: Invoke #11565
Changes from 1 commit
51e47d8
fbb40d6
e14080a
32d0e26
494334b
ac29551
c47bb7e
9cd40c7
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change | ||||||||
---|---|---|---|---|---|---|---|---|---|---|
@@ -0,0 +1,277 @@ | ||||||||||
import * as iam from '@aws-cdk/aws-iam'; | ||||||||||
import * as sfn from '@aws-cdk/aws-stepfunctions'; | ||||||||||
import { Stack } from '@aws-cdk/core'; | ||||||||||
import { Construct } from 'constructs'; | ||||||||||
import { integrationResourceArn, validatePatternSupported } from '../private/task-utils'; | ||||||||||
|
||||||||||
/** | ||||||||||
* Properties for invoking an API Endpoint with ApiGatewayInvoke | ||||||||||
*/ | ||||||||||
export interface ApiGatewayInvokeProps extends sfn.TaskStateBaseProps { | ||||||||||
|
||||||||||
/** | ||||||||||
* hostname of an API Gateway URL | ||||||||||
* @example {ApiId}.execute-api.{region}.amazonaws.com | ||||||||||
*/ | ||||||||||
readonly apiEndpoint: string; | ||||||||||
Sumeet-Badyal marked this conversation as resolved.
Show resolved
Hide resolved
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I think this should not be needed anymore. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. the endpoint can be inferred from the |
||||||||||
|
||||||||||
/** | ||||||||||
* Http method for the API | ||||||||||
*/ | ||||||||||
readonly method: HttpMethod; | ||||||||||
|
||||||||||
/** | ||||||||||
* HTTP headers string to list of strings | ||||||||||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I think we can provide a more informative doc string here. maybe throw in an example too? |
||||||||||
* @default - No headers | ||||||||||
*/ | ||||||||||
readonly headers?: sfn.TaskInput; | ||||||||||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. does this need to be a There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. It's just a json. I can change the type. |
||||||||||
|
||||||||||
/** | ||||||||||
* Name of the stage where the API is deployed to in API Gateway | ||||||||||
* @default - Required for REST and $default for HTTP | ||||||||||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. nit: be sure to describe what |
||||||||||
*/ | ||||||||||
readonly stage?: string; | ||||||||||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I believe this is called |
||||||||||
|
||||||||||
/** | ||||||||||
* Path parameters appended after API endpoint | ||||||||||
* @default - No path | ||||||||||
*/ | ||||||||||
readonly path?: string; | ||||||||||
|
||||||||||
/** | ||||||||||
* Query strings string to list of strings | ||||||||||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. an example might communicate this better |
||||||||||
* @default - No query parameters | ||||||||||
*/ | ||||||||||
readonly queryParameters?: sfn.TaskInput; | ||||||||||
|
||||||||||
/** | ||||||||||
* HTTP Request body | ||||||||||
* @default - No requestBody | ||||||||||
*/ | ||||||||||
readonly requestBody?: sfn.TaskInput; | ||||||||||
/** | ||||||||||
* Authentication methods | ||||||||||
* | ||||||||||
* NO_AUTH: call the API direclty with no authorization method | ||||||||||
* | ||||||||||
* IAM_ROLE: Use the IAM role associated with the current state machine for authorization | ||||||||||
* | ||||||||||
* RESOURCE_POLICY: Use the resource policy of the API for authorization | ||||||||||
Sumeet-Badyal marked this conversation as resolved.
Show resolved
Hide resolved
|
||||||||||
* | ||||||||||
* @default - NO_AUTH | ||||||||||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
Suggested change
|
||||||||||
*/ | ||||||||||
readonly authType?: sfn.TaskInput; | ||||||||||
Sumeet-Badyal marked this conversation as resolved.
Show resolved
Hide resolved
|
||||||||||
|
||||||||||
} | ||||||||||
/** | ||||||||||
* Invoke an API endpoint as a Task | ||||||||||
* | ||||||||||
* @see https://docs.aws.amazon.com/step-functions/latest/dg/connect-api-gateway.html | ||||||||||
*/ | ||||||||||
export class ApiGatewayInvoke extends sfn.TaskStateBase { | ||||||||||
|
||||||||||
private static readonly SUPPORTED_INTEGRATION_PATTERNS: sfn.IntegrationPattern[] = [ | ||||||||||
sfn.IntegrationPattern.REQUEST_RESPONSE, | ||||||||||
sfn.IntegrationPattern.WAIT_FOR_TASK_TOKEN, | ||||||||||
]; | ||||||||||
|
||||||||||
protected readonly taskMetrics?: sfn.TaskMetricsConfig; | ||||||||||
protected readonly taskPolicies?: iam.PolicyStatement[]; | ||||||||||
|
||||||||||
private readonly integrationPattern: sfn.IntegrationPattern; | ||||||||||
|
||||||||||
constructor(scope: Construct, id: string, private readonly props: ApiGatewayInvokeProps) { | ||||||||||
super(scope, id, props); | ||||||||||
this.integrationPattern = props.integrationPattern ?? sfn.IntegrationPattern.REQUEST_RESPONSE; | ||||||||||
|
||||||||||
validatePatternSupported(this.integrationPattern, ApiGatewayInvoke.SUPPORTED_INTEGRATION_PATTERNS); | ||||||||||
const authType = this.props.authType ? this.props.authType.value : sfn.TaskInput.fromDataAt('$.AuthType').value; | ||||||||||
Sumeet-Badyal marked this conversation as resolved.
Show resolved
Hide resolved
|
||||||||||
if (authType === 'IAM_ROLE') { | ||||||||||
const resource = props.apiEndpoint.split('.', 1)[0] + '/' + (props.stage ? props.stage + '/' : '$default/') + props.method + '/' + (props.path ?? ''); | ||||||||||
Sumeet-Badyal marked this conversation as resolved.
Show resolved
Hide resolved
|
||||||||||
|
||||||||||
this.taskPolicies = [ | ||||||||||
new iam.PolicyStatement({ | ||||||||||
resources: [ | ||||||||||
Stack.of(this).formatArn({ | ||||||||||
service: 'execute-api', | ||||||||||
resource: resource, | ||||||||||
Sumeet-Badyal marked this conversation as resolved.
Show resolved
Hide resolved
|
||||||||||
}), | ||||||||||
], | ||||||||||
actions: ['execute-api:Invoke'], | ||||||||||
}), | ||||||||||
]; | ||||||||||
} else if (authType === 'RESOURCE_POLICY') { | ||||||||||
if (!sfn.FieldUtils.containsTaskToken(props.headers)) { | ||||||||||
throw new Error('Task Token is required in `headers` Use JsonPath.taskToken to set the token.'); | ||||||||||
} | ||||||||||
const resource = props.apiEndpoint.split('.', 1)[0] + '/' + (props.stage ? props.stage + '/' : '') + props.method + '/' + (props.path ? props.path + '/*' : '*'); | ||||||||||
|
||||||||||
this.taskPolicies = [ | ||||||||||
new iam.PolicyStatement({ | ||||||||||
resources: [ | ||||||||||
Stack.of(this).formatArn({ | ||||||||||
service: 'execute-api', | ||||||||||
resource: resource, | ||||||||||
}), | ||||||||||
], | ||||||||||
actions: ['execute-api:Invoke'], | ||||||||||
conditions: { | ||||||||||
StringEquals: { | ||||||||||
'aws:SourceArn': '*', | ||||||||||
}, | ||||||||||
}, | ||||||||||
}), | ||||||||||
]; | ||||||||||
Sumeet-Badyal marked this conversation as resolved.
Show resolved
Hide resolved
|
||||||||||
} | ||||||||||
} | ||||||||||
|
||||||||||
/** | ||||||||||
* Provides the API Gateway Invoke service integration task configuration | ||||||||||
*/ | ||||||||||
/** | ||||||||||
* @internal | ||||||||||
*/ | ||||||||||
protected _renderTask(): any { | ||||||||||
Sumeet-Badyal marked this conversation as resolved.
Show resolved
Hide resolved
|
||||||||||
if (this.props.headers && this.props.queryParameters && this.props.requestBody) { | ||||||||||
return { | ||||||||||
Resource: integrationResourceArn('apigateway', 'invoke', this.integrationPattern), | ||||||||||
Parameters: sfn.FieldUtils.renderObject({ | ||||||||||
ApiEndpoint: this.props.apiEndpoint, | ||||||||||
Sumeet-Badyal marked this conversation as resolved.
Show resolved
Hide resolved
|
||||||||||
Method: this.props.method, | ||||||||||
Headers: this.props.headers ? this.props.headers.value : sfn.TaskInput.fromDataAt('$.Headers').value, | ||||||||||
Sumeet-Badyal marked this conversation as resolved.
Show resolved
Hide resolved
|
||||||||||
Stage: this.props.stage, | ||||||||||
Path: this.props.path, | ||||||||||
QueryParameters: this.props.queryParameters ? this.props.queryParameters.value : sfn.TaskInput.fromDataAt('$.QueryParameters').value, | ||||||||||
RequestBody: this.props.requestBody ? this.props.requestBody.value : sfn.TaskInput.fromDataAt('$.RequestBody').value, | ||||||||||
AuthType: (this.props.authType ? this.props.authType.value : sfn.TaskInput.fromDataAt('$.AuthType').value) === '$' ? (this.props.authType ? this.props.authType.value : sfn.TaskInput.fromDataAt('$.AuthType').value) : 'NO_AUTH', | ||||||||||
}), | ||||||||||
}; | ||||||||||
} else if (this.props.headers && this.props.queryParameters) { | ||||||||||
return { | ||||||||||
Resource: integrationResourceArn('apigateway', 'invoke', this.integrationPattern), | ||||||||||
Parameters: sfn.FieldUtils.renderObject({ | ||||||||||
ApiEndpoint: this.props.apiEndpoint, | ||||||||||
Method: this.props.method, | ||||||||||
Headers: this.props.headers ? this.props.headers.value : sfn.TaskInput.fromDataAt('$.Headers').value, | ||||||||||
Stage: this.props.stage, | ||||||||||
Path: this.props.path, | ||||||||||
QueryParameters: this.props.queryParameters ? this.props.queryParameters.value : sfn.TaskInput.fromDataAt('$.QueryParameters').value, | ||||||||||
AuthType: (this.props.authType ? this.props.authType.value : sfn.TaskInput.fromDataAt('$.AuthType').value) === '$' ? (this.props.authType ? this.props.authType.value : sfn.TaskInput.fromDataAt('$.AuthType').value) : 'NO_AUTH', | ||||||||||
}), | ||||||||||
}; | ||||||||||
} else if (this.props.queryParameters && this.props.requestBody) { | ||||||||||
return { | ||||||||||
Resource: integrationResourceArn('apigateway', 'invoke', this.integrationPattern), | ||||||||||
Parameters: sfn.FieldUtils.renderObject({ | ||||||||||
ApiEndpoint: this.props.apiEndpoint, | ||||||||||
Method: this.props.method, | ||||||||||
Stage: this.props.stage, | ||||||||||
Path: this.props.path, | ||||||||||
QueryParameters: this.props.queryParameters ? this.props.queryParameters.value : sfn.TaskInput.fromDataAt('$.QueryParameters').value, | ||||||||||
RequestBody: this.props.requestBody ? this.props.requestBody.value : sfn.TaskInput.fromDataAt('$.RequestBody').value, | ||||||||||
AuthType: (this.props.authType ? this.props.authType.value : sfn.TaskInput.fromDataAt('$.AuthType').value) === '$' ? (this.props.authType ? this.props.authType.value : sfn.TaskInput.fromDataAt('$.AuthType').value) : 'NO_AUTH', | ||||||||||
}), | ||||||||||
}; | ||||||||||
} else if (this.props.headers && this.props.requestBody) { | ||||||||||
return { | ||||||||||
Resource: integrationResourceArn('apigateway', 'invoke', this.integrationPattern), | ||||||||||
Parameters: sfn.FieldUtils.renderObject({ | ||||||||||
ApiEndpoint: this.props.apiEndpoint, | ||||||||||
Method: this.props.method, | ||||||||||
Headers: this.props.headers ? this.props.headers.value : sfn.TaskInput.fromDataAt('$.Headers').value, | ||||||||||
Stage: this.props.stage, | ||||||||||
Path: this.props.path, | ||||||||||
RequestBody: this.props.requestBody ? this.props.requestBody.value : sfn.TaskInput.fromDataAt('$.RequestBody').value, | ||||||||||
AuthType: (this.props.authType ? this.props.authType.value : sfn.TaskInput.fromDataAt('$.AuthType').value) === '$' ? (this.props.authType ? this.props.authType.value : sfn.TaskInput.fromDataAt('$.AuthType').value) : 'NO_AUTH', | ||||||||||
}), | ||||||||||
}; | ||||||||||
} else if (this.props.headers) { | ||||||||||
return { | ||||||||||
Resource: integrationResourceArn('apigateway', 'invoke', this.integrationPattern), | ||||||||||
Parameters: sfn.FieldUtils.renderObject({ | ||||||||||
ApiEndpoint: this.props.apiEndpoint, | ||||||||||
Method: this.props.method, | ||||||||||
Headers: this.props.headers ? this.props.headers.value : sfn.TaskInput.fromDataAt('$.Headers').value, | ||||||||||
Stage: this.props.stage, | ||||||||||
Path: this.props.path, | ||||||||||
AuthType: (this.props.authType ? this.props.authType.value : sfn.TaskInput.fromDataAt('$.AuthType').value) === '$' ? (this.props.authType ? this.props.authType.value : sfn.TaskInput.fromDataAt('$.AuthType').value) : 'NO_AUTH', | ||||||||||
}), | ||||||||||
}; | ||||||||||
} else if (this.props.queryParameters) { | ||||||||||
return { | ||||||||||
Resource: integrationResourceArn('apigateway', 'invoke', this.integrationPattern), | ||||||||||
Parameters: sfn.FieldUtils.renderObject({ | ||||||||||
ApiEndpoint: this.props.apiEndpoint, | ||||||||||
Method: this.props.method, | ||||||||||
Stage: this.props.stage, | ||||||||||
Path: this.props.path, | ||||||||||
QueryParameters: this.props.queryParameters ? this.props.queryParameters.value : sfn.TaskInput.fromDataAt('$.QueryParameters').value, | ||||||||||
AuthType: (this.props.authType ? this.props.authType.value : sfn.TaskInput.fromDataAt('$.AuthType').value) === '$' ? (this.props.authType ? this.props.authType.value : sfn.TaskInput.fromDataAt('$.AuthType').value) : 'NO_AUTH', | ||||||||||
}), | ||||||||||
}; | ||||||||||
} else if (this.props.requestBody) { | ||||||||||
return { | ||||||||||
Resource: integrationResourceArn('apigateway', 'invoke', this.integrationPattern), | ||||||||||
Parameters: sfn.FieldUtils.renderObject({ | ||||||||||
ApiEndpoint: this.props.apiEndpoint, | ||||||||||
Method: this.props.method, | ||||||||||
Stage: this.props.stage, | ||||||||||
Path: this.props.path, | ||||||||||
RequestBody: this.props.requestBody ? this.props.requestBody.value : sfn.TaskInput.fromDataAt('$.RequestBody').value, | ||||||||||
AuthType: (this.props.authType ? this.props.authType.value : sfn.TaskInput.fromDataAt('$.AuthType').value) === '$' ? (this.props.authType ? this.props.authType.value : sfn.TaskInput.fromDataAt('$.AuthType').value) : 'NO_AUTH', | ||||||||||
}), | ||||||||||
}; | ||||||||||
} else { | ||||||||||
return { | ||||||||||
Resource: integrationResourceArn('apigateway', 'invoke', this.integrationPattern), | ||||||||||
Parameters: sfn.FieldUtils.renderObject({ | ||||||||||
ApiEndpoint: this.props.apiEndpoint, | ||||||||||
Method: this.props.method, | ||||||||||
Stage: this.props.stage, | ||||||||||
Path: this.props.path, | ||||||||||
AuthType: (this.props.authType ? this.props.authType.value : sfn.TaskInput.fromDataAt('$.AuthType').value) === '$' ? (this.props.authType ? this.props.authType.value : sfn.TaskInput.fromDataAt('$.AuthType').value) : 'NO_AUTH', | ||||||||||
}), | ||||||||||
}; | ||||||||||
} | ||||||||||
} | ||||||||||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. there's a lot of duplicated code in this control flow chain from the |
||||||||||
} | ||||||||||
|
||||||||||
/** | ||||||||||
* Http Methods that API Gateway supports | ||||||||||
*/ | ||||||||||
export enum HttpMethod { | ||||||||||
Sumeet-Badyal marked this conversation as resolved.
Show resolved
Hide resolved
|
||||||||||
/** | ||||||||||
* Retreive data from a server at the specified resource | ||||||||||
*/ | ||||||||||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
Suggested change
|
||||||||||
GET = 'GET', | ||||||||||
|
||||||||||
/** | ||||||||||
* Send data to the API endpoint to create or udpate a resource | ||||||||||
*/ | ||||||||||
Sumeet-Badyal marked this conversation as resolved.
Show resolved
Hide resolved
|
||||||||||
POST = 'POST', | ||||||||||
|
||||||||||
/** | ||||||||||
* Send data to the API endpoint to update or create a resource | ||||||||||
*/ | ||||||||||
PUT = 'PUT', | ||||||||||
|
||||||||||
/** | ||||||||||
* Delete the resource at the specified endpoint | ||||||||||
*/ | ||||||||||
DELETE = 'DELETE', | ||||||||||
|
||||||||||
/** | ||||||||||
* Apply partial modifications to the resource | ||||||||||
*/ | ||||||||||
PATCH = 'PATCH', | ||||||||||
|
||||||||||
/** | ||||||||||
* Retreive data from a server at the specified resource without the response body | ||||||||||
*/ | ||||||||||
HEAD = 'HEAD', | ||||||||||
|
||||||||||
/** | ||||||||||
* Return data describing what other methods and operations the server supports | ||||||||||
*/ | ||||||||||
OPTIONS = 'OPTIONS' | ||||||||||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,65 @@ | ||
{ | ||
"Resources": { | ||
"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" | ||
} | ||
} | ||
}, | ||
"StateMachine2E01A3A5": { | ||
"Type": "AWS::StepFunctions::StateMachine", | ||
"Properties": { | ||
"RoleArn": { | ||
"Fn::GetAtt": [ | ||
"StateMachineRoleB840431D", | ||
"Arn" | ||
] | ||
}, | ||
"DefinitionString": { | ||
"Fn::Join": [ | ||
"", | ||
[ | ||
"{\"StartAt\":\"Invoke APIGW\",\"States\":{\"Invoke APIGW\":{\"End\":true,\"Type\":\"Task\",\"Resource\":\"arn:", | ||
{ | ||
"Ref": "AWS::Partition" | ||
}, | ||
":states:::apigateway:invoke\",\"Parameters\":{\"ApiEndpoint\":\"apiid.execute-api.us-east-1.amazonaws.com\",\"Method\":\"GET\",\"Stage\":\"prod\",\"AuthType\":\"NO_AUTH\"}}},\"TimeoutSeconds\":30}" | ||
] | ||
] | ||
} | ||
}, | ||
"DependsOn": [ | ||
"StateMachineRoleB840431D" | ||
] | ||
} | ||
}, | ||
"Outputs": { | ||
"stateMachineArn": { | ||
"Value": { | ||
"Ref": "StateMachine2E01A3A5" | ||
} | ||
} | ||
} | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
just confirming: this is not the same as
apigatewayv2
correct?There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Not the same.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
But this works to invoke functions defined via apigatewayv2, correct?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
haven't played with it myself yet, can you clarify @Sumeet-Badyal
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
From this page - https://docs.aws.amazon.com/step-functions/latest/dg/connect-api-gateway.html
This task supports invoking Rest APIs (v1) and HTTP APIs (v2) but not websocket APIs (v2).
Given our modeling of higher level constructs, we'll need to implement this as two tasks (not necessarily in the same PR). One that takes
IRestApi
from the@aws-cdk/aws-apigateway
module and another that takesIHttpApi
from the@aws-cdk/aws-apigatewayv2
module.There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Ah; looks like someone still needs to add IHttpApi too
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@Sumeet-Badyal are you still working on this? Any plans to add the second task for the IHttpApi case? Else when my aws-cdk dev container finishes spinning up I could have a go -- imagine there'd be common code between the two we'd want to abstract into a shared place in the end