From 71e8f6ef5b3a8e64a3bdbe506a16d6b3c82d9044 Mon Sep 17 00:00:00 2001 From: GuyTempleton Date: Thu, 13 Apr 2017 08:54:25 +0100 Subject: [PATCH 1/5] First cut of container instance deregistration --- moto/ecs/models.py | 26 +++++++++++++++++++++++++- moto/ecs/responses.py | 11 +++++++++++ 2 files changed, 36 insertions(+), 1 deletion(-) diff --git a/moto/ecs/models.py b/moto/ecs/models.py index aadc76bc..e4258cee 100644 --- a/moto/ecs/models.py +++ b/moto/ecs/models.py @@ -716,7 +716,31 @@ class EC2ContainerServiceBackend(BaseBackend): else: resource["stringSetValue"].append(str(port)) - def deregister_container_instance(self, cluster_str, container_instance_str): + def deregister_container_instance(self, cluster_str, container_instance_str, force): + 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_tasks_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 deregistration is forced or no tasks were + # running already + elif force and container_instance.running_tasks_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) + pass + + 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 9c452481..6c8fd8e2 100644 --- a/moto/ecs/responses.py +++ b/moto/ecs/responses.py @@ -203,6 +203,17 @@ class EC2ContainerServiceResponse(BaseResponse): 'containerInstance': container_instance.response_object }) + def deregister_container_instance(self): + cluster_str = self._get_param('cluster', 'default'), + container_instance_str = self._get_param('containerInstance') + force = self._get_param('force', False) + container_instance = 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( From acb6c3ce01d36a3a90d80d7b10eb8e9899ae3a40 Mon Sep 17 00:00:00 2001 From: GuyTempleton Date: Thu, 13 Apr 2017 17:46:15 +0100 Subject: [PATCH 2/5] Implement container instance deregistration --- AUTHORS.md | 1 + moto/ecs/models.py | 9 ++-- moto/ecs/responses.py | 8 +-- tests/test_ecs/test_ecs_boto3.py | 87 ++++++++++++++++++++++++++++++++ 4 files changed, 99 insertions(+), 6 deletions(-) diff --git a/AUTHORS.md b/AUTHORS.md index 08757d2b..5d5b99a0 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 e4258cee..22835ecb 100644 --- a/moto/ecs/models.py +++ b/moto/ecs/models.py @@ -652,6 +652,7 @@ class EC2ContainerServiceBackend(BaseBackend): '/')[-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): @@ -715,8 +716,10 @@ class EC2ContainerServiceBackend(BaseBackend): resource["stringSetValue"].remove(str(port)) else: resource["stringSetValue"].append(str(port)) + container_instance.runningTaskCount += resource_multiplier * 1 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)) @@ -724,18 +727,18 @@ class EC2ContainerServiceBackend(BaseBackend): 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_tasks_count > 0: + if not force and container_instance.runningTaskCount > 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 deregistration is forced or no tasks were # running already - elif force and container_instance.running_tasks_count > 0: + elif force and container_instance.runningTaskCount > 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) - pass + return container_instance, failures def _respond_to_cluster_state_update(self, cluster_str): cluster_name = cluster_str.split('/')[-1] diff --git a/moto/ecs/responses.py b/moto/ecs/responses.py index 6c8fd8e2..50d9e3cd 100644 --- a/moto/ecs/responses.py +++ b/moto/ecs/responses.py @@ -204,10 +204,12 @@ class EC2ContainerServiceResponse(BaseResponse): }) def deregister_container_instance(self): - cluster_str = self._get_param('cluster', 'default'), + 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', False) - container_instance = self.ecs_backend.deregister_container_instance( + force = self._get_param('force') + container_instance, failures = self.ecs_backend.deregister_container_instance( cluster_str, container_instance_str, force ) return json.dumps({ diff --git a/tests/test_ecs/test_ecs_boto3.py b/tests/test_ecs/test_ecs_boto3.py index e76be8cb..7be78274 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 @@ -542,6 +543,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(): From f3aff0f356196f29c3b7598499bf452fcd05e650 Mon Sep 17 00:00:00 2001 From: GuyTempleton Date: Thu, 13 Apr 2017 17:53:23 +0100 Subject: [PATCH 3/5] Switch ContainerInstance model to snake case --- moto/ecs/models.py | 35 +++++++++++++++++------------------ 1 file changed, 17 insertions(+), 18 deletions(-) diff --git a/moto/ecs/models.py b/moto/ecs/models.py index 22835ecb..06600cab 100644 --- a/moto/ecs/models.py +++ b/moto/ecs/models.py @@ -241,7 +241,7 @@ class ContainerInstance(BaseObject): def __init__(self, ec2_instance_id): self.ec2_instance_id = ec2_instance_id self.status = 'ACTIVE' - self.registeredResources = [ + self.registered_resources = [ {'doubleValue': 0.0, 'integerValue': 4096, 'longValue': 0, @@ -264,11 +264,11 @@ 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.agent_connected = True + 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, @@ -292,8 +292,8 @@ 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' @@ -434,7 +434,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) @@ -475,7 +475,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": @@ -512,7 +512,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) @@ -648,7 +648,7 @@ 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 @@ -660,7 +660,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): @@ -705,7 +705,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": @@ -716,7 +716,7 @@ class EC2ContainerServiceBackend(BaseBackend): resource["stringSetValue"].remove(str(port)) else: resource["stringSetValue"].append(str(port)) - container_instance.runningTaskCount += resource_multiplier * 1 + container_instance.running_task_count += resource_multiplier * 1 def deregister_container_instance(self, cluster_str, container_instance_str, force): failures = [] @@ -727,12 +727,11 @@ class EC2ContainerServiceBackend(BaseBackend): 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.runningTaskCount > 0: + 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 deregistration is forced or no tasks were - # running already - elif force and container_instance.runningTaskCount > 0: + # 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 From 69b86b2c7a25b225fc9da2f76f6dbb9f5adfee23 Mon Sep 17 00:00:00 2001 From: GuyTempleton Date: Thu, 13 Apr 2017 18:41:29 +0100 Subject: [PATCH 4/5] Fix indentation of ContainerInstance response object --- moto/ecs/models.py | 9 ++++----- 1 file changed, 4 insertions(+), 5 deletions(-) diff --git a/moto/ecs/models.py b/moto/ecs/models.py index 06600cab..359cddec 100644 --- a/moto/ecs/models.py +++ b/moto/ecs/models.py @@ -299,11 +299,10 @@ class ContainerInstance(BaseObject): '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): From 47bc23f4810051a7b6670f276ed5229fd00baa6a Mon Sep 17 00:00:00 2001 From: GuyTempleton Date: Sat, 15 Apr 2017 16:24:30 +0100 Subject: [PATCH 5/5] Move agent_connected assignation --- moto/ecs/models.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/moto/ecs/models.py b/moto/ecs/models.py index 359cddec..f73cd353 100644 --- a/moto/ecs/models.py +++ b/moto/ecs/models.py @@ -240,6 +240,7 @@ class ContainerInstance(BaseObject): def __init__(self, ec2_instance_id): self.ec2_instance_id = ec2_instance_id + self.agent_connected = True self.status = 'ACTIVE' self.registered_resources = [ {'doubleValue': 0.0, @@ -264,7 +265,6 @@ class ContainerInstance(BaseObject): 'name': 'PORTS_UDP', 'stringSetValue': [], 'type': 'STRINGSET'}] - self.agent_connected = True self.container_instance_arn = "arn:aws:ecs:us-east-1:012345678910:container-instance/{0}".format( str(uuid.uuid1())) self.pending_task_count = 0