ECS - support for CapacityProviders (#4977)

This commit is contained in:
Bert Blommers 2022-03-29 14:19:09 +00:00 committed by GitHub
parent 60e48c101e
commit 1a5c18878c
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
6 changed files with 364 additions and 44 deletions

View File

@ -1995,21 +1995,21 @@
## ecs
<details>
<summary>73% implemented</summary>
<summary>78% implemented</summary>
- [ ] create_capacity_provider
- [X] create_capacity_provider
- [X] create_cluster
- [X] create_service
- [X] create_task_set
- [X] delete_account_setting
- [X] delete_attributes
- [ ] delete_capacity_provider
- [X] delete_capacity_provider
- [X] delete_cluster
- [X] delete_service
- [X] delete_task_set
- [X] deregister_container_instance
- [X] deregister_task_definition
- [ ] describe_capacity_providers
- [X] describe_capacity_providers
- [X] describe_clusters
- [X] describe_container_instances
- [X] describe_services

View File

@ -27,13 +27,17 @@ ecs
|start-h3| Implemented features for this service |end-h3|
- [ ] create_capacity_provider
- [X] create_capacity_provider
- [X] create_cluster
The following parameters are not yet implemented: configuration, capacityProviders, defaultCapacityProviderStrategy
- [X] create_service
- [X] create_task_set
- [X] delete_account_setting
- [X] delete_attributes
- [ ] delete_capacity_provider
- [X] delete_capacity_provider
- [X] delete_cluster
- [X] delete_service
- [X] delete_task_set
@ -43,7 +47,7 @@ ecs
- [X] deregister_container_instance
- [X] deregister_task_definition
- [ ] describe_capacity_providers
- [X] describe_capacity_providers
- [X] describe_clusters
Only include=TAGS is currently supported.

View File

@ -61,7 +61,7 @@ class AccountSetting(BaseObject):
class Cluster(BaseObject, CloudFormationModel):
def __init__(self, cluster_name, region_name):
def __init__(self, cluster_name, region_name, cluster_settings=None):
self.active_services_count = 0
self.arn = "arn:aws:ecs:{0}:{1}:cluster/{2}".format(
region_name, ACCOUNT_ID, cluster_name
@ -72,6 +72,7 @@ class Cluster(BaseObject, CloudFormationModel):
self.running_tasks_count = 0
self.status = "ACTIVE"
self.region_name = region_name
self.settings = cluster_settings
@property
def physical_resource_id(self):
@ -326,6 +327,31 @@ class Task(BaseObject):
return response_object
class CapacityProvider(BaseObject):
def __init__(self, region_name, name, asg_details, tags):
self._id = str(uuid.uuid4())
self.capacity_provider_arn = f"arn:aws:ecs:{region_name}:{ACCOUNT_ID}:capacity_provider/{name}/{self._id}"
self.name = name
self.status = "ACTIVE"
self.auto_scaling_group_provider = asg_details
self.tags = tags
class CapacityProviderFailure(BaseObject):
def __init__(self, reason, name, region_name):
self.reason = reason
self.arn = "arn:aws:ecs:{0}:{1}:capacity_provider/{2}".format(
region_name, ACCOUNT_ID, name
)
@property
def response_object(self):
response_object = self.gen_response_object()
response_object["reason"] = self.reason
response_object["arn"] = self.arn
return response_object
class Service(BaseObject, CloudFormationModel):
def __init__(
self,
@ -727,6 +753,7 @@ class EC2ContainerServiceBackend(BaseBackend):
def __init__(self, region_name):
super().__init__()
self.account_settings = dict()
self.capacity_providers = dict()
self.clusters = {}
self.task_definitions = {}
self.tasks = {}
@ -760,6 +787,13 @@ class EC2ContainerServiceBackend(BaseBackend):
return cluster
def create_capacity_provider(self, name, asg_details, tags):
capacity_provider = CapacityProvider(self.region_name, name, asg_details, tags)
self.capacity_providers[name] = capacity_provider
if tags:
self.tagger.tag_resource(capacity_provider.capacity_provider_arn, tags)
return capacity_provider
def describe_task_definition(self, task_definition_str):
task_definition_name = task_definition_str.split("/")[-1]
if ":" in task_definition_name:
@ -777,13 +811,42 @@ class EC2ContainerServiceBackend(BaseBackend):
else:
raise Exception("{0} is not a task_definition".format(task_definition_name))
def create_cluster(self, cluster_name, tags=None):
cluster = Cluster(cluster_name, self.region_name)
def create_cluster(self, cluster_name, tags=None, cluster_settings=None):
"""
The following parameters are not yet implemented: configuration, capacityProviders, defaultCapacityProviderStrategy
"""
cluster = Cluster(cluster_name, self.region_name, cluster_settings)
self.clusters[cluster_name] = cluster
if tags:
self.tagger.tag_resource(cluster.arn, tags)
return cluster
def _get_provider(self, name_or_arn):
for provider in self.capacity_providers.values():
if (
provider.name == name_or_arn
or provider.capacity_provider_arn == name_or_arn
):
return provider
def describe_capacity_providers(self, names):
providers = []
failures = []
for name in names:
provider = self._get_provider(name)
if provider:
providers.append(provider)
else:
failures.append(
CapacityProviderFailure("MISSING", name, self.region_name)
)
return providers, failures
def delete_capacity_provider(self, name_or_arn):
provider = self._get_provider(name_or_arn)
self.capacity_providers.pop(provider.name)
return provider
def list_clusters(self):
"""
maxSize and pagination not implemented
@ -1165,6 +1228,15 @@ class EC2ContainerServiceBackend(BaseBackend):
"Could not find task {} on cluster {}".format(task_str, cluster.name)
)
def _get_service(self, cluster_str, service_str):
cluster = self._get_cluster(cluster_str)
for service in self.services.values():
if service.cluster_name == cluster.name and (
service.name == service_str or service.arn == service_str
):
return service
raise ServiceNotFoundException
def create_service(
self,
cluster_str,
@ -1223,31 +1295,19 @@ class EC2ContainerServiceBackend(BaseBackend):
def describe_services(self, cluster_str, service_names_or_arns):
cluster = self._get_cluster(cluster_str)
service_names = [name.split("/")[-1] for name in service_names_or_arns]
result = []
failures = []
for existing_service_name, existing_service_obj in sorted(
self.services.items()
):
for requested_name_or_arn in service_names_or_arns:
cluster_service_pair = "{0}:{1}".format(
cluster.name, requested_name_or_arn
for name in service_names:
cluster_service_pair = "{0}:{1}".format(cluster.name, name)
if cluster_service_pair in self.services:
result.append(self.services[cluster_service_pair])
else:
missing_arn = (
f"arn:aws:ecs:{self.region_name}:{ACCOUNT_ID}:service/{name}"
)
if (
cluster_service_pair == existing_service_name
or existing_service_obj.arn == requested_name_or_arn
):
result.append(existing_service_obj)
else:
service_name = requested_name_or_arn.split("/")[-1]
failures.append(
{
"arn": "arn:aws:ecs:eu-central-1:{0}:service/{1}".format(
ACCOUNT_ID, service_name
),
"reason": "MISSING",
}
)
failures.append({"arn": missing_arn, "reason": "MISSING"})
return result, failures
@ -1272,18 +1332,17 @@ class EC2ContainerServiceBackend(BaseBackend):
def delete_service(self, cluster_name, service_name, force):
cluster = self._get_cluster(cluster_name)
cluster_service_pair = "{0}:{1}".format(cluster.name, service_name)
service = self._get_service(cluster_name, service_name)
if cluster_service_pair in self.services:
service = self.services[cluster_service_pair]
if service.desired_count > 0 and not force:
raise InvalidParameterException(
"The service cannot be stopped while it is scaled above 0."
)
else:
return self.services.pop(cluster_service_pair)
cluster_service_pair = "{0}:{1}".format(cluster.name, service.name)
service = self.services[cluster_service_pair]
if service.desired_count > 0 and not force:
raise InvalidParameterException(
"The service cannot be stopped while it is scaled above 0."
)
else:
raise ServiceNotFoundException
return self.services.pop(cluster_service_pair)
def register_container_instance(self, cluster_str, ec2_instance_id):
cluster_name = cluster_str.split("/")[-1]

View File

@ -25,12 +25,20 @@ class EC2ContainerServiceResponse(BaseResponse):
def _get_param(self, param_name, if_none=None):
return self.request_params.get(param_name, if_none)
def create_capacity_provider(self):
name = self._get_param("name")
asg_provider = self._get_param("autoScalingGroupProvider")
tags = self._get_param("tags")
provider = self.ecs_backend.create_capacity_provider(name, asg_provider, tags)
return json.dumps({"capacityProvider": provider.response_object})
def create_cluster(self):
cluster_name = self._get_param("clusterName")
tags = self._get_param("tags")
settings = self._get_param("settings")
if cluster_name is None:
cluster_name = "default"
cluster = self.ecs_backend.create_cluster(cluster_name, tags)
cluster = self.ecs_backend.create_cluster(cluster_name, tags, settings)
return json.dumps({"cluster": cluster.response_object})
def list_clusters(self):
@ -42,6 +50,21 @@ class EC2ContainerServiceResponse(BaseResponse):
}
)
def delete_capacity_provider(self):
name = self._get_param("capacityProvider")
provider = self.ecs_backend.delete_capacity_provider(name)
return json.dumps({"capacityProvider": provider.response_object})
def describe_capacity_providers(self):
names = self._get_param("capacityProviders")
providers, failures = self.ecs_backend.describe_capacity_providers(names)
return json.dumps(
{
"capacityProviders": [p.response_object for p in providers],
"failures": [p.response_object for p in failures],
}
)
def describe_clusters(self):
names = self._get_param("clusters")
include = self._get_param("include")

View File

@ -39,6 +39,20 @@ def test_create_cluster():
response["cluster"]["activeServicesCount"].should.equal(0)
@mock_ecs
def test_create_cluster_with_setting():
client = boto3.client("ecs", region_name="us-east-1")
cluster = client.create_cluster(
clusterName="test_ecs_cluster",
settings=[{"name": "containerInsights", "value": "disabled"}],
)["cluster"]
cluster["clusterName"].should.equal("test_ecs_cluster")
cluster["status"].should.equal("ACTIVE")
cluster.should.have.key("settings").equals(
[{"name": "containerInsights", "value": "disabled"}]
)
@mock_ecs
def test_list_clusters():
client = boto3.client("ecs", region_name="us-east-2")
@ -112,7 +126,7 @@ def test_delete_cluster():
response["cluster"]["activeServicesCount"].should.equal(0)
response = client.list_clusters()
len(response["clusterArns"]).should.equal(0)
response["clusterArns"].should.have.length_of(0)
@mock_ecs
@ -685,7 +699,9 @@ def test_list_services():
@mock_ecs
def test_describe_services():
client = boto3.client("ecs", region_name="us-east-1")
_ = client.create_cluster(clusterName="test_ecs_cluster")
cluster_arn = client.create_cluster(clusterName="test_ecs_cluster")["cluster"][
"clusterArn"
]
_ = client.register_task_definition(
family="test_ecs_task",
containerDefinitions=[
@ -721,6 +737,14 @@ def test_describe_services():
taskDefinition="test_ecs_task",
desiredCount=2,
)
# Verify we can describe services using the cluster ARN
response = client.describe_services(
cluster=cluster_arn, services=["test_ecs_service1"]
)
response.should.have.key("services").length_of(1)
# Verify we can describe services using both names and ARN's
response = client.describe_services(
cluster="test_ecs_cluster",
services=[
@ -1072,6 +1096,43 @@ def test_delete_service():
)
@mock_ecs
def test_delete_service__using_arns():
client = boto3.client("ecs", region_name="us-east-1")
cluster_arn = client.create_cluster(clusterName="test_ecs_cluster")["cluster"][
"clusterArn"
]
_ = 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"},
}
],
)
service_arn = client.create_service(
cluster="test_ecs_cluster",
serviceName="test_ecs_service",
taskDefinition="test_ecs_task",
desiredCount=2,
)["service"]["serviceArn"]
_ = client.update_service(
cluster="test_ecs_cluster", service="test_ecs_service", desiredCount=0
)
response = client.delete_service(cluster=cluster_arn, service=service_arn)
response["service"]["clusterArn"].should.equal(
"arn:aws:ecs:us-east-1:{}:cluster/test_ecs_cluster".format(ACCOUNT_ID)
)
@mock_ecs
def test_delete_service_force():
client = boto3.client("ecs", region_name="us-east-1")

View File

@ -0,0 +1,173 @@
import boto3
from moto import mock_ecs
from moto.core import ACCOUNT_ID
@mock_ecs
def test_create_capacity_provider():
client = boto3.client("ecs", region_name="us-west-1")
resp = client.create_capacity_provider(
name="my_provider",
autoScalingGroupProvider={
"autoScalingGroupArn": "asg:arn",
"managedScaling": {
"status": "ENABLED",
"targetCapacity": 5,
"maximumScalingStepSize": 2,
},
"managedTerminationProtection": "DISABLED",
},
)
resp.should.have.key("capacityProvider")
provider = resp["capacityProvider"]
provider.should.have.key("capacityProviderArn")
provider.should.have.key("name").equals("my_provider")
provider.should.have.key("status").equals("ACTIVE")
provider.should.have.key("autoScalingGroupProvider").equals(
{
"autoScalingGroupArn": "asg:arn",
"managedScaling": {
"status": "ENABLED",
"targetCapacity": 5,
"maximumScalingStepSize": 2,
},
"managedTerminationProtection": "DISABLED",
}
)
@mock_ecs
def test_create_capacity_provider_with_tags():
client = boto3.client("ecs", region_name="us-west-1")
resp = client.create_capacity_provider(
name="my_provider",
autoScalingGroupProvider={"autoScalingGroupArn": "asg:arn"},
tags=[{"key": "k1", "value": "v1"}],
)
resp.should.have.key("capacityProvider")
provider = resp["capacityProvider"]
provider.should.have.key("capacityProviderArn")
provider.should.have.key("name").equals("my_provider")
provider.should.have.key("tags").equals([{"key": "k1", "value": "v1"}])
@mock_ecs
def test_describe_capacity_provider__using_name():
client = boto3.client("ecs", region_name="us-west-1")
client.create_capacity_provider(
name="my_provider",
autoScalingGroupProvider={
"autoScalingGroupArn": "asg:arn",
"managedScaling": {
"status": "ENABLED",
"targetCapacity": 5,
"maximumScalingStepSize": 2,
},
"managedTerminationProtection": "DISABLED",
},
)
resp = client.describe_capacity_providers(capacityProviders=["my_provider"])
resp.should.have.key("capacityProviders").length_of(1)
provider = resp["capacityProviders"][0]
provider.should.have.key("capacityProviderArn")
provider.should.have.key("name").equals("my_provider")
provider.should.have.key("status").equals("ACTIVE")
provider.should.have.key("autoScalingGroupProvider").equals(
{
"autoScalingGroupArn": "asg:arn",
"managedScaling": {
"status": "ENABLED",
"targetCapacity": 5,
"maximumScalingStepSize": 2,
},
"managedTerminationProtection": "DISABLED",
}
)
@mock_ecs
def test_describe_capacity_provider__using_arn():
client = boto3.client("ecs", region_name="us-west-1")
provider_arn = client.create_capacity_provider(
name="my_provider",
autoScalingGroupProvider={
"autoScalingGroupArn": "asg:arn",
"managedScaling": {
"status": "ENABLED",
"targetCapacity": 5,
"maximumScalingStepSize": 2,
},
"managedTerminationProtection": "DISABLED",
},
)["capacityProvider"]["capacityProviderArn"]
resp = client.describe_capacity_providers(capacityProviders=[provider_arn])
resp.should.have.key("capacityProviders").length_of(1)
provider = resp["capacityProviders"][0]
provider.should.have.key("name").equals("my_provider")
@mock_ecs
def test_describe_capacity_provider__missing():
client = boto3.client("ecs", region_name="us-west-1")
client.create_capacity_provider(
name="my_provider",
autoScalingGroupProvider={
"autoScalingGroupArn": "asg:arn",
"managedScaling": {
"status": "ENABLED",
"targetCapacity": 5,
"maximumScalingStepSize": 2,
},
"managedTerminationProtection": "DISABLED",
},
)
resp = client.describe_capacity_providers(
capacityProviders=["my_provider", "another_provider"]
)
resp.should.have.key("capacityProviders").length_of(1)
resp.should.have.key("failures").length_of(1)
resp["failures"].should.contain(
{
"arn": f"arn:aws:ecs:us-west-1:{ACCOUNT_ID}:capacity_provider/another_provider",
"reason": "MISSING",
}
)
@mock_ecs
def test_delete_capacity_provider():
client = boto3.client("ecs", region_name="us-west-1")
client.create_capacity_provider(
name="my_provider", autoScalingGroupProvider={"autoScalingGroupArn": "asg:arn"}
)
resp = client.delete_capacity_provider(capacityProvider="my_provider")
resp.should.have.key("capacityProvider")
resp["capacityProvider"].should.have.key("name").equals("my_provider")
# We can't find either provider
resp = client.describe_capacity_providers(
capacityProviders=["my_provider", "another_provider"]
)
resp.should.have.key("capacityProviders").length_of(0)
resp.should.have.key("failures").length_of(2)
resp["failures"].should.contain(
{
"arn": f"arn:aws:ecs:us-west-1:{ACCOUNT_ID}:capacity_provider/another_provider",
"reason": "MISSING",
}
)
resp["failures"].should.contain(
{
"arn": f"arn:aws:ecs:us-west-1:{ACCOUNT_ID}:capacity_provider/my_provider",
"reason": "MISSING",
}
)