From b4e961148f8319f1e2af83879316130e345dd24f Mon Sep 17 00:00:00 2001 From: Jordan Sanders <10291790+jmsanders@users.noreply.github.com> Date: Tue, 8 Dec 2020 06:55:49 -0600 Subject: [PATCH] Pass the default ECS cluster and raise accurate exceptions (#3522) * Pass the "default" cluster * Mock ECS exceptions more accurately Moto's mock ECS has drifted fairly far from the actual ECS API in terms of which exceptions it throws. This change begins to bring mock ECS's exceptions in line with actual ECS exceptions. Most notably: - Several custom exceptions have been replaced with their real ECS exception. For example, "{0} is not a cluster" has been replaced with ClusterNotFoundException - Tests have been added to verify (most of) these exceptions work correctly. The test coverage was a little spotty to begin with. - The new exceptions plus the change to pass the "default" cluster exposed a lot of places where mock ECS was behaving incorrectly. For example, the ListTasks action is always scoped to a single cluster in ECS but it listed tasks for all clusters in the mock. I've minimally updated the tests to make them pass, but there's lots of opportunity to refactor both this method's test and its implementation. This does not provide full coverage of exceptions. In general, I ran these operations against actual ECS resources and cross-referenced the documentation to figure out what actual exceptions should be thrown and what the messages should be. Consequently, I didn't update any exceptions that took more than trivial amount of time to reproduce with real resources. --- moto/ecs/exceptions.py | 24 ++- moto/ecs/models.py | 89 +++++----- moto/ecs/responses.py | 42 +++-- tests/test_ecs/test_ecs_boto3.py | 290 +++++++++++++++++++++++++------ 4 files changed, 325 insertions(+), 120 deletions(-) diff --git a/moto/ecs/exceptions.py b/moto/ecs/exceptions.py index 72129224e..a9c6f4a71 100644 --- a/moto/ecs/exceptions.py +++ b/moto/ecs/exceptions.py @@ -5,11 +5,9 @@ from moto.core.exceptions import RESTError, JsonRESTError class ServiceNotFoundException(RESTError): code = 400 - def __init__(self, service_name): + def __init__(self): super(ServiceNotFoundException, self).__init__( - error_type="ServiceNotFoundException", - message="The service {0} does not exist".format(service_name), - template="error_json", + error_type="ServiceNotFoundException", message="Service not found." ) @@ -23,6 +21,15 @@ class TaskDefinitionNotFoundException(JsonRESTError): ) +class RevisionNotFoundException(JsonRESTError): + code = 400 + + def __init__(self): + super(RevisionNotFoundException, self).__init__( + error_type="ClientException", message="Revision is missing.", + ) + + class TaskSetNotFoundException(JsonRESTError): code = 400 @@ -40,3 +47,12 @@ class ClusterNotFoundException(JsonRESTError): super(ClusterNotFoundException, self).__init__( error_type="ClientException", message="Cluster not found", ) + + +class InvalidParameterException(JsonRESTError): + code = 400 + + def __init__(self, message): + super(InvalidParameterException, self).__init__( + error_type="ClientException", message=message, + ) diff --git a/moto/ecs/models.py b/moto/ecs/models.py index a4522660e..bb353d8a5 100644 --- a/moto/ecs/models.py +++ b/moto/ecs/models.py @@ -18,6 +18,8 @@ from .exceptions import ( TaskDefinitionNotFoundException, TaskSetNotFoundException, ClusterNotFoundException, + InvalidParameterException, + RevisionNotFoundException, ) @@ -668,7 +670,7 @@ class EC2ContainerServiceBackend(BaseBackend): if cluster_name in self.clusters: return self.clusters.pop(cluster_name) else: - raise Exception("{0} is not a cluster".format(cluster_name)) + raise ClusterNotFoundException def register_task_definition( self, @@ -713,25 +715,30 @@ class EC2ContainerServiceBackend(BaseBackend): def deregister_task_definition(self, task_definition_str): task_definition_name = task_definition_str.split("/")[-1] - family, revision = task_definition_name.split(":") - revision = int(revision) + try: + family, revision = task_definition_name.split(":") + except ValueError: + raise RevisionNotFoundException + try: + revision = int(revision) + except ValueError: + raise InvalidParameterException( + "Invalid revision number. Number: " + revision + ) if ( family in self.task_definitions and revision in self.task_definitions[family] ): return self.task_definitions[family].pop(revision) else: - raise Exception("{0} is not a task_definition".format(task_definition_name)) + raise TaskDefinitionNotFoundException def run_task(self, cluster_str, task_definition_str, count, overrides, started_by): - if cluster_str: - cluster_name = cluster_str.split("/")[-1] - else: - cluster_name = "default" + cluster_name = cluster_str.split("/")[-1] if cluster_name in self.clusters: cluster = self.clusters[cluster_name] else: - raise Exception("{0} is not a cluster".format(cluster_name)) + raise ClusterNotFoundException task_definition = self.describe_task_definition(task_definition_str) if cluster_name not in self.tasks: self.tasks[cluster_name] = {} @@ -862,13 +869,13 @@ class EC2ContainerServiceBackend(BaseBackend): if cluster_name in self.clusters: cluster = self.clusters[cluster_name] else: - raise Exception("{0} is not a cluster".format(cluster_name)) + raise ClusterNotFoundException task_definition = self.describe_task_definition(task_definition_str) if cluster_name not in self.tasks: self.tasks[cluster_name] = {} tasks = [] if not container_instances: - raise Exception("No container instance list provided") + raise InvalidParameterException("Container Instances cannot be empty.") container_instance_ids = [x.split("/")[-1] for x in container_instances] resource_requirements = self._calculate_task_resource_requirements( @@ -898,9 +905,9 @@ class EC2ContainerServiceBackend(BaseBackend): if cluster_name in self.clusters: cluster = self.clusters[cluster_name] else: - raise Exception("{0} is not a cluster".format(cluster_name)) + raise ClusterNotFoundException if not tasks: - raise Exception("tasks cannot be empty") + raise InvalidParameterException("Tasks cannot be empty.") response = [] for cluster, cluster_tasks in self.tasks.items(): for task_arn, task in cluster_tasks.items(): @@ -929,7 +936,7 @@ class EC2ContainerServiceBackend(BaseBackend): if 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)) + raise ClusterNotFoundException filtered_tasks = list( filter(lambda t: cluster_name in t.cluster_arn, filtered_tasks) ) @@ -971,10 +978,8 @@ class EC2ContainerServiceBackend(BaseBackend): def stop_task(self, cluster_str, task_str, reason): cluster_name = cluster_str.split("/")[-1] if cluster_name not in self.clusters: - raise Exception("{0} is not a cluster".format(cluster_name)) + raise ClusterNotFoundException - if not task_str: - raise Exception("A task ID or ARN is required") task_id = task_str.split("/")[-1] tasks = self.tasks.get(cluster_name, None) if not tasks: @@ -1011,7 +1016,7 @@ class EC2ContainerServiceBackend(BaseBackend): if cluster_name in self.clusters: cluster = self.clusters[cluster_name] else: - raise Exception("{0} is not a cluster".format(cluster_name)) + raise ClusterNotFoundException if task_definition_str is not None: task_definition = self.describe_task_definition(task_definition_str) else: @@ -1069,6 +1074,8 @@ class EC2ContainerServiceBackend(BaseBackend): self, cluster_str, service_str, task_definition_str, desired_count ): cluster_name = cluster_str.split("/")[-1] + if cluster_name not in self.clusters: + raise ClusterNotFoundException service_name = service_str.split("/")[-1] cluster_service_pair = "{0}:{1}".format(cluster_name, service_name) if cluster_service_pair in self.services: @@ -1081,22 +1088,23 @@ class EC2ContainerServiceBackend(BaseBackend): self.services[cluster_service_pair].desired_count = desired_count return self.services[cluster_service_pair] else: - raise ServiceNotFoundException(service_name) + raise ServiceNotFoundException def delete_service(self, cluster_name, service_name): cluster_service_pair = "{0}:{1}".format(cluster_name, service_name) + if cluster_name not in self.clusters: + raise ClusterNotFoundException + if cluster_service_pair in self.services: service = self.services[cluster_service_pair] if service.desired_count > 0: - raise Exception("Service must have desiredCount=0") + raise InvalidParameterException( + "The service cannot be stopped while it is scaled above 0." + ) else: return self.services.pop(cluster_service_pair) else: - raise Exception( - "cluster {0} or service {1} does not exist".format( - cluster_name, service_name - ) - ) + raise ServiceNotFoundException def register_container_instance(self, cluster_str, ec2_instance_id): cluster_name = cluster_str.split("/")[-1] @@ -1125,11 +1133,9 @@ class EC2ContainerServiceBackend(BaseBackend): def describe_container_instances(self, cluster_str, list_container_instance_ids): cluster_name = cluster_str.split("/")[-1] if cluster_name not in self.clusters: - raise Exception("{0} is not a cluster".format(cluster_name)) + raise ClusterNotFoundException if not list_container_instance_ids: - raise JsonRESTError( - "InvalidParameterException", "Container instance cannot be empty" - ) + raise InvalidParameterException("Container Instances cannot be empty.") failures = [] container_instance_objects = [] for container_instance_id in list_container_instance_ids: @@ -1153,12 +1159,11 @@ class EC2ContainerServiceBackend(BaseBackend): ): cluster_name = cluster_str.split("/")[-1] if cluster_name not in self.clusters: - raise Exception("{0} is not a cluster".format(cluster_name)) + raise ClusterNotFoundException status = status.upper() if status not in ["ACTIVE", "DRAINING"]: - raise Exception( - "An error occurred (InvalidParameterException) when calling the UpdateContainerInstancesState operation: \ - Container instances status should be one of [ACTIVE,DRAINING]" + raise InvalidParameterException( + "Container instance status should be one of [ACTIVE, DRAINING]" ) failures = [] container_instance_objects = [] @@ -1208,7 +1213,7 @@ class EC2ContainerServiceBackend(BaseBackend): failures = [] cluster_name = cluster_str.split("/")[-1] if cluster_name not in self.clusters: - raise Exception("{0} is not a cluster".format(cluster_name)) + raise ClusterNotFoundException container_instance_id = container_instance_str.split("/")[-1] container_instance = self.container_instances[cluster_name].get( container_instance_id @@ -1232,7 +1237,7 @@ class EC2ContainerServiceBackend(BaseBackend): 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)) + raise ClusterNotFoundException pass def put_attributes(self, cluster_name, attributes=None): @@ -1240,9 +1245,7 @@ class EC2ContainerServiceBackend(BaseBackend): raise ClusterNotFoundException if attributes is None: - raise JsonRESTError( - "InvalidParameterException", "attributes value is required" - ) + raise InvalidParameterException("attributes can not be empty") for attr in attributes: self._put_attribute( @@ -1413,7 +1416,7 @@ class EC2ContainerServiceBackend(BaseBackend): if service.arn == resource_arn: return service.tags else: - raise ServiceNotFoundException(service_name=parsed_arn["id"]) + raise ServiceNotFoundException raise NotImplementedError() def _get_last_task_definition_revision_id(self, family): @@ -1430,7 +1433,7 @@ class EC2ContainerServiceBackend(BaseBackend): service.tags = self._merge_tags(service.tags, tags) return {} else: - raise ServiceNotFoundException(service_name=parsed_arn["id"]) + raise ServiceNotFoundException raise NotImplementedError() def _merge_tags(self, existing_tags, new_tags): @@ -1456,7 +1459,7 @@ class EC2ContainerServiceBackend(BaseBackend): ] return {} else: - raise ServiceNotFoundException(service_name=parsed_arn["id"]) + raise ServiceNotFoundException raise NotImplementedError() def create_task_set( @@ -1497,7 +1500,7 @@ class EC2ContainerServiceBackend(BaseBackend): service_obj = self.services.get("{0}:{1}".format(cluster_name, service_name)) if not service_obj: - raise ServiceNotFoundException(service_name=service_name) + raise ServiceNotFoundException cluster_obj = self.clusters.get(cluster_name) if not cluster_obj: @@ -1522,7 +1525,7 @@ class EC2ContainerServiceBackend(BaseBackend): service_obj = self.services.get(service_key) if not service_obj: - raise ServiceNotFoundException(service_name=service_name) + raise ServiceNotFoundException cluster_obj = self.clusters.get(cluster_name) if not cluster_obj: diff --git a/moto/ecs/responses.py b/moto/ecs/responses.py index 15d2f0c4b..1c7e459c4 100644 --- a/moto/ecs/responses.py +++ b/moto/ecs/responses.py @@ -100,7 +100,7 @@ class EC2ContainerServiceResponse(BaseResponse): return json.dumps({"taskDefinition": task_definition.response_object}) def run_task(self): - cluster_str = self._get_param("cluster") + cluster_str = self._get_param("cluster", "default") overrides = self._get_param("overrides") task_definition_str = self._get_param("taskDefinition") count = self._get_int_param("count") @@ -113,7 +113,7 @@ class EC2ContainerServiceResponse(BaseResponse): ) def describe_tasks(self): - cluster = self._get_param("cluster") + cluster = self._get_param("cluster", "default") tasks = self._get_param("tasks") data = self.ecs_backend.describe_tasks(cluster, tasks) return json.dumps( @@ -121,7 +121,7 @@ class EC2ContainerServiceResponse(BaseResponse): ) def start_task(self): - cluster_str = self._get_param("cluster") + cluster_str = self._get_param("cluster", "default") overrides = self._get_param("overrides") task_definition_str = self._get_param("taskDefinition") container_instances = self._get_param("containerInstances") @@ -134,7 +134,7 @@ class EC2ContainerServiceResponse(BaseResponse): ) def list_tasks(self): - cluster_str = self._get_param("cluster") + cluster_str = self._get_param("cluster", "default") container_instance = self._get_param("containerInstance") family = self._get_param("family") started_by = self._get_param("startedBy") @@ -151,14 +151,14 @@ class EC2ContainerServiceResponse(BaseResponse): return json.dumps({"taskArns": task_arns}) def stop_task(self): - cluster_str = self._get_param("cluster") + cluster_str = self._get_param("cluster", "default") task = self._get_param("task") reason = self._get_param("reason") task = self.ecs_backend.stop_task(cluster_str, task, reason) return json.dumps({"task": task.response_object}) def create_service(self): - cluster_str = self._get_param("cluster") + cluster_str = self._get_param("cluster", "default") service_name = self._get_param("serviceName") task_definition_str = self._get_param("taskDefinition") desired_count = self._get_int_param("desiredCount") @@ -179,7 +179,7 @@ class EC2ContainerServiceResponse(BaseResponse): return json.dumps({"service": service.response_object}) def list_services(self): - cluster_str = self._get_param("cluster") + cluster_str = self._get_param("cluster", "default") scheduling_strategy = self._get_param("schedulingStrategy") service_arns = self.ecs_backend.list_services(cluster_str, scheduling_strategy) return json.dumps( @@ -191,7 +191,7 @@ class EC2ContainerServiceResponse(BaseResponse): ) def describe_services(self): - cluster_str = self._get_param("cluster") + cluster_str = self._get_param("cluster", "default") service_names = self._get_param("services") services = self.ecs_backend.describe_services(cluster_str, service_names) resp = { @@ -206,7 +206,7 @@ class EC2ContainerServiceResponse(BaseResponse): return json.dumps(resp) def update_service(self): - cluster_str = self._get_param("cluster") + cluster_str = self._get_param("cluster", "default") service_name = self._get_param("service") task_definition = self._get_param("taskDefinition") desired_count = self._get_int_param("desiredCount") @@ -217,12 +217,12 @@ class EC2ContainerServiceResponse(BaseResponse): def delete_service(self): service_name = self._get_param("service") - cluster_name = self._get_param("cluster") + cluster_name = self._get_param("cluster", "default") service = self.ecs_backend.delete_service(cluster_name, service_name) return json.dumps({"service": service.response_object}) def register_container_instance(self): - cluster_str = self._get_param("cluster") + cluster_str = self._get_param("cluster", "default") instance_identity_document_str = self._get_param("instanceIdentityDocument") instance_identity_document = json.loads(instance_identity_document_str) ec2_instance_id = instance_identity_document["instanceId"] @@ -232,9 +232,7 @@ class EC2ContainerServiceResponse(BaseResponse): return json.dumps({"containerInstance": container_instance.response_object}) def deregister_container_instance(self): - cluster_str = self._get_param("cluster") - if not cluster_str: - cluster_str = "default" + cluster_str = self._get_param("cluster", "default") container_instance_str = self._get_param("containerInstance") force = self._get_param("force") container_instance, failures = self.ecs_backend.deregister_container_instance( @@ -243,12 +241,12 @@ class EC2ContainerServiceResponse(BaseResponse): return json.dumps({"containerInstance": container_instance.response_object}) def list_container_instances(self): - cluster_str = self._get_param("cluster") + cluster_str = self._get_param("cluster", "default") container_instance_arns = self.ecs_backend.list_container_instances(cluster_str) return json.dumps({"containerInstanceArns": container_instance_arns}) def describe_container_instances(self): - cluster_str = self._get_param("cluster") + cluster_str = self._get_param("cluster", "default") list_container_instance_arns = self._get_param("containerInstances") container_instances, failures = self.ecs_backend.describe_container_instances( cluster_str, list_container_instance_arns @@ -263,7 +261,7 @@ class EC2ContainerServiceResponse(BaseResponse): ) def update_container_instances_state(self): - cluster_str = self._get_param("cluster") + cluster_str = self._get_param("cluster", "default") list_container_instance_arns = self._get_param("containerInstances") status_str = self._get_param("status") ( @@ -312,7 +310,7 @@ class EC2ContainerServiceResponse(BaseResponse): return json.dumps({"attributes": formatted_results}) def delete_attributes(self): - cluster_name = self._get_param("cluster") + cluster_name = self._get_param("cluster", "default") attributes = self._get_param("attributes") self.ecs_backend.delete_attributes(cluster_name, attributes) @@ -359,7 +357,7 @@ class EC2ContainerServiceResponse(BaseResponse): def create_task_set(self): service_str = self._get_param("service") - cluster_str = self._get_param("cluster") + cluster_str = self._get_param("cluster", "default") task_definition = self._get_param("taskDefinition") external_id = self._get_param("externalId") network_configuration = self._get_param("networkConfiguration") @@ -389,7 +387,7 @@ class EC2ContainerServiceResponse(BaseResponse): return json.dumps({"taskSet": task_set.response_object}) def describe_task_sets(self): - cluster_str = self._get_param("cluster") + cluster_str = self._get_param("cluster", "default") service_str = self._get_param("service") task_sets = self._get_param("taskSets") include = self._get_param("include", []) @@ -414,7 +412,7 @@ class EC2ContainerServiceResponse(BaseResponse): return json.dumps({"taskSet": task_set.response_object}) def update_task_set(self): - cluster_str = self._get_param("cluster") + cluster_str = self._get_param("cluster", "default") service_str = self._get_param("service") task_set = self._get_param("taskSet") scale = self._get_param("scale") @@ -425,7 +423,7 @@ class EC2ContainerServiceResponse(BaseResponse): return json.dumps({"taskSet": task_set.response_object}) def update_service_primary_task_set(self): - cluster_str = self._get_param("cluster") + cluster_str = self._get_param("cluster", "default") service_str = self._get_param("service") primary_task_set = self._get_param("primaryTaskSet") diff --git a/tests/test_ecs/test_ecs_boto3.py b/tests/test_ecs/test_ecs_boto3.py index 8b6b27987..1649b1753 100644 --- a/tests/test_ecs/test_ecs_boto3.py +++ b/tests/test_ecs/test_ecs_boto3.py @@ -10,6 +10,13 @@ from uuid import UUID from moto import mock_ecs from moto import mock_ec2 +from moto.ecs.exceptions import ( + ClusterNotFoundException, + ServiceNotFoundException, + InvalidParameterException, + TaskDefinitionNotFoundException, + RevisionNotFoundException, +) import pytest @@ -73,6 +80,14 @@ def test_delete_cluster(): len(response["clusterArns"]).should.equal(0) +@mock_ecs +def test_delete_cluster_exceptions(): + client = boto3.client("ecs", region_name="us-east-1") + client.delete_cluster.when.called_with(cluster="not_a_cluster").should.throw( + ClientError, ClusterNotFoundException().message + ) + + @mock_ecs def test_register_task_definition(): client = boto3.client("ecs", region_name="us-east-1") @@ -347,6 +362,23 @@ def test_deregister_task_definition(): ].should.equal("json-file") +@mock_ecs +def test_deregister_task_definition(): + client = boto3.client("ecs", region_name="us-east-1") + client.deregister_task_definition.when.called_with( + taskDefinition="fake_task" + ).should.throw(ClientError, RevisionNotFoundException().message) + client.deregister_task_definition.when.called_with( + taskDefinition="fake_task:foo" + ).should.throw( + ClientError, + InvalidParameterException("Invalid revision number. Number: foo").message, + ) + client.deregister_task_definition.when.called_with( + taskDefinition="fake_task:1" + ).should.throw(ClientError, TaskDefinitionNotFoundException().message) + + @mock_ecs def test_create_service(): client = boto3.client("ecs", region_name="us-east-1") @@ -751,17 +783,56 @@ def test_delete_service(): @mock_ecs -def test_update_non_existent_service(): +def test_delete_service_exceptions(): client = boto3.client("ecs", region_name="us-east-1") - try: - client.update_service( - cluster="my-clustet", service="my-service", desiredCount=0 - ) - except ClientError as exc: - error_code = exc.response["Error"]["Code"] - error_code.should.equal("ServiceNotFoundException") - else: - raise Exception("Didn't raise ClientError") + + # Raises ClusterNotFoundException because "default" is not a cluster + client.delete_service.when.called_with(service="not_as_service").should.throw( + ClientError, ClusterNotFoundException().message + ) + + _ = client.create_cluster() + client.delete_service.when.called_with(service="not_as_service").should.throw( + ClientError, ServiceNotFoundException().message + ) + + _ = client.register_task_definition( + family="test_ecs_task", + containerDefinitions=[ + { + "name": "hello_world", + "image": "docker/hello-world:latest", + "cpu": 1024, + "memory": 400, + } + ], + ) + + _ = client.create_service( + serviceName="test_ecs_service", taskDefinition="test_ecs_task", desiredCount=1, + ) + + client.delete_service.when.called_with(service="test_ecs_service").should.throw( + ClientError, + InvalidParameterException( + "The service cannot be stopped while it is scaled above 0." + ).message, + ) + + +@mock_ecs +def test_update_service_exceptions(): + client = boto3.client("ecs", region_name="us-east-1") + + client.update_service.when.called_with( + service="not_a_service", desiredCount=0 + ).should.throw(ClientError, ClusterNotFoundException().message) + + _ = client.create_cluster() + + client.update_service.when.called_with( + service="not_a_service", desiredCount=0 + ).should.throw(ClientError, ServiceNotFoundException().message) @mock_ec2 @@ -958,6 +1029,23 @@ def test_describe_container_instances(): ) +@mock_ecs +def test_describe_container_instances_exceptions(): + client = boto3.client("ecs", region_name="us-east-1") + + client.describe_container_instances.when.called_with( + containerInstances=[] + ).should.throw(ClientError, ClusterNotFoundException().message) + + _ = client.create_cluster() + client.describe_container_instances.when.called_with( + containerInstances=[] + ).should.throw( + ClientError, + InvalidParameterException("Container Instances cannot be empty.").message, + ) + + @mock_ec2 @mock_ecs def test_update_container_instances_state(): @@ -1213,6 +1301,26 @@ def test_run_task_default_cluster(): response["tasks"][0]["stoppedReason"].should.equal("") +@mock_ecs +def test_run_task_exceptions(): + client = boto3.client("ecs", region_name="us-east-1") + _ = client.register_task_definition( + family="test_ecs_task", + containerDefinitions=[ + { + "name": "hello_world", + "image": "docker/hello-world:latest", + "cpu": 1024, + "memory": 400, + } + ], + ) + + client.run_task.when.called_with( + cluster="not_a_cluster", taskDefinition="test_ecs_task" + ).should.throw(ClientError, ClusterNotFoundException().message) + + @mock_ec2 @mock_ecs def test_start_task(): @@ -1287,15 +1395,40 @@ def test_start_task(): response["tasks"][0]["stoppedReason"].should.equal("") +@mock_ecs +def test_start_task_exceptions(): + client = boto3.client("ecs", region_name="us-east-1") + _ = client.register_task_definition( + family="test_ecs_task", + containerDefinitions=[ + { + "name": "hello_world", + "image": "docker/hello-world:latest", + "cpu": 1024, + "memory": 400, + } + ], + ) + + client.start_task.when.called_with( + taskDefinition="test_ecs_task", containerInstances=["not_a_container_instance"] + ).should.throw(ClientError, ClusterNotFoundException().message) + + _ = client.create_cluster() + client.start_task.when.called_with( + taskDefinition="test_ecs_task", containerInstances=[] + ).should.throw( + ClientError, InvalidParameterException("Container Instances cannot be empty.") + ) + + @mock_ec2 @mock_ecs def test_list_tasks(): client = boto3.client("ecs", region_name="us-east-1") ec2 = boto3.resource("ec2", region_name="us-east-1") - test_cluster_name = "test_ecs_cluster" - - _ = client.create_cluster(clusterName=test_cluster_name) + _ = client.create_cluster() test_instance = ec2.create_instances( ImageId="ami-1234abcd", MinCount=1, MaxCount=1 @@ -1306,10 +1439,10 @@ def test_list_tasks(): ) _ = client.register_container_instance( - cluster=test_cluster_name, instanceIdentityDocument=instance_id_document + instanceIdentityDocument=instance_id_document ) - container_instances = client.list_container_instances(cluster=test_cluster_name) + container_instances = client.list_container_instances() container_instance_id = container_instances["containerInstanceArns"][0].split("/")[ -1 ] @@ -1332,7 +1465,6 @@ def test_list_tasks(): ) _ = client.start_task( - cluster="test_ecs_cluster", taskDefinition="test_ecs_task", overrides={}, containerInstances=[container_instance_id], @@ -1340,7 +1472,6 @@ def test_list_tasks(): ) _ = client.start_task( - cluster="test_ecs_cluster", taskDefinition="test_ecs_task", overrides={}, containerInstances=[container_instance_id], @@ -1348,12 +1479,17 @@ def test_list_tasks(): ) assert len(client.list_tasks()["taskArns"]).should.equal(2) - assert len(client.list_tasks(cluster="test_ecs_cluster")["taskArns"]).should.equal( - 2 - ) assert len(client.list_tasks(startedBy="foo")["taskArns"]).should.equal(1) +@mock_ecs +def test_list_tasks_exceptions(): + client = boto3.client("ecs", region_name="us-east-1") + client.list_tasks.when.called_with(cluster="not_a_cluster").should.throw( + ClientError, ClusterNotFoundException().message + ) + + @mock_ec2 @mock_ecs def test_describe_tasks(): @@ -1416,6 +1552,20 @@ def test_describe_tasks(): len(response["tasks"]).should.equal(1) +@mock_ecs +def test_describe_tasks_exceptions(): + client = boto3.client("ecs", region_name="us-east-1") + + client.describe_tasks.when.called_with(tasks=[]).should.throw( + ClientError, ClusterNotFoundException().message + ) + + _ = client.create_cluster() + client.describe_tasks.when.called_with(tasks=[]).should.throw( + ClientError, InvalidParameterException("Tasks cannot be empty.").message + ) + + @mock_ecs def describe_task_definition(): client = boto3.client("ecs", region_name="us-east-1") @@ -1499,6 +1649,15 @@ def test_stop_task(): stop_response["task"]["stoppedReason"].should.equal("moto testing") +@mock_ecs +def test_stop_task_exceptions(): + client = boto3.client("ecs", region_name="us-east-1") + + client.stop_task.when.called_with(task="fake_task").should.throw( + ClientError, ClusterNotFoundException().message + ) + + @mock_ec2 @mock_ecs def test_resource_reservation_and_release(): @@ -2160,13 +2319,14 @@ def test_list_tags_for_resource(): @mock_ecs -def test_list_tags_for_resource_unknown(): +def test_list_tags_exceptions(): client = boto3.client("ecs", region_name="us-east-1") - task_definition_arn = "arn:aws:ecs:us-east-1:012345678910:task-definition/unknown:1" - try: - client.list_tags_for_resource(resourceArn=task_definition_arn) - except ClientError as err: - err.response["Error"]["Code"].should.equal("ClientException") + client.list_tags_for_resource.when.called_with( + resourceArn="arn:aws:ecs:us-east-1:012345678910:service/fake_service:1" + ).should.throw(ClientError, ServiceNotFoundException().message) + client.list_tags_for_resource.when.called_with( + resourceArn="arn:aws:ecs:us-east-1:012345678910:task-definition/fake_task:1" + ).should.throw(ClientError, TaskDefinitionNotFoundException().message) @mock_ecs @@ -2208,16 +2368,6 @@ def test_list_tags_for_resource_ecs_service(): ) -@mock_ecs -def test_list_tags_for_resource_unknown_service(): - client = boto3.client("ecs", region_name="us-east-1") - service_arn = "arn:aws:ecs:us-east-1:012345678910:service/unknown:1" - try: - client.list_tags_for_resource(resourceArn=service_arn) - except ClientError as err: - err.response["Error"]["Code"].should.equal("ServiceNotFoundException") - - @mock_ecs def test_ecs_service_tag_resource(): client = boto3.client("ecs", region_name="us-east-1") @@ -2816,30 +2966,68 @@ def test_list_tasks_with_filters(): startedBy="bar", ) - len(ecs.list_tasks()["taskArns"]).should.equal(3) - len(ecs.list_tasks(cluster="test_cluster_1")["taskArns"]).should.equal(2) len(ecs.list_tasks(cluster="test_cluster_2")["taskArns"]).should.equal(1) - len(ecs.list_tasks(containerInstance="bad-id")["taskArns"]).should.equal(0) - len(ecs.list_tasks(containerInstance=container_id_1)["taskArns"]).should.equal(2) - len(ecs.list_tasks(containerInstance=container_id_2)["taskArns"]).should.equal(1) + len( + ecs.list_tasks(cluster="test_cluster_1", containerInstance="bad-id")["taskArns"] + ).should.equal(0) + len( + ecs.list_tasks(cluster="test_cluster_1", containerInstance=container_id_1)[ + "taskArns" + ] + ).should.equal(2) + len( + ecs.list_tasks(cluster="test_cluster_2", containerInstance=container_id_2)[ + "taskArns" + ] + ).should.equal(1) - len(ecs.list_tasks(family="non-existent-family")["taskArns"]).should.equal(0) - len(ecs.list_tasks(family="test_task_def_1")["taskArns"]).should.equal(2) - len(ecs.list_tasks(family="test_task_def_2")["taskArns"]).should.equal(1) + len( + ecs.list_tasks(cluster="test_cluster_1", family="non-existent-family")[ + "taskArns" + ] + ).should.equal(0) + len( + ecs.list_tasks(cluster="test_cluster_1", family="test_task_def_1")["taskArns"] + ).should.equal(2) + len( + ecs.list_tasks(cluster="test_cluster_2", family="test_task_def_2")["taskArns"] + ).should.equal(1) - len(ecs.list_tasks(startedBy="non-existent-entity")["taskArns"]).should.equal(0) - len(ecs.list_tasks(startedBy="foo")["taskArns"]).should.equal(2) - len(ecs.list_tasks(startedBy="bar")["taskArns"]).should.equal(1) + len( + ecs.list_tasks(cluster="test_cluster_1", startedBy="non-existent-entity")[ + "taskArns" + ] + ).should.equal(0) + len( + ecs.list_tasks(cluster="test_cluster_1", startedBy="foo")["taskArns"] + ).should.equal(1) + len( + ecs.list_tasks(cluster="test_cluster_1", startedBy="bar")["taskArns"] + ).should.equal(1) + len( + ecs.list_tasks(cluster="test_cluster_2", startedBy="foo")["taskArns"] + ).should.equal(1) - len(ecs.list_tasks(desiredStatus="RUNNING")["taskArns"]).should.equal(3) + len( + ecs.list_tasks(cluster="test_cluster_1", desiredStatus="RUNNING")["taskArns"] + ).should.equal(2) + len( + ecs.list_tasks(cluster="test_cluster_2", desiredStatus="RUNNING")["taskArns"] + ).should.equal(1) _ = ecs.stop_task(cluster="test_cluster_2", task=task_to_stop, reason="for testing") - len(ecs.list_tasks(desiredStatus="RUNNING")["taskArns"]).should.equal(2) - len(ecs.list_tasks(desiredStatus="STOPPED")["taskArns"]).should.equal(1) + len( + ecs.list_tasks(cluster="test_cluster_1", desiredStatus="RUNNING")["taskArns"] + ).should.equal(2) + len( + ecs.list_tasks(cluster="test_cluster_2", desiredStatus="STOPPED")["taskArns"] + ).should.equal(1) resp = ecs.list_tasks(cluster="test_cluster_1", startedBy="foo") len(resp["taskArns"]).should.equal(1) - resp = ecs.list_tasks(containerInstance=container_id_1, startedBy="bar") + resp = ecs.list_tasks( + cluster="test_cluster_1", containerInstance=container_id_1, startedBy="bar" + ) len(resp["taskArns"]).should.equal(1)