Skip to content
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: add UnescapeMappingTemplate to state machine Api event #2495

Closed
wants to merge 7 commits into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
[
{ "LogicalResourceId": "HelloWorldFunction", "ResourceType": "AWS::Lambda::Function" },
{ "LogicalResourceId": "HelloWorldFunctionRole", "ResourceType": "AWS::IAM::Role" },
{ "LogicalResourceId": "MyApi", "ResourceType": "AWS::ApiGateway::RestApi" },
{ "LogicalResourceId": "MyApiDeployment", "ResourceType": "AWS::ApiGateway::Deployment" },
{ "LogicalResourceId": "MyApiProdStage", "ResourceType": "AWS::ApiGateway::Stage" },
{ "LogicalResourceId": "Post", "ResourceType": "AWS::StepFunctions::StateMachine" },
{ "LogicalResourceId": "PostPostEchoRole", "ResourceType": "AWS::IAM::Role" },
{ "LogicalResourceId": "PostRole", "ResourceType": "AWS::IAM::Role" }
]
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
Transform: AWS::Serverless-2016-10-31
Resources:
MyApi:
Type: AWS::Serverless::Api
Properties:
StageName: Prod
HelloWorldFunction:
Type: AWS::Serverless::Function
Properties:
InlineCode: |
def handler(event, context):
print(event)
return "do nothing"
Handler: index.handler
Runtime: python3.8
Post:
Type: AWS::Serverless::StateMachine
Properties:
Policies:
- arn:aws:iam::aws:policy/AWSLambda_FullAccess
Definition:
StartAt: One
States:
One:
Type: Task
Resource: !GetAtt HelloWorldFunction.Arn
End: true
Events:
PostEcho:
Type: Api
Properties:
RestApiId: !Ref MyApi
Path: /echo
Method: POST
UnescapeMappingTemplate: true

Outputs:
ApiEndpoint:
Value: !Sub "https://${MyApi}.execute-api.${AWS::Region}.amazonaws.com/Prod/echo"
21 changes: 21 additions & 0 deletions integration/single/test_basic_api.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
import json
import logging
from unittest.case import skipIf

import requests
from tenacity import stop_after_attempt, wait_exponential, retry_if_exception_type, after_log, wait_random

from integration.helpers.base_test import BaseTest
Expand Down Expand Up @@ -113,3 +115,22 @@ def test_basic_api_with_tags(self):
self.assertIsNotNone(stage)
self.assertEqual(stage["tags"]["TagKey1"], "TagValue1")
self.assertEqual(stage["tags"]["TagKey2"], "")

def test_state_machine_with_api_single_quotes_input(self):
"""
Pass single quotes in input JSON to a StateMachine
See https://github.com/aws/serverless-application-model/issues/1895
"""
self.create_and_verify_stack("single/state_machine_with_api")

stack_output = self.get_stack_outputs()
api_endpoint = stack_output.get("ApiEndpoint")

input_json = {"f'oo": {"hello": "'wor'l'd'''"}}
response = requests.post(api_endpoint, json=input_json)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can we add something similar to this so we will log the request too?

This will help us investigate in case of flaky test runs

self.assertEqual(response.status_code, 200)

execution_arn = response.json()["executionArn"]
execution = self.client_provider.sfn_client.describe_execution(executionArn=execution_arn)
execution_input = json.loads(execution["input"])
self.assertEqual(execution_input, input_json)
33 changes: 32 additions & 1 deletion samtranslator/model/stepfunctions/events.py
Original file line number Diff line number Diff line change
Expand Up @@ -252,6 +252,7 @@ class Api(EventSource):
"RestApiId": PropertyType(True, is_str()),
"Stage": PropertyType(False, is_str()),
"Auth": PropertyType(False, is_type(dict)),
"UnescapeMappingTemplate": PropertyType(False, is_type(bool)),
}

def resources_to_link(self, resources):
Expand Down Expand Up @@ -356,12 +357,18 @@ def _add_swagger_integration(self, api, resource, role, intrinsics_resolver):
if CONDITION in resource.resource_attributes:
condition = resource.resource_attributes[CONDITION]

request_template = (
self._generate_request_template_unescaped(resource)
if self.UnescapeMappingTemplate
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Probably a very rare use case - If this is a Fn::If, the new behavior will be used regardless (as SAM-T can't/doesn't resolve Fn::If). Is that wanted?

We had a similar discussion for PassthroughCondition under DeploymentPreference in #1578

Copy link
Contributor Author

@hoffa hoffa Nov 3, 2022

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Doesn't

        "UnescapeMappingTemplate": PropertyType(False, is_type(bool)),

already guarantee it's a bool?

Edit: for posterity, no. We've deprecated PropertyType since it's a footgun and we don't need to resolve intrinsics anymore.

else self._generate_request_template(resource)
)

editor.add_state_machine_integration(
self.Path,
self.Method,
integration_uri,
role.get_runtime_attr("arn"),
self._generate_request_template(resource),
request_template,
condition=condition,
)

Expand Down Expand Up @@ -453,3 +460,27 @@ def _generate_request_template(self, resource):
)
}
return request_templates

def _generate_request_template_unescaped(self, resource):
"""Generates the Body mapping request template for the Api. This allows for the input
request to the Api to be passed as the execution input to the associated state machine resource.

Unescapes single quotes such that it's valid JSON.

:param model.stepfunctions.resources.StepFunctionsStateMachine resource; the state machine
resource to which the Api event source must be associated

:returns: a body mapping request which passes the Api input to the state machine execution
:rtype: dict
"""
request_templates = {
"application/json": fnSub(
# Need to unescape single quotes escaped by escapeJavaScript.
# Also the mapping template isn't valid JSON, so can't use json.dumps().
# See https://docs.aws.amazon.com/apigateway/latest/developerguide/api-gateway-mapping-template-reference.html#util-template-reference
"""{"input": "$util.escapeJavaScript($input.json('$')).replaceAll("\\\\'","'")", "stateMachineArn": "${"""
+ resource.logical_id
+ """}"}"""
)
}
return request_templates
35 changes: 35 additions & 0 deletions tests/translator/input/state_machine_with_api.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
Transform: AWS::Serverless-2016-10-31
Resources:
MyApi:
Type: AWS::Serverless::Api
Properties:
StageName: Prod
HelloWorldFunction:
Type: AWS::Serverless::Function
Properties:
InlineCode: |
def handler(event, context):
print(event)
return "do nothing"
Handler: index.handler
Runtime: python3.8
Post:
Type: AWS::Serverless::StateMachine
Properties:
Policies:
- arn:aws:iam::aws:policy/AWSLambda_FullAccess
Definition:
StartAt: One
States:
One:
Type: Task
Resource: !GetAtt HelloWorldFunction.Arn
End: true
Events:
PostEcho:
Type: Api
Properties:
RestApiId: !Ref MyApi
Path: /echo
Method: POST
UnescapeMappingTemplate: true
Loading