Skip to content

Commit 8d0e058

Browse files
keetonianhawflautorresxb1
authored
fix: apply resource conditions to DeploymentPreference resources (#1578)
* fix code deploy conditions bug * Cover all condition cases, update tests * black reformat * Update tests to use .get_codedeploy_iam_role() instead of .codedeploy_iam_role * Fixing unit tests after merging Condition fix commit * Use feature toggle to gate deployment preference condition fix * Add tests * Revert using feature toggle * Add property to opt-in deployment preference condition passthrough * Add tests for PassthroughCondition * Fix passthrough condition logic and add tests * Update PassthroughCondition to support intrinsic * intrinscs support + tests * update invalid intrinsics end-to-end test * uncomment and update invalid value unit test * black Co-authored-by: Wing Fung Lau <[email protected]> Co-authored-by: Ruperto Torres <[email protected]>
1 parent bdbe412 commit 8d0e058

File tree

40 files changed

+8694
-345
lines changed

40 files changed

+8694
-345
lines changed

samtranslator/model/preferences/deployment_preference.py

Lines changed: 23 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,16 @@
2525
"""
2626
DeploymentPreferenceTuple = namedtuple(
2727
"DeploymentPreferenceTuple",
28-
["deployment_type", "pre_traffic_hook", "post_traffic_hook", "alarms", "enabled", "role", "trigger_configurations"],
28+
[
29+
"deployment_type",
30+
"pre_traffic_hook",
31+
"post_traffic_hook",
32+
"alarms",
33+
"enabled",
34+
"role",
35+
"trigger_configurations",
36+
"condition",
37+
],
2938
)
3039

3140

@@ -37,17 +46,18 @@ class DeploymentPreference(DeploymentPreferenceTuple):
3746
"""
3847

3948
@classmethod
40-
def from_dict(cls, logical_id, deployment_preference_dict):
49+
def from_dict(cls, logical_id, deployment_preference_dict, condition=None):
4150
"""
4251
:param logical_id: the logical_id of the resource that owns this deployment preference
4352
:param deployment_preference_dict: the dict object taken from the SAM template
53+
:param condition: condition on this deployment preference
4454
:return:
4555
"""
4656
enabled = deployment_preference_dict.get("Enabled", True)
4757
enabled = False if enabled in ["false", "False"] else enabled
4858

4959
if not enabled:
50-
return DeploymentPreference(None, None, None, None, False, None, None)
60+
return DeploymentPreference(None, None, None, None, False, None, None, None)
5161

5262
if "Type" not in deployment_preference_dict:
5363
raise InvalidResourceException(logical_id, "'DeploymentPreference' is missing required Property 'Type'")
@@ -64,6 +74,15 @@ def from_dict(cls, logical_id, deployment_preference_dict):
6474
alarms = deployment_preference_dict.get("Alarms", None)
6575
role = deployment_preference_dict.get("Role", None)
6676
trigger_configurations = deployment_preference_dict.get("TriggerConfigurations", None)
77+
passthrough_condition = deployment_preference_dict.get("PassthroughCondition", False)
78+
6779
return DeploymentPreference(
68-
deployment_type, pre_traffic_hook, post_traffic_hook, alarms, enabled, role, trigger_configurations
80+
deployment_type,
81+
pre_traffic_hook,
82+
post_traffic_hook,
83+
alarms,
84+
enabled,
85+
role,
86+
trigger_configurations,
87+
condition if passthrough_condition else None,
6988
)

samtranslator/model/preferences/deployment_preference_collection.py

Lines changed: 59 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,9 @@
99
is_intrinsic_if,
1010
is_intrinsic_no_value,
1111
validate_intrinsic_if_items,
12+
make_combined_condition,
13+
ref,
14+
fnGetAtt,
1215
)
1316
from samtranslator.model.update_policy import UpdatePolicy
1417
from samtranslator.translator.arn_generator import ArnGenerator
@@ -27,6 +30,7 @@
2730
"Linear10PercentEvery10Minutes",
2831
"AllAtOnce",
2932
]
33+
CODE_DEPLOY_CONDITION_NAME = "ServerlessCodeDeployCondition"
3034

3135

3236
class DeploymentPreferenceCollection(object):
@@ -41,20 +45,19 @@ class DeploymentPreferenceCollection(object):
4145

4246
def __init__(self):
4347
"""
44-
This collection stores an intenral dict of the deployment preferences for each function's
45-
deployment preference in the SAM Template.
48+
This collection stores an internal dict of the deployment preferences for each function's
49+
deployment preference in the SAM Template.
4650
"""
4751
self._resource_preferences = {}
48-
self.codedeploy_application = self._codedeploy_application()
49-
self.codedeploy_iam_role = self._codedeploy_iam_role()
5052

51-
def add(self, logical_id, deployment_preference_dict):
53+
def add(self, logical_id, deployment_preference_dict, condition=None):
5254
"""
5355
Add this deployment preference to the collection
5456
5557
:raise ValueError if an existing logical id already exists in the _resource_preferences
5658
:param logical_id: logical id of the resource where this deployment preference applies
5759
:param deployment_preference_dict: the input SAM template deployment preference mapping
60+
:param condition: the condition (if it exists) on the serverless function
5861
"""
5962
if logical_id in self._resource_preferences:
6063
raise ValueError(
@@ -63,7 +66,9 @@ def add(self, logical_id, deployment_preference_dict):
6366
)
6467
)
6568

66-
self._resource_preferences[logical_id] = DeploymentPreference.from_dict(logical_id, deployment_preference_dict)
69+
self._resource_preferences[logical_id] = DeploymentPreference.from_dict(
70+
logical_id, deployment_preference_dict, condition
71+
)
6772

6873
def get(self, logical_id):
6974
"""
@@ -85,18 +90,52 @@ def can_skip_service_role(self):
8590
"""
8691
return all(preference.role or not preference.enabled for preference in self._resource_preferences.values())
8792

93+
def needs_resource_condition(self):
94+
"""
95+
If all preferences have a condition, all code deploy resources need to be conditionally created
96+
:return: True, if a condition needs to be created
97+
"""
98+
# If there are any enabled deployment preferences without conditions, return false
99+
return self._resource_preferences and not any(
100+
not preference.condition and preference.enabled for preference in self._resource_preferences.values()
101+
)
102+
103+
def get_all_deployment_conditions(self):
104+
"""
105+
Returns a list of all conditions associated with the deployment preference resources
106+
:return: List of condition names
107+
"""
108+
conditions_set = set([preference.condition for preference in self._resource_preferences.values()])
109+
if None in conditions_set:
110+
# None can exist if there are disabled deployment preference(s)
111+
conditions_set.remove(None)
112+
return list(conditions_set)
113+
114+
def create_aggregate_deployment_condition(self):
115+
"""
116+
Creates an aggregate deployment condition if necessary
117+
:return: None if <2 conditions are found, otherwise a dictionary of new conditions to add to template
118+
"""
119+
return make_combined_condition(self.get_all_deployment_conditions(), CODE_DEPLOY_CONDITION_NAME)
120+
88121
def enabled_logical_ids(self):
89122
"""
90123
:return: only the logical id's for the deployment preferences in this collection which are enabled
91124
"""
92125
return [logical_id for logical_id, preference in self._resource_preferences.items() if preference.enabled]
93126

94-
def _codedeploy_application(self):
127+
def get_codedeploy_application(self):
95128
codedeploy_application_resource = CodeDeployApplication(CODEDEPLOY_APPLICATION_LOGICAL_ID)
96129
codedeploy_application_resource.ComputePlatform = "Lambda"
130+
if self.needs_resource_condition():
131+
conditions = self.get_all_deployment_conditions()
132+
condition_name = CODE_DEPLOY_CONDITION_NAME
133+
if len(conditions) <= 1:
134+
condition_name = conditions.pop()
135+
codedeploy_application_resource.set_resource_attribute("Condition", condition_name)
97136
return codedeploy_application_resource
98137

99-
def _codedeploy_iam_role(self):
138+
def get_codedeploy_iam_role(self):
100139
iam_role = IAMRole(CODE_DEPLOY_SERVICE_ROLE_LOGICAL_ID)
101140
iam_role.AssumeRolePolicyDocument = {
102141
"Version": "2012-10-17",
@@ -120,6 +159,12 @@ def _codedeploy_iam_role(self):
120159
ArnGenerator.generate_aws_managed_policy_arn("service-role/AWSCodeDeployRoleForLambda")
121160
]
122161

162+
if self.needs_resource_condition():
163+
conditions = self.get_all_deployment_conditions()
164+
condition_name = CODE_DEPLOY_CONDITION_NAME
165+
if len(conditions) <= 1:
166+
condition_name = conditions.pop()
167+
iam_role.set_resource_attribute("Condition", condition_name)
123168
return iam_role
124169

125170
def deployment_group(self, function_logical_id):
@@ -137,7 +182,7 @@ def deployment_group(self, function_logical_id):
137182
except ValueError as e:
138183
raise InvalidResourceException(function_logical_id, str(e))
139184

140-
deployment_group.ApplicationName = self.codedeploy_application.get_runtime_attr("name")
185+
deployment_group.ApplicationName = ref(CODEDEPLOY_APPLICATION_LOGICAL_ID)
141186
deployment_group.AutoRollbackConfiguration = {
142187
"Enabled": True,
143188
"Events": ["DEPLOYMENT_FAILURE", "DEPLOYMENT_STOP_ON_ALARM", "DEPLOYMENT_STOP_ON_REQUEST"],
@@ -149,13 +194,16 @@ def deployment_group(self, function_logical_id):
149194

150195
deployment_group.DeploymentStyle = {"DeploymentType": "BLUE_GREEN", "DeploymentOption": "WITH_TRAFFIC_CONTROL"}
151196

152-
deployment_group.ServiceRoleArn = self.codedeploy_iam_role.get_runtime_attr("arn")
197+
deployment_group.ServiceRoleArn = fnGetAtt(CODE_DEPLOY_SERVICE_ROLE_LOGICAL_ID, "Arn")
153198
if deployment_preference.role:
154199
deployment_group.ServiceRoleArn = deployment_preference.role
155200

156201
if deployment_preference.trigger_configurations:
157202
deployment_group.TriggerConfigurations = deployment_preference.trigger_configurations
158203

204+
if deployment_preference.condition:
205+
deployment_group.set_resource_attribute("Condition", deployment_preference.condition)
206+
159207
return deployment_group
160208

161209
def _convert_alarms(self, preference_alarms):
@@ -240,7 +288,7 @@ def update_policy(self, function_logical_id):
240288
deployment_preference = self.get(function_logical_id)
241289

242290
return UpdatePolicy(
243-
self.codedeploy_application.get_runtime_attr("name"),
291+
ref(CODEDEPLOY_APPLICATION_LOGICAL_ID),
244292
self.deployment_group(function_logical_id).get_runtime_attr("name"),
245293
deployment_preference.pre_traffic_hook,
246294
deployment_preference.post_traffic_hook,

samtranslator/model/sam_resources.py

Lines changed: 66 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,7 @@
11
""" SAM macro definitions """
22
import copy
3+
from typing import Union
4+
from samtranslator.intrinsics.resolver import IntrinsicsResolver
35

46
import samtranslator.model.eventsources
57
import samtranslator.model.eventsources.pull
@@ -139,6 +141,7 @@ def to_cloudformation(self, **kwargs):
139141
intrinsics_resolver = kwargs["intrinsics_resolver"]
140142
mappings_resolver = kwargs.get("mappings_resolver", None)
141143
conditions = kwargs.get("conditions", {})
144+
feature_toggle = kwargs.get("feature_toggle")
142145

143146
if self.DeadLetterQueue:
144147
self._validate_dlq()
@@ -185,6 +188,8 @@ def to_cloudformation(self, **kwargs):
185188
lambda_alias,
186189
intrinsics_resolver,
187190
mappings_resolver,
191+
self.get_passthrough_resource_attributes(),
192+
feature_toggle,
188193
)
189194
event_invoke_policies = []
190195
if self.EventInvokeConfig:
@@ -827,10 +832,16 @@ def _construct_alias(self, name, function, version):
827832
return alias
828833

829834
def _validate_deployment_preference_and_add_update_policy(
830-
self, deployment_preference_collection, lambda_alias, intrinsics_resolver, mappings_resolver
835+
self,
836+
deployment_preference_collection,
837+
lambda_alias,
838+
intrinsics_resolver,
839+
mappings_resolver,
840+
passthrough_resource_attributes,
841+
feature_toggle=None,
831842
):
832843
if "Enabled" in self.DeploymentPreference:
833-
# resolve intrinsics and mappings for Type
844+
# resolve intrinsics and mappings for Enabled
834845
enabled = self.DeploymentPreference["Enabled"]
835846
enabled = intrinsics_resolver.resolve_parameter_refs(enabled)
836847
enabled = mappings_resolver.resolve_parameter_refs(enabled)
@@ -843,10 +854,28 @@ def _validate_deployment_preference_and_add_update_policy(
843854
preference_type = mappings_resolver.resolve_parameter_refs(preference_type)
844855
self.DeploymentPreference["Type"] = preference_type
845856

857+
if "PassthroughCondition" in self.DeploymentPreference:
858+
self.DeploymentPreference["PassthroughCondition"] = self._resolve_property_to_boolean(
859+
self.DeploymentPreference["PassthroughCondition"],
860+
"PassthroughCondition",
861+
intrinsics_resolver,
862+
mappings_resolver,
863+
)
864+
elif feature_toggle:
865+
self.DeploymentPreference["PassthroughCondition"] = feature_toggle.is_enabled(
866+
"deployment_preference_condition_fix"
867+
)
868+
else:
869+
self.DeploymentPreference["PassthroughCondition"] = False
870+
846871
if deployment_preference_collection is None:
847872
raise ValueError("deployment_preference_collection required for parsing the deployment preference")
848873

849-
deployment_preference_collection.add(self.logical_id, self.DeploymentPreference)
874+
deployment_preference_collection.add(
875+
self.logical_id,
876+
self.DeploymentPreference,
877+
passthrough_resource_attributes.get("Condition"),
878+
)
850879

851880
if deployment_preference_collection.get(self.logical_id).enabled:
852881
if self.AutoPublishAlias is None:
@@ -860,6 +889,40 @@ def _validate_deployment_preference_and_add_update_policy(
860889
"UpdatePolicy", deployment_preference_collection.update_policy(self.logical_id).to_dict()
861890
)
862891

892+
def _resolve_property_to_boolean(
893+
self,
894+
property_value: Union[bool, str, dict],
895+
property_name: str,
896+
intrinsics_resolver: IntrinsicsResolver,
897+
mappings_resolver: IntrinsicsResolver,
898+
) -> bool:
899+
"""
900+
Resolves intrinsics, if any, and/or converts string in a given property to boolean.
901+
Raises InvalidResourceException if can't resolve intrinsic or can't resolve string to boolean
902+
903+
:param property_value: property value to resolve
904+
:param property_name: name/key of property to resolve
905+
:param intrinsics_resolver: resolves intrinsics
906+
:param mappings_resolver: resolves FindInMap
907+
:return bool: resolved boolean value
908+
"""
909+
processed_property_value = intrinsics_resolver.resolve_parameter_refs(property_value)
910+
processed_property_value = mappings_resolver.resolve_parameter_refs(processed_property_value)
911+
912+
# FIXME: We should support not only true/false, but also yes/no, on/off? See https://yaml.org/type/bool.html
913+
if processed_property_value in [True, "true", "True"]:
914+
return True
915+
elif processed_property_value in [False, "false", "False"]:
916+
return False
917+
elif is_intrinsic(processed_property_value): # couldn't resolve intrinsic
918+
raise InvalidResourceException(
919+
self.logical_id,
920+
f"Unsupported intrinsic: the only intrinsic functions supported for "
921+
f"property {property_name} are FindInMap and parameter Refs.",
922+
)
923+
else:
924+
raise InvalidResourceException(self.logical_id, f"Invalid value for property {property_name}.")
925+
863926
def _construct_function_url(self, lambda_function, lambda_alias):
864927
"""
865928
This method is used to construct a lambda url resource

samtranslator/translator/translator.py

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -143,6 +143,7 @@ def translate(self, sam_template, parameter_values, feature_toggle=None, passthr
143143
)
144144
kwargs["redeploy_restapi_parameters"] = self.redeploy_restapi_parameters
145145
kwargs["shared_api_usage_plan"] = shared_api_usage_plan
146+
kwargs["feature_toggle"] = self.feature_toggle
146147
translated = macro.to_cloudformation(**kwargs)
147148

148149
supported_resource_refs = macro.get_resource_references(translated, supported_resource_refs)
@@ -168,10 +169,14 @@ def translate(self, sam_template, parameter_values, feature_toggle=None, passthr
168169
document_errors.append(e)
169170

170171
if deployment_preference_collection.any_enabled():
171-
template["Resources"].update(deployment_preference_collection.codedeploy_application.to_dict())
172+
template["Resources"].update(deployment_preference_collection.get_codedeploy_application().to_dict())
173+
if deployment_preference_collection.needs_resource_condition():
174+
new_conditions = deployment_preference_collection.create_aggregate_deployment_condition()
175+
if new_conditions:
176+
template.get("Conditions").update(new_conditions)
172177

173178
if not deployment_preference_collection.can_skip_service_role():
174-
template["Resources"].update(deployment_preference_collection.codedeploy_iam_role.to_dict())
179+
template["Resources"].update(deployment_preference_collection.get_codedeploy_iam_role().to_dict())
175180

176181
for logical_id in deployment_preference_collection.enabled_logical_ids():
177182
try:

0 commit comments

Comments
 (0)