diff --git a/AUTHORS.md b/AUTHORS.md index 08757d2bb..5d5b99a06 100644 --- a/AUTHORS.md +++ b/AUTHORS.md @@ -45,3 +45,4 @@ Moto is written by Steve Pulec with contributions from: * [Tom Viner](https://github.com/tomviner) * [Justin Wiley](https://github.com/SectorNine50) * [Adam Stauffer](https://github.com/adamstauffer) +* [Guy Templeton](https://github.com/gjtempleton) diff --git a/moto/ecs/models.py b/moto/ecs/models.py index fa589b161..d3ec2b7f7 100644 --- a/moto/ecs/models.py +++ b/moto/ecs/models.py @@ -262,8 +262,9 @@ class ContainerInstance(BaseObject): def __init__(self, ec2_instance_id): self.ec2_instance_id = ec2_instance_id + self.agent_connected = True self.status = 'ACTIVE' - self.registeredResources = [ + self.registered_resources = [ {'doubleValue': 0.0, 'integerValue': 4096, 'longValue': 0, @@ -286,11 +287,10 @@ class ContainerInstance(BaseObject): 'name': 'PORTS_UDP', 'stringSetValue': [], 'type': 'STRINGSET'}] - self.agentConnected = True - self.containerInstanceArn = "arn:aws:ecs:us-east-1:012345678910:container-instance/{0}".format( + self.container_instance_arn = "arn:aws:ecs:us-east-1:012345678910:container-instance/{0}".format( str(uuid.uuid1())) - self.pendingTaskCount = 0 - self.remainingResources = [ + self.pending_task_count = 0 + self.remaining_resources = [ {'doubleValue': 0.0, 'integerValue': 4096, 'longValue': 0, @@ -314,18 +314,17 @@ class ContainerInstance(BaseObject): 'stringSetValue': [], 'type': 'STRINGSET'} ] - self.runningTaskCount = 0 - self.versionInfo = { + self.running_task_count = 0 + self.version_info = { 'agentVersion': "1.0.0", 'agentHash': '4023248', 'dockerVersion': 'DockerVersion: 1.5.0' } - @property - def response_object(self): - response_object = self.gen_response_object() - del response_object['name'], response_object['arn'] - return response_object + @property + def response_object(self): + response_object = self.gen_response_object() + return response_object class ContainerInstanceFailure(BaseObject): @@ -456,7 +455,7 @@ class EC2ContainerServiceBackend(BaseBackend): placed_count = 0 for container_instance in active_container_instances: container_instance = self.container_instances[cluster_name][container_instance] - container_instance_arn = container_instance.containerInstanceArn + container_instance_arn = container_instance.container_instance_arn try_to_place = True while try_to_place: can_be_placed, message = self._can_be_placed(container_instance, resource_requirements) @@ -497,7 +496,7 @@ class EC2ContainerServiceBackend(BaseBackend): remaining_cpu = 0 remaining_memory = 0 reserved_ports = [] - for resource in container_instance.remainingResources: + for resource in container_instance.remaining_resources: if resource.get("name") == "CPU": remaining_cpu = resource.get("integerValue") elif resource.get("name") == "MEMORY": @@ -534,7 +533,7 @@ class EC2ContainerServiceBackend(BaseBackend): container_instance = self.container_instances[cluster_name][ container_instance_id ] - task = Task(cluster, task_definition, container_instance.containerInstanceArn, + task = Task(cluster, task_definition, container_instance.container_instance_arn, resource_requirements, overrides or {}, started_by or '') tasks.append(task) self.update_container_instance_resources(container_instance, resource_requirements) @@ -670,10 +669,11 @@ class EC2ContainerServiceBackend(BaseBackend): container_instance = ContainerInstance(ec2_instance_id) if not self.container_instances.get(cluster_name): self.container_instances[cluster_name] = {} - container_instance_id = container_instance.containerInstanceArn.split( + container_instance_id = container_instance.container_instance_arn.split( '/')[-1] self.container_instances[cluster_name][ container_instance_id] = container_instance + self.clusters[cluster_name].registered_container_instances_count += 1 return container_instance def list_container_instances(self, cluster_str): @@ -681,7 +681,7 @@ class EC2ContainerServiceBackend(BaseBackend): container_instances_values = self.container_instances.get( cluster_name, {}).values() container_instances = [ - ci.containerInstanceArn for ci in container_instances_values] + ci.container_instance_arn for ci in container_instances_values] return sorted(container_instances) def describe_container_instances(self, cluster_str, list_container_instance_ids): @@ -726,7 +726,7 @@ class EC2ContainerServiceBackend(BaseBackend): resource_multiplier = 1 if removing: resource_multiplier = -1 - for resource in container_instance.remainingResources: + for resource in container_instance.remaining_resources: if resource.get("name") == "CPU": resource["integerValue"] -= task_resources.get('CPU') * resource_multiplier elif resource.get("name") == "MEMORY": @@ -737,8 +737,33 @@ class EC2ContainerServiceBackend(BaseBackend): resource["stringSetValue"].remove(str(port)) else: resource["stringSetValue"].append(str(port)) + container_instance.running_task_count += resource_multiplier * 1 - def deregister_container_instance(self, cluster_str, container_instance_str): + def deregister_container_instance(self, cluster_str, container_instance_str, force): + failures = [] + cluster_name = cluster_str.split('/')[-1] + if cluster_name not in self.clusters: + raise Exception("{0} is not a cluster".format(cluster_name)) + container_instance_id = container_instance_str.split('/')[-1] + container_instance = self.container_instances[cluster_name].get(container_instance_id) + if container_instance is None: + raise Exception("{0} is not a container id in the cluster") + if not force and container_instance.running_task_count > 0: + raise Exception("Found running tasks on the instance.") + # Currently assume that people might want to do something based around deregistered instances + # with tasks left running on them - but nothing if no tasks were running already + elif force and container_instance.running_task_count > 0: + if not self.container_instances.get('orphaned'): + self.container_instances['orphaned'] = {} + self.container_instances['orphaned'][container_instance_id] = container_instance + del(self.container_instances[cluster_name][container_instance_id]) + self._respond_to_cluster_state_update(cluster_str) + return container_instance, failures + + def _respond_to_cluster_state_update(self, cluster_str): + cluster_name = cluster_str.split('/')[-1] + if cluster_name not in self.clusters: + raise Exception("{0} is not a cluster".format(cluster_name)) pass diff --git a/moto/ecs/responses.py b/moto/ecs/responses.py index 9c4524816..50d9e3cd4 100644 --- a/moto/ecs/responses.py +++ b/moto/ecs/responses.py @@ -203,6 +203,19 @@ class EC2ContainerServiceResponse(BaseResponse): 'containerInstance': container_instance.response_object }) + def deregister_container_instance(self): + cluster_str = self._get_param('cluster') + if not cluster_str: + cluster_str = 'default' + container_instance_str = self._get_param('containerInstance') + force = self._get_param('force') + container_instance, failures = self.ecs_backend.deregister_container_instance( + cluster_str, container_instance_str, force + ) + return json.dumps({ + 'containerInstance': container_instance.response_object + }) + def list_container_instances(self): cluster_str = self._get_param('cluster') container_instance_arns = self.ecs_backend.list_container_instances( diff --git a/tests/test_ecs/test_ecs_boto3.py b/tests/test_ecs/test_ecs_boto3.py index 54d0d2320..1cc147fc5 100644 --- a/tests/test_ecs/test_ecs_boto3.py +++ b/tests/test_ecs/test_ecs_boto3.py @@ -11,6 +11,7 @@ from uuid import UUID from moto import mock_cloudformation from moto import mock_ecs from moto import mock_ec2 +from nose.tools import assert_raises @mock_ecs @@ -548,6 +549,92 @@ def test_register_container_instance(): 'dockerVersion'].should.equal('DockerVersion: 1.5.0') +@mock_ec2 +@mock_ecs +def test_deregister_container_instance(): + ecs_client = boto3.client('ecs', region_name='us-east-1') + ec2 = boto3.resource('ec2', region_name='us-east-1') + + test_cluster_name = 'test_ecs_cluster' + + _ = ecs_client.create_cluster( + clusterName=test_cluster_name + ) + + test_instance = ec2.create_instances( + ImageId="ami-1234abcd", + MinCount=1, + MaxCount=1, + )[0] + + instance_id_document = json.dumps( + ec2_utils.generate_instance_identity_document(test_instance) + ) + + response = ecs_client.register_container_instance( + cluster=test_cluster_name, + instanceIdentityDocument=instance_id_document + ) + container_instance_id = response['containerInstance']['containerInstanceArn'] + response = ecs_client.deregister_container_instance( + cluster=test_cluster_name, + containerInstance=container_instance_id + ) + container_instances_response = ecs_client.list_container_instances( + cluster=test_cluster_name + ) + len(container_instances_response['containerInstanceArns']).should.equal(0) + + response = ecs_client.register_container_instance( + cluster=test_cluster_name, + instanceIdentityDocument=instance_id_document + ) + container_instance_id = response['containerInstance']['containerInstanceArn'] + _ = ecs_client.register_task_definition( + family='test_ecs_task', + containerDefinitions=[ + { + 'name': 'hello_world', + 'image': 'docker/hello-world:latest', + 'cpu': 1024, + 'memory': 400, + 'essential': True, + 'environment': [{ + 'name': 'AWS_ACCESS_KEY_ID', + 'value': 'SOME_ACCESS_KEY' + }], + 'logConfiguration': {'logDriver': 'json-file'} + } + ] + ) + + response = ecs_client.start_task( + cluster='test_ecs_cluster', + taskDefinition='test_ecs_task', + overrides={}, + containerInstances=[container_instance_id], + startedBy='moto' + ) + with assert_raises(Exception) as e: + ecs_client.deregister_container_instance( + cluster=test_cluster_name, + containerInstance=container_instance_id + ).should.have.raised(Exception) + container_instances_response = ecs_client.list_container_instances( + cluster=test_cluster_name + ) + len(container_instances_response['containerInstanceArns']).should.equal(1) + ecs_client.deregister_container_instance( + cluster=test_cluster_name, + containerInstance=container_instance_id, + force=True + ) + container_instances_response = ecs_client.list_container_instances( + cluster=test_cluster_name + ) + len(container_instances_response['containerInstanceArns']).should.equal(0) + + @mock_ec2 @mock_ecs def test_list_container_instances():