Merge pull request #1314 from spulec/cloudformation-support-to-elbv2
Add CloudFormation support to ELB V2
This commit is contained in:
commit
439a3f9aab
@ -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
|
||||
|
||||
|
@ -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(
|
||||
|
@ -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>
|
||||
|
@ -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']},
|
||||
])
|
||||
|
Loading…
Reference in New Issue
Block a user