diff --git a/moto/ecs/models.py b/moto/ecs/models.py index b89bd8a7c..6e230c9b9 100644 --- a/moto/ecs/models.py +++ b/moto/ecs/models.py @@ -2,6 +2,7 @@ import re from copy import copy from datetime import datetime, timezone from typing import Any, Dict, Iterator, List, Optional, Tuple +from os import getenv from moto import settings from moto.core import BaseBackend, BackendDict, BaseModel, CloudFormationModel @@ -488,6 +489,14 @@ class CapacityProviderFailure(BaseObject): class Service(BaseObject, CloudFormationModel): + """Set the environment variable MOTO_ECS_SERVICE_RUNNING to a number of running tasks you want + the service to transition to, ie if set to 2: + + MOTO_ECS_SERVICE_RUNNING=2 + + then describe_services call to return runningCount of the service AND deployment to 2 + """ + def __init__( self, cluster: Cluster, @@ -507,7 +516,6 @@ class Service(BaseObject, CloudFormationModel): self.cluster_arn = cluster.arn self.name = service_name self.status = "ACTIVE" - self.running_count = 0 self.task_definition = task_definition.arn if task_definition else None self.desired_count = desired_count self.task_sets: List[TaskSet] = [] @@ -515,6 +523,25 @@ class Service(BaseObject, CloudFormationModel): self.events: List[Dict[str, Any]] = [] self.launch_type = launch_type self.service_registries = service_registries or [] + self.load_balancers = load_balancers if load_balancers is not None else [] + self.scheduling_strategy = ( + scheduling_strategy if scheduling_strategy is not None else "REPLICA" + ) + self.platform_version = platform_version + self.tags = tags if tags is not None else [] + self.region_name = cluster.region_name + self._account_id = backend.account_id + self._backend = backend + + try: + # negative running count not allowed, set to 0 if so + ecs_running_count = max(int(getenv("MOTO_ECS_SERVICE_RUNNING", 0)), 0) + except ValueError: + # Unable to parse value of MOTO_ECS_SERVICE_RUNNING as an integer, set to default 0 + ecs_running_count = 0 + + self.running_count = ecs_running_count + self.pending_count = desired_count - ecs_running_count if self.deployment_controller["type"] == "ECS": self.deployments = [ { @@ -522,8 +549,8 @@ class Service(BaseObject, CloudFormationModel): "desiredCount": self.desired_count, "id": f"ecs-svc/{mock_random.randint(0, 32**12)}", "launchType": self.launch_type, - "pendingCount": self.desired_count, - "runningCount": 0, + "pendingCount": self.pending_count, + "runningCount": ecs_running_count, "status": "PRIMARY", "taskDefinition": self.task_definition, "updatedAt": datetime.now(timezone.utc), @@ -531,16 +558,6 @@ class Service(BaseObject, CloudFormationModel): ] else: self.deployments = [] - self.load_balancers = load_balancers if load_balancers is not None else [] - self.scheduling_strategy = ( - scheduling_strategy if scheduling_strategy is not None else "REPLICA" - ) - self.platform_version = platform_version - self.tags = tags if tags is not None else [] - self.pending_count = 0 - self.region_name = cluster.region_name - self._account_id = backend.account_id - self._backend = backend @property def arn(self) -> str: @@ -1641,6 +1658,7 @@ class EC2ContainerServiceBackend(BaseBackend): # It is only deleted later on # https://boto3.amazonaws.com/v1/documentation/api/latest/reference/services/ecs.html#ECS.Client.delete_service service.status = "INACTIVE" + service.pending_count = 0 return service def register_container_instance( diff --git a/moto/s3/responses.py b/moto/s3/responses.py index 7bf70dfbf..c0f24fe85 100644 --- a/moto/s3/responses.py +++ b/moto/s3/responses.py @@ -2102,10 +2102,10 @@ class S3Response(BaseResponse): ps = minidom.parseString(body).getElementsByTagName("Part") prev = 0 for p in ps: - pn = int(p.getElementsByTagName("PartNumber")[0].firstChild.wholeText) + pn = int(p.getElementsByTagName("PartNumber")[0].firstChild.wholeText) # type: ignore[union-attr] if pn <= prev: raise InvalidPartOrder() - yield (pn, p.getElementsByTagName("ETag")[0].firstChild.wholeText) + yield (pn, p.getElementsByTagName("ETag")[0].firstChild.wholeText) # type: ignore[union-attr] def _key_response_post( self, diff --git a/tests/test_ecs/test_ecs_boto3.py b/tests/test_ecs/test_ecs_boto3.py index d834162f0..5ef1374f6 100644 --- a/tests/test_ecs/test_ecs_boto3.py +++ b/tests/test_ecs/test_ecs_boto3.py @@ -589,7 +589,7 @@ def test_create_service(): response["service"]["desiredCount"].should.equal(2) len(response["service"]["events"]).should.equal(0) len(response["service"]["loadBalancers"]).should.equal(0) - response["service"]["pendingCount"].should.equal(0) + response["service"]["pendingCount"].should.equal(2) response["service"]["runningCount"].should.equal(0) response["service"]["serviceArn"].should.equal( f"arn:aws:ecs:us-east-1:{ACCOUNT_ID}:service/test_ecs_cluster/test_ecs_service" @@ -604,6 +604,77 @@ def test_create_service(): response["service"]["platformVersion"].should.equal("2") +@mock_ecs +@mock_ec2 +def test_create_running_service(): + if settings.TEST_SERVER_MODE: + raise SkipTest( + "Can't set environment variables in server mode for a single test" + ) + running_service_count = 3 + with mock.patch.dict( + os.environ, {"MOTO_ECS_SERVICE_RUNNING": str(running_service_count)} + ): + client = boto3.client("ecs", region_name="us-east-1") + ec2 = boto3.resource("ec2", region_name="us-east-1") + setup_ecs(client, ec2) + + response = client.create_service( + cluster="test_ecs_cluster", + serviceName="test_ecs_service", + taskDefinition="test_ecs_task", + desiredCount=4, + platformVersion="2", + ) + + assert response["service"]["runningCount"] == running_service_count + assert response["service"]["pendingCount"] == 1 + + +@mock_ecs +@mock_ec2 +def test_create_running_service_bad_env_var(): + running_service_count = "ALSDHLHA;''" + with mock.patch.dict( + os.environ, {"MOTO_ECS_SERVICE_RUNNING": str(running_service_count)} + ): + client = boto3.client("ecs", region_name="us-east-1") + ec2 = boto3.resource("ec2", region_name="us-east-1") + setup_ecs(client, ec2) + + response = client.create_service( + cluster="test_ecs_cluster", + serviceName="test_ecs_service", + taskDefinition="test_ecs_task", + desiredCount=2, + platformVersion="2", + ) + + assert response["service"]["runningCount"] == 0 + + +@mock_ecs +@mock_ec2 +def test_create_running_service_negative_env_var(): + running_service_count = "-20" + with mock.patch.dict( + os.environ, {"MOTO_ECS_SERVICE_RUNNING": str(running_service_count)} + ): + client = boto3.client("ecs", region_name="us-east-1") + ec2 = boto3.resource("ec2", region_name="us-east-1") + setup_ecs(client, ec2) + + response = client.create_service( + cluster="test_ecs_cluster", + serviceName="test_ecs_service", + taskDefinition="test_ecs_task", + desiredCount=2, + platformVersion="2", + ) + + assert response["service"]["runningCount"] == 0 + + @mock_ecs def test_create_service_errors(): # given @@ -680,7 +751,7 @@ def test_create_service_scheduling_strategy(): response["service"]["desiredCount"].should.equal(2) len(response["service"]["events"]).should.equal(0) len(response["service"]["loadBalancers"]).should.equal(0) - response["service"]["pendingCount"].should.equal(0) + response["service"]["pendingCount"].should.equal(2) response["service"]["runningCount"].should.equal(0) response["service"]["serviceArn"].should.equal( f"arn:aws:ecs:us-east-1:{ACCOUNT_ID}:service/test_ecs_cluster/test_ecs_service" @@ -870,7 +941,9 @@ def test_describe_services(): @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") + raise SkipTest( + "Can't set environment variables in server mode for a single test" + ) client = boto3.client("ecs", region_name="us-east-1") _ = client.create_cluster(clusterName="test_ecs_cluster") _ = client.register_task_definition( @@ -1338,7 +1411,9 @@ def test_register_container_instance(): @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") + raise SkipTest( + "Can't set environment variables in server mode for a single test" + ) ecs_client = boto3.client("ecs", region_name="us-east-1") ec2 = boto3.resource("ec2", region_name="us-east-1") @@ -1876,7 +1951,9 @@ def test_run_task_default_cluster(): @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") + raise SkipTest( + "Can't set environment variables in server mode for a single test" + ) client = boto3.client("ecs", region_name="us-east-1") ec2 = boto3.resource("ec2", region_name="us-east-1") @@ -2953,7 +3030,7 @@ def test_create_service_load_balancing(): "test_container_name" ) response["service"]["loadBalancers"][0]["containerPort"].should.equal(123) - response["service"]["pendingCount"].should.equal(0) + response["service"]["pendingCount"].should.equal(2) response["service"]["runningCount"].should.equal(0) response["service"]["serviceArn"].should.equal( f"arn:aws:ecs:us-east-1:{ACCOUNT_ID}:service/test_ecs_cluster/test_ecs_service"