Add ECS CloudFormation support (#795)
* Add cloudformation support to AWS::ECS::Cluster * Add CloudFormation support to AWS::ECS::TaskDefinition * Add CloudFormation support to AWS::ECS::Service * Add support to update AWS::ECS::Cluster through CloudFormation * Fix Cluster.update_from_cloudformation_json to return original_resource if nothing changed * Implement TaskDefinition.update_from_cloudformation_json * Implement Service.update_from_cloudformation_json
This commit is contained in:
parent
a20906ff15
commit
0115267f2a
@ -3,11 +3,13 @@ import collections
|
||||
import functools
|
||||
import logging
|
||||
import copy
|
||||
import warnings
|
||||
|
||||
from moto.autoscaling import models as autoscaling_models
|
||||
from moto.awslambda import models as lambda_models
|
||||
from moto.datapipeline import models as datapipeline_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.iam import models as iam_models
|
||||
from moto.kms import models as kms_models
|
||||
@ -43,6 +45,9 @@ MODEL_MAP = {
|
||||
"AWS::EC2::VPC": ec2_models.VPC,
|
||||
"AWS::EC2::VPCGatewayAttachment": ec2_models.VPCGatewayAttachment,
|
||||
"AWS::EC2::VPCPeeringConnection": ec2_models.VPCPeeringConnection,
|
||||
"AWS::ECS::Cluster": ecs_models.Cluster,
|
||||
"AWS::ECS::TaskDefinition": ecs_models.TaskDefinition,
|
||||
"AWS::ECS::Service": ecs_models.Service,
|
||||
"AWS::ElasticLoadBalancing::LoadBalancer": elb_models.FakeLoadBalancer,
|
||||
"AWS::DataPipeline::Pipeline": datapipeline_models.Pipeline,
|
||||
"AWS::IAM::InstanceProfile": iam_models.InstanceProfile,
|
||||
@ -175,6 +180,8 @@ def parse_resource(logical_id, resource_json, resources_map):
|
||||
resource_type = resource_json['Type']
|
||||
resource_class = resource_class_from_type(resource_type)
|
||||
if not resource_class:
|
||||
warnings.warn(
|
||||
"Tried to parse {0} but it's not supported by moto's CloudFormation implementation".format(resource_type))
|
||||
return None
|
||||
|
||||
resource_json = clean_json(resource_json, resources_map)
|
||||
|
@ -1,6 +1,6 @@
|
||||
from __future__ import unicode_literals
|
||||
import uuid
|
||||
from random import randint
|
||||
from random import randint, random
|
||||
|
||||
from moto.core import BaseBackend
|
||||
from moto.ec2 import ec2_backends
|
||||
@ -48,13 +48,39 @@ class Cluster(BaseObject):
|
||||
del response_object['arn'], response_object['name']
|
||||
return response_object
|
||||
|
||||
@classmethod
|
||||
def create_from_cloudformation_json(cls, resource_name, cloudformation_json, region_name):
|
||||
properties = cloudformation_json['Properties']
|
||||
|
||||
ecs_backend = ecs_backends[region_name]
|
||||
return ecs_backend.create_cluster(
|
||||
# ClusterName is optional in CloudFormation, thus create a random name if necessary
|
||||
cluster_name=properties.get('ClusterName', 'ecscluster{0}'.format(int(random() * 10 ** 6))),
|
||||
)
|
||||
@classmethod
|
||||
def update_from_cloudformation_json(cls, original_resource, new_resource_name, cloudformation_json, region_name):
|
||||
properties = cloudformation_json['Properties']
|
||||
|
||||
if original_resource.name != properties['ClusterName']:
|
||||
ecs_backend = ecs_backends[region_name]
|
||||
ecs_backend.delete_cluster(original_resource.arn)
|
||||
return ecs_backend.create_cluster(
|
||||
# ClusterName is optional in CloudFormation, thus create a random name if necessary
|
||||
cluster_name=properties.get('ClusterName', 'ecscluster{0}'.format(int(random() * 10 ** 6))),
|
||||
)
|
||||
else:
|
||||
# no-op when nothing changed between old and new resources
|
||||
return original_resource
|
||||
|
||||
|
||||
class TaskDefinition(BaseObject):
|
||||
def __init__(self, family, revision, container_definitions, volumes=None):
|
||||
self.family = family
|
||||
self.arn = 'arn:aws:ecs:us-east-1:012345678910:task-definition/{0}:{1}'.format(family, revision)
|
||||
self.container_definitions = container_definitions
|
||||
if volumes is not None:
|
||||
if volumes is None:
|
||||
self.volumes = []
|
||||
else:
|
||||
self.volumes = volumes
|
||||
|
||||
@property
|
||||
@ -64,6 +90,37 @@ class TaskDefinition(BaseObject):
|
||||
del response_object['arn']
|
||||
return response_object
|
||||
|
||||
@classmethod
|
||||
def create_from_cloudformation_json(cls, resource_name, cloudformation_json, region_name):
|
||||
properties = cloudformation_json['Properties']
|
||||
|
||||
family = properties.get('Family', 'task-definition-{0}'.format(int(random() * 10 ** 6)))
|
||||
container_definitions = properties['ContainerDefinitions']
|
||||
volumes = properties['Volumes']
|
||||
|
||||
ecs_backend = ecs_backends[region_name]
|
||||
return ecs_backend.register_task_definition(
|
||||
family=family, container_definitions=container_definitions, volumes=volumes)
|
||||
|
||||
@classmethod
|
||||
def update_from_cloudformation_json(cls, original_resource, new_resource_name, cloudformation_json, region_name):
|
||||
properties = cloudformation_json['Properties']
|
||||
|
||||
family = properties.get('Family', 'task-definition-{0}'.format(int(random() * 10 ** 6)))
|
||||
container_definitions = properties['ContainerDefinitions']
|
||||
volumes = properties['Volumes']
|
||||
if (original_resource.family != family or
|
||||
original_resource.container_definitions != container_definitions or
|
||||
original_resource.volumes != volumes
|
||||
# currently TaskRoleArn isn't stored at TaskDefinition instances
|
||||
):
|
||||
ecs_backend = ecs_backends[region_name]
|
||||
ecs_backend.deregister_task_definition(original_resource.arn)
|
||||
return ecs_backend.register_task_definition(
|
||||
family=family, container_definitions=container_definitions, volumes=volumes)
|
||||
else:
|
||||
# no-op when nothing changed between old and new resources
|
||||
return original_resource
|
||||
|
||||
class Task(BaseObject):
|
||||
def __init__(self, cluster, task_definition, container_instance_arn, overrides={}, started_by=''):
|
||||
@ -105,6 +162,51 @@ class Service(BaseObject):
|
||||
response_object['serviceArn'] = self.arn
|
||||
return response_object
|
||||
|
||||
@classmethod
|
||||
def create_from_cloudformation_json(cls, resource_name, cloudformation_json, region_name):
|
||||
properties = cloudformation_json['Properties']
|
||||
if isinstance(properties['Cluster'], Cluster):
|
||||
cluster = properties['Cluster'].name
|
||||
else:
|
||||
cluster = properties['Cluster']
|
||||
if isinstance(properties['TaskDefinition'], TaskDefinition):
|
||||
task_definition = properties['TaskDefinition'].family
|
||||
else:
|
||||
task_definition = properties['TaskDefinition']
|
||||
service_name = '{0}Service{1}'.format(cluster, int(random() * 10 ** 6))
|
||||
desired_count = properties['DesiredCount']
|
||||
# TODO: LoadBalancers
|
||||
# TODO: Role
|
||||
|
||||
ecs_backend = ecs_backends[region_name]
|
||||
return ecs_backend.create_service(
|
||||
cluster, service_name, task_definition, desired_count)
|
||||
|
||||
@classmethod
|
||||
def update_from_cloudformation_json(cls, original_resource, new_resource_name, cloudformation_json, region_name):
|
||||
properties = cloudformation_json['Properties']
|
||||
if isinstance(properties['Cluster'], Cluster):
|
||||
cluster_name = properties['Cluster'].name
|
||||
else:
|
||||
cluster_name = properties['Cluster']
|
||||
if isinstance(properties['TaskDefinition'], TaskDefinition):
|
||||
task_definition = properties['TaskDefinition'].family
|
||||
else:
|
||||
task_definition = properties['TaskDefinition']
|
||||
desired_count = properties['DesiredCount']
|
||||
|
||||
ecs_backend = ecs_backends[region_name]
|
||||
service_name = original_resource.name
|
||||
if original_resource.cluster_arn != Cluster(cluster_name).arn:
|
||||
# TODO: LoadBalancers
|
||||
# TODO: Role
|
||||
ecs_backend.delete_service(cluster_name, service_name)
|
||||
new_service_name = '{0}Service{1}'.format(cluster_name, int(random() * 10 ** 6))
|
||||
return ecs_backend.create_service(
|
||||
cluster_name, new_service_name, task_definition, desired_count)
|
||||
else:
|
||||
return ecs_backend.update_service(cluster_name, service_name, task_definition, desired_count)
|
||||
|
||||
|
||||
class ContainerInstance(BaseObject):
|
||||
def __init__(self, ec2_instance_id):
|
||||
|
@ -1,10 +1,14 @@
|
||||
from __future__ import unicode_literals
|
||||
|
||||
from copy import deepcopy
|
||||
|
||||
import boto3
|
||||
import sure # noqa
|
||||
import json
|
||||
from moto.ec2 import utils as ec2_utils
|
||||
from uuid import UUID
|
||||
|
||||
from moto import mock_cloudformation
|
||||
from moto import mock_ecs
|
||||
from moto import mock_ec2
|
||||
|
||||
@ -918,3 +922,254 @@ def test_stop_task():
|
||||
stop_response['task']['lastStatus'].should.equal('STOPPED')
|
||||
stop_response['task']['desiredStatus'].should.equal('STOPPED')
|
||||
stop_response['task']['stoppedReason'].should.equal('moto testing')
|
||||
|
||||
|
||||
@mock_ecs
|
||||
@mock_cloudformation
|
||||
def test_create_cluster_through_cloudformation():
|
||||
template = {
|
||||
"AWSTemplateFormatVersion": "2010-09-09",
|
||||
"Description": "ECS Cluster Test CloudFormation",
|
||||
"Resources": {
|
||||
"testCluster": {
|
||||
"Type": "AWS::ECS::Cluster",
|
||||
"Properties": {
|
||||
"ClusterName": "testcluster"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
template_json = json.dumps(template)
|
||||
cfn_conn = boto3.client('cloudformation', region_name='us-west-1')
|
||||
cfn_conn.create_stack(
|
||||
StackName="test_stack",
|
||||
TemplateBody=template_json,
|
||||
)
|
||||
|
||||
ecs_conn = boto3.client('ecs', region_name='us-west-1')
|
||||
resp = ecs_conn.list_clusters()
|
||||
len(resp['clusterArns']).should.equal(1)
|
||||
|
||||
|
||||
@mock_ecs
|
||||
@mock_cloudformation
|
||||
def test_update_cluster_name_through_cloudformation_should_trigger_a_replacement():
|
||||
template1 = {
|
||||
"AWSTemplateFormatVersion": "2010-09-09",
|
||||
"Description": "ECS Cluster Test CloudFormation",
|
||||
"Resources": {
|
||||
"testCluster": {
|
||||
"Type": "AWS::ECS::Cluster",
|
||||
"Properties": {
|
||||
"ClusterName": "testcluster1"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
template2 = deepcopy(template1)
|
||||
template2['Resources']['testCluster']['Properties']['ClusterName'] = 'testcluster2'
|
||||
template1_json = json.dumps(template1)
|
||||
cfn_conn = boto3.client('cloudformation', region_name='us-west-1')
|
||||
stack_resp = cfn_conn.create_stack(
|
||||
StackName="test_stack",
|
||||
TemplateBody=template1_json,
|
||||
)
|
||||
|
||||
template2_json = json.dumps(template2)
|
||||
cfn_conn.update_stack(
|
||||
StackName=stack_resp['StackId'],
|
||||
TemplateBody=template2_json
|
||||
)
|
||||
ecs_conn = boto3.client('ecs', region_name='us-west-1')
|
||||
resp = ecs_conn.list_clusters()
|
||||
len(resp['clusterArns']).should.equal(1)
|
||||
resp['clusterArns'][0].endswith('testcluster2').should.be.true
|
||||
|
||||
|
||||
@mock_ecs
|
||||
@mock_cloudformation
|
||||
def test_create_task_definition_through_cloudformation():
|
||||
template = {
|
||||
"AWSTemplateFormatVersion": "2010-09-09",
|
||||
"Description": "ECS Cluster Test CloudFormation",
|
||||
"Resources": {
|
||||
"testTaskDefinition": {
|
||||
"Type" : "AWS::ECS::TaskDefinition",
|
||||
"Properties" : {
|
||||
"ContainerDefinitions" : [
|
||||
{
|
||||
"Name": "ecs-sample",
|
||||
"Image":"amazon/amazon-ecs-sample",
|
||||
"Cpu": "200",
|
||||
"Memory": "500",
|
||||
"Essential": "true"
|
||||
}
|
||||
],
|
||||
"Volumes" : [],
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
template_json = json.dumps(template)
|
||||
cfn_conn = boto3.client('cloudformation', region_name='us-west-1')
|
||||
cfn_conn.create_stack(
|
||||
StackName="test_stack",
|
||||
TemplateBody=template_json,
|
||||
)
|
||||
|
||||
ecs_conn = boto3.client('ecs', region_name='us-west-1')
|
||||
resp = ecs_conn.list_task_definitions()
|
||||
len(resp['taskDefinitionArns']).should.equal(1)
|
||||
|
||||
|
||||
@mock_ecs
|
||||
@mock_cloudformation
|
||||
def test_update_task_definition_family_through_cloudformation_should_trigger_a_replacement():
|
||||
template1 = {
|
||||
"AWSTemplateFormatVersion": "2010-09-09",
|
||||
"Description": "ECS Cluster Test CloudFormation",
|
||||
"Resources": {
|
||||
"testTaskDefinition": {
|
||||
"Type" : "AWS::ECS::TaskDefinition",
|
||||
"Properties" : {
|
||||
"Family": "testTaskDefinition1",
|
||||
"ContainerDefinitions" : [
|
||||
{
|
||||
"Name": "ecs-sample",
|
||||
"Image":"amazon/amazon-ecs-sample",
|
||||
"Cpu": "200",
|
||||
"Memory": "500",
|
||||
"Essential": "true"
|
||||
}
|
||||
],
|
||||
"Volumes" : [],
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
template1_json = json.dumps(template1)
|
||||
cfn_conn = boto3.client('cloudformation', region_name='us-west-1')
|
||||
cfn_conn.create_stack(
|
||||
StackName="test_stack",
|
||||
TemplateBody=template1_json,
|
||||
)
|
||||
|
||||
template2 = deepcopy(template1)
|
||||
template2['Resources']['testTaskDefinition']['Properties']['Family'] = 'testTaskDefinition2'
|
||||
template2_json = json.dumps(template2)
|
||||
cfn_conn.update_stack(
|
||||
StackName="test_stack",
|
||||
TemplateBody=template2_json,
|
||||
)
|
||||
|
||||
ecs_conn = boto3.client('ecs', region_name='us-west-1')
|
||||
resp = ecs_conn.list_task_definitions(familyPrefix='testTaskDefinition')
|
||||
len(resp['taskDefinitionArns']).should.equal(1)
|
||||
resp['taskDefinitionArns'][0].endswith('testTaskDefinition2:1').should.be.true
|
||||
|
||||
|
||||
@mock_ecs
|
||||
@mock_cloudformation
|
||||
def test_create_service_through_cloudformation():
|
||||
template = {
|
||||
"AWSTemplateFormatVersion": "2010-09-09",
|
||||
"Description": "ECS Cluster Test CloudFormation",
|
||||
"Resources": {
|
||||
"testCluster": {
|
||||
"Type": "AWS::ECS::Cluster",
|
||||
"Properties": {
|
||||
"ClusterName": "testcluster"
|
||||
}
|
||||
},
|
||||
"testTaskDefinition": {
|
||||
"Type" : "AWS::ECS::TaskDefinition",
|
||||
"Properties" : {
|
||||
"ContainerDefinitions" : [
|
||||
{
|
||||
"Name": "ecs-sample",
|
||||
"Image":"amazon/amazon-ecs-sample",
|
||||
"Cpu": "200",
|
||||
"Memory": "500",
|
||||
"Essential": "true"
|
||||
}
|
||||
],
|
||||
"Volumes" : [],
|
||||
}
|
||||
},
|
||||
"testService": {
|
||||
"Type": "AWS::ECS::Service",
|
||||
"Properties": {
|
||||
"Cluster": {"Ref": "testCluster"},
|
||||
"DesiredCount": 10,
|
||||
"TaskDefinition": {"Ref": "testTaskDefinition"},
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
template_json = json.dumps(template)
|
||||
cfn_conn = boto3.client('cloudformation', region_name='us-west-1')
|
||||
cfn_conn.create_stack(
|
||||
StackName="test_stack",
|
||||
TemplateBody=template_json,
|
||||
)
|
||||
|
||||
ecs_conn = boto3.client('ecs', region_name='us-west-1')
|
||||
resp = ecs_conn.list_services(cluster='testcluster')
|
||||
len(resp['serviceArns']).should.equal(1)
|
||||
|
||||
|
||||
@mock_ecs
|
||||
@mock_cloudformation
|
||||
def test_update_service_through_cloudformation_should_trigger_replacement():
|
||||
template1 = {
|
||||
"AWSTemplateFormatVersion": "2010-09-09",
|
||||
"Description": "ECS Cluster Test CloudFormation",
|
||||
"Resources": {
|
||||
"testCluster": {
|
||||
"Type": "AWS::ECS::Cluster",
|
||||
"Properties": {
|
||||
"ClusterName": "testcluster"
|
||||
}
|
||||
},
|
||||
"testTaskDefinition": {
|
||||
"Type" : "AWS::ECS::TaskDefinition",
|
||||
"Properties" : {
|
||||
"ContainerDefinitions" : [
|
||||
{
|
||||
"Name": "ecs-sample",
|
||||
"Image":"amazon/amazon-ecs-sample",
|
||||
"Cpu": "200",
|
||||
"Memory": "500",
|
||||
"Essential": "true"
|
||||
}
|
||||
],
|
||||
"Volumes" : [],
|
||||
}
|
||||
},
|
||||
"testService": {
|
||||
"Type": "AWS::ECS::Service",
|
||||
"Properties": {
|
||||
"Cluster": {"Ref": "testCluster"},
|
||||
"TaskDefinition": {"Ref": "testTaskDefinition"},
|
||||
"DesiredCount": 10,
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
template_json1 = json.dumps(template1)
|
||||
cfn_conn = boto3.client('cloudformation', region_name='us-west-1')
|
||||
cfn_conn.create_stack(
|
||||
StackName="test_stack",
|
||||
TemplateBody=template_json1,
|
||||
)
|
||||
template2 = deepcopy(template1)
|
||||
template2['Resources']['testService']['Properties']['DesiredCount'] = 5
|
||||
template2_json = json.dumps(template2)
|
||||
cfn_conn.update_stack(
|
||||
StackName="test_stack",
|
||||
TemplateBody=template2_json,
|
||||
)
|
||||
|
||||
ecs_conn = boto3.client('ecs', region_name='us-west-1')
|
||||
resp = ecs_conn.list_services(cluster='testcluster')
|
||||
len(resp['serviceArns']).should.equal(1)
|
||||
|
Loading…
Reference in New Issue
Block a user