ECS: delete/list/put_account_setting (#4456)

This commit is contained in:
Bert Blommers 2021-10-21 22:00:32 +00:00 committed by GitHub
parent 64e16d970a
commit 3d6ffcc74d
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
4 changed files with 349 additions and 20 deletions

View File

@ -48,10 +48,26 @@ class ClusterNotFoundException(JsonRESTError):
) )
class EcsClientException(JsonRESTError):
code = 400
def __init__(self, message):
super(EcsClientException, self).__init__(
error_type="ClientException", message=message,
)
class InvalidParameterException(JsonRESTError): class InvalidParameterException(JsonRESTError):
code = 400 code = 400
def __init__(self, message): def __init__(self, message):
super(InvalidParameterException, self).__init__( 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]"
) )

View File

@ -13,12 +13,14 @@ from moto.core.exceptions import JsonRESTError
from moto.core.utils import unix_time, pascal_to_camelcase, remap_nested_keys from moto.core.utils import unix_time, pascal_to_camelcase, remap_nested_keys
from moto.ec2 import ec2_backends from moto.ec2 import ec2_backends
from .exceptions import ( from .exceptions import (
EcsClientException,
ServiceNotFoundException, ServiceNotFoundException,
TaskDefinitionNotFoundException, TaskDefinitionNotFoundException,
TaskSetNotFoundException, TaskSetNotFoundException,
ClusterNotFoundException, ClusterNotFoundException,
InvalidParameterException, InvalidParameterException,
RevisionNotFoundException, RevisionNotFoundException,
UnknownAccountSettingException,
) )
@ -35,7 +37,9 @@ class BaseObject(BaseModel):
def gen_response_object(self): def gen_response_object(self):
response_object = copy(self.__dict__) response_object = copy(self.__dict__)
for key, value in self.__dict__.items(): 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 response_object[self.camelCase(key)] = value
del response_object[key] del response_object[key]
return response_object return response_object
@ -45,6 +49,12 @@ class BaseObject(BaseModel):
return self.gen_response_object() return self.gen_response_object()
class AccountSetting(BaseObject):
def __init__(self, name, value):
self.name = name
self.value = value
class Cluster(BaseObject, CloudFormationModel): class Cluster(BaseObject, CloudFormationModel):
def __init__(self, cluster_name, region_name): def __init__(self, cluster_name, region_name):
self.active_services_count = 0 self.active_services_count = 0
@ -269,6 +279,7 @@ class Task(BaseObject):
task_definition, task_definition,
container_instance_arn, container_instance_arn,
resource_requirements, resource_requirements,
backend,
overrides={}, overrides={},
started_by="", started_by="",
tags=[], tags=[],
@ -287,10 +298,11 @@ class Task(BaseObject):
self.stopped_reason = "" self.stopped_reason = ""
self.resource_requirements = resource_requirements self.resource_requirements = resource_requirements
self.region_name = cluster.region_name self.region_name = cluster.region_name
self._backend = backend
@property @property
def task_arn(self): 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 f"arn:aws:ecs:{self.region_name}:{ACCOUNT_ID}:task/{self.cluster_name}/{self.id}"
return "arn:aws:ecs:{0}:{1}:task/{2}".format( return "arn:aws:ecs:{0}:{1}:task/{2}".format(
self.region_name, ACCOUNT_ID, self.id self.region_name, ACCOUNT_ID, self.id
@ -315,6 +327,7 @@ class Service(BaseObject, CloudFormationModel):
tags=None, tags=None,
deployment_controller=None, deployment_controller=None,
launch_type=None, launch_type=None,
backend=None,
service_registries=None, service_registries=None,
): ):
self.cluster_name = cluster.name self.cluster_name = cluster.name
@ -355,10 +368,11 @@ class Service(BaseObject, CloudFormationModel):
self.tags = tags if tags is not None else [] self.tags = tags if tags is not None else []
self.pending_count = 0 self.pending_count = 0
self.region_name = cluster.region_name self.region_name = cluster.region_name
self._backend = backend
@property @property
def arn(self): 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 f"arn:aws:ecs:{self.region_name}:{ACCOUNT_ID}:service/{self.cluster_name}/{self.name}"
return "arn:aws:ecs:{0}:{1}:service/{2}".format( return "arn:aws:ecs:{0}:{1}:service/{2}".format(
self.region_name, ACCOUNT_ID, self.name self.region_name, ACCOUNT_ID, self.name
@ -467,7 +481,7 @@ class Service(BaseObject, CloudFormationModel):
class ContainerInstance(BaseObject): 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.ec2_instance_id = ec2_instance_id
self.agent_connected = True self.agent_connected = True
self.status = "ACTIVE" self.status = "ACTIVE"
@ -556,10 +570,13 @@ class ContainerInstance(BaseObject):
self.region_name = region_name self.region_name = region_name
self.id = str(uuid.uuid4()) self.id = str(uuid.uuid4())
self.cluster_name = cluster_name self.cluster_name = cluster_name
self._backend = backend
@property @property
def container_instance_arn(self): 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.cluster_name}/{self.id}"
return ( return (
f"arn:aws:ecs:{self.region_name}:{ACCOUNT_ID}:container-instance/{self.id}" f"arn:aws:ecs:{self.region_name}:{ACCOUNT_ID}:container-instance/{self.id}"
@ -686,6 +703,7 @@ class TaskSet(BaseObject):
class EC2ContainerServiceBackend(BaseBackend): class EC2ContainerServiceBackend(BaseBackend):
def __init__(self, region_name): def __init__(self, region_name):
super(EC2ContainerServiceBackend, self).__init__() super(EC2ContainerServiceBackend, self).__init__()
self.account_settings = dict()
self.clusters = {} self.clusters = {}
self.task_definitions = {} self.task_definitions = {}
self.tasks = {} self.tasks = {}
@ -879,9 +897,10 @@ class EC2ContainerServiceBackend(BaseBackend):
task_definition, task_definition,
container_instance_arn, container_instance_arn,
resource_requirements, resource_requirements,
overrides or {}, backend=self,
started_by or "", overrides=overrides or {},
tags or [], started_by=started_by or "",
tags=tags or [],
) )
self.update_container_instance_resources( self.update_container_instance_resources(
container_instance, resource_requirements container_instance, resource_requirements
@ -978,7 +997,7 @@ class EC2ContainerServiceBackend(BaseBackend):
self.tasks[cluster.name] = {} self.tasks[cluster.name] = {}
tasks = [] tasks = []
if not container_instances: 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] container_instance_ids = [x.split("/")[-1] for x in container_instances]
resource_requirements = self._calculate_task_resource_requirements( resource_requirements = self._calculate_task_resource_requirements(
@ -993,8 +1012,9 @@ class EC2ContainerServiceBackend(BaseBackend):
task_definition, task_definition,
container_instance.container_instance_arn, container_instance.container_instance_arn,
resource_requirements, resource_requirements,
overrides or {}, backend=self,
started_by or "", overrides=overrides or {},
started_by=started_by or "",
) )
tasks.append(task) tasks.append(task)
self.update_container_instance_resources( self.update_container_instance_resources(
@ -1121,9 +1141,7 @@ class EC2ContainerServiceBackend(BaseBackend):
launch_type = launch_type if launch_type is not None else "EC2" launch_type = launch_type if launch_type is not None else "EC2"
if launch_type not in ["EC2", "FARGATE"]: if launch_type not in ["EC2", "FARGATE"]:
raise InvalidParameterException( raise EcsClientException("launch type should be one of [EC2,FARGATE]")
"launch type should be one of [EC2,FARGATE]"
)
service = Service( service = Service(
cluster, cluster,
@ -1135,6 +1153,7 @@ class EC2ContainerServiceBackend(BaseBackend):
tags, tags,
deployment_controller, deployment_controller,
launch_type, launch_type,
backend=self,
service_registries=service_registries, service_registries=service_registries,
) )
cluster_service_pair = "{0}:{1}".format(cluster.name, service_name) cluster_service_pair = "{0}:{1}".format(cluster.name, service_name)
@ -1225,7 +1244,7 @@ class EC2ContainerServiceBackend(BaseBackend):
if cluster_name not in self.clusters: if cluster_name not in self.clusters:
raise Exception("{0} is not a cluster".format(cluster_name)) raise Exception("{0} is not a cluster".format(cluster_name))
container_instance = ContainerInstance( 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): if not self.container_instances.get(cluster_name):
self.container_instances[cluster_name] = {} self.container_instances[cluster_name] = {}
@ -1250,7 +1269,7 @@ class EC2ContainerServiceBackend(BaseBackend):
cluster = self._get_cluster(cluster_str) cluster = self._get_cluster(cluster_str)
if not list_container_instance_ids: if not list_container_instance_ids:
raise InvalidParameterException("Container Instances cannot be empty.") raise EcsClientException("Container Instances cannot be empty.")
failures = [] failures = []
container_instance_objects = [] container_instance_objects = []
for container_instance_id in list_container_instance_ids: for container_instance_id in list_container_instance_ids:
@ -1590,9 +1609,7 @@ class EC2ContainerServiceBackend(BaseBackend):
): ):
launch_type = launch_type if launch_type is not None else "EC2" launch_type = launch_type if launch_type is not None else "EC2"
if launch_type not in ["EC2", "FARGATE"]: if launch_type not in ["EC2", "FARGATE"]:
raise InvalidParameterException( raise EcsClientException("launch type should be one of [EC2,FARGATE]")
"launch type should be one of [EC2,FARGATE]"
)
task_set = TaskSet( task_set = TaskSet(
service, service,
@ -1702,6 +1719,41 @@ class EC2ContainerServiceBackend(BaseBackend):
task_set.status = "ACTIVE" task_set.status = "ACTIVE"
return task_set_obj 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 = {} ecs_backends = {}
for region in Session().get_available_regions("ecs"): for region in Session().get_available_regions("ecs"):

View File

@ -449,3 +449,20 @@ class EC2ContainerServiceResponse(BaseResponse):
cluster_str, service_str, primary_task_set cluster_str, service_str, primary_task_set
) )
return json.dumps({"taskSet": task_set.response_object}) 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 "{}"

View File

@ -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-]+$"
)