From 6977bba3e16ca992bb5023b306e178e43d4c9a5f Mon Sep 17 00:00:00 2001 From: Sahil Shah <53784576+sahilshah6196@users.noreply.github.com> Date: Fri, 11 Jun 2021 16:56:28 -0400 Subject: [PATCH] Add capability to update `AWS::ElasticLoadBalancingV2`(Listener and ListenerRule) resource (#4005) * Add ssm parsing support for cloudformation stacks * Start adding unit tests for ssm parameter parsing * Add tests for code update * Add tests to parse ssm parameters code * Fix black lint errors * Fix bug. * Need to specify region_name * region needs to be same * Use ssm_backends[region] instead of ssm_backend * StringList -> string * Linting * check if servermode tests are on * Typo * Added support for ListenerRule. Will remove cruft * Pushing latest * Something works * Put back ripped out code * Save point. Incase I need more validations * Revert "Save point. Incase I need more validations" This reverts commit dac4953335dd9335eddb7a91a63667bc3c17104c. * Fixed validations and some refactor * Fix formatting * Linting * Cannot refactor if I have to fix all tests * Remove exceptions for now. Will do in another PR * Remove validations. Will add in next PR * Fix broken tests. Almost.: * Fix all tests. Some sneaky for now. * Python2 making me write bad code * OrderedDict.move_to_end() does not work in python2 * Linting * Add more checks to field in conditions later. * Unwnated change in FakeListener * Revert "Unwnated change in FakeListener" This reverts commit 962c2fdfd76fce999de9feccf1dd1c3ec48c459f. * Add back default listener rule * Linting fix * Fix priority sorting * Add cloudformation test for edge case * Add validation for ForwardConfig in Action of ListernRule CF * use not in * set the priority template correctly * Check for boolean in condition * One more check * Implement update_from_cloudformation_json for Listener and ListenerRule * Unwanted spaces * Linting issues * Add tests for code coverage Co-authored-by: Bert Blommers --- moto/elbv2/models.py | 210 +++++++++++------- moto/elbv2/responses.py | 8 +- .../test_cloudformation_stack_integration.py | 107 ++++++++- 3 files changed, 243 insertions(+), 82 deletions(-) diff --git a/moto/elbv2/models.py b/moto/elbv2/models.py index 399155ba2..ee258ca43 100644 --- a/moto/elbv2/models.py +++ b/moto/elbv2/models.py @@ -263,40 +263,36 @@ class FakeListener(CloudFormationModel): port = properties.get("Port") ssl_policy = properties.get("SslPolicy") certificates = properties.get("Certificates") - # transform default actions to confirm with the rest of the code and XML templates - default_actions = [] - for i, action in enumerate(properties["DefaultActions"]): - action_type = action["Type"] - if action_type == "forward": - default_actions.append( - {"type": action_type, "target_group_arn": action["TargetGroupArn"],} - ) - elif action_type in [ - "redirect", - "authenticate-cognito", - "fixed-response", - ]: - redirect_action = {"type": action_type} - key = ( - underscores_to_camelcase(action_type.capitalize().replace("-", "_")) - + "Config" - ) - for redirect_config_key, redirect_config_value in action[key].items(): - # need to match the output of _get_list_prefix - redirect_action[ - camelcase_to_underscores(key) - + "._" - + camelcase_to_underscores(redirect_config_key) - ] = redirect_config_value - default_actions.append(redirect_action) - else: - raise InvalidActionTypeError(action_type, i + 1) + default_actions = elbv2_backend.convert_and_validate_properties(properties) listener = elbv2_backend.create_listener( load_balancer_arn, protocol, port, ssl_policy, certificates, default_actions ) return listener + @classmethod + def update_from_cloudformation_json( + cls, original_resource, new_resource_name, cloudformation_json, region_name + ): + properties = cloudformation_json["Properties"] + + elbv2_backend = elbv2_backends[region_name] + protocol = properties.get("Protocol") + port = properties.get("Port") + ssl_policy = properties.get("SslPolicy") + certificates = properties.get("Certificates") + + default_actions = elbv2_backend.convert_and_validate_properties(properties) + listener = elbv2_backend.modify_listener( + original_resource.arn, + port, + protocol, + ssl_policy, + certificates, + default_actions, + ) + return listener + class FakeListenerRule(CloudFormationModel): def __init__( @@ -326,54 +322,28 @@ class FakeListenerRule(CloudFormationModel): listener_arn = properties.get("ListenerArn") priority = properties.get("Priority") conditions = properties.get("Conditions") - # transform Actions to confirm with the rest of the code and XML templates - default_actions = [] - for i, action in enumerate(properties["Actions"]): - action_type = action["Type"] - if action_type == "forward" and "ForwardConfig" in action: - action_forward_config = action["ForwardConfig"] - action_target_groups = action_forward_config["TargetGroups"] - target_group_action = [] - for action_target_group in action_target_groups: - target_group_action.append( - { - "target_group_arn": action_target_group["TargetGroupArn"], - "weight": action_target_group["Weight"], - } - ) - default_actions.append( - { - "type": action_type, - "forward_config": {"target_groups": target_group_action}, - } - ) - elif action_type == "forward" and "ForwardConfig" not in action: - default_actions.append( - {"type": action_type, "target_group_arn": action["TargetGroupArn"],} - ) - elif action_type in [ - "redirect", - "authenticate-cognito", - "fixed-response", - ]: - redirect_action = {"type": action_type} - key = ( - underscores_to_camelcase(action_type.capitalize().replace("-", "_")) - + "Config" - ) - for redirect_config_key, redirect_config_value in action[key].items(): - # need to match the output of _get_list_prefix - redirect_action[ - camelcase_to_underscores(key) - + "._" - + camelcase_to_underscores(redirect_config_key) - ] = redirect_config_value - default_actions.append(redirect_action) - else: - raise InvalidActionTypeError(action_type, i + 1) + actions = elbv2_backend.convert_and_validate_action_properties(properties) + conditions = elbv2_backend.convert_and_validate_condition_properties(properties) listener_rule = elbv2_backend.create_rule( - listener_arn, conditions, priority, default_actions + listener_arn, conditions, priority, actions + ) + return listener_rule + + @classmethod + def update_from_cloudformation_json( + cls, original_resource, new_resource_name, cloudformation_json, region_name + ): + + properties = cloudformation_json["Properties"] + + elbv2_backend = elbv2_backends[region_name] + conditions = properties.get("Conditions") + + actions = elbv2_backend.convert_and_validate_action_properties(properties) + conditions = elbv2_backend.convert_and_validate_condition_properties(properties) + listener_rule = elbv2_backend.modify_rule( + original_resource.arn, conditions, actions ) return listener_rule @@ -639,6 +609,64 @@ class ELBv2Backend(BaseBackend): self.load_balancers[arn] = new_load_balancer return new_load_balancer + def convert_and_validate_action_properties(self, properties): + + # transform Actions to confirm with the rest of the code and XML templates + default_actions = [] + for i, action in enumerate(properties["Actions"]): + action_type = action["Type"] + if action_type == "forward" and "ForwardConfig" in action: + action_forward_config = action["ForwardConfig"] + action_target_groups = action_forward_config["TargetGroups"] + target_group_action = [] + for action_target_group in action_target_groups: + target_group_action.append( + { + "target_group_arn": action_target_group["TargetGroupArn"], + "weight": action_target_group["Weight"], + } + ) + default_actions.append( + { + "type": action_type, + "forward_config": {"target_groups": target_group_action}, + } + ) + elif action_type == "forward" and "ForwardConfig" not in action: + default_actions.append( + {"type": action_type, "target_group_arn": action["TargetGroupArn"],} + ) + elif action_type in [ + "redirect", + "authenticate-cognito", + "fixed-response", + ]: + redirect_action = {"type": action_type} + key = ( + underscores_to_camelcase(action_type.capitalize().replace("-", "_")) + + "Config" + ) + for redirect_config_key, redirect_config_value in action[key].items(): + # need to match the output of _get_list_prefix + redirect_action[ + camelcase_to_underscores(key) + + "._" + + camelcase_to_underscores(redirect_config_key) + ] = redirect_config_value + default_actions.append(redirect_action) + else: + raise InvalidActionTypeError(action_type, i + 1) + return default_actions + + def convert_and_validate_condition_properties(self, properties): + + conditions = [] + for i, condition in enumerate(properties["Conditions"]): + conditions.append( + {"field": condition["Field"], "values": condition["Values"],} + ) + return conditions + def create_rule(self, listener_arn, conditions, priority, actions): actions = [FakeAction(action) for action in actions] listeners = self.describe_listeners(None, [listener_arn]) @@ -805,6 +833,38 @@ Member must satisfy regular expression pattern: {}".format( self.target_groups[target_group.arn] = target_group return target_group + def convert_and_validate_properties(self, properties): + + # transform default actions to confirm with the rest of the code and XML templates + default_actions = [] + for i, action in enumerate(properties["DefaultActions"]): + action_type = action["Type"] + if action_type == "forward": + default_actions.append( + {"type": action_type, "target_group_arn": action["TargetGroupArn"],} + ) + elif action_type in [ + "redirect", + "authenticate-cognito", + "fixed-response", + ]: + redirect_action = {"type": action_type} + key = ( + underscores_to_camelcase(action_type.capitalize().replace("-", "_")) + + "Config" + ) + for redirect_config_key, redirect_config_value in action[key].items(): + # need to match the output of _get_list_prefix + redirect_action[ + camelcase_to_underscores(key) + + "._" + + camelcase_to_underscores(redirect_config_key) + ] = redirect_config_value + default_actions.append(redirect_action) + else: + raise InvalidActionTypeError(action_type, i + 1) + return default_actions + def create_listener( self, load_balancer_arn, @@ -1236,8 +1296,6 @@ Member must satisfy regular expression pattern: {}".format( for listener_arn, current_listener in load_balancer.listeners.items(): if listener_arn == arn: continue - if listener.port == port: - raise DuplicateListenerError() listener.port = port diff --git a/moto/elbv2/responses.py b/moto/elbv2/responses.py index e6240ae49..b3d24cee3 100644 --- a/moto/elbv2/responses.py +++ b/moto/elbv2/responses.py @@ -735,6 +735,7 @@ CREATE_RULE_TEMPLATE = """