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

Allow SAM Api and HttpApi to Propagate Tags to Generated Resources #3193

Merged
merged 3 commits into from
Jun 1, 2023
Merged
Show file tree
Hide file tree
Changes from 2 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,27 @@
from unittest.case import skipIf

from integration.config.service_names import HTTP_API, REST_API
from integration.helpers.base_test import BaseTest
from integration.helpers.resource import current_region_does_not_support


@skipIf(
current_region_does_not_support([HTTP_API, REST_API]),
"REST_API or HTTP_API is not supported in this testing region",
)
class TestApiAndHttpiWithPropagateTags(BaseTest):
def test_api_and_httpapi_with_propagate_tags(self):
self.create_and_verify_stack("combination/api_with_propagate_tags")

outputs = self.get_stack_outputs()

api_client = self.client_provider.api_client
api_v2_client = self.client_provider.api_v2_client

tags = api_client.get_tags(resourceArn=outputs["ApiArn"])
self.assertEqual(tags["tags"]["Key1"], "Value1")
self.assertEqual(tags["tags"]["Key2"], "Value2")

tags = api_v2_client.get_tags(ResourceArn=outputs["HttpApiArn"])
self.assertEqual(tags["Tags"]["Tag1"], "value1")
self.assertEqual(tags["Tags"]["Tag2"], "value2")
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
[
{
"LogicalResourceId": "MyApiDeployment",
"ResourceType": "AWS::ApiGateway::Deployment"
},
{
"LogicalResourceId": "MyApi",
"ResourceType": "AWS::ApiGateway::RestApi"
},
{
"LogicalResourceId": "MyApiProdStage",
"ResourceType": "AWS::ApiGateway::Stage"
},
{
"LogicalResourceId": "MyFunction",
"ResourceType": "AWS::Lambda::Function"
},
{
"LogicalResourceId": "MyFunctionPostApiPermission",
"ResourceType": "AWS::Lambda::Permission"
},
{
"LogicalResourceId": "MyFunctionRestApiPermissionProd",
"ResourceType": "AWS::Lambda::Permission"
},
{
"LogicalResourceId": "MyFunctionRole",
"ResourceType": "AWS::IAM::Role"
},
{
"LogicalResourceId": "MyHttpApi",
"ResourceType": "AWS::ApiGatewayV2::Api"
},
{
"LogicalResourceId": "MyHttpApiApiGatewayDefaultStage",
"ResourceType": "AWS::ApiGatewayV2::Stage"
}
]
Original file line number Diff line number Diff line change
@@ -0,0 +1,69 @@
Resources:
MyApi:
Type: AWS::Serverless::Api
Properties:
StageName: Prod
Tags:
Key1: Value1
Key2: Value2
PropagateTags: true
GatewayResponses:
DEFAULT_4XX:
ResponseParameters:
Headers:
Access-Control-Allow-Origin: "'*'"

MyFunction:
Type: AWS::Serverless::Function
Properties:
Handler: index.handler
Runtime: nodejs14.x
InlineCode: |
exports.handler = async (event, context, callback) => {
return {
statusCode: 200,
body: 'Success'
}
}
Events:
RestApi:
Type: Api
Properties:
RestApiId:
Ref: MyApi
Method: get
Path: /iam
Auth:
Authorizer: AWS_IAM

PostApi:
Type: HttpApi
Properties:
ApiId:
Ref: MyHttpApi
Method: POST
Path: /post

MyHttpApi:
Type: AWS::Serverless::HttpApi
Properties:
Tags:
Tag1: value1
Tag2: value2

Outputs:
ApiArn:
Description: API endpoint URL for Prod environment
Value:
Fn::Sub:
- arn:${AWS::Partition}:apigateway:${AWS::Region}::/restapis/${ApiId}
- ApiId: !Ref MyApi

HttpApiArn:
Description: API endpoint URL for Prod environment
Value:
Fn::Sub:
- arn:${AWS::Partition}:apigateway:${AWS::Region}::/apis/${ApiId}
- ApiId: !Ref MyHttpApi
Metadata:
SamTransformTest: true
2 changes: 2 additions & 0 deletions samtranslator/internal/schema_source/aws_serverless_api.py
Original file line number Diff line number Diff line change
Expand Up @@ -308,6 +308,7 @@ class Properties(BaseModel):
OpenApiVersion: Optional[OpenApiVersion] = properties("OpenApiVersion")
StageName: SamIntrinsicable[str] = properties("StageName")
Tags: Optional[DictStrAny] = properties("Tags")
PropagateTags: Optional[bool] # TODO: add docs
TracingEnabled: Optional[TracingEnabled] = passthrough_prop(
PROPERTIES_STEM,
"TracingEnabled",
Expand Down Expand Up @@ -373,6 +374,7 @@ class Globals(BaseModel):
OpenApiVersion: Optional[OpenApiVersion] = properties("OpenApiVersion")
Domain: Optional[Domain] = properties("Domain")
AlwaysDeploy: Optional[AlwaysDeploy] = properties("AlwaysDeploy")
PropagateTags: Optional[bool] # TODO: add docs


class Resource(ResourceAttributes):
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -127,6 +127,7 @@ class Properties(BaseModel):
StageName: Optional[PassThroughProp] = properties("StageName")
StageVariables: Optional[StageVariables] = properties("StageVariables")
Tags: Optional[Tags] = properties("Tags")
PropagateTags: Optional[bool] # TODO: add docs
Name: Optional[PassThroughProp] = properties("Name")


Expand All @@ -140,6 +141,7 @@ class Globals(BaseModel):
Domain: Optional[Domain] = properties("Domain")
CorsConfiguration: Optional[CorsConfigurationType] = properties("CorsConfiguration")
DefaultRouteSettings: Optional[DefaultRouteSettings] = properties("DefaultRouteSettings")
PropagateTags: Optional[bool] # TODO: add docs


class Resource(ResourceAttributes):
Expand Down
37 changes: 36 additions & 1 deletion samtranslator/model/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@
InvalidResourcePropertyTypeException,
)
from samtranslator.model.tags.resource_tagging import get_tag_list
from samtranslator.model.types import IS_DICT, IS_STR, Validator, any_type, is_type
from samtranslator.model.types import IS_DICT, IS_STR, PassThrough, Validator, any_type, is_type
from samtranslator.plugins import LifeCycleEvents

RT = TypeVar("RT", bound=BaseModel) # return type
Expand Down Expand Up @@ -126,6 +126,7 @@ class Resource(ABC):
# are in "property_types" or "_keywords". We can set this to False in the inheriting class definition so we can
# update other class variables as well after instantiation.
validate_setattr: bool = True
Tags: Optional[PassThrough]

def __init__(
self,
Expand Down Expand Up @@ -433,6 +434,21 @@ def get_passthrough_resource_attributes(self) -> Dict[str, Any]:
attributes[resource_attribute] = self.resource_attributes.get(resource_attribute)
return attributes

def assign_tags(self, tags: Dict[str, Any]) -> None:
GavinZZ marked this conversation as resolved.
Show resolved Hide resolved
"""
Assigns tags to the resource. This function assumes that generated resources always have
the tags property called `Tags`

There are CFN resources that do not follow the assumption such as 'AWS::Backup::Framework'
We may need to modify the code to account for thia scenario in the future. We could do this
GavinZZ marked this conversation as resolved.
Show resolved Hide resolved
by function overriding this function in the class definition of that resource. An example
is provided in 'AWS::ApiGatewayV2::Api'

:param tags: Dictionary of tags to be assigned to the resource
GavinZZ marked this conversation as resolved.
Show resolved Hide resolved
"""
if "Tags" in self.property_types:
self.Tags = get_tag_list(tags)


class ResourceMacro(Resource, metaclass=ABCMeta):
"""A ResourceMacro object represents a CloudFormation macro. A macro appears in the CloudFormation template in the
Expand Down Expand Up @@ -536,6 +552,25 @@ def _construct_tag_list(
# customer's knowledge.
return get_tag_list(sam_tag) + get_tag_list(additional_tags) + get_tag_list(tags)

@staticmethod
def propagate_tags(
resources: List[Resource], tags: Optional[Dict[str, Any]], propagate_tags: Optional[bool] = False
GavinZZ marked this conversation as resolved.
Show resolved Hide resolved
) -> None:
"""
Propagates tags to the resources.

:param propagate_tags: Whether we should pass the tags to generated resources.
:param resources: List of generated resources
:param tags: dictionary of tags to propagate to the resources.

:return: None
"""
if not propagate_tags or not tags:
return

for resource in resources:
ssenchenko marked this conversation as resolved.
Show resolved Hide resolved
resource.assign_tags(tags)

def _check_tag(self, reserved_tag_name, tags): # type: ignore[no-untyped-def]
if reserved_tag_name in tags:
raise InvalidResourceException(
Expand Down
5 changes: 5 additions & 0 deletions samtranslator/model/apigateway.py
GavinZZ marked this conversation as resolved.
Show resolved Hide resolved
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@ class ApiGatewayRestApi(Resource):
"MinimumCompressionSize": GeneratedProperty(),
"Mode": GeneratedProperty(),
"ApiKeySourceType": GeneratedProperty(),
"Tags": GeneratedProperty(),
}

Body: Optional[Dict[str, Any]]
Expand All @@ -42,6 +43,7 @@ class ApiGatewayRestApi(Resource):
MinimumCompressionSize: Optional[PassThrough]
Mode: Optional[PassThrough]
ApiKeySourceType: Optional[PassThrough]
Tags: Optional[PassThrough]

runtime_attrs = {"rest_api_id": lambda self: ref(self.logical_id)}

Expand Down Expand Up @@ -214,6 +216,7 @@ class ApiGatewayDomainName(Resource):
"MutualTlsAuthentication": GeneratedProperty(),
"SecurityPolicy": GeneratedProperty(),
"CertificateArn": GeneratedProperty(),
"Tags": GeneratedProperty(),
"OwnershipVerificationCertificateArn": GeneratedProperty(),
}

Expand All @@ -223,6 +226,7 @@ class ApiGatewayDomainName(Resource):
MutualTlsAuthentication: Optional[Dict[str, Any]]
SecurityPolicy: Optional[PassThrough]
CertificateArn: Optional[PassThrough]
Tags: Optional[PassThrough]
OwnershipVerificationCertificateArn: Optional[PassThrough]


Expand Down Expand Up @@ -266,6 +270,7 @@ class ApiGatewayApiKey(Resource):
"Enabled": GeneratedProperty(),
"GenerateDistinctId": GeneratedProperty(),
"Name": GeneratedProperty(),
"Tags": GeneratedProperty(),
"StageKeys": GeneratedProperty(),
"Value": GeneratedProperty(),
}
Expand Down
35 changes: 34 additions & 1 deletion samtranslator/model/apigatewayv2.py
Original file line number Diff line number Diff line change
Expand Up @@ -20,10 +20,22 @@ class ApiGatewayV2HttpApi(Resource):
"FailOnWarnings": GeneratedProperty(),
"DisableExecuteApiEndpoint": GeneratedProperty(),
"BasePath": GeneratedProperty(),
"Tags": GeneratedProperty(),
"CorsConfiguration": GeneratedProperty(),
}

runtime_attrs = {"http_api_id": lambda self: ref(self.logical_id)}
Tags: Optional[PassThrough]

def assign_tags(self, tags: Dict[str, Any]) -> None:
"""Overriding default 'assign_tags' function in Resource class

Function to assign tags to the resource
:param tags: Tags to be assigned to the resource

"""
if tags is not None and "Tags" in self.property_types:
self.Tags = tags


class ApiGatewayV2Stage(Resource):
Expand All @@ -42,6 +54,17 @@ class ApiGatewayV2Stage(Resource):
}

runtime_attrs = {"stage_name": lambda self: ref(self.logical_id)}
Tags: Optional[PassThrough]

def assign_tags(self, tags: Dict[str, Any]) -> None:
"""Overriding default 'assign_tags' function in Resource class

Function to assign tags to the resource
:param tags: Tags to be assigned to the resource

"""
if tags is not None and "Tags" in self.property_types:
self.Tags = tags


class ApiGatewayV2DomainName(Resource):
Expand All @@ -56,7 +79,17 @@ class ApiGatewayV2DomainName(Resource):
DomainName: Intrinsicable[str]
DomainNameConfigurations: Optional[List[Dict[str, Any]]]
MutualTlsAuthentication: Optional[Dict[str, Any]]
Tags: Optional[Dict[str, Any]]
Tags: Optional[PassThrough]

def assign_tags(self, tags: Dict[str, Any]) -> None:
"""Overriding default 'assign_tags' function in Resource class

Function to assign tags to the resource
:param tags: Tags to be assigned to the resource

"""
if tags is not None and "Tags" in self.property_types:
self.Tags = tags


class ApiGatewayV2ApiMapping(Resource):
Expand Down
14 changes: 12 additions & 2 deletions samtranslator/model/sam_resources.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
""" SAM macro definitions """
""" SAM macro definitions """
import copy
from contextlib import suppress
from typing import Any, Callable, Dict, List, Optional, Tuple, Union, cast
Expand Down Expand Up @@ -1155,6 +1155,7 @@ class SamApi(SamResourceMacro):
"Name": PropertyType(False, one_of(IS_STR, IS_DICT)),
"StageName": PropertyType(True, one_of(IS_STR, IS_DICT)),
"Tags": PropertyType(False, IS_DICT),
"PropagateTags": PropertyType(False, IS_BOOL),
"DefinitionBody": PropertyType(False, IS_DICT),
"DefinitionUri": PropertyType(False, one_of(IS_STR, IS_DICT)),
"MergeDefinitions": Property(False, IS_BOOL),
Expand Down Expand Up @@ -1185,6 +1186,7 @@ class SamApi(SamResourceMacro):
Name: Optional[Intrinsicable[str]]
StageName: Optional[Intrinsicable[str]]
Tags: Optional[Dict[str, Any]]
PropagateTags: Optional[bool]
DefinitionBody: Optional[Dict[str, Any]]
DefinitionUri: Optional[Intrinsicable[str]]
MergeDefinitions: Optional[bool]
Expand Down Expand Up @@ -1276,7 +1278,11 @@ def to_cloudformation(self, **kwargs) -> List[Resource]: # type: ignore[no-unty
always_deploy=self.AlwaysDeploy,
)

return api_generator.to_cloudformation(redeploy_restapi_parameters, route53_record_set_groups)
generated_resources = api_generator.to_cloudformation(redeploy_restapi_parameters, route53_record_set_groups)

self.propagate_tags(generated_resources, self.Tags, self.PropagateTags)

return generated_resources


class SamHttpApi(SamResourceMacro):
Expand All @@ -1293,6 +1299,7 @@ class SamHttpApi(SamResourceMacro):
"Name": PassThroughProperty(False),
"StageName": PropertyType(False, one_of(IS_STR, IS_DICT)),
"Tags": PropertyType(False, IS_DICT),
"PropagateTags": PropertyType(False, IS_BOOL),
"DefinitionBody": PropertyType(False, IS_DICT),
"DefinitionUri": PropertyType(False, one_of(IS_STR, IS_DICT)),
"StageVariables": PropertyType(False, IS_DICT),
Expand All @@ -1310,6 +1317,7 @@ class SamHttpApi(SamResourceMacro):
Name: Optional[Any]
StageName: Optional[Intrinsicable[str]]
Tags: Optional[Dict[str, Any]]
PropagateTags: Optional[bool]
DefinitionBody: Optional[Dict[str, Any]]
DefinitionUri: Optional[Intrinsicable[str]]
StageVariables: Optional[Dict[str, Intrinsicable[str]]]
Expand Down Expand Up @@ -1387,6 +1395,8 @@ def to_cloudformation(self, **kwargs): # type: ignore[no-untyped-def]
if stage:
resources.append(stage)

self.propagate_tags(resources, self.Tags, self.PropagateTags)

return resources


Expand Down
Loading