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

fix: apply resource conditions to DeploymentPreference resources #1578

Merged
merged 16 commits into from
May 19, 2022
Merged
Show file tree
Hide file tree
Changes from 7 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
25 changes: 21 additions & 4 deletions samtranslator/model/preferences/deployment_preference.py
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,16 @@
"""
DeploymentPreferenceTuple = namedtuple(
"DeploymentPreferenceTuple",
["deployment_type", "pre_traffic_hook", "post_traffic_hook", "alarms", "enabled", "role", "trigger_configurations"],
[
"deployment_type",
"pre_traffic_hook",
"post_traffic_hook",
"alarms",
"enabled",
"role",
"trigger_configurations",
"condition",
],
)


Expand All @@ -37,17 +46,18 @@ class DeploymentPreference(DeploymentPreferenceTuple):
"""

@classmethod
def from_dict(cls, logical_id, deployment_preference_dict):
def from_dict(cls, logical_id, deployment_preference_dict, condition=None):
"""
:param logical_id: the logical_id of the resource that owns this deployment preference
:param deployment_preference_dict: the dict object taken from the SAM template
:param condition: condition on this deployment preference
:return:
"""
enabled = deployment_preference_dict.get("Enabled", True)
enabled = False if enabled in ["false", "False"] else enabled

if not enabled:
return DeploymentPreference(None, None, None, None, False, None, None)
return DeploymentPreference(None, None, None, None, False, None, None, None)

if "Type" not in deployment_preference_dict:
raise InvalidResourceException(logical_id, "'DeploymentPreference' is missing required Property 'Type'")
Expand All @@ -65,5 +75,12 @@ def from_dict(cls, logical_id, deployment_preference_dict):
role = deployment_preference_dict.get("Role", None)
trigger_configurations = deployment_preference_dict.get("TriggerConfigurations", None)
return DeploymentPreference(
deployment_type, pre_traffic_hook, post_traffic_hook, alarms, enabled, role, trigger_configurations
deployment_type,
pre_traffic_hook,
post_traffic_hook,
alarms,
enabled,
role,
trigger_configurations,
condition,
)
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,9 @@
is_intrinsic_if,
is_intrinsic_no_value,
validate_intrinsic_if_items,
make_combined_condition,
ref,
fnGetAtt,
)
from samtranslator.model.update_policy import UpdatePolicy
from samtranslator.translator.arn_generator import ArnGenerator
Expand All @@ -27,6 +30,7 @@
"Linear10PercentEvery10Minutes",
"AllAtOnce",
]
CODE_DEPLOY_CONDITION_NAME = "ServerlessCodeDeployCondition"


class DeploymentPreferenceCollection(object):
Expand All @@ -39,22 +43,22 @@ class DeploymentPreferenceCollection(object):
resources.
"""

def __init__(self):
def __init__(self, use_condition_fix=False):
"""
This collection stores an intenral dict of the deployment preferences for each function's
deployment preference in the SAM Template.
This collection stores an internal dict of the deployment preferences for each function's
deployment preference in the SAM Template.
"""
self._resource_preferences = {}
self.codedeploy_application = self._codedeploy_application()
self.codedeploy_iam_role = self._codedeploy_iam_role()
self._use_condition_fix = use_condition_fix

def add(self, logical_id, deployment_preference_dict):
def add(self, logical_id, deployment_preference_dict, condition=None):
"""
Add this deployment preference to the collection

:raise ValueError if an existing logical id already exists in the _resource_preferences
:param logical_id: logical id of the resource where this deployment preference applies
:param deployment_preference_dict: the input SAM template deployment preference mapping
:param condition: the condition (if it exists) on the serverless function
"""
if logical_id in self._resource_preferences:
raise ValueError(
Expand All @@ -63,7 +67,11 @@ def add(self, logical_id, deployment_preference_dict):
)
)

self._resource_preferences[logical_id] = DeploymentPreference.from_dict(logical_id, deployment_preference_dict)
self._resource_preferences[logical_id] = DeploymentPreference.from_dict(
logical_id,
deployment_preference_dict,
condition=condition if self._use_condition_fix else None,
)

def get(self, logical_id):
"""
Expand All @@ -85,18 +93,52 @@ def can_skip_service_role(self):
"""
return all(preference.role or not preference.enabled for preference in self._resource_preferences.values())

def needs_resource_condition(self):
"""
If all preferences have a condition, all code deploy resources need to be conditionally created
:return: True, if a condition needs to be created
"""
# If there are any enabled deployment preferences without conditions, return false
return self._resource_preferences and not any(
not preference.condition and preference.enabled for preference in self._resource_preferences.values()
)

def get_all_deployment_conditions(self):
"""
Returns a list of all conditions associated with the deployment preference resources
:return: List of condition names
"""
conditions_set = set([preference.condition for preference in self._resource_preferences.values()])
if None in conditions_set:
# None can exist if there are disabled deployment preference(s)
conditions_set.remove(None)
return list(conditions_set)

def create_aggregate_deployment_condition(self):
"""
Creates an aggregate deployment condition if necessary
:return: None if <2 conditions are found, otherwise a dictionary of new conditions to add to template
"""
return make_combined_condition(self.get_all_deployment_conditions(), CODE_DEPLOY_CONDITION_NAME)

def enabled_logical_ids(self):
"""
:return: only the logical id's for the deployment preferences in this collection which are enabled
"""
return [logical_id for logical_id, preference in self._resource_preferences.items() if preference.enabled]

def _codedeploy_application(self):
def get_codedeploy_application(self):
codedeploy_application_resource = CodeDeployApplication(CODEDEPLOY_APPLICATION_LOGICAL_ID)
codedeploy_application_resource.ComputePlatform = "Lambda"
if self.needs_resource_condition():
conditions = self.get_all_deployment_conditions()
condition_name = CODE_DEPLOY_CONDITION_NAME
if len(conditions) <= 1:
condition_name = conditions.pop()
codedeploy_application_resource.set_resource_attribute("Condition", condition_name)
return codedeploy_application_resource

def _codedeploy_iam_role(self):
def get_codedeploy_iam_role(self):
iam_role = IAMRole(CODE_DEPLOY_SERVICE_ROLE_LOGICAL_ID)
iam_role.AssumeRolePolicyDocument = {
"Version": "2012-10-17",
Expand All @@ -120,6 +162,12 @@ def _codedeploy_iam_role(self):
ArnGenerator.generate_aws_managed_policy_arn("service-role/AWSCodeDeployRoleForLambda")
]

if self.needs_resource_condition():
conditions = self.get_all_deployment_conditions()
condition_name = CODE_DEPLOY_CONDITION_NAME
if len(conditions) <= 1:
condition_name = conditions.pop()
iam_role.set_resource_attribute("Condition", condition_name)
return iam_role

def deployment_group(self, function_logical_id):
Expand All @@ -137,7 +185,7 @@ def deployment_group(self, function_logical_id):
except ValueError as e:
raise InvalidResourceException(function_logical_id, str(e))

deployment_group.ApplicationName = self.codedeploy_application.get_runtime_attr("name")
deployment_group.ApplicationName = ref(CODEDEPLOY_APPLICATION_LOGICAL_ID)
deployment_group.AutoRollbackConfiguration = {
"Enabled": True,
"Events": ["DEPLOYMENT_FAILURE", "DEPLOYMENT_STOP_ON_ALARM", "DEPLOYMENT_STOP_ON_REQUEST"],
Expand All @@ -149,13 +197,16 @@ def deployment_group(self, function_logical_id):

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

deployment_group.ServiceRoleArn = self.codedeploy_iam_role.get_runtime_attr("arn")
deployment_group.ServiceRoleArn = fnGetAtt(CODE_DEPLOY_SERVICE_ROLE_LOGICAL_ID, "Arn")
if deployment_preference.role:
deployment_group.ServiceRoleArn = deployment_preference.role

if deployment_preference.trigger_configurations:
deployment_group.TriggerConfigurations = deployment_preference.trigger_configurations

if deployment_preference.condition:
deployment_group.set_resource_attribute("Condition", deployment_preference.condition)

return deployment_group

def _convert_alarms(self, preference_alarms):
Expand Down Expand Up @@ -240,7 +291,7 @@ def update_policy(self, function_logical_id):
deployment_preference = self.get(function_logical_id)

return UpdatePolicy(
self.codedeploy_application.get_runtime_attr("name"),
ref(CODEDEPLOY_APPLICATION_LOGICAL_ID),
self.deployment_group(function_logical_id).get_runtime_attr("name"),
deployment_preference.pre_traffic_hook,
deployment_preference.post_traffic_hook,
Expand Down
12 changes: 10 additions & 2 deletions samtranslator/model/sam_resources.py
Original file line number Diff line number Diff line change
Expand Up @@ -185,6 +185,7 @@ def to_cloudformation(self, **kwargs):
lambda_alias,
intrinsics_resolver,
mappings_resolver,
self.get_passthrough_resource_attributes(),
)
event_invoke_policies = []
if self.EventInvokeConfig:
Expand Down Expand Up @@ -827,7 +828,7 @@ def _construct_alias(self, name, function, version):
return alias

def _validate_deployment_preference_and_add_update_policy(
self, deployment_preference_collection, lambda_alias, intrinsics_resolver, mappings_resolver
self, deployment_preference_collection, lambda_alias, intrinsics_resolver, mappings_resolver, condition
):
if "Enabled" in self.DeploymentPreference:
# resolve intrinsics and mappings for Type
Expand All @@ -843,10 +844,17 @@ def _validate_deployment_preference_and_add_update_policy(
preference_type = mappings_resolver.resolve_parameter_refs(preference_type)
self.DeploymentPreference["Type"] = preference_type

if condition:
condition = condition.get("Condition")

if deployment_preference_collection is None:
raise ValueError("deployment_preference_collection required for parsing the deployment preference")

deployment_preference_collection.add(self.logical_id, self.DeploymentPreference)
deployment_preference_collection.add(
self.logical_id,
self.DeploymentPreference,
condition,
)

if deployment_preference_collection.get(self.logical_id).enabled:
if self.AutoPublishAlias is None:
Expand Down
12 changes: 9 additions & 3 deletions samtranslator/translator/translator.py
Original file line number Diff line number Diff line change
Expand Up @@ -119,7 +119,9 @@ def translate(self, sam_template, parameter_values, feature_toggle=None, passthr
mappings_resolver = IntrinsicsResolver(
template.get("Mappings", {}), {FindInMapAction.intrinsic_name: FindInMapAction()}
)
deployment_preference_collection = DeploymentPreferenceCollection()
deployment_preference_collection = DeploymentPreferenceCollection(
use_condition_fix=self.feature_toggle.is_enabled("deployment_preference_condition_fix")
)
supported_resource_refs = SupportedResourceReferences()
shared_api_usage_plan = SharedApiUsagePlan()
document_errors = []
Expand Down Expand Up @@ -167,10 +169,14 @@ def translate(self, sam_template, parameter_values, feature_toggle=None, passthr
document_errors.append(e)

if deployment_preference_collection.any_enabled():
template["Resources"].update(deployment_preference_collection.codedeploy_application.to_dict())
template["Resources"].update(deployment_preference_collection.get_codedeploy_application().to_dict())
if deployment_preference_collection.needs_resource_condition():
new_conditions = deployment_preference_collection.create_aggregate_deployment_condition()
if new_conditions:
template.get("Conditions").update(new_conditions)

if not deployment_preference_collection.can_skip_service_role():
template["Resources"].update(deployment_preference_collection.codedeploy_iam_role.to_dict())
template["Resources"].update(deployment_preference_collection.get_codedeploy_iam_role().to_dict())

for logical_id in deployment_preference_collection.enabled_logical_ids():
try:
Expand Down
Original file line number Diff line number Diff line change
@@ -1,3 +1,9 @@
Conditions:
Condition1:
Fn::Equals:
- true
- true

Globals:
Function:
AutoPublishAlias: live
Expand All @@ -15,6 +21,7 @@ Resources:
Runtime: python2.7

OtherFunction:
Condition: Condition1
Type: 'AWS::Serverless::Function'
Properties:
CodeUri: s3://sam-demo-bucket/hello.zip
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
Parameters:
FnName:
Type: String
ProvisionedConcurrency:
Type: String
Default: 10
EnableAliasProvisionedConcurrency:
Type: String
AllowedValues:
- true
- false
Default: true
Conditions:
AliasProvisionedConcurrencyEnabled: !Equals [!Ref EnableAliasProvisionedConcurrency, true]
FunctionCondition: !Equals [true, true]
Resources:
MinimalFunction:
Type: 'AWS::Serverless::Function'
Condition: FunctionCondition
Properties:
CodeUri: s3://sam-demo-bucket/hello.zip
Handler: hello.handler
Runtime: python2.7
AutoPublishAlias: live
DeploymentPreference:
Type: Linear10PercentEvery3Minutes
ProvisionedConcurrencyConfig: !If
- AliasProvisionedConcurrencyEnabled
- ProvisionedConcurrentExecutions: !Ref ProvisionedConcurrency
- !Ref 'AWS::NoValue'
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
Parameters:
FnName:
Type: String
ProvisionedConcurrency:
Type: String
Default: 10
EnableAliasProvisionedConcurrency:
Type: String
AllowedValues:
- true
- false
Default: true
Conditions:
AliasProvisionedConcurrencyEnabled: !Equals [!Ref EnableAliasProvisionedConcurrency, true]
FunctionCondition: !Equals [true, true]
Resources:
MinimalFunction:
Type: 'AWS::Serverless::Function'
Condition: FunctionCondition
Properties:
CodeUri: s3://sam-demo-bucket/hello.zip
Handler: hello.handler
Runtime: python2.7
AutoPublishAlias: live
DeploymentPreference:
Type: Linear10PercentEvery3Minutes
ProvisionedConcurrencyConfig: !If
- AliasProvisionedConcurrencyEnabled
- ProvisionedConcurrentExecutions: !Ref ProvisionedConcurrency
- !Ref 'AWS::NoValue'
Loading