From da4de072a9a12818a2a9d869a98d4fd7caf10744 Mon Sep 17 00:00:00 2001 From: jweite Date: Sat, 19 Sep 2020 15:13:44 -0400 Subject: [PATCH] ApplicationAutoscaling: support for all the current various forms of resource_id (#3305) * Change to test_s3 method test_presigned_url_restrict_parameters to tolerate change in exception messages, spurred by boto3 1.14.59 release. * ApplicationAutoscaling: support for all the current various forms of resource_id. * Factored logic for extracting application autoscaling resource_type from resource_id to separate function, per PR3304 comment. Co-authored-by: Joseph Weitekamp --- moto/applicationautoscaling/models.py | 29 +++++++- .../test_applicationautoscaling.py | 73 +++++++++++++++++++ 2 files changed, 101 insertions(+), 1 deletion(-) diff --git a/moto/applicationautoscaling/models.py b/moto/applicationautoscaling/models.py index 39bb497aa..a6303c75c 100644 --- a/moto/applicationautoscaling/models.py +++ b/moto/applicationautoscaling/models.py @@ -135,7 +135,7 @@ def _target_params_are_valid(namespace, r_id, dimension): try: valid_dimensions = [d.value for d in ScalableDimensionValueSet] d_namespace, d_resource_type, scaling_property = dimension.split(":") - resource_type, cluster, service = r_id.split("/") + resource_type = _get_resource_type_from_resource_id(r_id) if ( dimension not in valid_dimensions or d_namespace != namespace @@ -151,6 +151,33 @@ def _target_params_are_valid(namespace, r_id, dimension): return is_valid +def _get_resource_type_from_resource_id(resource_id): + # AWS Application Autoscaling resource_ids are multi-component (path-like) identifiers that vary in format, + # depending on the type of resource it identifies. resource_type is one of its components. + # resource_id format variations are described in + # https://docs.aws.amazon.com/autoscaling/application/APIReference/API_RegisterScalableTarget.html + # In a nutshell: + # - Most use slash separators, but some use colon separators. + # - The resource type is usually the first component of the resource_id... + # - ...except for sagemaker endpoints, dynamodb GSIs and keyspaces tables, where it's the third. + # - Comprehend uses an arn, with the resource type being the last element. + + if resource_id.startswith("arn:aws:comprehend"): + resource_id = resource_id.split(":")[-1] + resource_split = ( + resource_id.split("/") if "/" in resource_id else resource_id.split(":") + ) + if ( + resource_split[0] == "endpoint" + or (resource_split[0] == "table" and len(resource_split) > 2) + or (resource_split[0] == "keyspace") + ): + resource_type = resource_split[2] + else: + resource_type = resource_split[0] + return resource_type + + class FakeScalableTarget(BaseModel): def __init__( self, backend, service_namespace, resource_id, scalable_dimension, **kwargs diff --git a/tests/test_applicationautoscaling/test_applicationautoscaling.py b/tests/test_applicationautoscaling/test_applicationautoscaling.py index 632804992..8e5e136e5 100644 --- a/tests/test_applicationautoscaling/test_applicationautoscaling.py +++ b/tests/test_applicationautoscaling/test_applicationautoscaling.py @@ -187,3 +187,76 @@ def register_scalable_target(client, **kwargs): RoleARN=kwargs.get("RoleARN", DEFAULT_ROLE_ARN), SuspendedState=kwargs.get("SuspendedState", DEFAULT_SUSPENDED_STATE), ) + + +@mock_ecs +@mock_applicationautoscaling +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", + ), + ] + + 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, + ) + response = client.describe_scalable_targets(ServiceNamespace=namespace) + response["ResponseMetadata"]["HTTPStatusCode"].should.equal(200) + num_targets = 2 if namespace == "dynamodb" and "index" in resource_id else 1 + len(response["ScalableTargets"]).should.equal(num_targets) + t = response["ScalableTargets"][-1] + t.should.have.key("ServiceNamespace").which.should.equal(namespace) + t.should.have.key("ResourceId").which.should.equal(resource_id) + t.should.have.key("ScalableDimension").which.should.equal(scalable_dimension) + t.should.have.key("CreationTime").which.should.be.a("datetime.datetime")