diff --git a/moto/ecs/models.py b/moto/ecs/models.py index 3245ce03e..82505f944 100644 --- a/moto/ecs/models.py +++ b/moto/ecs/models.py @@ -7,6 +7,7 @@ from random import random, randint import pytz from boto3 import Session +from moto import settings from moto.core import BaseBackend, BaseModel, CloudFormationModel, ACCOUNT_ID from moto.core.exceptions import JsonRESTError from moto.core.utils import unix_time, pascal_to_camelcase, remap_nested_keys @@ -272,10 +273,9 @@ class Task(BaseObject): started_by="", tags=[], ): + self.id = str(uuid.uuid4()) + self.cluster_name = cluster.name self.cluster_arn = cluster.arn - self.task_arn = "arn:aws:ecs:{0}:{1}:task/{2}".format( - cluster.region_name, ACCOUNT_ID, str(uuid.uuid4()) - ) self.container_instance_arn = container_instance_arn self.last_status = "RUNNING" self.desired_status = "RUNNING" @@ -286,10 +286,20 @@ class Task(BaseObject): self.tags = tags self.stopped_reason = "" self.resource_requirements = resource_requirements + self.region_name = cluster.region_name + + @property + def task_arn(self): + if settings.ecs_new_arn_format(): + return f"arn:aws:ecs:{self.region_name}:{ACCOUNT_ID}:task/{self.cluster_name}/{self.id}" + return "arn:aws:ecs:{0}:{1}:task/{2}".format( + self.region_name, ACCOUNT_ID, self.id + ) @property def response_object(self): response_object = self.gen_response_object() + response_object["taskArn"] = self.task_arn return response_object @@ -307,10 +317,8 @@ class Service(BaseObject, CloudFormationModel): launch_type=None, service_registries=None, ): + self.cluster_name = cluster.name self.cluster_arn = cluster.arn - self.arn = "arn:aws:ecs:{0}:{1}:service/{2}".format( - cluster.region_name, ACCOUNT_ID, service_name - ) self.name = service_name self.status = "ACTIVE" self.running_count = 0 @@ -346,6 +354,15 @@ class Service(BaseObject, CloudFormationModel): ) self.tags = tags if tags is not None else [] self.pending_count = 0 + self.region_name = cluster.region_name + + @property + def arn(self): + if settings.ecs_new_arn_format(): + return f"arn:aws:ecs:{self.region_name}:{ACCOUNT_ID}:service/{self.cluster_name}/{self.name}" + return "arn:aws:ecs:{0}:{1}:service/{2}".format( + self.region_name, ACCOUNT_ID, self.name + ) @property def physical_resource_id(self): @@ -354,7 +371,7 @@ class Service(BaseObject, CloudFormationModel): @property def response_object(self): response_object = self.gen_response_object() - del response_object["name"], response_object["arn"], response_object["tags"] + del response_object["name"], response_object["tags"] response_object["serviceName"] = self.name response_object["serviceArn"] = self.arn response_object["schedulingStrategy"] = self.scheduling_strategy @@ -450,7 +467,7 @@ class Service(BaseObject, CloudFormationModel): class ContainerInstance(BaseObject): - def __init__(self, ec2_instance_id, region_name): + def __init__(self, ec2_instance_id, region_name, cluster_name): self.ec2_instance_id = ec2_instance_id self.agent_connected = True self.status = "ACTIVE" @@ -486,9 +503,6 @@ class ContainerInstance(BaseObject): "type": "STRINGSET", }, ] - self.container_instance_arn = "arn:aws:ecs:{0}:{1}:container-instance/{2}".format( - region_name, ACCOUNT_ID, str(uuid.uuid4()) - ) self.pending_tasks_count = 0 self.remaining_resources = [ { @@ -539,10 +553,22 @@ class ContainerInstance(BaseObject): else "linux", # options are windows and linux, linux is default } self.registered_at = datetime.now(pytz.utc) + self.region_name = region_name + self.id = str(uuid.uuid4()) + self.cluster_name = cluster_name + + @property + def container_instance_arn(self): + if settings.ecs_new_arn_format(): + return f"arn:aws:ecs:{self.region_name}:{ACCOUNT_ID}:container-instance/{self.cluster_name}/{self.id}" + return ( + f"arn:aws:ecs:{self.region_name}:{ACCOUNT_ID}:container-instance/{self.id}" + ) @property def response_object(self): response_object = self.gen_response_object() + response_object["containerInstanceArn"] = self.container_instance_arn response_object["attributes"] = [ self._format_attribute(name, value) for name, value in response_object["attributes"].items() @@ -1198,7 +1224,9 @@ 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)) - container_instance = ContainerInstance(ec2_instance_id, self.region_name) + container_instance = ContainerInstance( + ec2_instance_id, self.region_name, cluster_name + ) if not self.container_instances.get(cluster_name): self.container_instances[cluster_name] = {} container_instance_id = container_instance.container_instance_arn.split("/")[-1] diff --git a/moto/settings.py b/moto/settings.py index 696c5d83b..04973dc29 100644 --- a/moto/settings.py +++ b/moto/settings.py @@ -32,3 +32,7 @@ def get_s3_default_key_buffer_size(): "MOTO_S3_DEFAULT_KEY_BUFFER_SIZE", S3_UPLOAD_PART_MIN_SIZE - 1024 ) ) + + +def ecs_new_arn_format(): + return os.environ.get("MOTO_ECS_NEW_ARN", "false").lower() == "true" diff --git a/tests/test_ecs/test_ecs_boto3.py b/tests/test_ecs/test_ecs_boto3.py index 02ae901de..04daf7e34 100644 --- a/tests/test_ecs/test_ecs_boto3.py +++ b/tests/test_ecs/test_ecs_boto3.py @@ -2,15 +2,16 @@ from datetime import datetime from botocore.exceptions import ClientError import boto3 +import mock import sure # noqa # pylint: disable=unused-import import json +import os from moto.core import ACCOUNT_ID from moto.ec2 import utils as ec2_utils from uuid import UUID -from moto import mock_ecs -from moto import mock_ec2 +from moto import mock_ecs, mock_ec2, settings from moto.ecs.exceptions import ( ClusterNotFoundException, ServiceNotFoundException, @@ -20,6 +21,7 @@ from moto.ecs.exceptions import ( ) import pytest from tests import EXAMPLE_AMI_ID +from unittest import SkipTest @mock_ecs @@ -724,6 +726,36 @@ def test_describe_services(): response["services"][1]["launchType"].should.equal("EC2") +@mock_ecs +@mock.patch.dict(os.environ, {"MOTO_ECS_NEW_ARN": "TrUe"}) +def test_describe_services_new_arn(): + if settings.TEST_SERVER_MODE: + raise SkipTest("Cant set environment variables in server mode") + client = boto3.client("ecs", region_name="us-east-1") + _ = client.create_cluster(clusterName="test_ecs_cluster") + _ = client.register_task_definition( + family="test_ecs_task", + containerDefinitions=[ + {"name": "hello_world", "image": "docker/hello-world:latest",} + ], + ) + _ = client.create_service( + cluster="test_ecs_cluster", + serviceName="test_ecs_service1", + taskDefinition="test_ecs_task", + desiredCount=2, + tags=[{"key": "Name", "value": "test_ecs_service1"}], + ) + response = client.describe_services( + cluster="test_ecs_cluster", services=["test_ecs_service1"] + ) + response["services"][0]["serviceArn"].should.equal( + "arn:aws:ecs:us-east-1:{}:service/test_ecs_cluster/test_ecs_service1".format( + ACCOUNT_ID + ) + ) + + @mock_ecs def test_describe_services_scheduling_strategy(): client = boto3.client("ecs", region_name="us-east-1") @@ -1125,6 +1157,37 @@ def test_register_container_instance(): ) +@mock_ec2 +@mock_ecs +@mock.patch.dict(os.environ, {"MOTO_ECS_NEW_ARN": "TrUe"}) +def test_register_container_instance_new_arn_format(): + if settings.TEST_SERVER_MODE: + raise SkipTest("Cant set environment variables in server mode") + 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=EXAMPLE_AMI_ID, 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 + ) + + full_arn = response["containerInstance"]["containerInstanceArn"] + full_arn.should.match( + f"arn:aws:ecs:us-east-1:{ACCOUNT_ID}:container-instance/{test_cluster_name}/[a-z0-9-]+$" + ) + + @mock_ec2 @mock_ecs def test_deregister_container_instance(): @@ -1545,8 +1608,8 @@ def test_run_task_default_cluster(): startedBy="moto", ) len(response["tasks"]).should.equal(2) - response["tasks"][0]["taskArn"].should.contain( - "arn:aws:ecs:us-east-1:{}:task/".format(ACCOUNT_ID) + response["tasks"][0]["taskArn"].should.match( + "arn:aws:ecs:us-east-1:{}:task/[a-z0-9-]+$".format(ACCOUNT_ID) ) response["tasks"][0]["clusterArn"].should.equal( "arn:aws:ecs:us-east-1:{}:cluster/default".format(ACCOUNT_ID) @@ -1564,6 +1627,54 @@ def test_run_task_default_cluster(): response["tasks"][0]["stoppedReason"].should.equal("") +@mock_ec2 +@mock_ecs +@mock.patch.dict(os.environ, {"MOTO_ECS_NEW_ARN": "TrUe"}) +def test_run_task_default_cluster_new_arn_format(): + if settings.TEST_SERVER_MODE: + raise SkipTest("Cant set environment variables in server mode") + client = boto3.client("ecs", region_name="us-east-1") + ec2 = boto3.resource("ec2", region_name="us-east-1") + + test_cluster_name = "default" + + client.create_cluster(clusterName=test_cluster_name) + + test_instance = ec2.create_instances( + ImageId=EXAMPLE_AMI_ID, MinCount=1, MaxCount=1 + )[0] + + instance_id_document = json.dumps( + ec2_utils.generate_instance_identity_document(test_instance) + ) + + client.register_container_instance( + cluster=test_cluster_name, instanceIdentityDocument=instance_id_document + ) + + client.register_task_definition( + family="test_ecs_task", + containerDefinitions=[ + { + "name": "hello_world", + "image": "docker/hello-world:latest", + "cpu": 1024, + "memory": 400, + } + ], + ) + response = client.run_task( + launchType="FARGATE", + overrides={}, + taskDefinition="test_ecs_task", + count=1, + startedBy="moto", + ) + response["tasks"][0]["taskArn"].should.match( + f"arn:aws:ecs:us-east-1:{ACCOUNT_ID}:task/{test_cluster_name}/[a-z0-9-]+$" + ) + + @mock_ecs def test_run_task_exceptions(): client = boto3.client("ecs", region_name="us-east-1") @@ -1849,7 +1960,7 @@ def test_describe_task_definition_by_family(): task["containerDefinitions"][0].should.equal( dict( container_definition, - **{"mountPoints": [], "portMappings": [], "volumesFrom": []} + **{"mountPoints": [], "portMappings": [], "volumesFrom": []}, ) ) task["taskDefinitionArn"].should.equal(