Skip to content

Commit aaa1d15

Browse files
authored
feat: Handle Fn::If in DeploymentPreference Alarms (#1923)
1 parent 1170f78 commit aaa1d15

20 files changed

+1035
-51
lines changed

integration/helpers/base_test.py

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -347,7 +347,9 @@ def verify_stack(self):
347347
# verify if the stack was successfully created
348348
self.assertEqual(self.stack_description["Stacks"][0]["StackStatus"], "CREATE_COMPLETE")
349349
# verify if the stack contains the expected resources
350-
self.assertTrue(verify_stack_resources(self.expected_resource_path, self.stack_resources))
350+
error = verify_stack_resources(self.expected_resource_path, self.stack_resources)
351+
if error:
352+
self.fail(error)
351353

352354
def _load_yaml(self, file_path):
353355
"""

integration/helpers/resource.py

Lines changed: 11 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -34,7 +34,7 @@ def verify_stack_resources(expected_file_path, stack_resources):
3434
parsed_resources = _sort_resources(stack_resources["StackResourceSummaries"])
3535

3636
if len(expected_resources) != len(parsed_resources):
37-
return False
37+
return "'{}' resources expected, '{}' found".format(len(expected_resources), len(parsed_resources))
3838

3939
for i in range(len(expected_resources)):
4040
exp = expected_resources[i]
@@ -43,10 +43,17 @@ def verify_stack_resources(expected_file_path, stack_resources):
4343
"^" + exp["LogicalResourceId"] + "([0-9a-f]{" + str(LogicalIdGenerator.HASH_LENGTH) + "})?$",
4444
parsed["LogicalResourceId"],
4545
):
46-
return False
46+
parsed_trimed_down = {
47+
"LogicalResourceId": parsed["LogicalResourceId"],
48+
"ResourceType": parsed["ResourceType"],
49+
}
50+
51+
return "'{}' expected, '{}' found (Resources must appear in the same order, don't include the LogicalId random suffix)".format(
52+
exp, parsed_trimed_down
53+
)
4754
if exp["ResourceType"] != parsed["ResourceType"]:
48-
return False
49-
return True
55+
return "'{}' expected, '{}' found".format(exp["ResourceType"], parsed["ResourceType"])
56+
return None
5057

5158

5259
def generate_suffix():
Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
[
2+
{
3+
"LogicalResourceId": "ServerlessDeploymentApplication",
4+
"ResourceType": "AWS::CodeDeploy::Application"
5+
},
6+
{
7+
"LogicalResourceId": "MyLambdaFunctionRole",
8+
"ResourceType": "AWS::IAM::Role"
9+
},
10+
{
11+
"LogicalResourceId": "CodeDeployServiceRole",
12+
"ResourceType": "AWS::IAM::Role"
13+
},
14+
{
15+
"LogicalResourceId": "MyLambdaFunction",
16+
"ResourceType": "AWS::Lambda::Function"
17+
},
18+
{
19+
"LogicalResourceId": "MyLambdaFunctionDeploymentGroup",
20+
"ResourceType": "AWS::CodeDeploy::DeploymentGroup"
21+
},
22+
{
23+
"LogicalResourceId": "MyLambdaFunctionVersion",
24+
"ResourceType": "AWS::Lambda::Version"
25+
},
26+
{
27+
"LogicalResourceId": "MyLambdaFunctionAliaslive",
28+
"ResourceType": "AWS::Lambda::Alias"
29+
}
30+
]
Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
Conditions:
2+
MyCondition:
3+
Fn::Equals:
4+
- true
5+
- false
6+
Resources:
7+
MyLambdaFunction:
8+
Type: "AWS::Serverless::Function"
9+
Properties:
10+
CodeUri: ${codeuri}
11+
Handler: hello.handler
12+
Runtime: python2.7
13+
AutoPublishAlias: live
14+
DeploymentPreference:
15+
Type: Linear10PercentEvery3Minutes
16+
Alarms:
17+
Fn::If:
18+
- MyCondition
19+
- - Alarm1
20+
- Alarm2
21+
- Alarm3
22+
- - Alarm1
23+
- Alarm5

integration/single/test_basic_function.py

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,9 @@ def test_function_with_http_api_events(self, file_name):
4040

4141
self.assertEqual(requests.get(endpoint).text, self.FUNCTION_OUTPUT)
4242

43+
def test_function_with_deployment_preference_alarms_intrinsic_if(self):
44+
self.create_and_verify_stack("function_with_deployment_preference_alarms_intrinsic_if")
45+
4346
@parameterized.expand(
4447
[
4548
("basic_function_with_sns_dlq", "sns:Publish"),

samtranslator/model/function_policies.py

Lines changed: 10 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,12 @@
33

44
from six import string_types
55

6-
from samtranslator.model.intrinsics import is_intrinsic, is_intrinsic_if, is_intrinsic_no_value
6+
from samtranslator.model.intrinsics import (
7+
is_intrinsic,
8+
is_intrinsic_if,
9+
is_intrinsic_no_value,
10+
validate_intrinsic_if_items,
11+
)
712
from samtranslator.model.exceptions import InvalidTemplateException
813

914
PolicyEntry = namedtuple("PolicyEntry", "data type")
@@ -165,8 +170,10 @@ def _get_type_from_intrinsic_if(self, policy):
165170
"""
166171
intrinsic_if_value = policy["Fn::If"]
167172

168-
if not len(intrinsic_if_value) == 3:
169-
raise InvalidTemplateException("Fn::If requires 3 arguments")
173+
try:
174+
validate_intrinsic_if_items(intrinsic_if_value)
175+
except ValueError as e:
176+
raise InvalidTemplateException(e)
170177

171178
if_data = intrinsic_if_value[1]
172179
else_data = intrinsic_if_value[2]

samtranslator/model/intrinsics.py

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -162,6 +162,24 @@ def is_intrinsic_if(input):
162162
return key == "Fn::If"
163163

164164

165+
def validate_intrinsic_if_items(items):
166+
"""
167+
Validates Fn::If items
168+
169+
Parameters
170+
----------
171+
items : list
172+
Fn::If items
173+
174+
Raises
175+
------
176+
ValueError
177+
If the items are invalid
178+
"""
179+
if not isinstance(items, list) or len(items) != 3:
180+
raise ValueError("Fn::If requires 3 arguments")
181+
182+
165183
def is_intrinsic_no_value(input):
166184
"""
167185
Is the given input an intrinsic Ref: AWS::NoValue? Intrinsic function is a dictionary with single

samtranslator/model/preferences/deployment_preference_collection.py

Lines changed: 74 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,15 @@
11
from .deployment_preference import DeploymentPreference
22
from samtranslator.model.codedeploy import CodeDeployApplication
33
from samtranslator.model.codedeploy import CodeDeployDeploymentGroup
4+
from samtranslator.model.exceptions import InvalidResourceException
45
from samtranslator.model.iam import IAMRole
5-
from samtranslator.model.intrinsics import fnSub, is_intrinsic
6+
from samtranslator.model.intrinsics import (
7+
fnSub,
8+
is_intrinsic,
9+
is_intrinsic_if,
10+
is_intrinsic_no_value,
11+
validate_intrinsic_if_items,
12+
)
613
from samtranslator.model.update_policy import UpdatePolicy
714
from samtranslator.translator.arn_generator import ArnGenerator
815
import copy
@@ -125,11 +132,10 @@ def deployment_group(self, function_logical_id):
125132

126133
deployment_group = CodeDeployDeploymentGroup(self.deployment_group_logical_id(function_logical_id))
127134

128-
if deployment_preference.alarms is not None:
129-
deployment_group.AlarmConfiguration = {
130-
"Enabled": True,
131-
"Alarms": [{"Name": alarm} for alarm in deployment_preference.alarms],
132-
}
135+
try:
136+
deployment_group.AlarmConfiguration = self._convert_alarms(deployment_preference.alarms)
137+
except ValueError as e:
138+
raise InvalidResourceException(function_logical_id, str(e))
133139

134140
deployment_group.ApplicationName = self.codedeploy_application.get_runtime_attr("name")
135141
deployment_group.AutoRollbackConfiguration = {
@@ -152,6 +158,68 @@ def deployment_group(self, function_logical_id):
152158

153159
return deployment_group
154160

161+
def _convert_alarms(self, preference_alarms):
162+
"""
163+
Converts deployment preference alarms to an AlarmsConfiguration
164+
165+
Parameters
166+
----------
167+
preference_alarms : dict
168+
Deployment preference alarms
169+
170+
Returns
171+
-------
172+
dict
173+
AlarmsConfiguration if alarms is set, None otherwise
174+
175+
Raises
176+
------
177+
ValueError
178+
If Alarms is in the wrong format
179+
"""
180+
if not preference_alarms or is_intrinsic_no_value(preference_alarms):
181+
return None
182+
183+
if is_intrinsic_if(preference_alarms):
184+
processed_alarms = copy.deepcopy(preference_alarms)
185+
alarms_list = processed_alarms.get("Fn::If")
186+
validate_intrinsic_if_items(alarms_list)
187+
alarms_list[1] = self._build_alarm_configuration(alarms_list[1])
188+
alarms_list[2] = self._build_alarm_configuration(alarms_list[2])
189+
return processed_alarms
190+
191+
return self._build_alarm_configuration(preference_alarms)
192+
193+
def _build_alarm_configuration(self, alarms):
194+
"""
195+
Builds an AlarmConfiguration from a list of alarms
196+
197+
Parameters
198+
----------
199+
alarms : list[str]
200+
Alarms
201+
202+
Returns
203+
-------
204+
dict
205+
AlarmsConfiguration for a deployment group
206+
207+
Raises
208+
------
209+
ValueError
210+
If alarms is not a list
211+
"""
212+
if not isinstance(alarms, list):
213+
raise ValueError("Alarms must be a list")
214+
215+
if len(alarms) == 0 or is_intrinsic_no_value(alarms[0]):
216+
return {}
217+
218+
return {
219+
"Enabled": True,
220+
"Alarms": [{"Name": alarm} for alarm in alarms],
221+
}
222+
155223
def _replace_deployment_types(self, value, key=None):
156224
if isinstance(value, list):
157225
for i in range(len(value)):

samtranslator/model/resource_policies.py

Lines changed: 10 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,12 @@
33

44
from six import string_types
55

6-
from samtranslator.model.intrinsics import is_intrinsic, is_intrinsic_if, is_intrinsic_no_value
6+
from samtranslator.model.intrinsics import (
7+
is_intrinsic,
8+
is_intrinsic_if,
9+
is_intrinsic_no_value,
10+
validate_intrinsic_if_items,
11+
)
712
from samtranslator.model.exceptions import InvalidTemplateException
813

914
PolicyEntry = namedtuple("PolicyEntry", "data type")
@@ -165,8 +170,10 @@ def _get_type_from_intrinsic_if(self, policy):
165170
"""
166171
intrinsic_if_value = policy["Fn::If"]
167172

168-
if not len(intrinsic_if_value) == 3:
169-
raise InvalidTemplateException("Fn::If requires 3 arguments")
173+
try:
174+
validate_intrinsic_if_items(intrinsic_if_value)
175+
except ValueError as e:
176+
raise InvalidTemplateException(e)
170177

171178
if_data = intrinsic_if_value[1]
172179
else_data = intrinsic_if_value[2]

samtranslator/translator/translator.py

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -156,7 +156,12 @@ def translate(self, sam_template, parameter_values, feature_toggle=None):
156156
template["Resources"].update(deployment_preference_collection.codedeploy_iam_role.to_dict())
157157

158158
for logical_id in deployment_preference_collection.enabled_logical_ids():
159-
template["Resources"].update(deployment_preference_collection.deployment_group(logical_id).to_dict())
159+
try:
160+
template["Resources"].update(
161+
deployment_preference_collection.deployment_group(logical_id).to_dict()
162+
)
163+
except InvalidResourceException as e:
164+
document_errors.append(e)
160165

161166
# Run the after-transform plugin target
162167
try:

0 commit comments

Comments
 (0)