Skip to content
Merged
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
9 changes: 9 additions & 0 deletions integration/combination/test_function_with_http_api.py
Original file line number Diff line number Diff line change
Expand Up @@ -21,3 +21,12 @@ def test_function_with_http_api(self):
self.verify_get_request_response(base_url + "some/path", 200)
self.verify_get_request_response(base_url + "something", 404)
self.verify_get_request_response(base_url + "another/endpoint", 404)

def test_function_with_http_api_default_path(self):
self.create_and_verify_stack("combination/function_with_http_api_default_path")

stack_outputs = self.get_stack_outputs()
base_url = stack_outputs["ApiUrl"]
# The $default route catches requests that don't explicitly match other routes
self.verify_get_request_response(base_url, 200)
self.verify_get_request_response(base_url + "something", 200)
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
[
{ "LogicalResourceId":"MyLambdaFunction", "ResourceType":"AWS::Lambda::Function" },
{ "LogicalResourceId":"MyLambdaFunctionRole", "ResourceType":"AWS::IAM::Role" },
{ "LogicalResourceId":"MyLambdaFunctionGetApiPermission", "ResourceType":"AWS::Lambda::Permission" },
{ "LogicalResourceId":"MyApi", "ResourceType":"AWS::ApiGatewayV2::Api" },
{ "LogicalResourceId":"MyApiApiGatewayDefaultStage", "ResourceType":"AWS::ApiGatewayV2::Stage" }
]
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
Resources:
MyLambdaFunction:
Type: AWS::Serverless::Function
Properties:
Handler: index.handler
Runtime: python3.7
InlineCode: |
def handler(event, context):
return {'body': 'Hello World!', 'statusCode': 200}
MemorySize: 128
Events:
GetApi:
Type: HttpApi
Properties:
ApiId:
Ref: MyApi
Method: ANY
Path: /$default

MyApi:
Type: AWS::Serverless::HttpApi
Properties:
DefinitionBody:
info:
version: '1.0'
title:
Ref: AWS::StackName
paths:
/$default:
x-amazon-apigateway-any-method:
responses: { }
isDefaultRoute: true
openapi: 3.0.1

Outputs:
ApiUrl:
Description: "API endpoint URL for Prod environment"
Value:
Fn::Sub: "https://${MyApi}.execute-api.${AWS::Region}.${AWS::URLSuffix}/"
10 changes: 9 additions & 1 deletion samtranslator/model/eventsources/push.py
Original file line number Diff line number Diff line change
Expand Up @@ -1173,8 +1173,16 @@ def _get_permission(self, resources_to_link, stage): # type: ignore[no-untyped-

api_id = self.ApiId # type: ignore[attr-defined]

# when the Method is "ANY" and the path is '/$default' it adds an extra "*" which causes a bug
# the generated ARN for permissions ends with /*/*/$default which causes the path to be invalid
# see this issue: https://github.com/aws/serverless-application-model/issues/1860
resource = "${__ApiId__}/${__Stage__}"
if self.Method.lower() == "any" and path == f"/{OpenApiEditor._DEFAULT_PATH}":
resource += path
else:
resource += f"/{method}{path}"

# ApiId can be a simple string or intrinsic function like !Ref. Using Fn::Sub will handle both cases
resource = "${__ApiId__}/" + "${__Stage__}/" + method + path
source_arn = fnSub(
ArnGenerator.generate_arn(partition="${AWS::Partition}", service="execute-api", resource=resource), # type: ignore[no-untyped-call]
{"__ApiId__": api_id, "__Stage__": stage},
Expand Down
45 changes: 45 additions & 0 deletions tests/translator/input/explicit_http_api_default_path.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
Transform: AWS::Serverless-2016-10-31
Resources:
Function:
Type: AWS::Serverless::Function
Properties:
CodeUri: s3://sam-demo-bucket/todo_list.zip
Handler: index.restapi
Runtime: python3.7
Events:
HttpApiANYdefault:
Type: HttpApi
Properties:
Path: /$default
Method: ANY
ApiId: !Ref HttpApi
Function2:
Type: AWS::Serverless::Function
Properties:
CodeUri: s3://sam-demo-bucket/todo_list.zip
Handler: index.restapi
Runtime: python3.7
Events:
HttpApiANYhello:
Type: HttpApi
Properties:
Path: /hello
Method: ANY
ApiId: !Ref HttpApi
HttpApi:
Type: AWS::Serverless::HttpApi
Properties:
DefinitionBody:
openapi: '3.0'
info:
title: !Sub ${AWS::StackName}-HttpApi
version: '1.0'
paths:
/$default:
x-amazon-apigateway-any-method:
responses: {}
isDefaultRoute: true
/hello:
x-amazon-apigateway-any-method:
responses: {}
FailOnWarnings: true
215 changes: 215 additions & 0 deletions tests/translator/output/aws-cn/explicit_http_api_default_path.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,215 @@
{
"Resources": {
"Function": {
"Properties": {
"Code": {
"S3Bucket": "sam-demo-bucket",
"S3Key": "todo_list.zip"
},
"Handler": "index.restapi",
"Role": {
"Fn::GetAtt": [
"FunctionRole",
"Arn"
]
},
"Runtime": "python3.7",
"Tags": [
{
"Key": "lambda:createdBy",
"Value": "SAM"
}
]
},
"Type": "AWS::Lambda::Function"
},
"Function2": {
"Properties": {
"Code": {
"S3Bucket": "sam-demo-bucket",
"S3Key": "todo_list.zip"
},
"Handler": "index.restapi",
"Role": {
"Fn::GetAtt": [
"Function2Role",
"Arn"
]
},
"Runtime": "python3.7",
"Tags": [
{
"Key": "lambda:createdBy",
"Value": "SAM"
}
]
},
"Type": "AWS::Lambda::Function"
},
"Function2HttpApiANYhelloPermission": {
"Properties": {
"Action": "lambda:InvokeFunction",
"FunctionName": {
"Ref": "Function2"
},
"Principal": "apigateway.amazonaws.com",
"SourceArn": {
"Fn::Sub": [
"arn:${AWS::Partition}:execute-api:${AWS::Region}:${AWS::AccountId}:${__ApiId__}/${__Stage__}/*/hello",
{
"__ApiId__": {
"Ref": "HttpApi"
},
"__Stage__": "*"
}
]
}
},
"Type": "AWS::Lambda::Permission"
},
"Function2Role": {
"Properties": {
"AssumeRolePolicyDocument": {
"Statement": [
{
"Action": [
"sts:AssumeRole"
],
"Effect": "Allow",
"Principal": {
"Service": [
"lambda.amazonaws.com"
]
}
}
],
"Version": "2012-10-17"
},
"ManagedPolicyArns": [
"arn:aws-cn:iam::aws:policy/service-role/AWSLambdaBasicExecutionRole"
],
"Tags": [
{
"Key": "lambda:createdBy",
"Value": "SAM"
}
]
},
"Type": "AWS::IAM::Role"
},
"FunctionHttpApiANYdefaultPermission": {
"Properties": {
"Action": "lambda:InvokeFunction",
"FunctionName": {
"Ref": "Function"
},
"Principal": "apigateway.amazonaws.com",
"SourceArn": {
"Fn::Sub": [
"arn:${AWS::Partition}:execute-api:${AWS::Region}:${AWS::AccountId}:${__ApiId__}/${__Stage__}/$default",
{
"__ApiId__": {
"Ref": "HttpApi"
},
"__Stage__": "*"
}
]
}
},
"Type": "AWS::Lambda::Permission"
},
"FunctionRole": {
"Properties": {
"AssumeRolePolicyDocument": {
"Statement": [
{
"Action": [
"sts:AssumeRole"
],
"Effect": "Allow",
"Principal": {
"Service": [
"lambda.amazonaws.com"
]
}
}
],
"Version": "2012-10-17"
},
"ManagedPolicyArns": [
"arn:aws-cn:iam::aws:policy/service-role/AWSLambdaBasicExecutionRole"
],
"Tags": [
{
"Key": "lambda:createdBy",
"Value": "SAM"
}
]
},
"Type": "AWS::IAM::Role"
},
"HttpApi": {
"Properties": {
"Body": {
"info": {
"title": {
"Fn::Sub": "${AWS::StackName}-HttpApi"
},
"version": "1.0"
},
"openapi": "3.0",
"paths": {
"/$default": {
"x-amazon-apigateway-any-method": {
"isDefaultRoute": true,
"responses": {},
"x-amazon-apigateway-integration": {
"httpMethod": "POST",
"payloadFormatVersion": "2.0",
"type": "aws_proxy",
"uri": {
"Fn::Sub": "arn:${AWS::Partition}:apigateway:${AWS::Region}:lambda:path/2015-03-31/functions/${Function.Arn}/invocations"
}
}
}
},
"/hello": {
"x-amazon-apigateway-any-method": {
"responses": {},
"x-amazon-apigateway-integration": {
"httpMethod": "POST",
"payloadFormatVersion": "2.0",
"type": "aws_proxy",
"uri": {
"Fn::Sub": "arn:${AWS::Partition}:apigateway:${AWS::Region}:lambda:path/2015-03-31/functions/${Function2.Arn}/invocations"
}
}
}
}
},
"tags": [
{
"name": "httpapi:createdBy",
"x-amazon-apigateway-tag-value": "SAM"
}
]
},
"FailOnWarnings": true
},
"Type": "AWS::ApiGatewayV2::Api"
},
"HttpApiApiGatewayDefaultStage": {
"Properties": {
"ApiId": {
"Ref": "HttpApi"
},
"AutoDeploy": true,
"StageName": "$default",
"Tags": {
"httpapi:createdBy": "SAM"
}
},
"Type": "AWS::ApiGatewayV2::Stage"
}
}
}
Loading