From 307e13796b194e8395ae2333b61806c629626dcc Mon Sep 17 00:00:00 2001 From: Hugo Lopes Tavares Date: Fri, 27 Oct 2017 11:53:42 -0400 Subject: [PATCH 1/9] Add IpAddressType to DESCRIBE_LOAD_BALANCERS_TEMPLATE --- moto/elbv2/responses.py | 1 + 1 file changed, 1 insertion(+) diff --git a/moto/elbv2/responses.py b/moto/elbv2/responses.py index 3e8535187..3a83dbfa5 100644 --- a/moto/elbv2/responses.py +++ b/moto/elbv2/responses.py @@ -572,6 +572,7 @@ DESCRIBE_LOAD_BALANCERS_TEMPLATE = """ Date: Fri, 27 Oct 2017 13:25:22 -0400 Subject: [PATCH 3/9] Add CloudFormation support to AWS::ElasticLoadBalancingV2::TargetGroup --- moto/cloudformation/parsing.py | 1 + moto/elbv2/models.py | 43 ++++++++++++++++++- moto/elbv2/responses.py | 16 +++++-- .../test_cloudformation_stack_integration.py | 25 ++++++++--- 4 files changed, 76 insertions(+), 9 deletions(-) diff --git a/moto/cloudformation/parsing.py b/moto/cloudformation/parsing.py index 7402d86c7..5da6c8cb0 100644 --- a/moto/cloudformation/parsing.py +++ b/moto/cloudformation/parsing.py @@ -63,6 +63,7 @@ MODEL_MAP = { "AWS::ECS::Service": ecs_models.Service, "AWS::ElasticLoadBalancing::LoadBalancer": elb_models.FakeLoadBalancer, "AWS::ElasticLoadBalancingV2::LoadBalancer": elbv2_models.FakeLoadBalancer, + "AWS::ElasticLoadBalancingV2::TargetGroup": elbv2_models.FakeTargetGroup, "AWS::DataPipeline::Pipeline": datapipeline_models.Pipeline, "AWS::IAM::InstanceProfile": iam_models.InstanceProfile, "AWS::IAM::Role": iam_models.Role, diff --git a/moto/elbv2/models.py b/moto/elbv2/models.py index 391372608..5e4f58469 100644 --- a/moto/elbv2/models.py +++ b/moto/elbv2/models.py @@ -52,7 +52,9 @@ class FakeTargetGroup(BaseModel): healthcheck_interval_seconds, healthcheck_timeout_seconds, healthy_threshold_count, - unhealthy_threshold_count): + unhealthy_threshold_count, + matcher=None, + target_type=None): self.name = name self.arn = arn self.vpc_id = vpc_id @@ -67,6 +69,8 @@ class FakeTargetGroup(BaseModel): self.unhealthy_threshold_count = unhealthy_threshold_count self.load_balancer_arns = [] self.tags = {} + self.matcher = matcher + self.target_type = target_type self.attributes = { 'deregistration_delay.timeout_seconds': 300, @@ -99,6 +103,43 @@ class FakeTargetGroup(BaseModel): raise InvalidTargetError() return FakeHealthStatus(t['id'], t['port'], self.healthcheck_port, 'healthy') + @classmethod + def create_from_cloudformation_json(cls, resource_name, cloudformation_json, region_name): + properties = cloudformation_json['Properties'] + + elbv2_backend = elbv2_backends[region_name] + + name = properties.get('Name') + vpc_id = properties.get("VpcId") + protocol = properties.get('Protocol') + port = properties.get("Port") + healthcheck_protocol = properties.get("HealthCheckProtocol") + healthcheck_port = properties.get("HealthCheckPort") + healthcheck_path = properties.get("HealthCheckPath") + healthcheck_interval_seconds = properties.get("HealthCheckIntervalSeconds") + healthcheck_timeout_seconds = properties.get("HealthCheckTimeoutSeconds") + healthy_threshold_count = properties.get("HealthyThresholdCount") + unhealthy_threshold_count = properties.get("UnhealthyThresholdCount") + matcher = properties.get("Matcher") + target_type = properties.get("TargetType") + + target_group = elbv2_backend.create_target_group( + name=name, + vpc_id=vpc_id, + protocol=protocol, + port=port, + healthcheck_protocol=healthcheck_protocol, + healthcheck_port=healthcheck_port, + healthcheck_path=healthcheck_path, + healthcheck_interval_seconds=healthcheck_interval_seconds, + healthcheck_timeout_seconds=healthcheck_timeout_seconds, + healthy_threshold_count=healthy_threshold_count, + unhealthy_threshold_count=unhealthy_threshold_count, + matcher=matcher, + target_type=target_type, + ) + return target_group + class FakeListener(BaseModel): diff --git a/moto/elbv2/responses.py b/moto/elbv2/responses.py index 3a83dbfa5..e8bc5bc23 100644 --- a/moto/elbv2/responses.py +++ b/moto/elbv2/responses.py @@ -472,9 +472,14 @@ CREATE_TARGET_GROUP_TEMPLATE = """ Date: Fri, 27 Oct 2017 14:24:45 -0400 Subject: [PATCH 4/9] Add CloudFormation support to AWS::ElasticLoadBalancingV2::Listener --- moto/cloudformation/parsing.py | 1 + moto/elbv2/models.py | 32 ++++++++++++++++++- .../test_cloudformation_stack_integration.py | 10 ++++++ 3 files changed, 42 insertions(+), 1 deletion(-) diff --git a/moto/cloudformation/parsing.py b/moto/cloudformation/parsing.py index 5da6c8cb0..deafd7cff 100644 --- a/moto/cloudformation/parsing.py +++ b/moto/cloudformation/parsing.py @@ -64,6 +64,7 @@ MODEL_MAP = { "AWS::ElasticLoadBalancing::LoadBalancer": elb_models.FakeLoadBalancer, "AWS::ElasticLoadBalancingV2::LoadBalancer": elbv2_models.FakeLoadBalancer, "AWS::ElasticLoadBalancingV2::TargetGroup": elbv2_models.FakeTargetGroup, + "AWS::ElasticLoadBalancingV2::Listener": elbv2_models.FakeListener, "AWS::DataPipeline::Pipeline": datapipeline_models.Pipeline, "AWS::IAM::InstanceProfile": iam_models.InstanceProfile, "AWS::IAM::Role": iam_models.Role, diff --git a/moto/elbv2/models.py b/moto/elbv2/models.py index 5e4f58469..4fd4147f7 100644 --- a/moto/elbv2/models.py +++ b/moto/elbv2/models.py @@ -79,6 +79,10 @@ class FakeTargetGroup(BaseModel): self.targets = OrderedDict() + @property + def physical_resource_id(self): + return self.arn + def register(self, targets): for target in targets: self.targets[target['id']] = { @@ -160,6 +164,10 @@ class FakeListener(BaseModel): is_default=True ) + @property + def physical_resource_id(self): + return self.arn + @property def rules(self): return self._non_default_rules + [self._default_rule] @@ -171,6 +179,28 @@ class FakeListener(BaseModel): self._non_default_rules.append(rule) self._non_default_rules = sorted(self._non_default_rules, key=lambda x: x.priority) + @classmethod + def create_from_cloudformation_json(cls, resource_name, cloudformation_json, region_name): + properties = cloudformation_json['Properties'] + + elbv2_backend = elbv2_backends[region_name] + load_balancer_arn = properties.get("LoadBalancerArn") + protocol = properties.get("Protocol") + 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 + if "DefaultActions" in properties: + default_actions = [] + for action in properties['DefaultActions']: + default_actions.append({'type': action['Type'], 'target_group_arn': action['TargetGroupArn']}) + else: + default_actions = None + + listener = elbv2_backend.create_listener( + load_balancer_arn, protocol, port, ssl_policy, certificates, default_actions) + return listener + class FakeRule(BaseModel): @@ -209,7 +239,7 @@ class FakeLoadBalancer(BaseModel): @property def physical_resource_id(self): - return self.name + return self.arn def add_tag(self, key, value): if len(self.tags) >= 10 and key not in self.tags: diff --git a/tests/test_cloudformation/test_cloudformation_stack_integration.py b/tests/test_cloudformation/test_cloudformation_stack_integration.py index 61eec3997..cd3f61b9e 100644 --- a/tests/test_cloudformation/test_cloudformation_stack_integration.py +++ b/tests/test_cloudformation/test_cloudformation_stack_integration.py @@ -2236,3 +2236,13 @@ def test_stack_elbv2_resources_integration(): target_groups[0]['Port'].should.equal(80) target_groups[0]['Protocol'].should.equal('HTTP') target_groups[0]['TargetType'].should.equal('instance') + + listeners = elbv2_conn.describe_listeners(LoadBalancerArn=load_balancers[0]['LoadBalancerArn'])['Listeners'] + len(listeners).should.equal(1) + listeners[0]['LoadBalancerArn'].should.equal(load_balancers[0]['LoadBalancerArn']) + listeners[0]['Port'].should.equal(80) + listeners[0]['Protocol'].should.equal('HTTP') + listeners[0]['DefaultActions'].should.equal([{ + "Type": "forward", + "TargetGroupArn": target_groups[0]['TargetGroupArn'] + }]) From 82ad66256ae6e304c08f0b8553273cc78f1d6714 Mon Sep 17 00:00:00 2001 From: Hugo Lopes Tavares Date: Fri, 27 Oct 2017 15:01:17 -0400 Subject: [PATCH 5/9] Add default name to TargetGroup (cloudformation) --- moto/elbv2/models.py | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/moto/elbv2/models.py b/moto/elbv2/models.py index 4fd4147f7..4e3c928a3 100644 --- a/moto/elbv2/models.py +++ b/moto/elbv2/models.py @@ -113,7 +113,10 @@ class FakeTargetGroup(BaseModel): elbv2_backend = elbv2_backends[region_name] - name = properties.get('Name') + # per cloudformation docs: + # The target group name should be shorter than 22 characters because + # AWS CloudFormation uses the target group name to create the name of the load balancer. + name = properties.get('Name', resource_name[:22]) vpc_id = properties.get("VpcId") protocol = properties.get('Protocol') port = properties.get("Port") @@ -364,7 +367,7 @@ class ELBv2Backend(BaseBackend): def create_target_group(self, name, **kwargs): if len(name) > 32: raise InvalidTargetGroupNameError( - "Target group name '%s' cannot be longer than '32' characters" % name + "Target group name '%s' cannot be longer than '22' characters" % name ) if not re.match('^[a-zA-Z0-9\-]+$', name): raise InvalidTargetGroupNameError( From 474ebfea40fa9f796d54dc8aa7bba2243ece9be3 Mon Sep 17 00:00:00 2001 From: Hugo Lopes Tavares Date: Fri, 27 Oct 2017 15:01:25 -0400 Subject: [PATCH 6/9] Add default name to LoadBalancer (cloudformation) --- moto/elbv2/models.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/moto/elbv2/models.py b/moto/elbv2/models.py index 4e3c928a3..a34926ea2 100644 --- a/moto/elbv2/models.py +++ b/moto/elbv2/models.py @@ -266,7 +266,7 @@ class FakeLoadBalancer(BaseModel): elbv2_backend = elbv2_backends[region_name] - name = properties.get('Name') + name = properties.get('Name', resource_name) security_groups = properties.get("SecurityGroups") subnet_ids = properties.get('Subnets') scheme = properties.get('Scheme', 'internet-facing') From 9804d7a9634114c46741a550dae62e752a17eab3 Mon Sep 17 00:00:00 2001 From: Hugo Lopes Tavares Date: Fri, 27 Oct 2017 15:31:45 -0400 Subject: [PATCH 7/9] Evaluate output values Do not assume output values are string, evaluate them. --- moto/cloudformation/parsing.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/moto/cloudformation/parsing.py b/moto/cloudformation/parsing.py index deafd7cff..1c13c5058 100644 --- a/moto/cloudformation/parsing.py +++ b/moto/cloudformation/parsing.py @@ -330,7 +330,7 @@ def parse_output(output_logical_id, output_json, resources_map): output_json = clean_json(output_json, resources_map) output = Output() output.key = output_logical_id - output.value = output_json['Value'] + output.value = clean_json(output_json['Value'], resources_map) output.description = output_json.get('Description') return output From 63c33211ee707aa5fc8b92627ece3632855ee335 Mon Sep 17 00:00:00 2001 From: Hugo Lopes Tavares Date: Fri, 27 Oct 2017 15:32:16 -0400 Subject: [PATCH 8/9] Add GetAtt support to elbv2 LoadBalancer --- moto/elbv2/models.py | 6 +++++ .../test_cloudformation_stack_integration.py | 22 +++++++++++++++++-- 2 files changed, 26 insertions(+), 2 deletions(-) diff --git a/moto/elbv2/models.py b/moto/elbv2/models.py index a34926ea2..a612ba93a 100644 --- a/moto/elbv2/models.py +++ b/moto/elbv2/models.py @@ -274,6 +274,12 @@ class FakeLoadBalancer(BaseModel): load_balancer = elbv2_backend.create_load_balancer(name, security_groups, subnet_ids, scheme=scheme) return load_balancer + def get_cfn_attribute(self, attribute_name): + attributes = { + 'DNSName': self.dns_name, + 'LoadBalancerName': self.name, + } + return attributes[attribute_name] class ELBv2Backend(BaseBackend): diff --git a/tests/test_cloudformation/test_cloudformation_stack_integration.py b/tests/test_cloudformation/test_cloudformation_stack_integration.py index cd3f61b9e..2d44fb5b7 100644 --- a/tests/test_cloudformation/test_cloudformation_stack_integration.py +++ b/tests/test_cloudformation/test_cloudformation_stack_integration.py @@ -2119,6 +2119,16 @@ def test_stack_spot_fleet(): def test_stack_elbv2_resources_integration(): alb_template = { "AWSTemplateFormatVersion": "2010-09-09", + "Outputs": { + "albdns": { + "Description": "Load balanacer DNS", + "Value": {"Fn::GetAtt": ["alb", "DNSName"]}, + }, + "albname": { + "Description": "Load balancer name", + "Value": {"Fn::GetAtt": ["alb", "LoadBalancerName"]}, + }, + }, "Resources": { "alb": { "Type": "AWS::ElasticLoadBalancingV2::LoadBalancer", @@ -2207,8 +2217,8 @@ def test_stack_elbv2_resources_integration(): } alb_template_json = json.dumps(alb_template) - conn = boto3.client("cloudformation", "us-west-1") - conn.create_stack( + cfn_conn = boto3.client("cloudformation", "us-west-1") + cfn_conn.create_stack( StackName="elb_stack", TemplateBody=alb_template_json, ) @@ -2246,3 +2256,11 @@ def test_stack_elbv2_resources_integration(): "Type": "forward", "TargetGroupArn": target_groups[0]['TargetGroupArn'] }]) + + # test outputs + stacks = cfn_conn.describe_stacks(StackName='elb_stack')['Stacks'] + len(stacks).should.equal(1) + stacks[0]['Outputs'].should.equal([ + {'OutputKey': 'albdns', 'OutputValue': load_balancers[0]['DNSName']}, + {'OutputKey': 'albname', 'OutputValue': load_balancers[0]['LoadBalancerName']}, + ]) From cc777f6049b86553a30cb1faf7b1f218ae7bf9ef Mon Sep 17 00:00:00 2001 From: Hugo Lopes Tavares Date: Fri, 27 Oct 2017 16:25:59 -0400 Subject: [PATCH 9/9] Add missing newline (lint error) --- moto/elbv2/models.py | 1 + 1 file changed, 1 insertion(+) diff --git a/moto/elbv2/models.py b/moto/elbv2/models.py index a612ba93a..c565aa062 100644 --- a/moto/elbv2/models.py +++ b/moto/elbv2/models.py @@ -281,6 +281,7 @@ class FakeLoadBalancer(BaseModel): } return attributes[attribute_name] + class ELBv2Backend(BaseBackend): def __init__(self, region_name=None):