Skip to content
Open
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
7 changes: 5 additions & 2 deletions samtranslator/plugins/api/implicit_api_plugin.py
Original file line number Diff line number Diff line change
Expand Up @@ -108,8 +108,11 @@ def _add_tags_to_implicit_api_if_necessary(

implicit_api_resource = template.get(self.IMPLICIT_API_LOGICAL_ID)
globals_var = template.get_globals().get(SamResourceType(resource.type).name) or {}
should_propagate_tags = resource.properties.get("PropagateTags") or globals_var.get("PropagateTags")
tags_properties = resource.properties.get("Tags") or globals_var.get("Tags")
local_propagate_tags = resource.properties.get("PropagateTags")
should_propagate_tags = (
local_propagate_tags if local_propagate_tags is not None else globals_var.get("PropagateTags")
)
tags_properties = {**(globals_var.get("Tags") or {}), **(resource.properties.get("Tags") or {})}

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

[BUG] The new merge unpacks both Tags values as mappings unconditionally, before the if guard:

tags_properties = {**(globals_var.get("Tags") or {}), (resource.properties.get("Tags") or {})}

Two consequences that the previous a or b form did not have:

  1. A non-mapping Tags now crashes the transform with a raw TypeError. Tags is only type-checked much later, when the resource is constructed ("Tags": PropertyType(False, IS_DICT) in samtranslator/model/sam_resources.py:181); Parser.validate_datatypes only verifies that Properties is a map. So a CFN-style list, e.g.
Tags:
   - Key: env
    Value: prod

reaches this line and raises TypeError: 'list' object is not a mapping. on_before_transform_template only catches InvalidEventException, so it escapes as an unhandled error instead of the customer-facing InvalidResourceException about the invalid Tags type. Note this now happens even when tags are not propagated at all (PropagateTags unset or false), because the merge is evaluated before the should_propagate_tags check — previously that line was a plain assignment and the guard kept the bad value untouched.

  1. A whole-Tags intrinsic gets corrupted. For Tags: {"Fn::If": [...]} the merge yields {"env": "prod", "Fn::If": [...]}, so Fn::If becomes a tag key on the implicit API. This also diverges from the Globals semantics the merge is meant to mirror: GlobalProperties._token_of classifies intrinsic dicts as PRIMITIVE, so _do_merge returns _prefer_local and the function itself ends up with the local intrinsic only.

Guarding the merge keeps both paths behaving as before:

global_tags = globals_var.get("Tags")
local_tags = resource.properties.get("Tags")
if isinstance(global_tags, dict) and isinstance(local_tags, dict) and not is_intrinsics(local_tags):
   tags_properties = {**global_tags, local_tags}
else:
   tags_properties = local_tags if local_tags is not None else global_tags


if implicit_api_resource and tags_properties and should_propagate_tags:
# This makes an assumption that the SAM resource has 'Tags' property and is a dictionary.
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
Globals:
Function:
Tags:
env: prod

Resources:
ApiFunction: # Adds a GET api endpoint at "/" to the ApiGatewayApi via an Api event
Type: AWS::Serverless::Function
Properties:
PropagateTags: true
Tags:
app: foo
Events:
ApiEvent:
Type: Api
Properties:
Path: /
Method: get
Runtime: python3.7
Handler: index.handler
InlineCode: |-
def handler(event, context):
return {'body': 'Hello World!', 'statusCode': 200}
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
Globals:
Function:
PropagateTags: true
Tags:
test: 'yes'

Resources:
ApiFunction: # Adds a GET api endpoint at "/" to the ApiGatewayApi via an Api event
Type: AWS::Serverless::Function
Properties:
PropagateTags: false
Events:
ApiEvent:
Type: Api
Properties:
Path: /
Method: get
Runtime: python3.7
Handler: index.handler
InlineCode: |-
def handler(event, context):
return {'body': 'Hello World!', 'statusCode': 200}
Original file line number Diff line number Diff line change
@@ -0,0 +1,163 @@
{
"Resources": {
"ApiFunction": {
"Properties": {
"Code": {
"ZipFile": "def handler(event, context):\n return {'body': 'Hello World!', 'statusCode': 200}"
},
"Handler": "index.handler",
"Role": {
"Fn::GetAtt": [
"ApiFunctionRole",
"Arn"
]
},
"Runtime": "python3.7",
"Tags": [
{
"Key": "env",
"Value": "prod"
},
{
"Key": "app",
"Value": "foo"
}
]
},
"Type": "AWS::Lambda::Function"
},
"ApiFunctionApiEventPermissionProd": {
"Properties": {
"Action": "lambda:InvokeFunction",
"FunctionName": {
"Ref": "ApiFunction"
},
"Principal": "apigateway.amazonaws.com",
"SourceArn": {
"Fn::Sub": [
"arn:aws-cn:execute-api:${AWS::Region}:${AWS::AccountId}:${__ApiId__}/${__Stage__}/GET/",
{
"__ApiId__": {
"Ref": "ServerlessRestApi"
},
"__Stage__": "*"
}
]
}
},
"Type": "AWS::Lambda::Permission"
},
"ApiFunctionRole": {
"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": "env",
"Value": "prod"
},
{
"Key": "app",
"Value": "foo"
}
]
},
"Type": "AWS::IAM::Role"
},
"ServerlessRestApi": {
"Properties": {
"Body": {
"info": {
"title": {
"Ref": "AWS::StackName"
},
"version": "1.0"
},
"paths": {
"/": {
"get": {
"responses": {},
"x-amazon-apigateway-integration": {
"httpMethod": "POST",
"type": "aws_proxy",
"uri": {
"Fn::Sub": "arn:aws-cn:apigateway:${AWS::Region}:lambda:path/2015-03-31/functions/${ApiFunction.Arn}/invocations"
}
}
}
}
},
"swagger": "2.0"
},
"EndpointConfiguration": {
"Types": [
"REGIONAL"
]
},
"Parameters": {
"endpointConfigurationTypes": "REGIONAL"
},
"Tags": [
{
"Key": "env",
"Value": "prod"
},
{
"Key": "app",
"Value": "foo"
}
]
},
"Type": "AWS::ApiGateway::RestApi"
},
"ServerlessRestApiDeploymentb0ed1521b2": {
"Properties": {
"Description": "RestApi deployment id: b0ed1521b2c5e65da74d55d16b139143ae483503",
"RestApiId": {
"Ref": "ServerlessRestApi"
},
"StageName": "Stage"
},
"Type": "AWS::ApiGateway::Deployment"
},
"ServerlessRestApiProdStage": {
"Properties": {
"DeploymentId": {
"Ref": "ServerlessRestApiDeploymentb0ed1521b2"
},
"RestApiId": {
"Ref": "ServerlessRestApi"
},
"StageName": "Prod",
"Tags": [
{
"Key": "env",
"Value": "prod"
},
{
"Key": "app",
"Value": "foo"
}
]
},
"Type": "AWS::ApiGateway::Stage"
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,143 @@
{
"Resources": {
"ApiFunction": {
"Properties": {
"Code": {
"ZipFile": "def handler(event, context):\n return {'body': 'Hello World!', 'statusCode': 200}"
},
"Handler": "index.handler",
"Role": {
"Fn::GetAtt": [
"ApiFunctionRole",
"Arn"
]
},
"Runtime": "python3.7",
"Tags": [
{
"Key": "lambda:createdBy",
"Value": "SAM"
},
{
"Key": "test",
"Value": "yes"
}
]
},
"Type": "AWS::Lambda::Function"
},
"ApiFunctionApiEventPermissionProd": {
"Properties": {
"Action": "lambda:InvokeFunction",
"FunctionName": {
"Ref": "ApiFunction"
},
"Principal": "apigateway.amazonaws.com",
"SourceArn": {
"Fn::Sub": [
"arn:aws-cn:execute-api:${AWS::Region}:${AWS::AccountId}:${__ApiId__}/${__Stage__}/GET/",
{
"__ApiId__": {
"Ref": "ServerlessRestApi"
},
"__Stage__": "*"
}
]
}
},
"Type": "AWS::Lambda::Permission"
},
"ApiFunctionRole": {
"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"
},
{
"Key": "test",
"Value": "yes"
}
]
},
"Type": "AWS::IAM::Role"
},
"ServerlessRestApi": {
"Properties": {
"Body": {
"info": {
"title": {
"Ref": "AWS::StackName"
},
"version": "1.0"
},
"paths": {
"/": {
"get": {
"responses": {},
"x-amazon-apigateway-integration": {
"httpMethod": "POST",
"type": "aws_proxy",
"uri": {
"Fn::Sub": "arn:aws-cn:apigateway:${AWS::Region}:lambda:path/2015-03-31/functions/${ApiFunction.Arn}/invocations"
}
}
}
}
},
"swagger": "2.0"
},
"EndpointConfiguration": {
"Types": [
"REGIONAL"
]
},
"Parameters": {
"endpointConfigurationTypes": "REGIONAL"
}
},
"Type": "AWS::ApiGateway::RestApi"
},
"ServerlessRestApiDeploymentb0ed1521b2": {
"Properties": {
"Description": "RestApi deployment id: b0ed1521b2c5e65da74d55d16b139143ae483503",
"RestApiId": {
"Ref": "ServerlessRestApi"
},
"StageName": "Stage"
},
"Type": "AWS::ApiGateway::Deployment"
},
"ServerlessRestApiProdStage": {
"Properties": {
"DeploymentId": {
"Ref": "ServerlessRestApiDeploymentb0ed1521b2"
},
"RestApiId": {
"Ref": "ServerlessRestApi"
},
"StageName": "Prod"
},
"Type": "AWS::ApiGateway::Stage"
}
}
}
Loading