Skip to content

Commit a3a99d3

Browse files
qingchmmndeveciCoshUSmoelasmar
authored
Release/v1.36.0 (#2042) (#2050)
* refactor: Optimize shared API usage plan handling (#1973) * fix: use instance variables for generating shared api usage plan * add extra log statements * fix: Added SAR Support Check (#1972) * Added SAR Support Check * Added docstring and Removed Instance Initialization for Class Method * set log level explicitly * update pyyaml version to get the security update (#1974) * fix: use instance variables for generating shared api usage plan * add extra log statements * set log level explicitly * black formatting * black formatting Co-authored-by: Cosh_ <[email protected]> Co-authored-by: Mohamed Elasmar <[email protected]> * feat: Resource level attributes support (#2008) * Fix for invalid MQ event source managed policy * Fix for invalid managed policy for MQ, included support for new MQ event source property, updated test cases * Black reformatting * Test case changes * Changed policy name * Modified test cases with new policy name * Added resource attributes and unit tests * Resource attributes initial work * Passthrough attributes for some resources, updated some tests * Resolve merge conflicts * Fixed a typo * Modified implicit api plugin for resource attributes support * Partial update of the tests * Partially updated test cases, black reformatted * Partially updated test templates * Partially updated test templates * Partially updated test templates * Added event bridge support for passthrough resource attributes * Partially updated test templates (up to function with amq kms) * Partially updated test templates (up to sns) * Partially updated test templates (all the ones left) * Prevented passthrough resource attributes from changing layer version hashes * Added test to verify resource passthrough precedence for implicit api * Modified tests related to lambda layer to revert the hash changes, keeping the hash the same with resource attributes added * fix: mutable default values in method definitions (#1997) * fix: remove explicit logging level set in single module (#1998) * run automated tests for resource level attribute support * Skipping metadata in layer hashing * Refactored the classes for TestTranslatorEndToEnd and TestResourceLevelAttributes to share the same parent class * Added new translator tests for version and layer resources * Added new unit tests * Removed after transform resource plugin * Black reformatting * Refactoring implicit api plugin support for DeletionPolicy and UpdateReplacePolicy * Refactoring to improve code quality * Added simple documentation * Black reformatting * Added input template that was missing * Refactoring: use sets instead of lists for implicit api plugin * Changing import to be compatible with py2.7 * Changing test deployment hashes to their actual values Co-authored-by: Mehmet Nuri Deveci <[email protected]> * chore: bump version to 1.36.0 (#2014) * fix: Shared Usage Plan scenarios for Resource Level Attribute Support (#2040) * do not propagate attributes to CognitoUserPool * shared usage plan with propagated resource level attributes * add unit tests * Added test templates for shared usage plan with resource attributes * Black reformatting * Removing unused import * fix python2 hashes Co-authored-by: Qingchuan Ma <[email protected]> Co-authored-by: Mehmet Nuri Deveci <[email protected]> Co-authored-by: Cosh_ <[email protected]> Co-authored-by: Mohamed Elasmar <[email protected]> Co-authored-by: Mehmet Nuri Deveci <[email protected]> Co-authored-by: Cosh_ <[email protected]> Co-authored-by: Mohamed Elasmar <[email protected]>
1 parent c5d0ed2 commit a3a99d3

File tree

51 files changed

+6649
-1574
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

51 files changed

+6649
-1574
lines changed

samtranslator/__init__.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1 +1 @@
1-
__version__ = "1.35.0"
1+
__version__ = "1.36.0"

samtranslator/model/__init__.py

Lines changed: 25 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -43,7 +43,11 @@ class Resource(object):
4343
property_types = None
4444
_keywords = ["logical_id", "relative_id", "depends_on", "resource_attributes"]
4545

46-
_supported_resource_attributes = ["DeletionPolicy", "UpdatePolicy", "Condition"]
46+
# For attributes in this list, they will be passed into the translated template for the same resource itself.
47+
_supported_resource_attributes = ["DeletionPolicy", "UpdatePolicy", "Condition", "UpdateReplacePolicy", "Metadata"]
48+
# For attributes in this list, they will be passed into the translated template for the same resource,
49+
# as well as all the auto-generated resources that are created from this resource.
50+
_pass_through_attributes = ["Condition", "DeletionPolicy", "UpdateReplacePolicy"]
4751

4852
# Runtime attributes that can be qureied resource. They are CloudFormation attributes like ARN, Name etc that
4953
# will be resolvable at runtime. This map will be implemented by sub-classes to express list of attributes they
@@ -76,6 +80,22 @@ def __init__(self, logical_id, relative_id=None, depends_on=None, attributes=Non
7680
for attr, value in attributes.items():
7781
self.set_resource_attribute(attr, value)
7882

83+
@classmethod
84+
def get_supported_resource_attributes(cls):
85+
"""
86+
A getter method for the supported resource attributes
87+
returns: a tuple that contains the name of all supported resource attributes
88+
"""
89+
return tuple(cls._supported_resource_attributes)
90+
91+
@classmethod
92+
def get_pass_through_attributes(cls):
93+
"""
94+
A getter method for the resource attributes to be passed to auto-generated resources
95+
returns: a tuple that contains the name of all pass through attributes
96+
"""
97+
return tuple(cls._pass_through_attributes)
98+
7999
@classmethod
80100
def from_dict(cls, logical_id, resource_dict, relative_id=None, sam_plugins=None):
81101
"""Constructs a Resource object with the given logical id, based on the given resource dict. The resource dict
@@ -318,9 +338,10 @@ def get_passthrough_resource_attributes(self):
318338
319339
:return: Dictionary of resource attributes.
320340
"""
321-
attributes = None
322-
if "Condition" in self.resource_attributes:
323-
attributes = {"Condition": self.resource_attributes["Condition"]}
341+
attributes = {}
342+
for resource_attribute in self.get_pass_through_attributes():
343+
if resource_attribute in self.resource_attributes:
344+
attributes[resource_attribute] = self.resource_attributes.get(resource_attribute)
324345
return attributes
325346

326347

samtranslator/model/api/api_generator.py

Lines changed: 145 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,8 @@
1+
import logging
12
from collections import namedtuple
3+
24
from six import string_types
3-
from samtranslator.model.intrinsics import ref, fnGetAtt
5+
from samtranslator.model.intrinsics import ref, fnGetAtt, make_or_condition
46
from samtranslator.model.apigateway import (
57
ApiGatewayDeployment,
68
ApiGatewayRestApi,
@@ -14,7 +16,7 @@
1416
ApiGatewayApiKey,
1517
)
1618
from samtranslator.model.route53 import Route53RecordSetGroup
17-
from samtranslator.model.exceptions import InvalidResourceException
19+
from samtranslator.model.exceptions import InvalidResourceException, InvalidTemplateException
1820
from samtranslator.model.s3_utils.uri_parser import parse_s3_uri
1921
from samtranslator.region_configuration import RegionConfiguration
2022
from samtranslator.swagger.swagger import SwaggerEditor
@@ -24,6 +26,9 @@
2426
from samtranslator.translator.arn_generator import ArnGenerator
2527
from samtranslator.model.tags.resource_tagging import get_tag_list
2628

29+
LOG = logging.getLogger(__name__)
30+
LOG.setLevel(logging.INFO)
31+
2732
_CORS_WILDCARD = "'*'"
2833
CorsProperties = namedtuple(
2934
"_CorsProperties", ["AllowMethods", "AllowHeaders", "AllowOrigin", "MaxAge", "AllowCredentials"]
@@ -52,12 +57,100 @@
5257
GatewayResponseProperties = ["ResponseParameters", "ResponseTemplates", "StatusCode"]
5358

5459

55-
class ApiGenerator(object):
56-
usage_plan_shared = False
57-
stage_keys_shared = list()
58-
api_stages_shared = list()
59-
depends_on_shared = list()
60+
class SharedApiUsagePlan(object):
61+
"""
62+
Collects API information from different API resources in the same template,
63+
so that these information can be used in the shared usage plan
64+
"""
65+
66+
SHARED_USAGE_PLAN_CONDITION_NAME = "SharedUsagePlanCondition"
67+
68+
def __init__(self):
69+
self.usage_plan_shared = False
70+
self.stage_keys_shared = list()
71+
self.api_stages_shared = list()
72+
self.depends_on_shared = list()
73+
74+
# shared resource level attributes
75+
self.conditions = set()
76+
self.any_api_without_condition = False
77+
self.deletion_policy = None
78+
self.update_replace_policy = None
79+
80+
def get_combined_resource_attributes(self, resource_attributes, conditions):
81+
"""
82+
This method returns a dictionary which combines 'DeletionPolicy', 'UpdateReplacePolicy' and 'Condition'
83+
values of API definitions that could be used in Shared Usage Plan resources.
84+
85+
Parameters
86+
----------
87+
resource_attributes: Dict[str]
88+
A dictionary of resource level attributes of the API resource
89+
conditions: Dict[str]
90+
Conditions section of the template
91+
"""
92+
self._set_deletion_policy(resource_attributes.get("DeletionPolicy"))
93+
self._set_update_replace_policy(resource_attributes.get("UpdateReplacePolicy"))
94+
self._set_condition(resource_attributes.get("Condition"), conditions)
95+
96+
combined_resource_attributes = dict()
97+
if self.deletion_policy:
98+
combined_resource_attributes["DeletionPolicy"] = self.deletion_policy
99+
if self.update_replace_policy:
100+
combined_resource_attributes["UpdateReplacePolicy"] = self.update_replace_policy
101+
# do not set Condition if any of the API resource does not have Condition in it
102+
if self.conditions and not self.any_api_without_condition:
103+
combined_resource_attributes["Condition"] = SharedApiUsagePlan.SHARED_USAGE_PLAN_CONDITION_NAME
104+
105+
return combined_resource_attributes
106+
107+
def _set_deletion_policy(self, deletion_policy):
108+
if deletion_policy:
109+
if self.deletion_policy:
110+
# update only if new deletion policy is Retain
111+
if deletion_policy == "Retain":
112+
self.deletion_policy = deletion_policy
113+
else:
114+
self.deletion_policy = deletion_policy
115+
116+
def _set_update_replace_policy(self, update_replace_policy):
117+
if update_replace_policy:
118+
if self.update_replace_policy:
119+
# if new value is Retain or
120+
# new value is retain and current value is Delete then update its value
121+
if (update_replace_policy == "Retain") or (
122+
update_replace_policy == "Snapshot" and self.update_replace_policy == "Delete"
123+
):
124+
self.update_replace_policy = update_replace_policy
125+
else:
126+
self.update_replace_policy = update_replace_policy
127+
128+
def _set_condition(self, condition, template_conditions):
129+
# if there are any API without condition, then skip
130+
if self.any_api_without_condition:
131+
return
60132

133+
if condition and condition not in self.conditions:
134+
135+
if template_conditions is None:
136+
raise InvalidTemplateException(
137+
"Can't have condition without having 'Conditions' section in the template"
138+
)
139+
140+
if self.conditions:
141+
self.conditions.add(condition)
142+
or_condition = make_or_condition(self.conditions)
143+
template_conditions[SharedApiUsagePlan.SHARED_USAGE_PLAN_CONDITION_NAME] = or_condition
144+
else:
145+
self.conditions.add(condition)
146+
template_conditions[SharedApiUsagePlan.SHARED_USAGE_PLAN_CONDITION_NAME] = condition
147+
elif condition is None:
148+
self.any_api_without_condition = True
149+
if template_conditions and SharedApiUsagePlan.SHARED_USAGE_PLAN_CONDITION_NAME in template_conditions:
150+
del template_conditions[SharedApiUsagePlan.SHARED_USAGE_PLAN_CONDITION_NAME]
151+
152+
153+
class ApiGenerator(object):
61154
def __init__(
62155
self,
63156
logical_id,
@@ -69,6 +162,8 @@ def __init__(
69162
definition_uri,
70163
name,
71164
stage_name,
165+
shared_api_usage_plan,
166+
template_conditions,
72167
tags=None,
73168
endpoint_configuration=None,
74169
method_settings=None,
@@ -134,6 +229,8 @@ def __init__(
134229
self.models = models
135230
self.domain = domain
136231
self.description = description
232+
self.shared_api_usage_plan = shared_api_usage_plan
233+
self.template_conditions = template_conditions
137234

138235
def _construct_rest_api(self):
139236
"""Constructs and returns the ApiGateway RestApi.
@@ -617,7 +714,11 @@ def _construct_usage_plan(self, rest_api_stage=None):
617714
# create usage plan for this api only
618715
elif usage_plan_properties.get("CreateUsagePlan") == "PER_API":
619716
usage_plan_logical_id = self.logical_id + "UsagePlan"
620-
usage_plan = ApiGatewayUsagePlan(logical_id=usage_plan_logical_id, depends_on=[self.logical_id])
717+
usage_plan = ApiGatewayUsagePlan(
718+
logical_id=usage_plan_logical_id,
719+
depends_on=[self.logical_id],
720+
attributes=self.passthrough_resource_attributes,
721+
)
621722
api_stages = list()
622723
api_stage = dict()
623724
api_stage["ApiId"] = ref(self.logical_id)
@@ -630,18 +731,23 @@ def _construct_usage_plan(self, rest_api_stage=None):
630731

631732
# create a usage plan for all the Apis
632733
elif create_usage_plan == "SHARED":
734+
LOG.info("Creating SHARED usage plan for all the Apis")
633735
usage_plan_logical_id = "ServerlessUsagePlan"
634-
if self.logical_id not in ApiGenerator.depends_on_shared:
635-
ApiGenerator.depends_on_shared.append(self.logical_id)
736+
if self.logical_id not in self.shared_api_usage_plan.depends_on_shared:
737+
self.shared_api_usage_plan.depends_on_shared.append(self.logical_id)
636738
usage_plan = ApiGatewayUsagePlan(
637-
logical_id=usage_plan_logical_id, depends_on=ApiGenerator.depends_on_shared
739+
logical_id=usage_plan_logical_id,
740+
depends_on=self.shared_api_usage_plan.depends_on_shared,
741+
attributes=self.shared_api_usage_plan.get_combined_resource_attributes(
742+
self.passthrough_resource_attributes, self.template_conditions
743+
),
638744
)
639745
api_stage = dict()
640746
api_stage["ApiId"] = ref(self.logical_id)
641747
api_stage["Stage"] = ref(rest_api_stage.logical_id)
642-
if api_stage not in ApiGenerator.api_stages_shared:
643-
ApiGenerator.api_stages_shared.append(api_stage)
644-
usage_plan.ApiStages = ApiGenerator.api_stages_shared
748+
if api_stage not in self.shared_api_usage_plan.api_stages_shared:
749+
self.shared_api_usage_plan.api_stages_shared.append(api_stage)
750+
usage_plan.ApiStages = self.shared_api_usage_plan.api_stages_shared
645751

646752
api_key = self._construct_api_key(usage_plan_logical_id, create_usage_plan, rest_api_stage)
647753
usage_plan_key = self._construct_usage_plan_key(usage_plan_logical_id, create_usage_plan, api_key)
@@ -667,20 +773,31 @@ def _construct_api_key(self, usage_plan_logical_id, create_usage_plan, rest_api_
667773
"""
668774
if create_usage_plan == "SHARED":
669775
# create an api key resource for all the apis
776+
LOG.info("Creating api key resource for all the Apis from SHARED usage plan")
670777
api_key_logical_id = "ServerlessApiKey"
671-
api_key = ApiGatewayApiKey(logical_id=api_key_logical_id, depends_on=[usage_plan_logical_id])
778+
api_key = ApiGatewayApiKey(
779+
logical_id=api_key_logical_id,
780+
depends_on=[usage_plan_logical_id],
781+
attributes=self.shared_api_usage_plan.get_combined_resource_attributes(
782+
self.passthrough_resource_attributes, self.template_conditions
783+
),
784+
)
672785
api_key.Enabled = True
673786
stage_key = dict()
674787
stage_key["RestApiId"] = ref(self.logical_id)
675788
stage_key["StageName"] = ref(rest_api_stage.logical_id)
676-
if stage_key not in ApiGenerator.stage_keys_shared:
677-
ApiGenerator.stage_keys_shared.append(stage_key)
678-
api_key.StageKeys = ApiGenerator.stage_keys_shared
789+
if stage_key not in self.shared_api_usage_plan.stage_keys_shared:
790+
self.shared_api_usage_plan.stage_keys_shared.append(stage_key)
791+
api_key.StageKeys = self.shared_api_usage_plan.stage_keys_shared
679792
# for create_usage_plan = "PER_API"
680793
else:
681794
# create an api key resource for this api
682795
api_key_logical_id = self.logical_id + "ApiKey"
683-
api_key = ApiGatewayApiKey(logical_id=api_key_logical_id, depends_on=[usage_plan_logical_id])
796+
api_key = ApiGatewayApiKey(
797+
logical_id=api_key_logical_id,
798+
depends_on=[usage_plan_logical_id],
799+
attributes=self.passthrough_resource_attributes,
800+
)
684801
api_key.Enabled = True
685802
stage_keys = list()
686803
stage_key = dict()
@@ -700,12 +817,20 @@ def _construct_usage_plan_key(self, usage_plan_logical_id, create_usage_plan, ap
700817
if create_usage_plan == "SHARED":
701818
# create a mapping between api key and the usage plan
702819
usage_plan_key_logical_id = "ServerlessUsagePlanKey"
820+
resource_attributes = self.shared_api_usage_plan.get_combined_resource_attributes(
821+
self.passthrough_resource_attributes, self.template_conditions
822+
)
703823
# for create_usage_plan = "PER_API"
704824
else:
705825
# create a mapping between api key and the usage plan
706826
usage_plan_key_logical_id = self.logical_id + "UsagePlanKey"
827+
resource_attributes = self.passthrough_resource_attributes
707828

708-
usage_plan_key = ApiGatewayUsagePlanKey(logical_id=usage_plan_key_logical_id, depends_on=[api_key.logical_id])
829+
usage_plan_key = ApiGatewayUsagePlanKey(
830+
logical_id=usage_plan_key_logical_id,
831+
depends_on=[api_key.logical_id],
832+
attributes=resource_attributes,
833+
)
709834
usage_plan_key.KeyId = ref(api_key.logical_id)
710835
usage_plan_key.KeyType = "API_KEY"
711836
usage_plan_key.UsagePlanId = ref(usage_plan_logical_id)

samtranslator/model/eventbridge_utils.py

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -4,15 +4,15 @@
44

55
class EventBridgeRuleUtils:
66
@staticmethod
7-
def create_dead_letter_queue_with_policy(rule_logical_id, rule_arn, queue_logical_id=None):
7+
def create_dead_letter_queue_with_policy(rule_logical_id, rule_arn, queue_logical_id=None, attributes=None):
88
resources = []
99

10-
queue = SQSQueue(queue_logical_id or rule_logical_id + "Queue")
10+
queue = SQSQueue(queue_logical_id or rule_logical_id + "Queue", attributes=attributes)
1111
dlq_queue_arn = queue.get_runtime_attr("arn")
1212
dlq_queue_url = queue.get_runtime_attr("queue_url")
1313

1414
# grant necessary permission to Eventbridge Rule resource for sending messages to dead-letter queue
15-
policy = SQSQueuePolicy(rule_logical_id + "QueuePolicy")
15+
policy = SQSQueuePolicy(rule_logical_id + "QueuePolicy", attributes=attributes)
1616
policy.PolicyDocument = SQSQueuePolicies.eventbridge_dlq_send_message_resource_based_policy(
1717
rule_arn, dlq_queue_arn
1818
)
@@ -41,14 +41,14 @@ def validate_dlq_config(source_logical_id, dead_letter_config):
4141
raise InvalidEventException(source_logical_id, "No 'Arn' or 'Type' property provided for DeadLetterConfig")
4242

4343
@staticmethod
44-
def get_dlq_queue_arn_and_resources(cw_event_source, source_arn):
44+
def get_dlq_queue_arn_and_resources(cw_event_source, source_arn, attributes):
4545
"""returns dlq queue arn and dlq_resources, assuming cw_event_source.DeadLetterConfig has been validated"""
4646
dlq_queue_arn = cw_event_source.DeadLetterConfig.get("Arn")
4747
if dlq_queue_arn is not None:
4848
return dlq_queue_arn, []
4949
queue_logical_id = cw_event_source.DeadLetterConfig.get("QueueLogicalId")
5050
dlq_resources = EventBridgeRuleUtils.create_dead_letter_queue_with_policy(
51-
cw_event_source.logical_id, source_arn, queue_logical_id
51+
cw_event_source.logical_id, source_arn, queue_logical_id, attributes
5252
)
5353
dlq_queue_arn = dlq_resources[0].get_runtime_attr("arn")
5454
return dlq_queue_arn, dlq_resources

samtranslator/model/eventsources/cloudwatchlogs.py

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -43,11 +43,13 @@ def get_source_arn(self):
4343
)
4444

4545
def get_subscription_filter(self, function, permission):
46-
subscription_filter = SubscriptionFilter(self.logical_id, depends_on=[permission.logical_id])
46+
subscription_filter = SubscriptionFilter(
47+
self.logical_id,
48+
depends_on=[permission.logical_id],
49+
attributes=function.get_passthrough_resource_attributes(),
50+
)
4751
subscription_filter.LogGroupName = self.LogGroupName
4852
subscription_filter.FilterPattern = self.FilterPattern
4953
subscription_filter.DestinationArn = function.get_runtime_attr("arn")
50-
if "Condition" in function.resource_attributes:
51-
subscription_filter.set_resource_attribute("Condition", function.resource_attributes["Condition"])
5254

5355
return subscription_filter

samtranslator/model/eventsources/pull.py

Lines changed: 3 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -60,7 +60,9 @@ def to_cloudformation(self, **kwargs):
6060

6161
resources = []
6262

63-
lambda_eventsourcemapping = LambdaEventSourceMapping(self.logical_id)
63+
lambda_eventsourcemapping = LambdaEventSourceMapping(
64+
self.logical_id, attributes=function.get_passthrough_resource_attributes()
65+
)
6466
resources.append(lambda_eventsourcemapping)
6567

6668
try:
@@ -122,9 +124,6 @@ def to_cloudformation(self, **kwargs):
122124
)
123125
lambda_eventsourcemapping.DestinationConfig = self.DestinationConfig
124126

125-
if "Condition" in function.resource_attributes:
126-
lambda_eventsourcemapping.set_resource_attribute("Condition", function.resource_attributes["Condition"])
127-
128127
if "role" in kwargs:
129128
self._link_policy(kwargs["role"], destination_config_policy)
130129

0 commit comments

Comments
 (0)