Merge pull request #1314 from spulec/cloudformation-support-to-elbv2

Add CloudFormation support to ELB V2
This commit is contained in:
Jack Danger 2017-10-28 00:25:41 +02:00 committed by GitHub
commit 439a3f9aab
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
4 changed files with 271 additions and 8 deletions

View File

@ -15,6 +15,7 @@ from moto.dynamodb import models as dynamodb_models
from moto.ec2 import models as ec2_models
from moto.ecs import models as ecs_models
from moto.elb import models as elb_models
from moto.elbv2 import models as elbv2_models
from moto.iam import models as iam_models
from moto.kinesis import models as kinesis_models
from moto.kms import models as kms_models
@ -61,6 +62,9 @@ MODEL_MAP = {
"AWS::ECS::TaskDefinition": ecs_models.TaskDefinition,
"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::ElasticLoadBalancingV2::Listener": elbv2_models.FakeListener,
"AWS::DataPipeline::Pipeline": datapipeline_models.Pipeline,
"AWS::IAM::InstanceProfile": iam_models.InstanceProfile,
"AWS::IAM::Role": iam_models.Role,
@ -326,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

View File

@ -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,
@ -75,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']] = {
@ -99,6 +107,46 @@ 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]
# 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")
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):
@ -119,6 +167,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]
@ -130,6 +182,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):
@ -168,7 +242,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:
@ -186,6 +260,27 @@ class FakeLoadBalancer(BaseModel):
''' Not exposed as part of the ELB API - used for CloudFormation. '''
elbv2_backends[region].delete_load_balancer(self.arn)
@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', resource_name)
security_groups = properties.get("SecurityGroups")
subnet_ids = properties.get('Subnets')
scheme = properties.get('Scheme', 'internet-facing')
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):
@ -279,7 +374,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(

View File

@ -472,9 +472,14 @@ CREATE_TARGET_GROUP_TEMPLATE = """<CreateTargetGroupResponse xmlns="http://elast
<HealthCheckTimeoutSeconds>{{ target_group.healthcheck_timeout_seconds }}</HealthCheckTimeoutSeconds>
<HealthyThresholdCount>{{ target_group.healthy_threshold_count }}</HealthyThresholdCount>
<UnhealthyThresholdCount>{{ target_group.unhealthy_threshold_count }}</UnhealthyThresholdCount>
{% if target_group.matcher %}
<Matcher>
<HttpCode>200</HttpCode>
<HttpCode>{{ target_group.matcher['HttpCode'] }}</HttpCode>
</Matcher>
{% endif %}
{% if target_group.target_type %}
<TargetType>{{ target_group.target_type }}</TargetType>
{% endif %}
</member>
</TargetGroups>
</CreateTargetGroupResult>
@ -572,6 +577,7 @@ DESCRIBE_LOAD_BALANCERS_TEMPLATE = """<DescribeLoadBalancersResponse xmlns="http
<Code>provisioning</Code>
</State>
<Type>application</Type>
<IpAddressType>ipv4</IpAddressType>
</member>
{% endfor %}
</LoadBalancers>
@ -634,16 +640,21 @@ DESCRIBE_TARGET_GROUPS_TEMPLATE = """<DescribeTargetGroupsResponse xmlns="http:/
<Protocol>{{ target_group.protocol }}</Protocol>
<Port>{{ target_group.port }}</Port>
<VpcId>{{ target_group.vpc_id }}</VpcId>
<HealthCheckProtocol>{{ target_group.health_check_protocol }}</HealthCheckProtocol>
<HealthCheckProtocol>{{ target_group.healthcheck_protocol }}</HealthCheckProtocol>
<HealthCheckPort>{{ target_group.healthcheck_port }}</HealthCheckPort>
<HealthCheckPath>{{ target_group.healthcheck_path }}</HealthCheckPath>
<HealthCheckIntervalSeconds>{{ target_group.healthcheck_interval_seconds }}</HealthCheckIntervalSeconds>
<HealthCheckTimeoutSeconds>{{ target_group.healthcheck_timeout_seconds }}</HealthCheckTimeoutSeconds>
<HealthyThresholdCount>{{ target_group.healthy_threshold_count }}</HealthyThresholdCount>
<UnhealthyThresholdCount>{{ target_group.unhealthy_threshold_count }}</UnhealthyThresholdCount>
{% if target_group.matcher %}
<Matcher>
<HttpCode>200</HttpCode>
<HttpCode>{{ target_group.matcher['HttpCode'] }}</HttpCode>
</Matcher>
{% endif %}
{% if target_group.target_type %}
<TargetType>{{ target_group.target_type }}</TargetType>
{% endif %}
<LoadBalancerArns>
{% for load_balancer_arn in target_group.load_balancer_arns %}
<member>{{ load_balancer_arn }}</member>

View File

@ -38,7 +38,7 @@ from moto import (
mock_sns_deprecated,
mock_sqs,
mock_sqs_deprecated,
)
mock_elbv2)
from .fixtures import (
ec2_classic_eip,
@ -2111,3 +2111,156 @@ def test_stack_spot_fleet():
launch_spec['SubnetId'].should.equal(subnet_id)
launch_spec['SpotPrice'].should.equal("0.13")
launch_spec['WeightedCapacity'].should.equal(2.0)
@mock_ec2
@mock_elbv2
@mock_cloudformation
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",
"Properties": {
"Name": "myelbv2",
"Scheme": "internet-facing",
"Subnets": [{
"Ref": "mysubnet",
}],
"SecurityGroups": [{
"Ref": "mysg",
}],
"Type": "application",
"IpAddressType": "ipv4",
}
},
"mytargetgroup": {
"Type": "AWS::ElasticLoadBalancingV2::TargetGroup",
"Properties": {
"HealthCheckIntervalSeconds": 30,
"HealthCheckPath": "/status",
"HealthCheckPort": 80,
"HealthCheckProtocol": "HTTP",
"HealthCheckTimeoutSeconds": 5,
"HealthyThresholdCount": 30,
"UnhealthyThresholdCount": 5,
"Matcher": {
"HttpCode": "200,201"
},
"Name": "mytargetgroup",
"Port": 80,
"Protocol": "HTTP",
"TargetType": "instance",
"Targets": [{
"Id": {
"Ref": "ec2instance",
"Port": 80,
},
}],
"VpcId": {
"Ref": "myvpc",
}
}
},
"listener": {
"Type": "AWS::ElasticLoadBalancingV2::Listener",
"Properties": {
"DefaultActions": [{
"Type": "forward",
"TargetGroupArn": {"Ref": "mytargetgroup"}
}],
"LoadBalancerArn": {"Ref": "alb"},
"Port": "80",
"Protocol": "HTTP"
}
},
"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"}
}
},
"ec2instance": {
"Type": "AWS::EC2::Instance",
"Properties": {
"ImageId": "ami-1234abcd",
"UserData": "some user data",
}
},
},
}
alb_template_json = json.dumps(alb_template)
cfn_conn = boto3.client("cloudformation", "us-west-1")
cfn_conn.create_stack(
StackName="elb_stack",
TemplateBody=alb_template_json,
)
elbv2_conn = boto3.client("elbv2", "us-west-1")
load_balancers = elbv2_conn.describe_load_balancers()['LoadBalancers']
len(load_balancers).should.equal(1)
load_balancers[0]['LoadBalancerName'].should.equal('myelbv2')
load_balancers[0]['Scheme'].should.equal('internet-facing')
load_balancers[0]['Type'].should.equal('application')
load_balancers[0]['IpAddressType'].should.equal('ipv4')
target_groups = elbv2_conn.describe_target_groups()['TargetGroups']
len(target_groups).should.equal(1)
target_groups[0]['HealthCheckIntervalSeconds'].should.equal(30)
target_groups[0]['HealthCheckPath'].should.equal('/status')
target_groups[0]['HealthCheckPort'].should.equal('80')
target_groups[0]['HealthCheckProtocol'].should.equal('HTTP')
target_groups[0]['HealthCheckTimeoutSeconds'].should.equal(5)
target_groups[0]['HealthyThresholdCount'].should.equal(30)
target_groups[0]['UnhealthyThresholdCount'].should.equal(5)
target_groups[0]['Matcher'].should.equal({'HttpCode': '200,201'})
target_groups[0]['TargetGroupName'].should.equal('mytargetgroup')
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']
}])
# 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']},
])