Repository: getmoto/moto

Issue:
put_account_setting for ecs client not implemented 
Hi 

As the subject stated, I was testing my boto3 for ecs services to paginate the number of available services based on tags. 
In my actual package, `serviceLongArnFormat` is `enabled` so that means the service arn will have  clustername before `service` and actual servicename like this "arn:aws:ecs:eu-west-1:123456789012:service/dummyCluster/successful-service-name". 
But default latest moto has not enabled longformat i.e. it is missing the `clustername` from the servicearn .
So, when I carry out my testing it's throwing 
`E       NotImplementedError: The put_account_setting action has not been implemented` 
How to replicate:
```
@mock_ecs
def test_list_successful_service_name():
    client = boto3.client("ecs", region_name="eu-west-1")
    response_svcLongArnFormat = client.put_account_setting(name='serviceLongArnFormat', value='enabled') --> failing here
    assert response_svcLongArnFormat['ResponseMetadata']['HTTPStatusCode'] == 200 
    _ = client.create_cluster(clusterName="dummy-cluster")
    _ = 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"},
            }
        ],
    )
    _ = client.create_service(
        cluster="dummy-cluster",
        serviceName="test-ecs-service-1",
        taskDefinition="test_ecs_task",
        desiredCount=2,
        launchType="FARGATE",
        tags=[{"key": "ResourceOwner", "value": "Dummy"}],
    )
    _ = client.create_service(
        cluster="dummy-cluster",
        serviceName="test-ecs-service",
        taskDefinition="test_ecs_task",
        desiredCount=2,
        launchType="FARGATE",
        tags=[{"key": "ResourceOwner", "value": "Dummy"}],
    )
    response = client.list_services(
        cluster="dummy-cluster",
        launchType="FARGATE"
    )
    assert response["serviceArns"][0] == "arn:aws:ecs:eu-west-1:{}:service/test-ecs-service-1".format(ACCOUNT_ID)
    assert response["serviceArns"][1] == "arn:aws:ecs:eu-west-1:{}:service/test-ecs-service".format(ACCOUNT_ID)
    assert len(response["serviceArns"]) == 2
```
 
I'm using the latest boto3 and latest moto release. Now test is failing because return service arn has extra characters as clustername.


Discussion:
Hi @manas86, thanks for raising this! Marking it as an enhancement.

Relates to #3497, which talks about turning on the new format as default by setting an env var
@bblommers  any update on this? what is the new env var called? 
Hi @manas86, no update - don't think anyone has started development on this. The env var is a proposed solution, nothing more at the moment
@manas86 As of moto >= 2.2.11.dev22 there is an option to enable the long format for ECS ARN's.

Set the following environment variable to `true` to enable it:
`MOTO_ECS_NEW_ARN`

This environment variable defaults to False for now, i.e. if you do nothing there is no impact for existing tests.

I expect we'll change the default behaviour in a next major release, so that the long format becomes the norm, but we'll communicate so if and when it happens.



Resolving patch:
diff --git a/moto/ecs/exceptions.py b/moto/ecs/exceptions.py
--- a/moto/ecs/exceptions.py
+++ b/moto/ecs/exceptions.py
@@ -48,10 +48,26 @@ def __init__(self):
         )
 
 
+class EcsClientException(JsonRESTError):
+    code = 400
+
+    def __init__(self, message):
+        super(EcsClientException, self).__init__(
+            error_type="ClientException", message=message,
+        )
+
+
 class InvalidParameterException(JsonRESTError):
     code = 400
 
     def __init__(self, message):
         super(InvalidParameterException, self).__init__(
-            error_type="ClientException", message=message,
+            error_type="InvalidParameterException", message=message,
+        )
+
+
+class UnknownAccountSettingException(InvalidParameterException):
+    def __init__(self):
+        super().__init__(
+            "unknown should be one of [serviceLongArnFormat,taskLongArnFormat,containerInstanceLongArnFormat,containerLongArnFormat,awsvpcTrunking,containerInsights,dualStackIPv6]"
         )
diff --git a/moto/ecs/models.py b/moto/ecs/models.py
--- a/moto/ecs/models.py
+++ b/moto/ecs/models.py
@@ -13,12 +13,14 @@
 from moto.core.utils import unix_time, pascal_to_camelcase, remap_nested_keys
 from moto.ec2 import ec2_backends
 from .exceptions import (
+    EcsClientException,
     ServiceNotFoundException,
     TaskDefinitionNotFoundException,
     TaskSetNotFoundException,
     ClusterNotFoundException,
     InvalidParameterException,
     RevisionNotFoundException,
+    UnknownAccountSettingException,
 )
 
 
@@ -35,7 +37,9 @@ def camelCase(self, key):
     def gen_response_object(self):
         response_object = copy(self.__dict__)
         for key, value in self.__dict__.items():
-            if "_" in key:
+            if key.startswith("_"):
+                del response_object[key]
+            elif "_" in key:
                 response_object[self.camelCase(key)] = value
                 del response_object[key]
         return response_object
@@ -45,6 +49,12 @@ def response_object(self):
         return self.gen_response_object()
 
 
+class AccountSetting(BaseObject):
+    def __init__(self, name, value):
+        self.name = name
+        self.value = value
+
+
 class Cluster(BaseObject, CloudFormationModel):
     def __init__(self, cluster_name, region_name):
         self.active_services_count = 0
@@ -269,6 +279,7 @@ def __init__(
         task_definition,
         container_instance_arn,
         resource_requirements,
+        backend,
         overrides={},
         started_by="",
         tags=[],
@@ -287,10 +298,11 @@ def __init__(
         self.stopped_reason = ""
         self.resource_requirements = resource_requirements
         self.region_name = cluster.region_name
+        self._backend = backend
 
     @property
     def task_arn(self):
-        if settings.ecs_new_arn_format():
+        if self._backend.enable_long_arn_for_name(name="taskLongArnFormat"):
             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
@@ -315,6 +327,7 @@ def __init__(
         tags=None,
         deployment_controller=None,
         launch_type=None,
+        backend=None,
         service_registries=None,
     ):
         self.cluster_name = cluster.name
@@ -355,10 +368,11 @@ def __init__(
         self.tags = tags if tags is not None else []
         self.pending_count = 0
         self.region_name = cluster.region_name
+        self._backend = backend
 
     @property
     def arn(self):
-        if settings.ecs_new_arn_format():
+        if self._backend.enable_long_arn_for_name(name="serviceLongArnFormat"):
             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
@@ -467,7 +481,7 @@ def get_cfn_attribute(self, attribute_name):
 
 
 class ContainerInstance(BaseObject):
-    def __init__(self, ec2_instance_id, region_name, cluster_name):
+    def __init__(self, ec2_instance_id, region_name, cluster_name, backend):
         self.ec2_instance_id = ec2_instance_id
         self.agent_connected = True
         self.status = "ACTIVE"
@@ -556,10 +570,13 @@ def __init__(self, ec2_instance_id, region_name, cluster_name):
         self.region_name = region_name
         self.id = str(uuid.uuid4())
         self.cluster_name = cluster_name
+        self._backend = backend
 
     @property
     def container_instance_arn(self):
-        if settings.ecs_new_arn_format():
+        if self._backend.enable_long_arn_for_name(
+            name="containerInstanceLongArnFormat"
+        ):
             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}"
@@ -686,6 +703,7 @@ def response_object(self):
 class EC2ContainerServiceBackend(BaseBackend):
     def __init__(self, region_name):
         super(EC2ContainerServiceBackend, self).__init__()
+        self.account_settings = dict()
         self.clusters = {}
         self.task_definitions = {}
         self.tasks = {}
@@ -879,9 +897,10 @@ def run_task(
                         task_definition,
                         container_instance_arn,
                         resource_requirements,
-                        overrides or {},
-                        started_by or "",
-                        tags or [],
+                        backend=self,
+                        overrides=overrides or {},
+                        started_by=started_by or "",
+                        tags=tags or [],
                     )
                     self.update_container_instance_resources(
                         container_instance, resource_requirements
@@ -978,7 +997,7 @@ def start_task(
             self.tasks[cluster.name] = {}
         tasks = []
         if not container_instances:
-            raise InvalidParameterException("Container Instances cannot be empty.")
+            raise EcsClientException("Container Instances cannot be empty.")
 
         container_instance_ids = [x.split("/")[-1] for x in container_instances]
         resource_requirements = self._calculate_task_resource_requirements(
@@ -993,8 +1012,9 @@ def start_task(
                 task_definition,
                 container_instance.container_instance_arn,
                 resource_requirements,
-                overrides or {},
-                started_by or "",
+                backend=self,
+                overrides=overrides or {},
+                started_by=started_by or "",
             )
             tasks.append(task)
             self.update_container_instance_resources(
@@ -1121,9 +1141,7 @@ def create_service(
 
         launch_type = launch_type if launch_type is not None else "EC2"
         if launch_type not in ["EC2", "FARGATE"]:
-            raise InvalidParameterException(
-                "launch type should be one of [EC2,FARGATE]"
-            )
+            raise EcsClientException("launch type should be one of [EC2,FARGATE]")
 
         service = Service(
             cluster,
@@ -1135,6 +1153,7 @@ def create_service(
             tags,
             deployment_controller,
             launch_type,
+            backend=self,
             service_registries=service_registries,
         )
         cluster_service_pair = "{0}:{1}".format(cluster.name, service_name)
@@ -1225,7 +1244,7 @@ def register_container_instance(self, cluster_str, ec2_instance_id):
         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, cluster_name
+            ec2_instance_id, self.region_name, cluster_name, backend=self
         )
         if not self.container_instances.get(cluster_name):
             self.container_instances[cluster_name] = {}
@@ -1250,7 +1269,7 @@ def describe_container_instances(self, cluster_str, list_container_instance_ids)
         cluster = self._get_cluster(cluster_str)
 
         if not list_container_instance_ids:
-            raise InvalidParameterException("Container Instances cannot be empty.")
+            raise EcsClientException("Container Instances cannot be empty.")
         failures = []
         container_instance_objects = []
         for container_instance_id in list_container_instance_ids:
@@ -1590,9 +1609,7 @@ def create_task_set(
     ):
         launch_type = launch_type if launch_type is not None else "EC2"
         if launch_type not in ["EC2", "FARGATE"]:
-            raise InvalidParameterException(
-                "launch type should be one of [EC2,FARGATE]"
-            )
+            raise EcsClientException("launch type should be one of [EC2,FARGATE]")
 
         task_set = TaskSet(
             service,
@@ -1702,6 +1719,41 @@ def update_service_primary_task_set(self, cluster, service, primary_task_set):
                 task_set.status = "ACTIVE"
         return task_set_obj
 
+    def list_account_settings(self, name=None, value=None):
+        expected_names = [
+            "serviceLongArnFormat",
+            "taskLongArnFormat",
+            "containerInstanceLongArnFormat",
+            "containerLongArnFormat",
+            "awsvpcTrunking",
+            "containerInsights",
+            "dualStackIPv6",
+        ]
+        if name and name not in expected_names:
+            raise UnknownAccountSettingException()
+        all_settings = self.account_settings.values()
+        return [
+            s
+            for s in all_settings
+            if (not name or s.name == name) and (not value or s.value == value)
+        ]
+
+    def put_account_setting(self, name, value):
+        account_setting = AccountSetting(name, value)
+        self.account_settings[name] = account_setting
+        return account_setting
+
+    def delete_account_setting(self, name):
+        self.account_settings.pop(name, None)
+
+    def enable_long_arn_for_name(self, name):
+        if settings.ecs_new_arn_format():
+            return True
+        account = self.account_settings.get(name, None)
+        if account and account.value == "enabled":
+            return True
+        return False
+
 
 ecs_backends = {}
 for region in Session().get_available_regions("ecs"):
diff --git a/moto/ecs/responses.py b/moto/ecs/responses.py
--- a/moto/ecs/responses.py
+++ b/moto/ecs/responses.py
@@ -449,3 +449,20 @@ def update_service_primary_task_set(self):
             cluster_str, service_str, primary_task_set
         )
         return json.dumps({"taskSet": task_set.response_object})
+
+    def put_account_setting(self):
+        name = self._get_param("name")
+        value = self._get_param("value")
+        account_setting = self.ecs_backend.put_account_setting(name, value)
+        return json.dumps({"setting": account_setting.response_object})
+
+    def list_account_settings(self):
+        name = self._get_param("name")
+        value = self._get_param("value")
+        account_settings = self.ecs_backend.list_account_settings(name, value)
+        return json.dumps({"settings": [s.response_object for s in account_settings]})
+
+    def delete_account_setting(self):
+        name = self._get_param("name")
+        self.ecs_backend.delete_account_setting(name)
+        return "{}"


Test patch:
diff --git a/tests/test_ecs/test_ecs_account_settings.py b/tests/test_ecs/test_ecs_account_settings.py
new file mode 100644
--- /dev/null
+++ b/tests/test_ecs/test_ecs_account_settings.py
@@ -0,0 +1,244 @@
+from botocore.exceptions import ClientError
+import boto3
+import sure  # noqa # pylint: disable=unused-import
+import json
+
+from moto.core import ACCOUNT_ID
+from moto.ec2 import utils as ec2_utils
+
+from moto import mock_ecs, mock_ec2
+import pytest
+from tests import EXAMPLE_AMI_ID
+
+
+@mock_ecs
+def test_list_account_settings_initial():
+    client = boto3.client("ecs", region_name="eu-west-1")
+
+    resp = client.list_account_settings()
+    resp.should.have.key("settings").equal([])
+
+
+@mock_ecs
+@pytest.mark.parametrize(
+    "name",
+    ["containerInstanceLongArnFormat", "serviceLongArnFormat", "taskLongArnFormat"],
+)
+@pytest.mark.parametrize("value", ["enabled", "disabled"])
+def test_put_account_setting(name, value):
+    client = boto3.client("ecs", region_name="eu-west-1")
+
+    resp = client.put_account_setting(name=name, value=value)
+    resp.should.have.key("setting")
+    resp["setting"].should.equal({"name": name, "value": value})
+
+
+@mock_ecs
+def test_list_account_setting():
+    client = boto3.client("ecs", region_name="eu-west-1")
+
+    client.put_account_setting(name="containerInstanceLongArnFormat", value="enabled")
+    client.put_account_setting(name="serviceLongArnFormat", value="disabled")
+    client.put_account_setting(name="taskLongArnFormat", value="enabled")
+
+    resp = client.list_account_settings()
+    resp.should.have.key("settings").length_of(3)
+    resp["settings"].should.contain(
+        {"name": "containerInstanceLongArnFormat", "value": "enabled"}
+    )
+    resp["settings"].should.contain(
+        {"name": "serviceLongArnFormat", "value": "disabled"}
+    )
+    resp["settings"].should.contain({"name": "taskLongArnFormat", "value": "enabled"})
+
+    resp = client.list_account_settings(name="serviceLongArnFormat")
+    resp.should.have.key("settings").length_of(1)
+    resp["settings"].should.contain(
+        {"name": "serviceLongArnFormat", "value": "disabled"}
+    )
+
+    resp = client.list_account_settings(value="enabled")
+    resp.should.have.key("settings").length_of(2)
+    resp["settings"].should.contain(
+        {"name": "containerInstanceLongArnFormat", "value": "enabled"}
+    )
+    resp["settings"].should.contain({"name": "taskLongArnFormat", "value": "enabled"})
+
+
+@mock_ecs
+def test_list_account_settings_wrong_name():
+    client = boto3.client("ecs", region_name="eu-west-1")
+
+    with pytest.raises(ClientError) as exc:
+        client.list_account_settings(name="unknown")
+    err = exc.value.response["Error"]
+    err["Code"].should.equal("InvalidParameterException")
+    err["Message"].should.equal(
+        "unknown should be one of [serviceLongArnFormat,taskLongArnFormat,containerInstanceLongArnFormat,containerLongArnFormat,awsvpcTrunking,containerInsights,dualStackIPv6]"
+    )
+
+
+@mock_ecs
+def test_delete_account_setting():
+    client = boto3.client("ecs", region_name="eu-west-1")
+
+    client.put_account_setting(name="containerInstanceLongArnFormat", value="enabled")
+    client.put_account_setting(name="serviceLongArnFormat", value="enabled")
+    client.put_account_setting(name="taskLongArnFormat", value="enabled")
+
+    resp = client.list_account_settings()
+    resp.should.have.key("settings").length_of(3)
+
+    client.delete_account_setting(name="serviceLongArnFormat")
+
+    resp = client.list_account_settings()
+    resp.should.have.key("settings").length_of(2)
+    resp["settings"].should.contain(
+        {"name": "containerInstanceLongArnFormat", "value": "enabled"}
+    )
+    resp["settings"].should.contain({"name": "taskLongArnFormat", "value": "enabled"})
+
+
+@mock_ec2
+@mock_ecs
+def test_put_account_setting_changes_service_arn():
+    client = boto3.client("ecs", region_name="eu-west-1")
+    client.put_account_setting(name="serviceLongArnFormat", value="enabled")
+
+    _ = client.create_cluster(clusterName="dummy-cluster")
+    _ = client.register_task_definition(
+        family="test_ecs_task",
+        containerDefinitions=[
+            {
+                "name": "hello_world",
+                "image": "docker/hello-world:latest",
+                "cpu": 1024,
+                "memory": 400,
+            }
+        ],
+    )
+    client.create_service(
+        cluster="dummy-cluster",
+        serviceName="test-ecs-service",
+        taskDefinition="test_ecs_task",
+        desiredCount=2,
+        launchType="FARGATE",
+        tags=[{"key": "ResourceOwner", "value": "Dummy"}],
+    )
+
+    # Initial response is long
+    response = client.list_services(cluster="dummy-cluster", launchType="FARGATE")
+    service_arn = response["serviceArns"][0]
+    service_arn.should.equal(
+        "arn:aws:ecs:eu-west-1:{}:service/dummy-cluster/test-ecs-service".format(
+            ACCOUNT_ID
+        )
+    )
+
+    # Second invocation returns short ARN's, after deleting the longArn-preference
+    client.delete_account_setting(name="serviceLongArnFormat")
+    response = client.list_services(cluster="dummy-cluster", launchType="FARGATE")
+    service_arn = response["serviceArns"][0]
+    service_arn.should.equal(
+        "arn:aws:ecs:eu-west-1:{}:service/test-ecs-service".format(ACCOUNT_ID)
+    )
+
+
+@mock_ec2
+@mock_ecs
+def test_put_account_setting_changes_containerinstance_arn():
+    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)
+    )
+
+    # Initial ARN should be short
+    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/[a-z0-9-]+$"
+    )
+
+    # Now enable long-format
+    ecs_client.put_account_setting(
+        name="containerInstanceLongArnFormat", value="enabled"
+    )
+    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_run_task_default_cluster_new_arn_format():
+    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,
+            }
+        ],
+    )
+    # Initial ARN is short-format
+    client.put_account_setting(name="taskLongArnFormat", value="disabled")
+    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/[a-z0-9-]+$"
+    )
+
+    # Enable long-format for the next task
+    client.put_account_setting(name="taskLongArnFormat", value="enabled")
+    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-]+$"
+    )