Implement state transitions for ECS tasks (#6857)
This commit is contained in:
parent
fea098310a
commit
39b9c2f121
@ -134,3 +134,16 @@ Transition type: Manual - describe the resource 1 time before the state advances
|
|||||||
Advancement:
|
Advancement:
|
||||||
|
|
||||||
Call `boto3.client("transcribe").get_medical_transcription_job(..)`
|
Call `boto3.client("transcribe").get_medical_transcription_job(..)`
|
||||||
|
|
||||||
|
Service: ECS
|
||||||
|
--------------
|
||||||
|
|
||||||
|
**Model**: `ecs::task` :raw-html:`<br />`
|
||||||
|
Available states:
|
||||||
|
|
||||||
|
"RUNNING" --> "DEACTIVATING" --> "STOPPING" --> "DEPROVISIONING" --> "STOPPED"
|
||||||
|
|
||||||
|
Transition type: Manual - describe the resource 1 time before the state advances :raw-html:`<br />`
|
||||||
|
Advancement:
|
||||||
|
|
||||||
|
Call `boto3.client("ecs").describe_tasks(..)`
|
||||||
|
@ -11,7 +11,10 @@ from moto.core.utils import unix_time, pascal_to_camelcase, remap_nested_keys
|
|||||||
|
|
||||||
from ..ec2.utils import random_private_ip
|
from ..ec2.utils import random_private_ip
|
||||||
from moto.ec2 import ec2_backends
|
from moto.ec2 import ec2_backends
|
||||||
|
from moto.moto_api import state_manager
|
||||||
from moto.moto_api._internal import mock_random
|
from moto.moto_api._internal import mock_random
|
||||||
|
from moto.moto_api._internal.managed_state_model import ManagedState
|
||||||
|
|
||||||
from .exceptions import (
|
from .exceptions import (
|
||||||
EcsClientException,
|
EcsClientException,
|
||||||
ServiceNotFoundException,
|
ServiceNotFoundException,
|
||||||
@ -334,7 +337,7 @@ class TaskDefinition(BaseObject, CloudFormationModel):
|
|||||||
return original_resource
|
return original_resource
|
||||||
|
|
||||||
|
|
||||||
class Task(BaseObject):
|
class Task(BaseObject, ManagedState):
|
||||||
def __init__(
|
def __init__(
|
||||||
self,
|
self,
|
||||||
cluster: Cluster,
|
cluster: Cluster,
|
||||||
@ -348,11 +351,28 @@ class Task(BaseObject):
|
|||||||
tags: Optional[List[Dict[str, str]]] = None,
|
tags: Optional[List[Dict[str, str]]] = None,
|
||||||
networking_configuration: Optional[Dict[str, Any]] = None,
|
networking_configuration: Optional[Dict[str, Any]] = None,
|
||||||
):
|
):
|
||||||
|
# Configure ManagedState
|
||||||
|
# https://docs.aws.amazon.com/AmazonECS/latest/developerguide/task-lifecycle.html
|
||||||
|
super().__init__(
|
||||||
|
model_name="ecs::task",
|
||||||
|
transitions=[
|
||||||
|
# We start in RUNNING state in order not to break existing tests.
|
||||||
|
# ("PROVISIONING", "PENDING"),
|
||||||
|
# ("PENDING", "ACTIVATING"),
|
||||||
|
# ("ACTIVATING", "RUNNING"),
|
||||||
|
("RUNNING", "DEACTIVATING"),
|
||||||
|
("DEACTIVATING", "STOPPING"),
|
||||||
|
("STOPPING", "DEPROVISIONING"),
|
||||||
|
("DEPROVISIONING", "STOPPED"),
|
||||||
|
# There seems to be race condition, where the waiter expects the task to be in
|
||||||
|
# STOPPED state, but it is already in DELETED state.
|
||||||
|
# ("STOPPED", "DELETED"),
|
||||||
|
],
|
||||||
|
)
|
||||||
self.id = str(mock_random.uuid4())
|
self.id = str(mock_random.uuid4())
|
||||||
self.cluster_name = cluster.name
|
self.cluster_name = cluster.name
|
||||||
self.cluster_arn = cluster.arn
|
self.cluster_arn = cluster.arn
|
||||||
self.container_instance_arn = container_instance_arn
|
self.container_instance_arn = container_instance_arn
|
||||||
self.last_status = "RUNNING"
|
|
||||||
self.desired_status = "RUNNING"
|
self.desired_status = "RUNNING"
|
||||||
self.task_definition_arn = task_definition.arn
|
self.task_definition_arn = task_definition.arn
|
||||||
self.overrides = overrides or {}
|
self.overrides = overrides or {}
|
||||||
@ -401,6 +421,14 @@ class Task(BaseObject):
|
|||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@property
|
||||||
|
def last_status(self) -> Optional[str]:
|
||||||
|
return self.status # managed state
|
||||||
|
|
||||||
|
@last_status.setter
|
||||||
|
def last_status(self, value: Optional[str]) -> None:
|
||||||
|
self.status = value
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def task_arn(self) -> str:
|
def task_arn(self) -> str:
|
||||||
if self._backend.enable_long_arn_for_name(name="taskLongArnFormat"):
|
if self._backend.enable_long_arn_for_name(name="taskLongArnFormat"):
|
||||||
@ -411,6 +439,7 @@ class Task(BaseObject):
|
|||||||
def response_object(self) -> Dict[str, Any]: # type: ignore[misc]
|
def response_object(self) -> Dict[str, Any]: # type: ignore[misc]
|
||||||
response_object = self.gen_response_object()
|
response_object = self.gen_response_object()
|
||||||
response_object["taskArn"] = self.task_arn
|
response_object["taskArn"] = self.task_arn
|
||||||
|
response_object["lastStatus"] = self.last_status
|
||||||
return response_object
|
return response_object
|
||||||
|
|
||||||
|
|
||||||
@ -929,6 +958,11 @@ class EC2ContainerServiceBackend(BaseBackend):
|
|||||||
self.services: Dict[str, Service] = {}
|
self.services: Dict[str, Service] = {}
|
||||||
self.container_instances: Dict[str, Dict[str, ContainerInstance]] = {}
|
self.container_instances: Dict[str, Dict[str, ContainerInstance]] = {}
|
||||||
|
|
||||||
|
state_manager.register_default_transition(
|
||||||
|
model_name="ecs::task",
|
||||||
|
transition={"progression": "manual", "times": 1},
|
||||||
|
)
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def default_vpc_endpoint_service(service_region: str, zones: List[str]) -> List[Dict[str, Any]]: # type: ignore[misc]
|
def default_vpc_endpoint_service(service_region: str, zones: List[str]) -> List[Dict[str, Any]]: # type: ignore[misc]
|
||||||
"""Default VPC endpoint service."""
|
"""Default VPC endpoint service."""
|
||||||
@ -1438,6 +1472,7 @@ class EC2ContainerServiceBackend(BaseBackend):
|
|||||||
or task.task_arn in tasks
|
or task.task_arn in tasks
|
||||||
or any(task_id in task for task in tasks)
|
or any(task_id in task for task in tasks)
|
||||||
):
|
):
|
||||||
|
task.advance()
|
||||||
response.append(task)
|
response.append(task)
|
||||||
if "TAGS" in (include or []):
|
if "TAGS" in (include or []):
|
||||||
return response
|
return response
|
||||||
|
@ -11,6 +11,7 @@ from unittest import mock, SkipTest
|
|||||||
from uuid import UUID
|
from uuid import UUID
|
||||||
|
|
||||||
from moto import mock_ecs, mock_ec2, settings
|
from moto import mock_ecs, mock_ec2, settings
|
||||||
|
from moto.moto_api import state_manager
|
||||||
from tests import EXAMPLE_AMI_ID
|
from tests import EXAMPLE_AMI_ID
|
||||||
|
|
||||||
|
|
||||||
@ -1845,6 +1846,140 @@ def test_run_task():
|
|||||||
assert task["tags"][0].get("value") == "tagValue0"
|
assert task["tags"][0].get("value") == "tagValue0"
|
||||||
|
|
||||||
|
|
||||||
|
@mock_ec2
|
||||||
|
@mock_ecs
|
||||||
|
def test_wait_tasks_stopped():
|
||||||
|
if settings.TEST_SERVER_MODE:
|
||||||
|
raise SkipTest("Can't set transition directly in ServerMode")
|
||||||
|
|
||||||
|
state_manager.set_transition(
|
||||||
|
model_name="ecs::task",
|
||||||
|
transition={"progression": "immediate"},
|
||||||
|
)
|
||||||
|
|
||||||
|
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)
|
||||||
|
|
||||||
|
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 = 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,
|
||||||
|
"essential": True,
|
||||||
|
"environment": [
|
||||||
|
{"name": "AWS_ACCESS_KEY_ID", "value": "SOME_ACCESS_KEY"}
|
||||||
|
],
|
||||||
|
"logConfiguration": {"logDriver": "json-file"},
|
||||||
|
}
|
||||||
|
],
|
||||||
|
)
|
||||||
|
response = client.run_task(
|
||||||
|
cluster="test_ecs_cluster",
|
||||||
|
overrides={},
|
||||||
|
taskDefinition="test_ecs_task",
|
||||||
|
startedBy="moto",
|
||||||
|
)
|
||||||
|
task_arn = response["tasks"][0]["taskArn"]
|
||||||
|
|
||||||
|
assert len(response["tasks"]) == 1
|
||||||
|
client.get_waiter("tasks_stopped").wait(
|
||||||
|
cluster="test_ecs_cluster",
|
||||||
|
tasks=[task_arn],
|
||||||
|
)
|
||||||
|
|
||||||
|
response = client.describe_tasks(cluster="test_ecs_cluster", tasks=[task_arn])
|
||||||
|
assert response["tasks"][0]["lastStatus"] == "STOPPED"
|
||||||
|
|
||||||
|
state_manager.unset_transition("ecs::task")
|
||||||
|
|
||||||
|
|
||||||
|
@mock_ec2
|
||||||
|
@mock_ecs
|
||||||
|
def test_task_state_transitions():
|
||||||
|
if settings.TEST_SERVER_MODE:
|
||||||
|
raise SkipTest("Can't set transition directly in ServerMode")
|
||||||
|
|
||||||
|
state_manager.set_transition(
|
||||||
|
model_name="ecs::task",
|
||||||
|
transition={"progression": "manual", "times": 1},
|
||||||
|
)
|
||||||
|
|
||||||
|
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)
|
||||||
|
|
||||||
|
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 = 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,
|
||||||
|
"essential": True,
|
||||||
|
"environment": [
|
||||||
|
{"name": "AWS_ACCESS_KEY_ID", "value": "SOME_ACCESS_KEY"}
|
||||||
|
],
|
||||||
|
"logConfiguration": {"logDriver": "json-file"},
|
||||||
|
}
|
||||||
|
],
|
||||||
|
)
|
||||||
|
|
||||||
|
response = client.run_task(
|
||||||
|
cluster="test_ecs_cluster",
|
||||||
|
overrides={},
|
||||||
|
taskDefinition="test_ecs_task",
|
||||||
|
startedBy="moto",
|
||||||
|
)
|
||||||
|
task_arn = response["tasks"][0]["taskArn"]
|
||||||
|
assert len(response["tasks"]) == 1
|
||||||
|
|
||||||
|
task_status = response["tasks"][0]["lastStatus"]
|
||||||
|
assert task_status == "RUNNING"
|
||||||
|
|
||||||
|
for status in ("DEACTIVATING", "STOPPING", "DEPROVISIONING", "STOPPED"):
|
||||||
|
response = client.describe_tasks(cluster="test_ecs_cluster", tasks=[task_arn])
|
||||||
|
assert response["tasks"][0]["lastStatus"] == status
|
||||||
|
|
||||||
|
state_manager.unset_transition("ecs::task")
|
||||||
|
|
||||||
|
|
||||||
@mock_ec2
|
@mock_ec2
|
||||||
@mock_ecs
|
@mock_ecs
|
||||||
def test_run_task_awsvpc_network():
|
def test_run_task_awsvpc_network():
|
||||||
|
Loading…
Reference in New Issue
Block a user