Skip to content

Commit

Permalink
Allow SAM Api and HttpApi to Propagate Tags to Generated Resources (#…
Browse files Browse the repository at this point in the history
  • Loading branch information
GavinZZ committed Jun 1, 2023
1 parent fd19feb commit 27dfa85
Show file tree
Hide file tree
Showing 25 changed files with 1,186 additions and 6 deletions.
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
35 changes: 34 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,19 @@ 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:
"""
Assigns tags to the resource. This function assumes that generated resources always have
the tags property called `Tags` that takes a list of key-value objects.
Override this function if the above assumptions do not apply to the resource (e.g. different
property name or type (see e.g. 'AWS::ApiGatewayV2::Api').
:param tags: Dictionary of tags to be assigned to the resource
"""
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 +550,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
) -> 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:
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
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

0 comments on commit 27dfa85

Please sign in to comment.