Repository: localstack/localstack

Issue:
bug: Cannot create a target group using cloudformation
### Is there an existing issue for this?

- [X] I have searched the existing issues

### Current Behavior

When trying to create a stack with only a VPC and a TargetGroup, the deploy command fails with the following error:

`2022-03-14T14:28:13.883:WARNING:localstack.utils.cloudformation.template_deployer: Error calling <bound method ClientCreator._create_api_method.<locals>._api_call of <botocore.client.ElasticLoadBalancingv2 object at 0x7f8f4f3fa6a0>> with params: {'Name': 'dfsd', 'Protocol': 'HTTP', 'Port': '3000', 'VpcId': 'vpc-0336b473', 'TargetType': 'ip'} for resource: {'Type': 'AWS::ElasticLoadBalancingV2::TargetGroup', 'LogicalResourceId': 'targetGroup1', 'Properties': {'Name': 'dfsd', 'Port': '3000', 'Protocol': 'HTTP', 'TargetType': 'ip', 'VpcId': 'vpc-0336b473'}, '_state_': {}}`

### Expected Behavior

A working cloudformation template should be created

### How are you starting LocalStack?

With the `localstack` script

### Steps To Reproduce

#### How are you starting localstack (e.g., `bin/localstack` command, arguments, or `docker-compose.yml`)

    TEST_AWS_ACCOUNT_ID="XXXX" LOCALSTACK_API_KEY=XXXX localstack start -d

#### Client commands (e.g., AWS SDK code snippet, or sequence of "awslocal" commands)

    awslocal cloudformation deploy --stack-name cfn-quickstart-stack --template-file "./cfn-quickstart-stack.yaml" # The quickstart s3 bucket from the sample
    awslocal cloudformation deploy --stack-name my_stack --template-file deployment/environment/local-template.json --s3-bucket cfn-quickstart-bucket

#### The template used in the second command above:

```
{
  "AWSTemplateFormatVersion": "2010-09-09",
  "Resources": {
    "myVPC" : {
      "Type" : "AWS::EC2::VPC",
      "Properties" : {
        "CidrBlock" : "10.0.0.0/16",
        "EnableDnsSupport" : "true",
        "EnableDnsHostnames" : "true"
      }
    },
    "targetGroup1": {
      "Type": "AWS::ElasticLoadBalancingV2::TargetGroup",
      "Properties": {
        "Name": "dfsd",
        "Port": "3000",
        "Protocol": "HTTP",
        "TargetType": "ip",
        "VpcId": { "Ref" : "myVPC" }
      }
    }
  },
  "Description": "Room Service Stack"
}
```

### Environment

```markdown
- OS: MacOS Big Sur 11.6
- LocalStack: latest
```

### Anything else?

_No response_


Discussion:
Welcome to LocalStack! Thanks for reporting your first issue and our team will be working towards fixing the issue for you or reach out for more background information. We recommend joining our [Slack Community](https://localstack.cloud/contact/) for real-time help and drop a message to LocalStack Pro Support if you are a Pro user! If you are willing to contribute towards fixing this issue, please have a look at our [contributing guidelines](https://github.com/localstack/.github/blob/main/CONTRIBUTING.md) and our [developer guide](https://docs.localstack.cloud/developer-guide/).
After turning on `DEBUG=1` I saw that the error was because I passed Port as a string instead of int. This does not bother the real CFN service and also I would expect this kind of log to appear in the regular logs and not debug logs only.

Resolving patch:
diff --git a/localstack/services/cloudformation/deployment_utils.py b/localstack/services/cloudformation/deployment_utils.py
--- a/localstack/services/cloudformation/deployment_utils.py
+++ b/localstack/services/cloudformation/deployment_utils.py
@@ -1,5 +1,8 @@
+import builtins
 import json
 import os
+import re
+from copy import deepcopy
 from typing import Callable
 
 from localstack.config import dirs
@@ -161,3 +164,50 @@ def _pre_create_default_name(
             props[key] = generate_default_name(stack_name, resource_id)
 
     return _pre_create_default_name
+
+
+# Utils for parameter conversion
+
+# TODO: handling of multiple valid types
+param_validation = re.compile(
+    r"Invalid type for parameter (?P<param>\w+), value: (?P<value>\w+), type: <class '(?P<wrong_class>\w+)'>, valid types: <class '(?P<valid_class>\w+)'>"
+)
+
+
+def get_nested(obj: dict, path: str):
+    parts = path.split(".")
+    result = obj
+    for p in parts[:-1]:
+        result = result.get(p, {})
+    return result.get(parts[-1])
+
+
+def set_nested(obj: dict, path: str, value):
+    parts = path.split(".")
+    result = obj
+    for p in parts[:-1]:
+        result = result.get(p, {})
+    result[parts[-1]] = value
+
+
+def fix_boto_parameters_based_on_report(original_params: dict, report: str) -> dict:
+    """
+    Fix invalid type parameter validation errors in boto request parameters
+
+    :param original_params: original boto request parameters that lead to the parameter validation error
+    :param report: error report from botocore ParamValidator
+    :return: a copy of original_params with all values replaced by their correctly cast ones
+    """
+    params = deepcopy(original_params)
+    for found in param_validation.findall(report):
+        param_name, value, wrong_class, valid_class = found
+        cast_class = getattr(builtins, valid_class)
+        old_value = get_nested(params, param_name)
+
+        new_value = None
+        if cast_class == bool and str(old_value).lower() in ["true", "false"]:
+            new_value = str(old_value).lower() == "true"
+        else:
+            new_value = cast_class(old_value)
+        set_nested(params, param_name, new_value)
+    return params
diff --git a/localstack/services/sqs/provider.py b/localstack/services/sqs/provider.py
--- a/localstack/services/sqs/provider.py
+++ b/localstack/services/sqs/provider.py
@@ -625,6 +625,14 @@ def __init__(self, name: str, region: str, account_id: str, attributes=None, tag
         super().__init__(name, region, account_id, attributes, tags)
         self.deduplication = {}
 
+    def default_attributes(self) -> QueueAttributeMap:
+        return {
+            **super().default_attributes(),
+            QueueAttributeName.ContentBasedDeduplication: "false",
+            QueueAttributeName.DeduplicationScope: "queue",
+            QueueAttributeName.FifoThroughputLimit: "perQueue",
+        }
+
     def update_delay_seconds(self, value: int):
         super(FifoQueue, self).update_delay_seconds(value)
         for message in self.delayed:
diff --git a/localstack/utils/cloudformation/template_deployer.py b/localstack/utils/cloudformation/template_deployer.py
--- a/localstack/utils/cloudformation/template_deployer.py
+++ b/localstack/utils/cloudformation/template_deployer.py
@@ -1,5 +1,4 @@
 import base64
-import copy
 import json
 import logging
 import re
@@ -15,6 +14,7 @@
 from localstack.services.cloudformation.deployment_utils import (
     PLACEHOLDER_AWS_NO_VALUE,
     PLACEHOLDER_RESOURCE_NAME,
+    fix_boto_parameters_based_on_report,
     is_none_or_empty_value,
     remove_none_values,
 )
@@ -992,18 +992,11 @@ def fix_placeholders(o, **kwargs):
     # convert any moto account IDs (123456789012) in ARNs to our format (000000000000)
     params = fix_account_id_in_arns(params)
     # convert data types (e.g., boolean strings to bool)
+    # TODO: this might not be needed anymore
     params = convert_data_types(func_details, params)
     # remove None values, as they usually raise boto3 errors
     params = remove_none_values(params)
 
-    # convert boolean strings
-    #  (TODO: we should find a more reliable mechanism than this opportunistic/probabilistic approach!)
-    params_before_conversion = copy.deepcopy(params)
-    for param_key, param_value in dict(params).items():
-        # Convert to boolean (TODO: do this recursively?)
-        if str(param_value).lower() in ["true", "false"]:
-            params[param_key] = str(param_value).lower() == "true"
-
     # invoke function
     try:
         LOG.debug(
@@ -1016,10 +1009,17 @@ def fix_placeholders(o, **kwargs):
         try:
             result = function(**params)
         except botocore.exceptions.ParamValidationError as e:
-            LOG.debug(f"Trying original parameters: {params_before_conversion}")
-            if "type: <class 'bool'>" not in str(e):
+            # alternatively we could also use the ParamValidator directly
+            report = e.kwargs.get("report")
+            if not report:
                 raise
-            result = function(**params_before_conversion)
+
+            LOG.debug("Converting parameters to allowed types")
+            converted_params = fix_boto_parameters_based_on_report(params, report)
+            LOG.debug("Original parameters:  %s", params)
+            LOG.debug("Converted parameters: %s", converted_params)
+
+            result = function(**converted_params)
     except Exception as e:
         if action_name == "delete" and check_not_found_exception(e, resource_type, resource):
             return


Test patch:
diff --git a/tests/integration/cloudformation/test_cloudformation_engine.py b/tests/integration/cloudformation/test_cloudformation_engine.py
new file mode 100644
--- /dev/null
+++ b/tests/integration/cloudformation/test_cloudformation_engine.py
@@ -0,0 +1,34 @@
+import pytest
+
+#
+TMPL = """
+Resources:
+  blaBE223B94:
+    Type: AWS::SNS::Topic
+  queue276F7297:
+    Type: AWS::SQS::Queue
+    Properties:
+      DelaySeconds: "2"
+      FifoQueue: "true"
+    UpdateReplacePolicy: Delete
+    DeletionPolicy: Delete
+Outputs:
+  QueueName:
+    Value:
+      Fn::GetAtt:
+        - queue276F7297
+        - QueueName
+  QueueUrl:
+    Value:
+      Ref: queue276F7297
+"""
+
+
+@pytest.mark.aws_validated
+def test_implicit_type_conversion(deploy_cfn_template, cfn_client, sqs_client, snapshot):
+    snapshot.add_transformer(snapshot.transform.sqs_api())
+    stack = deploy_cfn_template(template=TMPL, max_wait=180)
+    queue = sqs_client.get_queue_attributes(
+        QueueUrl=stack.outputs["QueueUrl"], AttributeNames=["All"]
+    )
+    snapshot.match("queue", queue)
diff --git a/tests/integration/cloudformation/test_cloudformation_engine.snapshot.json b/tests/integration/cloudformation/test_cloudformation_engine.snapshot.json
new file mode 100644
--- /dev/null
+++ b/tests/integration/cloudformation/test_cloudformation_engine.snapshot.json
@@ -0,0 +1,31 @@
+{
+  "tests/integration/cloudformation/test_cloudformation_engine.py::test_implicit_type_conversion": {
+    "recorded-date": "12-08-2022, 16:14:04",
+    "recorded-content": {
+      "queue": {
+        "Attributes": {
+          "ApproximateNumberOfMessages": "0",
+          "ApproximateNumberOfMessagesDelayed": "0",
+          "ApproximateNumberOfMessagesNotVisible": "0",
+          "ContentBasedDeduplication": "false",
+          "CreatedTimestamp": "timestamp",
+          "DeduplicationScope": "queue",
+          "DelaySeconds": "2",
+          "FifoQueue": "true",
+          "FifoThroughputLimit": "perQueue",
+          "LastModifiedTimestamp": "timestamp",
+          "MaximumMessageSize": "262144",
+          "MessageRetentionPeriod": "345600",
+          "QueueArn": "arn:aws:sqs:<region>:111111111111:<resource:1>",
+          "ReceiveMessageWaitTimeSeconds": "0",
+          "SqsManagedSseEnabled": "false",
+          "VisibilityTimeout": "30"
+        },
+        "ResponseMetadata": {
+          "HTTPHeaders": {},
+          "HTTPStatusCode": 200
+        }
+      }
+    }
+  }
+}