Repository: getmoto/moto
Pull request: fix:params not sorted properly when tags are present
Attempt to fix #6033 
This is my first time trying to contribute, so please be indulgent :)



Problem statement:
Error with autoscaling group
Hello, 

I use moto_server with command `moto_server -p4566`

When trying to create an autoscaling group with Terraform, I get the following trace error :
```
127.0.0.1 - - [08/Mar/2023 08:24:52] "POST / HTTP/1.1" 500 -
Error on request:
Traceback (most recent call last):
  File "C:\Users\Kevin Valleau\AppData\Local\Programs\Python\Python311\Lib\site-packages\werkzeug\serving.py", line 333, in run_wsgi
    execute(self.server.app)
  File "C:\Users\Kevin Valleau\AppData\Local\Programs\Python\Python311\Lib\site-packages\werkzeug\serving.py", line 320, in execute
    application_iter = app(environ, start_response)
                       ^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "C:\Users\Kevin Valleau\AppData\Local\Programs\Python\Python311\Lib\site-packages\moto\moto_server\werkzeug_app.py", line 239, in __call__
    return backend_app(environ, start_response)
           ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "C:\Users\Kevin Valleau\AppData\Local\Programs\Python\Python311\Lib\site-packages\flask\app.py", line 2551, in __call__
    return self.wsgi_app(environ, start_response)
           ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "C:\Users\Kevin Valleau\AppData\Local\Programs\Python\Python311\Lib\site-packages\flask\app.py", line 2531, in wsgi_app
    response = self.handle_exception(e)
               ^^^^^^^^^^^^^^^^^^^^^^^^
  File "C:\Users\Kevin Valleau\AppData\Local\Programs\Python\Python311\Lib\site-packages\flask_cors\extension.py", line 165, in wrapped_function
    return cors_after_request(app.make_response(f(*args, **kwargs)))
                                                ^^^^^^^^^^^^^^^^^^^^
  File "C:\Users\Kevin Valleau\AppData\Local\Programs\Python\Python311\Lib\site-packages\flask\app.py", line 2528, in wsgi_app
    response = self.full_dispatch_request()
               ^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "C:\Users\Kevin Valleau\AppData\Local\Programs\Python\Python311\Lib\site-packages\flask\app.py", line 1825, in full_dispatch_request
    rv = self.handle_user_exception(e)
         ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "C:\Users\Kevin Valleau\AppData\Local\Programs\Python\Python311\Lib\site-packages\flask_cors\extension.py", line 165, in wrapped_function
    return cors_after_request(app.make_response(f(*args, **kwargs)))
                                                ^^^^^^^^^^^^^^^^^^^^
  File "C:\Users\Kevin Valleau\AppData\Local\Programs\Python\Python311\Lib\site-packages\flask\app.py", line 1823, in full_dispatch_request
    rv = self.dispatch_request()
         ^^^^^^^^^^^^^^^^^^^^^^^
  File "C:\Users\Kevin Valleau\AppData\Local\Programs\Python\Python311\Lib\site-packages\flask\app.py", line 1799, in dispatch_request
    return self.ensure_sync(self.view_functions[rule.endpoint])(**view_args)
           ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "C:\Users\Kevin Valleau\AppData\Local\Programs\Python\Python311\Lib\site-packages\moto\core\utils.py", line 106, in __call__
    result = self.callback(request, request.url, dict(request.headers))
             ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "C:\Users\Kevin Valleau\AppData\Local\Programs\Python\Python311\Lib\site-packages\moto\core\responses.py", line 229, in dispatch
    return cls()._dispatch(*args, **kwargs)
           ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "C:\Users\Kevin Valleau\AppData\Local\Programs\Python\Python311\Lib\site-packages\moto\core\responses.py", line 370, in _dispatch
    return self.call_action()
           ^^^^^^^^^^^^^^^^^^
  File "C:\Users\Kevin Valleau\AppData\Local\Programs\Python\Python311\Lib\site-packages\moto\core\responses.py", line 459, in call_action
    response = method()
               ^^^^^^^^
  File "C:\Users\Kevin Valleau\AppData\Local\Programs\Python\Python311\Lib\site-packages\moto\autoscaling\responses.py", line 79, in create_auto_scaling_group
    params = self._get_params()
             ^^^^^^^^^^^^^^^^^^
  File "C:\Users\Kevin Valleau\AppData\Local\Programs\Python\Python311\Lib\site-packages\moto\core\responses.py", line 691, in _get_params
    self._parse_param(k, v[0], params)
  File "C:\Users\Kevin Valleau\AppData\Local\Programs\Python\Python311\Lib\site-packages\moto\core\responses.py", line 722, in _parse_param
    obj = obj[index]
          ^^^^^^^^^^
IndexError: list index out of range
```

Here the Terraform resource :

```hcl
resource "aws_autoscaling_group" "test" {
  name_prefix         = "asg-instances-"
  min_size            = var.min
  max_size            = var.max
  desired_capacity    = var.desired
  vpc_zone_identifier = var.subnets
  target_group_arns   = [aws_lb_target_group.test.arn]

  health_check_grace_period = var.health_check_grace_period
  health_check_type         = var.health_check_type

  enabled_metrics = [
    "GroupDesiredCapacity",
    "GroupInServiceCapacity",
    "GroupPendingCapacity",
    "GroupMinSize", "GroupMaxSize",
    "GroupInServiceInstances",
    "GroupPendingInstances",
    "GroupStandbyInstances",
    "GroupStandbyCapacity",
    "GroupTerminatingCapacity",
    "GroupTerminatingInstances",
    "GroupTotalCapacity",
    "GroupTotalInstances"
  ]
  metrics_granularity = "1Minute"

  launch_template {
    id      = aws_launch_template.test.id
    version = "$Latest" #TODO: Variable
  }

  instance_refresh {
    strategy = "Rolling"
    preferences {
      min_healthy_percentage = 60
    }
  }


  tag {
    key                 = "Name"
    value               = var.autoscaling_tag
    propagate_at_launch = true
  }

  dynamic "tag" {
    for_each = var.tags

    content {
      key                 = tag.key
      value               = tag.value
      propagate_at_launch = true
    }
  }
}
```

Hints from issue discussion:
Thanks for raising this and sharing the TF code @kevinvalleau - marking it as a bug.
The problem is related to the tags. When I don't provide any tags, it works.

I managed to investigate a little :
debug _get_params line 653 core/responses.py :
```
Here are the received params :
{}
odict_items([('Action', ['CreateAutoScalingGroup']), ('AutoScalingGroupName', ['asg-instances-2023030814145626420000000d']), ('DesiredCapacity', ['1']), ('HealthCheckGracePeriod', ['1500']), ('HealthCheckType', ['EC2']), ('LaunchTemplate.LaunchTemplateId', ['lt-e09ced0eeb45cd84b']), ('LaunchTemplate.Version', ['$Latest']), ('MaxSize', ['3']), ('MinSize', ['1']), ('NewInstancesProtectedFromScaleIn', ['false']), ('Tags.member.1.Key', ['Organism']), ('Tags.member.1.PropagateAtLaunch', ['true']), ('Tags.member.1.ResourceId', ['asg-instances-2023030814145626420000000d']), ('Tags.member.1.ResourceType', ['auto-scaling-group']), ('Tags.member.1.Value', ['0000']), ('Tags.member.10.Key', ['Name']), ('Tags.member.10.PropagateAtLaunch', ['true']), ('Tags.member.10.ResourceId', ['asg-instances-2023030814145626420000000d']), ('Tags.member.10.ResourceType', ['auto-scaling-group']), ('Tags.member.10.Value', ['test-test']), ('Tags.member.11.Key', ['test_certificates_secret']), ('Tags.member.11.PropagateAtLaunch', ['true']), ('Tags.member.11.ResourceId', ['asg-instances-2023030814145626420000000d']), ('Tags.member.11.ResourceType', ['auto-scaling-group']), ('Tags.member.11.Value', ['']), ('Tags.member.12.Key', ['key_kms_test']), ('Tags.member.12.PropagateAtLaunch', ['true']), ('Tags.member.12.ResourceId', ['asg-instances-2023030814145626420000000d']), ('Tags.member.12.ResourceType', ['auto-scaling-group']), ('Tags.member.12.Value', ['arn:aws:kms:ca-central-1:123456789012:key/b1bdde7f-4f30-4df8-a818-6e1678d69b19']), ('Tags.member.2.Key', ['test_dns']), ('Tags.member.2.PropagateAtLaunch', ['true']), ('Tags.member.2.ResourceId', ['asg-instances-2023030814145626420000000d']), ('Tags.member.2.ResourceType', ['auto-scaling-group']), ('Tags.member.2.Value', ['test.lab.poctest.ca']), ('Tags.member.3.Key', ['log_stream']), ('Tags.member.3.PropagateAtLaunch', ['true']), ('Tags.member.3.ResourceId', ['asg-instances-2023030814145626420000000d']), ('Tags.member.3.ResourceType', ['auto-scaling-group']), ('Tags.member.3.Value', ['/test-Lab/test']), ('Tags.member.4.Key', ['test_license_secret']), ('Tags.member.4.PropagateAtLaunch', ['true']), ('Tags.member.4.ResourceId', ['asg-instances-2023030814145626420000000d']), ('Tags.member.4.ResourceType', ['auto-scaling-group']), ('Tags.member.4.Value', ['']), ('Tags.member.5.Key', ['test_join_tag']), ('Tags.member.5.PropagateAtLaunch', ['true']), ('Tags.member.5.ResourceId', ['asg-instances-2023030814145626420000000d']), ('Tags.member.5.ResourceType', ['auto-scaling-group']), ('Tags.member.5.Value', ['test-test']), ('Tags.member.6.Key', ['version_entreprise']), ('Tags.member.6.PropagateAtLaunch', ['true']), ('Tags.member.6.ResourceId', ['asg-instances-2023030814145626420000000d']), ('Tags.member.6.ResourceType', ['auto-scaling-group']), ('Tags.member.6.Value', ['false']), ('Tags.member.7.Key', ['Project']), ('Tags.member.7.PropagateAtLaunch', ['true']), ('Tags.member.7.ResourceId', ['asg-instances-2023030814145626420000000d']), ('Tags.member.7.ResourceType', ['auto-scaling-group']), ('Tags.member.7.Value', ['test']), ('Tags.member.8.Key', ['Terraform']), ('Tags.member.8.PropagateAtLaunch', ['true']), ('Tags.member.8.ResourceId', ['asg-instances-2023030814145626420000000d']), ('Tags.member.8.ResourceType', ['auto-scaling-group']), ('Tags.member.8.Value', ['true']), ('Tags.member.9.Key', ['Environment']), ('Tags.member.9.PropagateAtLaunch', ['true']), ('Tags.member.9.ResourceId', ['asg-instances-2023030814145626420000000d']), ('Tags.member.9.ResourceType', ['auto-scaling-group']), ('Tags.member.9.Value', ['Lab']), ('TargetGroupARNs.member.1', ['arn:aws:elasticloadbalancing:ca-central-1:123456789012:targetgroup/test-Lab-test-tg/50dc6c495c0c9188']), ('VPCZoneIdentifier', ['subnet-cca58511,subnet-d611f795']), ('Version', ['2011-01-01'])])
```

debug _parse_params, core/responses.py, line 694

```
The error raises when receiving
keylist : ['Tags', 'member', '10', 'Key']
key : 10
obj : [{'Key': 'Organism', 'PropagateAtLaunch': 'true', 'ResourceId': 'asg-instances-2023030814145626420000000d', 'ResourceType': 'auto-scaling-group', 'Value': '0000'}, {}]
```

The error is line 722, here is the block of code :
```python
                elif key.isdigit():
                    index = int(key) - 1
                    if len(obj) <= index:
                        # initialize list element
                        obj.insert(index, {})
                    # step into
                    parent = obj
                    obj = obj[index] #error here
```

The error raises from create_autoscaling_group method in autoscaling/responses.py line 79 with `        params = self._get_params()`

It seems the limit is 11 tags. 10 tags, with the last having the number 9 and it works. One more and it doesn't anymore.
It has to do with the line 690 in core/responses.py, method _get_params :
`for k, v in sorted(self.querystring.items()):`
Sorted is sorting alphetically but when you have more than 10 tags, it goes like this : "1,10,11,12,2,3,4" and so on.

So what we've got for the tags after sorting is something like this :
```
Tags.member.1.Key Organism
Tags.member.1.PropagateAtLaunch true
Tags.member.1.ResourceId asg-instances-2023030814145626420000000d
Tags.member.1.ResourceType auto-scaling-group
Tags.member.1.Value 0000
Tags.member.10.Key Name
Tags.member.10.PropagateAtLaunch true
Tags.member.10.ResourceId asg-instances-2023030814145626420000000d
Tags.member.10.ResourceType auto-scaling-group
Tags.member.10.Value test-test
Tags.member.11.Key test_certificates_secret
Tags.member.11.PropagateAtLaunch true
Tags.member.11.ResourceId asg-instances-2023030814145626420000000d
Tags.member.11.ResourceType auto-scaling-group
Tags.member.11.Value
Tags.member.12.Key key_kms_test
Tags.member.12.PropagateAtLaunch true
Tags.member.12.ResourceId asg-instances-2023030814145626420000000d
Tags.member.12.ResourceType auto-scaling-group
Tags.member.12.Value arn:aws:kms:ca-central-1:123456789012:key/b1bdde7f-4f30-4df8-a818-6e1678d69b19
Tags.member.2.Key test_dns
Tags.member.2.PropagateAtLaunch true
Tags.member.2.ResourceId asg-instances-2023030814145626420000000d
Tags.member.2.ResourceType auto-scaling-group
Tags.member.2.Value test.lab.poctest.ca
Tags.member.3.Key log_stream
Tags.member.3.PropagateAtLaunch true
Tags.member.3.ResourceId asg-instances-2023030814145626420000000d
Tags.member.3.ResourceType auto-scaling-group
Tags.member.3.Value /test-Lab/test
Tags.member.4.Key test_license_secret
Tags.member.4.PropagateAtLaunch true
Tags.member.4.ResourceId asg-instances-2023030814145626420000000d
Tags.member.4.ResourceType auto-scaling-group
Tags.member.4.Value
Tags.member.5.Key test_join_tag
Tags.member.5.PropagateAtLaunch true
Tags.member.5.ResourceId asg-instances-2023030814145626420000000d
Tags.member.5.ResourceType auto-scaling-group
Tags.member.5.Value test-test
Tags.member.6.Key version_entreprise
Tags.member.6.PropagateAtLaunch true
Tags.member.6.ResourceId asg-instances-2023030814145626420000000d
Tags.member.6.ResourceType auto-scaling-group
Tags.member.6.Value false
Tags.member.7.Key Project
Tags.member.7.PropagateAtLaunch true
Tags.member.7.ResourceId asg-instances-2023030814145626420000000d
Tags.member.7.ResourceType auto-scaling-group
Tags.member.7.Value test
Tags.member.8.Key Terraform
Tags.member.8.PropagateAtLaunch true
Tags.member.8.ResourceId asg-instances-2023030814145626420000000d
Tags.member.8.ResourceType auto-scaling-group
Tags.member.8.Value true
Tags.member.9.Key Environment
Tags.member.9.PropagateAtLaunch true
Tags.member.9.ResourceId asg-instances-2023030814145626420000000d
Tags.member.9.ResourceType auto-scaling-group
Tags.member.9.Value Lab
```

Unfortunately, I cannot investigate more. Hope it helps !

Commit message: fix:params not sorted properly - reformatting core/utils.py

Patch to review:
diff --git a/moto/core/responses.py b/moto/core/responses.py
index 04897ca858f7..8fc794261b3a 100644
--- a/moto/core/responses.py
+++ b/moto/core/responses.py
@@ -12,7 +12,11 @@
 from moto import settings
 from moto.core.common_types import TYPE_RESPONSE, TYPE_IF_NONE
 from moto.core.exceptions import DryRunClientError
-from moto.core.utils import camelcase_to_underscores, method_names_from_class
+from moto.core.utils import (
+    camelcase_to_underscores,
+    method_names_from_class,
+    params_sort_function,
+)
 from moto.utilities.utils import load_resource
 from jinja2 import Environment, DictLoader, Template
 from typing import (
@@ -122,7 +126,6 @@ def response_template(self, source: str) -> Template:
 
 
 class ActionAuthenticatorMixin(object):
-
     request_count: ClassVar[int] = 0
 
     def _authenticate_and_authorize_action(self, iam_request_cls: type) -> None:
@@ -204,7 +207,6 @@ def wrapper(*args: Any, **kwargs: Any) -> TYPE_RESPONSE:
 
 
 class BaseResponse(_TemplateEnvironmentMixin, ActionAuthenticatorMixin):
-
     default_region = "us-east-1"
     # to extract region, use [^.]
     # Note that the URL region can be anything, thanks to our MOTO_ALLOW_NONEXISTENT_REGION-config - so we can't have a very specific regex
@@ -687,7 +689,7 @@ def _get_params(self) -> Dict[str, Any]:
         }
         """
         params: Dict[str, Any] = {}
-        for k, v in sorted(self.querystring.items()):
+        for k, v in sorted(self.querystring.items(), key=params_sort_function):
             self._parse_param(k, v[0], params)
         return params
 
diff --git a/moto/core/utils.py b/moto/core/utils.py
index 7d4cf2f99dbb..99473b3551d7 100644
--- a/moto/core/utils.py
+++ b/moto/core/utils.py
@@ -307,3 +307,15 @@ def extract_region_from_aws_authorization(string: str) -> Optional[str]:
     if region == auth:
         return None
     return region
+
+
+def params_sort_function(item: tuple[str, Any]) -> tuple[str, Any]:
+    """
+    Comparison function used to sort params appropriately taking tags non
+    alphabetical order into consideration
+    """
+    key, _ = item
+    if key.startswith("Tags.member"):
+        member_num = int(key.split(".")[2])
+        return ("Tags.member", member_num)
+    return item


Code review comments:
diff_hunk: @@ -307,3 +307,15 @@ def extract_region_from_aws_authorization(string: str) -> Optional[str]:
     if region == auth:
         return None
     return region
+
+
+def params_sort_function(item: tuple[str, Any]) -> tuple[str, Any]:
path: moto/core/utils.py
text: @user1:
```suggestion
def params_sort_function(item: Tuple[str, Any]) -> Tuple[str, Any]:
```
MyPy has it's own tuple-type that can be used in these cases - that should fix the build