import re import boto3 import pytest from moto import mock_aws from moto.core import DEFAULT_ACCOUNT_ID as ACCOUNT_ID DEFAULT_REGION = "us-east-1" DEFAULT_ECS_CLUSTER = "default" DEFAULT_ECS_TASK = "test_ecs_task" DEFAULT_ECS_SERVICE = "sample-webapp" DEFAULT_SERVICE_NAMESPACE = "ecs" DEFAULT_RESOURCE_ID = f"service/{DEFAULT_ECS_CLUSTER}/{DEFAULT_ECS_SERVICE}" DEFAULT_SCALABLE_DIMENSION = "ecs:service:DesiredCount" DEFAULT_MIN_CAPACITY = 1 DEFAULT_MAX_CAPACITY = 1 DEFAULT_ROLE_ARN = "test:arn" DEFAULT_SUSPENDED_STATE = { "DynamicScalingInSuspended": True, "DynamicScalingOutSuspended": True, "ScheduledScalingSuspended": True, } def _create_ecs_defaults(ecs, create_service=True): _ = ecs.create_cluster(clusterName=DEFAULT_ECS_CLUSTER) _ = ecs.register_task_definition( family=DEFAULT_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"}, } ], ) if create_service: _ = ecs.create_service( cluster=DEFAULT_ECS_CLUSTER, serviceName=DEFAULT_ECS_SERVICE, taskDefinition=DEFAULT_ECS_TASK, desiredCount=2, ) @mock_aws def test_describe_scalable_targets_one_basic_ecs_success(): ecs = boto3.client("ecs", region_name=DEFAULT_REGION) _create_ecs_defaults(ecs) client = boto3.client("application-autoscaling", region_name=DEFAULT_REGION) client.register_scalable_target( ServiceNamespace=DEFAULT_SERVICE_NAMESPACE, ResourceId=DEFAULT_RESOURCE_ID, ScalableDimension=DEFAULT_SCALABLE_DIMENSION, ) response = client.describe_scalable_targets( ServiceNamespace=DEFAULT_SERVICE_NAMESPACE ) assert response["ResponseMetadata"]["HTTPStatusCode"] == 200 assert len(response["ScalableTargets"]) == 1 t = response["ScalableTargets"][0] assert t["ServiceNamespace"] == DEFAULT_SERVICE_NAMESPACE assert t["ResourceId"] == DEFAULT_RESOURCE_ID assert t["ScalableDimension"] == DEFAULT_SCALABLE_DIMENSION assert "CreationTime" in t @mock_aws def test_describe_scalable_targets_one_full_ecs_success(): ecs = boto3.client("ecs", region_name=DEFAULT_REGION) _create_ecs_defaults(ecs) client = boto3.client("application-autoscaling", region_name=DEFAULT_REGION) register_scalable_target(client) response = client.describe_scalable_targets( ServiceNamespace=DEFAULT_SERVICE_NAMESPACE ) assert response["ResponseMetadata"]["HTTPStatusCode"] == 200 assert len(response["ScalableTargets"]) == 1 t = response["ScalableTargets"][0] assert t["ServiceNamespace"] == DEFAULT_SERVICE_NAMESPACE assert t["ResourceId"] == DEFAULT_RESOURCE_ID assert t["ScalableDimension"] == DEFAULT_SCALABLE_DIMENSION assert t["MinCapacity"] == DEFAULT_MIN_CAPACITY assert t["MaxCapacity"] == DEFAULT_MAX_CAPACITY assert t["RoleARN"] == DEFAULT_ROLE_ARN assert "CreationTime" in t assert ( t["SuspendedState"]["DynamicScalingInSuspended"] == DEFAULT_SUSPENDED_STATE["DynamicScalingInSuspended"] ) @mock_aws def test_describe_scalable_targets_only_return_ecs_targets(): ecs = boto3.client("ecs", region_name=DEFAULT_REGION) _create_ecs_defaults(ecs, create_service=False) _ = ecs.create_service( cluster=DEFAULT_ECS_CLUSTER, serviceName="test1", taskDefinition=DEFAULT_ECS_TASK, desiredCount=2, ) _ = ecs.create_service( cluster=DEFAULT_ECS_CLUSTER, serviceName="test2", taskDefinition=DEFAULT_ECS_TASK, desiredCount=2, ) client = boto3.client("application-autoscaling", region_name=DEFAULT_REGION) register_scalable_target( client, ServiceNamespace="ecs", ResourceId=f"service/{DEFAULT_ECS_CLUSTER}/test1", ) register_scalable_target( client, ServiceNamespace="ecs", ResourceId=f"service/{DEFAULT_ECS_CLUSTER}/test2", ) register_scalable_target( client, ServiceNamespace="elasticmapreduce", ResourceId="instancegroup/j-2EEZNYKUA1NTV/ig-1791Y4E1L8YI0", ScalableDimension="elasticmapreduce:instancegroup:InstanceCount", ) response = client.describe_scalable_targets( ServiceNamespace=DEFAULT_SERVICE_NAMESPACE ) assert response["ResponseMetadata"]["HTTPStatusCode"] == 200 assert len(response["ScalableTargets"]) == 2 @mock_aws def test_describe_scalable_targets_next_token_success(): ecs = boto3.client("ecs", region_name=DEFAULT_REGION) _create_ecs_defaults(ecs, create_service=False) client = boto3.client("application-autoscaling", region_name=DEFAULT_REGION) for i in range(0, 100): _ = ecs.create_service( cluster=DEFAULT_ECS_CLUSTER, serviceName=str(i), taskDefinition=DEFAULT_ECS_TASK, desiredCount=2, ) register_scalable_target( client, ServiceNamespace="ecs", ResourceId=f"service/{DEFAULT_ECS_CLUSTER}/{i}", ) response = client.describe_scalable_targets( ServiceNamespace=DEFAULT_SERVICE_NAMESPACE ) assert response["ResponseMetadata"]["HTTPStatusCode"] == 200 assert len(response["ScalableTargets"]) == 50 assert response["ScalableTargets"][0]["ResourceId"] == "service/default/0" assert response["NextToken"] == "49" response = client.describe_scalable_targets( ServiceNamespace=DEFAULT_SERVICE_NAMESPACE, NextToken=str(response["NextToken"]) ) assert response["ResponseMetadata"]["HTTPStatusCode"] == 200 assert len(response["ScalableTargets"]) == 50 assert response["ScalableTargets"][0]["ResourceId"] == "service/default/50" assert "NextToken" not in response def register_scalable_target(client, **kwargs): """Build a default scalable target object for use in tests.""" return client.register_scalable_target( ServiceNamespace=kwargs.get("ServiceNamespace", DEFAULT_SERVICE_NAMESPACE), ResourceId=kwargs.get("ResourceId", DEFAULT_RESOURCE_ID), ScalableDimension=kwargs.get("ScalableDimension", DEFAULT_SCALABLE_DIMENSION), MinCapacity=kwargs.get("MinCapacity", DEFAULT_MIN_CAPACITY), MaxCapacity=kwargs.get("MaxCapacity", DEFAULT_MAX_CAPACITY), RoleARN=kwargs.get("RoleARN", DEFAULT_ROLE_ARN), SuspendedState=kwargs.get("SuspendedState", DEFAULT_SUSPENDED_STATE), ) @mock_aws def test_register_scalable_target_resource_id_variations(): # Required to register an ECS target in moto ecs = boto3.client("ecs", region_name=DEFAULT_REGION) _create_ecs_defaults(ecs) # See https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-resource-applicationautoscaling-scalabletarget.html resource_id_variations = [ ( DEFAULT_SERVICE_NAMESPACE, DEFAULT_RESOURCE_ID, DEFAULT_SCALABLE_DIMENSION, ), # ECS ( "ec2", "spot-fleet-request/sfr-73fbd2ce-aa30-494c-8788-1cee4EXAMPLE", "ec2:spot-fleet-request:TargetCapacity", ), ( "elasticmapreduce", "instancegroup/j-2EEZNYKUA1NTV/ig-1791Y4E1L8YI0", "elasticmapreduce:instancegroup:InstanceCount", ), ("appstream", "fleet/sample-fleet", "appstream:fleet:DesiredCapacity"), ("dynamodb", "table/my-table", "dynamodb:table:ReadCapacityUnits"), ( "dynamodb", "table/my-table/index/my-table-index", "dynamodb:index:ReadCapacityUnits", ), ("rds", "cluster:my-db-cluster", "rds:cluster:ReadReplicaCount"), ( "sagemaker", "endpoint/MyEndPoint/variant/MyVariant", "sagemaker:variant:DesiredInstanceCount", ), ( "comprehend", "arn:aws:comprehend:us-west-2:123456789012:document-classifier-endpoint/EXAMPLE", "comprehend:document-classifier-endpoint:DesiredInferenceUnits", ), ( "lambda", "function:my-function:prod", "lambda:function:ProvisionedConcurrency", ), ( "cassandra", "keyspace/mykeyspace/table/mytable", "cassandra:table:ReadCapacityUnits", ), ( "custom-resource", "https://test-endpoint.amazon.com/ScalableDimension/test-resource", "custom-resource:ResourceType:Property", ), ] client = boto3.client("application-autoscaling", region_name=DEFAULT_REGION) for namespace, resource_id, scalable_dimension in resource_id_variations: client.register_scalable_target( ServiceNamespace=namespace, ResourceId=resource_id, ScalableDimension=scalable_dimension, MinCapacity=1, MaxCapacity=8, ) response = client.describe_scalable_targets(ServiceNamespace=namespace) assert response["ResponseMetadata"]["HTTPStatusCode"] == 200 num_targets = 2 if namespace == "dynamodb" and "index" in resource_id else 1 assert len(response["ScalableTargets"]) == num_targets t = response["ScalableTargets"][-1] assert t["ServiceNamespace"] == namespace assert t["ResourceId"] == resource_id assert t["ScalableDimension"] == scalable_dimension assert "CreationTime" in t @mock_aws def test_register_scalable_target_updates_existing_target(): ecs = boto3.client("ecs", region_name=DEFAULT_REGION) _create_ecs_defaults(ecs) client = boto3.client("application-autoscaling", region_name=DEFAULT_REGION) register_scalable_target(client) updated_min_capacity = 3 updated_max_capacity = 10 updated_suspended_state = { "DynamicScalingInSuspended": False, "DynamicScalingOutSuspended": False, "ScheduledScalingSuspended": False, } client.register_scalable_target( ServiceNamespace=DEFAULT_SERVICE_NAMESPACE, ResourceId=DEFAULT_RESOURCE_ID, ScalableDimension=DEFAULT_SCALABLE_DIMENSION, MinCapacity=updated_min_capacity, MaxCapacity=updated_max_capacity, SuspendedState=updated_suspended_state, ) response = client.describe_scalable_targets( ServiceNamespace=DEFAULT_SERVICE_NAMESPACE ) assert len(response["ScalableTargets"]) == 1 t = response["ScalableTargets"][0] assert t["MinCapacity"] == updated_min_capacity assert t["MaxCapacity"] == updated_max_capacity assert ( t["SuspendedState"]["DynamicScalingInSuspended"] == updated_suspended_state["DynamicScalingInSuspended"] ) assert ( t["SuspendedState"]["DynamicScalingOutSuspended"] == updated_suspended_state["DynamicScalingOutSuspended"] ) assert ( t["SuspendedState"]["ScheduledScalingSuspended"] == updated_suspended_state["ScheduledScalingSuspended"] ) @pytest.mark.parametrize( ["policy_type", "policy_body_kwargs"], [ [ "TargetTrackingScaling", { "TargetTrackingScalingPolicyConfiguration": { "TargetValue": 70.0, "PredefinedMetricSpecification": { "PredefinedMetricType": "SageMakerVariantInvocationsPerInstance" }, } }, ], [ "TargetTrackingScaling", { "StepScalingPolicyConfiguration": { "AdjustmentType": "ChangeCapacity", "StepAdjustments": [{"ScalingAdjustment": 10}], "MinAdjustmentMagnitude": 2, }, }, ], ], ) @mock_aws def test_put_scaling_policy(policy_type, policy_body_kwargs): client = boto3.client("application-autoscaling", region_name=DEFAULT_REGION) namespace = "sagemaker" resource_id = "endpoint/MyEndPoint/variant/MyVariant" scalable_dimension = "sagemaker:variant:DesiredInstanceCount" client.register_scalable_target( ServiceNamespace=namespace, ResourceId=resource_id, ScalableDimension=scalable_dimension, MinCapacity=1, MaxCapacity=8, ) policy_name = "MyPolicy" with pytest.raises(client.exceptions.ValidationException) as e: client.put_scaling_policy( PolicyName=policy_name, ServiceNamespace=namespace, ResourceId=resource_id, ScalableDimension=scalable_dimension, PolicyType="ABCDEFG", **policy_body_kwargs, ) assert ( e.value.response["Error"]["Message"] == "Unknown policy type ABCDEFG specified." ) response = client.put_scaling_policy( PolicyName=policy_name, ServiceNamespace=namespace, ResourceId=resource_id, ScalableDimension=scalable_dimension, PolicyType=policy_type, **policy_body_kwargs, ) assert response["ResponseMetadata"]["HTTPStatusCode"] == 200 assert ( re.match( pattern=rf"arn:aws:autoscaling:{DEFAULT_REGION}:{ACCOUNT_ID}:scalingPolicy:.*:resource/{namespace}/{resource_id}:policyName/{policy_name}", string=response["PolicyARN"], ) is not None ) @mock_aws def test_describe_scaling_policies(): client = boto3.client("application-autoscaling", region_name=DEFAULT_REGION) namespace = "sagemaker" resource_id = "endpoint/MyEndPoint/variant/MyVariant" scalable_dimension = "sagemaker:variant:DesiredInstanceCount" client.register_scalable_target( ServiceNamespace=namespace, ResourceId=resource_id, ScalableDimension=scalable_dimension, MinCapacity=1, MaxCapacity=8, ) policy_name = "MyPolicy" policy_type = "TargetTrackingScaling" policy_body = { "TargetValue": 70.0, "PredefinedMetricSpecification": { "PredefinedMetricType": "SageMakerVariantInvocationsPerInstance" }, } response = client.put_scaling_policy( PolicyName=policy_name, ServiceNamespace=namespace, ResourceId=resource_id, ScalableDimension=scalable_dimension, PolicyType=policy_type, TargetTrackingScalingPolicyConfiguration=policy_body, ) assert response["ResponseMetadata"]["HTTPStatusCode"] == 200 response = client.describe_scaling_policies( PolicyNames=[policy_name], ServiceNamespace=namespace, ResourceId=resource_id, ScalableDimension=scalable_dimension, ) assert response["ResponseMetadata"]["HTTPStatusCode"] == 200 policy = response["ScalingPolicies"][0] assert policy["PolicyName"] == policy_name assert policy["ServiceNamespace"] == namespace assert policy["ResourceId"] == resource_id assert policy["ScalableDimension"] == scalable_dimension assert policy["PolicyType"] == policy_type assert policy["TargetTrackingScalingPolicyConfiguration"] == policy_body assert ( re.match( pattern=rf"arn:aws:autoscaling:{DEFAULT_REGION}:{ACCOUNT_ID}:scalingPolicy:.*:resource/{namespace}/{resource_id}:policyName/{policy_name}", string=policy["PolicyARN"], ) is not None ) assert "CreationTime" in policy @mock_aws def test_delete_scaling_policies(): client = boto3.client("application-autoscaling", region_name=DEFAULT_REGION) namespace = "sagemaker" resource_id = "endpoint/MyEndPoint/variant/MyVariant" scalable_dimension = "sagemaker:variant:DesiredInstanceCount" client.register_scalable_target( ServiceNamespace=namespace, ResourceId=resource_id, ScalableDimension=scalable_dimension, MinCapacity=1, MaxCapacity=8, ) policy_name = "MyPolicy" policy_type = "TargetTrackingScaling" policy_body = { "TargetValue": 70.0, "PredefinedMetricSpecification": { "PredefinedMetricType": "SageMakerVariantInvocationsPerInstance" }, } with pytest.raises(client.exceptions.ValidationException) as e: client.delete_scaling_policy( PolicyName=policy_name, ServiceNamespace=namespace, ResourceId=resource_id, ScalableDimension=scalable_dimension, ) assert "No scaling policy found" in e.value.response["Error"]["Message"] response = client.put_scaling_policy( PolicyName=policy_name, ServiceNamespace=namespace, ResourceId=resource_id, ScalableDimension=scalable_dimension, PolicyType=policy_type, TargetTrackingScalingPolicyConfiguration=policy_body, ) assert response["ResponseMetadata"]["HTTPStatusCode"] == 200 response = client.delete_scaling_policy( PolicyName=policy_name, ServiceNamespace=namespace, ResourceId=resource_id, ScalableDimension=scalable_dimension, ) assert response["ResponseMetadata"]["HTTPStatusCode"] == 200 response = client.describe_scaling_policies( PolicyNames=[policy_name], ServiceNamespace=namespace, ResourceId=resource_id, ScalableDimension=scalable_dimension, ) assert response["ResponseMetadata"]["HTTPStatusCode"] == 200 assert len(response["ScalingPolicies"]) == 0 @mock_aws def test_deregister_scalable_target(): client = boto3.client("application-autoscaling", region_name=DEFAULT_REGION) namespace = "sagemaker" resource_id = "endpoint/MyEndPoint/variant/MyVariant" scalable_dimension = "sagemaker:variant:DesiredInstanceCount" client.register_scalable_target( ServiceNamespace=namespace, ResourceId=resource_id, ScalableDimension=scalable_dimension, MinCapacity=1, MaxCapacity=8, ) response = client.describe_scalable_targets(ServiceNamespace=namespace) assert len(response["ScalableTargets"]) == 1 client.deregister_scalable_target( ServiceNamespace=namespace, ResourceId=resource_id, ScalableDimension=scalable_dimension, ) response = client.describe_scalable_targets(ServiceNamespace=namespace) assert len(response["ScalableTargets"]) == 0 with pytest.raises(client.exceptions.ValidationException) as e: client.deregister_scalable_target( ServiceNamespace=namespace, ResourceId=resource_id, ScalableDimension=scalable_dimension, ) assert "No scalable target found" in e.value.response["Error"]["Message"] @mock_aws def test_delete_scheduled_action(): client = boto3.client("application-autoscaling", region_name="eu-west-1") resp = client.describe_scheduled_actions(ServiceNamespace="ecs") assert len(resp["ScheduledActions"]) == 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") assert len(resp["ScheduledActions"]) == 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") assert len(resp["ScheduledActions"]) == 2 @mock_aws def test_describe_scheduled_actions(): client = boto3.client("application-autoscaling", region_name="eu-west-1") resp = client.describe_scheduled_actions(ServiceNamespace="ecs") assert len(resp["ScheduledActions"]) == 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") assert len(resp["ScheduledActions"]) == 3 resp = client.describe_scheduled_actions(ServiceNamespace="ec2") assert len(resp["ScheduledActions"]) == 0 resp = client.describe_scheduled_actions( ServiceNamespace="dynamodb", ScheduledActionNames=["ddb_action_0"] ) assert len(resp["ScheduledActions"]) == 1 resp = client.describe_scheduled_actions( ServiceNamespace="dynamodb", ResourceId="table/table_0", ScalableDimension="dynamodb:table:ReadCapacityUnits", ) assert len(resp["ScheduledActions"]) == 1 resp = client.describe_scheduled_actions( ServiceNamespace="dynamodb", ResourceId="table/table_0", ScalableDimension="dynamodb:table:WriteCapacityUnits", ) assert len(resp["ScheduledActions"]) == 0 @mock_aws 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") assert len(resp["ScheduledActions"]) == 1 action = resp["ScheduledActions"][0] assert action["ScheduledActionName"] == "action_name" assert ( action["ScheduledActionARN"] == f"arn:aws:autoscaling:ap-southeast-1:{ACCOUNT_ID}:scheduledAction:ecs:scheduledActionName/action_name" ) assert action["ServiceNamespace"] == "ecs" assert action["ResourceId"] == "ecs:cluster:x" assert action["ScalableDimension"] == "ecs:service:DesiredCount" assert "CreationTime" in action assert "ScalableTargetAction" not in action @mock_aws 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") assert len(resp["ScheduledActions"]) == 1 action = resp["ScheduledActions"][0] assert action["ScheduledActionName"] == "action_name_updated" assert ( action["ScheduledActionARN"] == f"arn:aws:autoscaling:ap-southeast-1:{ACCOUNT_ID}:scheduledAction:ecs:scheduledActionName/action_name" ) assert action["ServiceNamespace"] == "ecs" assert action["ResourceId"] == "ecs:cluster:x" assert action["ScalableDimension"] == "ecs:service:DesiredCount" assert "CreationTime" in action assert action["ScalableTargetAction"] == {"MaxCapacity": 23, "MinCapacity": 12}