From a0eb48d588d8a5b18b1ffd9fa6c48615505e3c1d Mon Sep 17 00:00:00 2001 From: Bert Blommers Date: Fri, 8 Apr 2022 10:08:28 +0000 Subject: [PATCH] ApplicationAutoscaling - Scheduled Actions (#5011) --- IMPLEMENTATION_COVERAGE.md | 8 +- .../docs/services/application-autoscaling.rst | 10 +- moto/applicationautoscaling/models.py | 131 +++++++++++++++- moto/applicationautoscaling/responses.py | 74 +++++++++ .../test_applicationautoscaling.py | 141 +++++++++++++++++- 5 files changed, 354 insertions(+), 10 deletions(-) diff --git a/IMPLEMENTATION_COVERAGE.md b/IMPLEMENTATION_COVERAGE.md index c9baf27fd..a09b206e9 100644 --- a/IMPLEMENTATION_COVERAGE.md +++ b/IMPLEMENTATION_COVERAGE.md @@ -226,17 +226,17 @@ ## application-autoscaling
-60% implemented +90% implemented - [X] delete_scaling_policy -- [ ] delete_scheduled_action +- [X] delete_scheduled_action - [X] deregister_scalable_target - [X] describe_scalable_targets - [ ] describe_scaling_activities - [X] describe_scaling_policies -- [ ] describe_scheduled_actions +- [X] describe_scheduled_actions - [X] put_scaling_policy -- [ ] put_scheduled_action +- [X] put_scheduled_action - [X] register_scalable_target
diff --git a/docs/docs/services/application-autoscaling.rst b/docs/docs/services/application-autoscaling.rst index ea8327e5a..a397336ea 100644 --- a/docs/docs/services/application-autoscaling.rst +++ b/docs/docs/services/application-autoscaling.rst @@ -26,7 +26,7 @@ application-autoscaling |start-h3| Implemented features for this service |end-h3| - [X] delete_scaling_policy -- [ ] delete_scheduled_action +- [X] delete_scheduled_action - [X] deregister_scalable_target Registers or updates a scalable target. @@ -35,9 +35,13 @@ application-autoscaling - [ ] describe_scaling_activities - [X] describe_scaling_policies -- [ ] describe_scheduled_actions +- [X] describe_scheduled_actions + + Pagination is not yet implemented + + - [X] put_scaling_policy -- [ ] put_scheduled_action +- [X] put_scheduled_action - [X] register_scalable_target Registers or updates a scalable target. diff --git a/moto/applicationautoscaling/models.py b/moto/applicationautoscaling/models.py index ca4d8cc2f..639756a12 100644 --- a/moto/applicationautoscaling/models.py +++ b/moto/applicationautoscaling/models.py @@ -1,4 +1,4 @@ -from moto.core import BaseBackend, BaseModel +from moto.core import ACCOUNT_ID, BaseBackend, BaseModel from moto.core.utils import BackendDict from moto.ecs import ecs_backends from .exceptions import AWSValidationException @@ -65,6 +65,7 @@ class ApplicationAutoscalingBackend(BaseBackend): self.ecs_backend = ecs_backends[region] self.targets = OrderedDict() self.policies = {} + self.scheduled_actions = list() def reset(self): region = self.region @@ -229,6 +230,87 @@ class ApplicationAutoscalingBackend(BaseBackend): ) ) + def delete_scheduled_action( + self, service_namespace, scheduled_action_name, resource_id, scalable_dimension + ): + self.scheduled_actions = [ + a + for a in self.scheduled_actions + if not ( + a.service_namespace == service_namespace + and a.scheduled_action_name == scheduled_action_name + and a.resource_id == resource_id + and a.scalable_dimension == scalable_dimension + ) + ] + + def describe_scheduled_actions( + self, scheduled_action_names, service_namespace, resource_id, scalable_dimension + ): + """ + Pagination is not yet implemented + """ + result = [ + a + for a in self.scheduled_actions + if a.service_namespace == service_namespace + ] + if scheduled_action_names: + result = [ + a for a in result if a.scheduled_action_name in scheduled_action_names + ] + if resource_id: + result = [a for a in result if a.resource_id == resource_id] + if scalable_dimension: + result = [a for a in result if a.scalable_dimension == scalable_dimension] + return result + + def put_scheduled_action( + self, + service_namespace, + schedule, + timezone, + scheduled_action_name, + resource_id, + scalable_dimension, + start_time, + end_time, + scalable_target_action, + ): + existing_action = next( + ( + a + for a in self.scheduled_actions + if a.service_namespace == service_namespace + and a.resource_id == resource_id + and a.scalable_dimension == scalable_dimension + ), + None, + ) + if existing_action: + existing_action.update( + schedule, + timezone, + scheduled_action_name, + start_time, + end_time, + scalable_target_action, + ) + else: + action = FakeScheduledAction( + service_namespace, + schedule, + timezone, + scheduled_action_name, + resource_id, + scalable_dimension, + start_time, + end_time, + scalable_target_action, + self.region, + ) + self.scheduled_actions.append(action) + def _target_params_are_valid(namespace, r_id, dimension): """Check whether namespace, resource_id and dimension are valid and consistent with each other.""" @@ -354,4 +436,51 @@ class FakeApplicationAutoscalingPolicy(BaseModel): ) +class FakeScheduledAction(BaseModel): + def __init__( + self, + service_namespace, + schedule, + timezone, + scheduled_action_name, + resource_id, + scalable_dimension, + start_time, + end_time, + scalable_target_action, + region, + ): + self.arn = f"arn:aws:autoscaling:{region}:{ACCOUNT_ID}:scheduledAction:{service_namespace}:scheduledActionName/{scheduled_action_name}" + self.service_namespace = service_namespace + self.schedule = schedule + self.timezone = timezone + self.scheduled_action_name = scheduled_action_name + self.resource_id = resource_id + self.scalable_dimension = scalable_dimension + self.start_time = start_time + self.end_time = end_time + self.scalable_target_action = scalable_target_action + self.creation_time = time.time() + + def update( + self, + schedule, + timezone, + scheduled_action_name, + start_time, + end_time, + scalable_target_action, + ): + if scheduled_action_name: + self.scheduled_action_name = scheduled_action_name + if schedule: + self.schedule = schedule + if timezone: + self.timezone = timezone + if scalable_target_action: + self.scalable_target_action = scalable_target_action + self.start_time = start_time + self.end_time = end_time + + applicationautoscaling_backends = BackendDict(ApplicationAutoscalingBackend, "ec2") diff --git a/moto/applicationautoscaling/responses.py b/moto/applicationautoscaling/responses.py index 513d63d04..f25520647 100644 --- a/moto/applicationautoscaling/responses.py +++ b/moto/applicationautoscaling/responses.py @@ -127,6 +127,63 @@ class ApplicationAutoScalingResponse(BaseResponse): if message: raise AWSValidationException(message) + def delete_scheduled_action(self): + params = json.loads(self.body) + service_namespace = params.get("ServiceNamespace") + scheduled_action_name = params.get("ScheduledActionName") + resource_id = params.get("ResourceId") + scalable_dimension = params.get("ScalableDimension") + self.applicationautoscaling_backend.delete_scheduled_action( + service_namespace=service_namespace, + scheduled_action_name=scheduled_action_name, + resource_id=resource_id, + scalable_dimension=scalable_dimension, + ) + return json.dumps(dict()) + + def put_scheduled_action(self): + params = json.loads(self.body) + service_namespace = params.get("ServiceNamespace") + schedule = params.get("Schedule") + timezone = params.get("Timezone") + scheduled_action_name = params.get("ScheduledActionName") + resource_id = params.get("ResourceId") + scalable_dimension = params.get("ScalableDimension") + start_time = params.get("StartTime") + end_time = params.get("EndTime") + scalable_target_action = params.get("ScalableTargetAction") + self.applicationautoscaling_backend.put_scheduled_action( + service_namespace=service_namespace, + schedule=schedule, + timezone=timezone, + scheduled_action_name=scheduled_action_name, + resource_id=resource_id, + scalable_dimension=scalable_dimension, + start_time=start_time, + end_time=end_time, + scalable_target_action=scalable_target_action, + ) + return json.dumps(dict()) + + def describe_scheduled_actions(self): + params = json.loads(self.body) + scheduled_action_names = params.get("ScheduledActionNames") + service_namespace = params.get("ServiceNamespace") + resource_id = params.get("ResourceId") + scalable_dimension = params.get("ScalableDimension") + scheduled_actions = ( + self.applicationautoscaling_backend.describe_scheduled_actions( + scheduled_action_names=scheduled_action_names, + service_namespace=service_namespace, + resource_id=resource_id, + scalable_dimension=scalable_dimension, + ) + ) + response_obj = { + "ScheduledActions": [_build_scheduled_action(a) for a in scheduled_actions] + } + return json.dumps(response_obj) + def _build_target(t): return { @@ -158,3 +215,20 @@ def _build_policy(p): "TargetTrackingScalingPolicyConfiguration" ] = p.target_tracking_scaling_policy_configuration return response + + +def _build_scheduled_action(a): + response = { + "ScheduledActionName": a.scheduled_action_name, + "ScheduledActionARN": a.arn, + "ServiceNamespace": a.service_namespace, + "Schedule": a.schedule, + "Timezone": a.timezone, + "ResourceId": a.resource_id, + "ScalableDimension": a.scalable_dimension, + "StartTime": a.start_time, + "EndTime": a.end_time, + "CreationTime": a.creation_time, + "ScalableTargetAction": a.scalable_target_action, + } + return response diff --git a/tests/test_applicationautoscaling/test_applicationautoscaling.py b/tests/test_applicationautoscaling/test_applicationautoscaling.py index 0c2a98bc4..2807535fb 100644 --- a/tests/test_applicationautoscaling/test_applicationautoscaling.py +++ b/tests/test_applicationautoscaling/test_applicationautoscaling.py @@ -2,6 +2,7 @@ import boto3 import pytest import sure # noqa # pylint: disable=unused-import from moto import mock_applicationautoscaling, mock_ecs +from moto.core import ACCOUNT_ID DEFAULT_REGION = "us-east-1" DEFAULT_ECS_CLUSTER = "default" @@ -362,7 +363,7 @@ def test_put_scaling_policy(policy_type, policy_body_kwargs): ResourceId=resource_id, ScalableDimension=scalable_dimension, PolicyType="ABCDEFG", - **policy_body_kwargs + **policy_body_kwargs, ) e.value.response["Error"]["Message"].should.match( r"Unknown policy type .* specified." @@ -374,7 +375,7 @@ def test_put_scaling_policy(policy_type, policy_body_kwargs): ResourceId=resource_id, ScalableDimension=scalable_dimension, PolicyType=policy_type, - **policy_body_kwargs + **policy_body_kwargs, ) response["ResponseMetadata"]["HTTPStatusCode"].should.equal(200) response["PolicyARN"].should.match( @@ -535,3 +536,139 @@ def test_deregister_scalable_target(): ScalableDimension=scalable_dimension, ) e.value.response["Error"]["Message"].should.match(r"No scalable target found .*") + + +@mock_applicationautoscaling +def test_delete_scheduled_action(): + client = boto3.client("application-autoscaling", region_name="eu-west-1") + resp = client.describe_scheduled_actions(ServiceNamespace="ecs") + resp.should.have.key("ScheduledActions").length_of(0) + + for i in range(3): + client.put_scheduled_action( + ServiceNamespace="ecs", + ScheduledActionName=f"ecs_action_{i}", + ResourceId=f"ecs:cluster:{i}", + ScalableDimension="ecs:service:DesiredCount", + ) + + resp = client.describe_scheduled_actions(ServiceNamespace="ecs") + resp.should.have.key("ScheduledActions").length_of(3) + + client.delete_scheduled_action( + ServiceNamespace="ecs", + ScheduledActionName="ecs_action_0", + ResourceId="ecs:cluster:0", + ScalableDimension="ecs:service:DesiredCount", + ) + + resp = client.describe_scheduled_actions(ServiceNamespace="ecs") + resp.should.have.key("ScheduledActions").length_of(2) + + +@mock_applicationautoscaling +def test_describe_scheduled_actions(): + client = boto3.client("application-autoscaling", region_name="eu-west-1") + resp = client.describe_scheduled_actions(ServiceNamespace="ecs") + resp.should.have.key("ScheduledActions").length_of(0) + + for i in range(3): + client.put_scheduled_action( + ServiceNamespace="ecs", + ScheduledActionName=f"ecs_action_{i}", + ResourceId=f"ecs:cluster:{i}", + ScalableDimension="ecs:service:DesiredCount", + ) + client.put_scheduled_action( + ServiceNamespace="dynamodb", + ScheduledActionName=f"ddb_action_{i}", + ResourceId=f"table/table_{i}", + ScalableDimension="dynamodb:table:ReadCapacityUnits", + ) + + resp = client.describe_scheduled_actions(ServiceNamespace="ecs") + resp.should.have.key("ScheduledActions").length_of(3) + + resp = client.describe_scheduled_actions(ServiceNamespace="ec2") + resp.should.have.key("ScheduledActions").length_of(0) + + resp = client.describe_scheduled_actions( + ServiceNamespace="dynamodb", ScheduledActionNames=["ddb_action_0"] + ) + resp.should.have.key("ScheduledActions").length_of(1) + + resp = client.describe_scheduled_actions( + ServiceNamespace="dynamodb", + ResourceId="table/table_0", + ScalableDimension="dynamodb:table:ReadCapacityUnits", + ) + resp.should.have.key("ScheduledActions").length_of(1) + + resp = client.describe_scheduled_actions( + ServiceNamespace="dynamodb", + ResourceId="table/table_0", + ScalableDimension="dynamodb:table:WriteCapacityUnits", + ) + resp.should.have.key("ScheduledActions").length_of(0) + + +@mock_applicationautoscaling +def test_put_scheduled_action(): + client = boto3.client("application-autoscaling", region_name="ap-southeast-1") + client.put_scheduled_action( + ServiceNamespace="ecs", + ScheduledActionName="action_name", + ResourceId="ecs:cluster:x", + ScalableDimension="ecs:service:DesiredCount", + ) + + resp = client.describe_scheduled_actions(ServiceNamespace="ecs") + resp.should.have.key("ScheduledActions").length_of(1) + + action = resp["ScheduledActions"][0] + action.should.have.key("ScheduledActionName").equals("action_name") + action.should.have.key("ScheduledActionARN").equals( + f"arn:aws:autoscaling:ap-southeast-1:{ACCOUNT_ID}:scheduledAction:ecs:scheduledActionName/action_name" + ) + action.should.have.key("ServiceNamespace").equals("ecs") + action.should.have.key("ResourceId").equals("ecs:cluster:x") + action.should.have.key("ScalableDimension").equals("ecs:service:DesiredCount") + action.should.have.key("CreationTime") + action.shouldnt.have.key("ScalableTargetAction") + + +@mock_applicationautoscaling +def test_put_scheduled_action__use_update(): + client = boto3.client("application-autoscaling", region_name="ap-southeast-1") + client.put_scheduled_action( + ServiceNamespace="ecs", + ScheduledActionName="action_name", + ResourceId="ecs:cluster:x", + ScalableDimension="ecs:service:DesiredCount", + ) + client.put_scheduled_action( + ServiceNamespace="ecs", + ScheduledActionName="action_name_updated", + ResourceId="ecs:cluster:x", + ScalableDimension="ecs:service:DesiredCount", + ScalableTargetAction={ + "MinCapacity": 12, + "MaxCapacity": 23, + }, + ) + + resp = client.describe_scheduled_actions(ServiceNamespace="ecs") + resp.should.have.key("ScheduledActions").length_of(1) + + action = resp["ScheduledActions"][0] + action.should.have.key("ScheduledActionName").equals("action_name_updated") + action.should.have.key("ScheduledActionARN").equals( + f"arn:aws:autoscaling:ap-southeast-1:{ACCOUNT_ID}:scheduledAction:ecs:scheduledActionName/action_name" + ) + action.should.have.key("ServiceNamespace").equals("ecs") + action.should.have.key("ResourceId").equals("ecs:cluster:x") + action.should.have.key("ScalableDimension").equals("ecs:service:DesiredCount") + action.should.have.key("CreationTime") + action.should.have.key("ScalableTargetAction").equals( + {"MaxCapacity": 23, "MinCapacity": 12} + )