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 <bblommers@users.noreply.github.com>
This commit is contained in:
Sahil Shah 2021-06-11 16:56:28 -04:00 committed by GitHub
parent 8d4007f2b6
commit 6977bba3e1
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
3 changed files with 243 additions and 82 deletions

View File

@ -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

View File

@ -735,6 +735,7 @@ CREATE_RULE_TEMPLATE = """<CreateRuleResponse xmlns="http://elasticloadbalancing
{% endfor %}
</Conditions>
<Priority>{{ rules.priority }}</Priority>
<RuleArn>{{ rules.arn }}</RuleArn>
<Actions>
{% for action in rules.actions %}
<member>
@ -759,7 +760,6 @@ CREATE_RULE_TEMPLATE = """<CreateRuleResponse xmlns="http://elasticloadbalancing
</member>
{% endfor %}
</Actions>
<RuleArn>{{ rules.arn }}</RuleArn>
</member>
</Rules>
</CreateRuleResult>
@ -922,6 +922,7 @@ DESCRIBE_RULES_TEMPLATE = """<DescribeRulesResponse xmlns="http://elasticloadbal
{% endfor %}
</Conditions>
<Priority>{{ rule.priority }}</Priority>
<RuleArn>{{ rule.arn }}</RuleArn>
<Actions>
{% for action in rule.actions %}
<member>
@ -929,7 +930,6 @@ DESCRIBE_RULES_TEMPLATE = """<DescribeRulesResponse xmlns="http://elasticloadbal
</member>
{% endfor %}
</Actions>
<RuleArn>{{ rule.arn }}</RuleArn>
</member>
{% endfor %}
</Rules>
@ -1063,6 +1063,7 @@ MODIFY_RULE_TEMPLATE = """<ModifyRuleResponse xmlns="http://elasticloadbalancing
{% endfor %}
</Conditions>
<Priority>{{ rules.priority }}</Priority>
<RuleArn>{{ rules.arn }}</RuleArn>
<Actions>
{% for action in rules.actions %}
<member>
@ -1070,7 +1071,6 @@ MODIFY_RULE_TEMPLATE = """<ModifyRuleResponse xmlns="http://elasticloadbalancing
</member>
{% endfor %}
</Actions>
<RuleArn>{{ rules.arn }}</RuleArn>
</member>
</Rules>
</ModifyRuleResult>
@ -1262,6 +1262,7 @@ SET_RULE_PRIORITIES_TEMPLATE = """<SetRulePrioritiesResponse xmlns="http://elast
{% endfor %}
</Conditions>
<Priority>{{ rules.priority }}</Priority>
<RuleArn>{{ rules.arn }}</RuleArn>
<Actions>
{% for action in rules.actions %}
<member>
@ -1284,7 +1285,6 @@ SET_RULE_PRIORITIES_TEMPLATE = """<SetRulePrioritiesResponse xmlns="http://elast
</member>
{% endfor %}
</Actions>
<RuleArn>{{ rules.arn }}</RuleArn>
</member>
</Rules>
</SetRulePrioritiesResult>

View File

@ -2223,6 +2223,109 @@ def test_invalid_action_type_listener_rule():
).should.throw(ClientError)
@mock_ec2
@mock_elbv2
@mock_cloudformation
@mock_events
def test_update_stack_listener_and_rule():
initial_template = {
"AWSTemplateFormatVersion": "2010-09-09",
"Resources": {
"alb": {
"Type": "AWS::ElasticLoadBalancingV2::LoadBalancer",
"Properties": {
"Name": "myelbv2",
"Scheme": "internet-facing",
"Subnets": [{"Ref": "mysubnet"}],
"SecurityGroups": [{"Ref": "mysg"}],
"Type": "application",
"IpAddressType": "ipv4",
},
},
"mytargetgroup1": {
"Type": "AWS::ElasticLoadBalancingV2::TargetGroup",
"Properties": {"Name": "mytargetgroup1",},
},
"mytargetgroup2": {
"Type": "AWS::ElasticLoadBalancingV2::TargetGroup",
"Properties": {"Name": "mytargetgroup2",},
},
"listener": {
"Type": "AWS::ElasticLoadBalancingV2::Listener",
"Properties": {
"DefaultActions": [
{"Type": "forward", "TargetGroupArn": {"Ref": "mytargetgroup1"}}
],
"LoadBalancerArn": {"Ref": "alb"},
"Port": "80",
"Protocol": "HTTP",
},
},
"rule": {
"Type": "AWS::ElasticLoadBalancingV2::ListenerRule",
"Properties": {
"Actions": [
{
"Type": "forward",
"TargetGroupArn": {"Ref": "mytargetgroup2"},
}
],
"Conditions": [{"Field": "path-pattern", "Values": ["/*"]}],
"ListenerArn": {"Ref": "listener"},
"Priority": 2,
},
},
"myvpc": {
"Type": "AWS::EC2::VPC",
"Properties": {"CidrBlock": "10.0.0.0/16"},
},
"mysubnet": {
"Type": "AWS::EC2::Subnet",
"Properties": {"CidrBlock": "10.0.0.0/27", "VpcId": {"Ref": "myvpc"}},
},
"mysg": {
"Type": "AWS::EC2::SecurityGroup",
"Properties": {
"GroupName": "mysg",
"GroupDescription": "test security group",
"VpcId": {"Ref": "myvpc"},
},
},
},
}
initial_template_json = json.dumps(initial_template)
cfn_conn = boto3.client("cloudformation", "us-west-1")
cfn_conn.create_stack(StackName="initial_stack", TemplateBody=initial_template_json)
elbv2_conn = boto3.client("elbv2", "us-west-1")
initial_template["Resources"]["rule"]["Properties"]["Conditions"][0][
"Field"
] = "host-header"
initial_template["Resources"]["rule"]["Properties"]["Conditions"][0]["Values"] = "*"
initial_template["Resources"]["listener"]["Properties"]["Port"] = 90
initial_template_json = json.dumps(initial_template)
cfn_conn.update_stack(StackName="initial_stack", TemplateBody=initial_template_json)
load_balancers = elbv2_conn.describe_load_balancers()["LoadBalancers"]
listeners = elbv2_conn.describe_listeners(
LoadBalancerArn=load_balancers[0]["LoadBalancerArn"]
)["Listeners"]
listeners[0]["Port"].should.equal(90)
listener_rule = elbv2_conn.describe_rules(ListenerArn=listeners[0]["ListenerArn"])[
"Rules"
]
listener_rule[0]["Conditions"].should.equal(
[{"Field": "host-header", "Values": ["*"],}]
)
@mock_ec2
@mock_elbv2
@mock_cloudformation
@ -2323,7 +2426,7 @@ def test_stack_elbv2_resources_integration():
},
}
],
"Conditions": [{"field": "path-pattern", "values": ["/*"]}],
"Conditions": [{"Field": "path-pattern", "Values": ["/*"]}],
"ListenerArn": {"Ref": "listener"},
"Priority": 2,
},
@ -2334,7 +2437,7 @@ def test_stack_elbv2_resources_integration():
"Actions": [
{"Type": "forward", "TargetGroupArn": {"Ref": "mytargetgroup2"}}
],
"Conditions": [{"field": "host-header", "values": ["example.com"]}],
"Conditions": [{"Field": "host-header", "Values": ["example.com"]}],
"ListenerArn": {"Ref": "listener"},
"Priority": 30,
},