Repository: getmoto/moto

Issue:
Short function references (!Equals, !Sub) not supported by moto's cloudformation mock?
Hello. Getting an exception from moto from the `cfn:ValidateTemplate` api. The template is valid and works with the CloudFormation service. It's using the [short form](https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/intrinsic-function-reference-conditions.html#intrinsic-function-reference-conditions-equals) (`!Equals`) for function references.

Btw, I also had to change `yaml.ParserError` to `yaml.parser.ParserError` in order to get to the reported exception. Details follow.

Versions:

```
$ python --version && pip freeze |grep oto
Python 3.7.1
boto==2.49.0
boto3==1.9.239
botocore==1.12.239
moto==1.3.13
```

Exception:

```
____________________________________________________________________ test_create_pipeline ____________________________________________________________________

boto3_session = Session(region_name='us-east-1')

    @moto.mock_cloudformation
    def test_create_pipeline(boto3_session):
        pipeline_utils.create_pipeline(
            boto3_session,
            repo_name='s3-bucket-cfn-resource',
            stack_name='s3-bucket-cfn-resource',
>           template_path='cfn/main.yml'
        )

account_welcomer/test_pipeline_utils.py:25: 
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ 
account_welcomer/pipeline_utils.py:20: in create_pipeline
    TemplateBody=template_body
../../../.pyenv/versions/3.7.1/lib/python3.7/site-packages/botocore/client.py:357: in _api_call
    return self._make_api_call(operation_name, kwargs)
../../../.pyenv/versions/3.7.1/lib/python3.7/site-packages/botocore/client.py:648: in _make_api_call
    operation_model, request_dict, request_context)
../../../.pyenv/versions/3.7.1/lib/python3.7/site-packages/botocore/client.py:667: in _make_request
    return self._endpoint.make_request(operation_model, request_dict)
../../../.pyenv/versions/3.7.1/lib/python3.7/site-packages/botocore/endpoint.py:102: in make_request
    return self._send_request(request_dict, operation_model)
../../../.pyenv/versions/3.7.1/lib/python3.7/site-packages/botocore/endpoint.py:137: in _send_request
    success_response, exception):
../../../.pyenv/versions/3.7.1/lib/python3.7/site-packages/botocore/endpoint.py:231: in _needs_retry
    caught_exception=caught_exception, request_dict=request_dict)
../../../.pyenv/versions/3.7.1/lib/python3.7/site-packages/botocore/hooks.py:356: in emit
    return self._emitter.emit(aliased_event_name, **kwargs)
../../../.pyenv/versions/3.7.1/lib/python3.7/site-packages/botocore/hooks.py:228: in emit
    return self._emit(event_name, kwargs)
../../../.pyenv/versions/3.7.1/lib/python3.7/site-packages/botocore/hooks.py:211: in _emit
    response = handler(**kwargs)
../../../.pyenv/versions/3.7.1/lib/python3.7/site-packages/botocore/retryhandler.py:183: in __call__
    if self._checker(attempts, response, caught_exception):
../../../.pyenv/versions/3.7.1/lib/python3.7/site-packages/botocore/retryhandler.py:251: in __call__
    caught_exception)
../../../.pyenv/versions/3.7.1/lib/python3.7/site-packages/botocore/retryhandler.py:269: in _should_retry
    return self._checker(attempt_number, response, caught_exception)
../../../.pyenv/versions/3.7.1/lib/python3.7/site-packages/botocore/retryhandler.py:317: in __call__
    caught_exception)
../../../.pyenv/versions/3.7.1/lib/python3.7/site-packages/botocore/retryhandler.py:223: in __call__
    attempt_number, caught_exception)
../../../.pyenv/versions/3.7.1/lib/python3.7/site-packages/botocore/retryhandler.py:359: in _check_caught_exception
    raise caught_exception
../../../.pyenv/versions/3.7.1/lib/python3.7/site-packages/botocore/endpoint.py:197: in _do_get_response
    responses = self._event_emitter.emit(event_name, request=request)
../../../.pyenv/versions/3.7.1/lib/python3.7/site-packages/botocore/hooks.py:356: in emit
    return self._emitter.emit(aliased_event_name, **kwargs)
../../../.pyenv/versions/3.7.1/lib/python3.7/site-packages/botocore/hooks.py:228: in emit
    return self._emit(event_name, kwargs)
../../../.pyenv/versions/3.7.1/lib/python3.7/site-packages/botocore/hooks.py:211: in _emit
    response = handler(**kwargs)
../../../.pyenv/versions/3.7.1/lib/python3.7/site-packages/moto/core/models.py:298: in __call__
    status, headers, body = response_callback(request, request.url, request.headers)
../../../.pyenv/versions/3.7.1/lib/python3.7/site-packages/moto/core/responses.py:117: in dispatch
    return cls()._dispatch(*args, **kwargs)
../../../.pyenv/versions/3.7.1/lib/python3.7/site-packages/moto/core/responses.py:207: in _dispatch
    return self.call_action()
../../../.pyenv/versions/3.7.1/lib/python3.7/site-packages/moto/core/responses.py:281: in call_action
    response = method()
../../../.pyenv/versions/3.7.1/lib/python3.7/site-packages/moto/cloudformation/responses.py:339: in validate_template
    description = yaml.load(self._get_param('TemplateBody'))['Description']
../../../.pyenv/versions/3.7.1/lib/python3.7/site-packages/yaml/__init__.py:114: in load
    return loader.get_single_data()
../../../.pyenv/versions/3.7.1/lib/python3.7/site-packages/yaml/constructor.py:43: in get_single_data
    return self.construct_document(node)
../../../.pyenv/versions/3.7.1/lib/python3.7/site-packages/yaml/constructor.py:52: in construct_document
    for dummy in generator:
../../../.pyenv/versions/3.7.1/lib/python3.7/site-packages/yaml/constructor.py:404: in construct_yaml_map
    value = self.construct_mapping(node)
../../../.pyenv/versions/3.7.1/lib/python3.7/site-packages/yaml/constructor.py:210: in construct_mapping
    return super().construct_mapping(node, deep=deep)
../../../.pyenv/versions/3.7.1/lib/python3.7/site-packages/yaml/constructor.py:135: in construct_mapping
    value = self.construct_object(value_node, deep=deep)
../../../.pyenv/versions/3.7.1/lib/python3.7/site-packages/yaml/constructor.py:92: in construct_object
    data = constructor(self, node)
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ 

self = <yaml.loader.FullLoader object at 0x109f9bda0>
node = SequenceNode(tag='!Equals', value=[ScalarNode(tag='!Ref', value='TemplateParameters'), ScalarNode(tag='tag:yaml.org,2002:str', value='')])

    def construct_undefined(self, node):
        raise ConstructorError(None, None,
                "could not determine a constructor for the tag %r" % node.tag,
>               node.start_mark)
E       yaml.constructor.ConstructorError: could not determine a constructor for the tag '!Equals'
E         in "<unicode string>", line 30, column 20:
E             HasNoParameters: !Equals [ !Ref TemplateParameter ... 
E                              ^

../../../.pyenv/versions/3.7.1/lib/python3.7/site-packages/yaml/constructor.py:420: ConstructorError
```


Discussion:
Thanks for opening. I am going to set this as a feature request.

Resolving patch:
diff --git a/moto/cloudformation/parsing.py b/moto/cloudformation/parsing.py
--- a/moto/cloudformation/parsing.py
+++ b/moto/cloudformation/parsing.py
@@ -378,12 +378,14 @@ def parse_condition(condition, resources_map, condition_map):
     condition_values = []
     for value in list(condition.values())[0]:
         # Check if we are referencing another Condition
-        if "Condition" in value:
+        if isinstance(value, dict) and "Condition" in value:
             condition_values.append(condition_map[value["Condition"]])
         else:
             condition_values.append(clean_json(value, resources_map))
 
     if condition_operator == "Fn::Equals":
+        if condition_values[1] in [True, False]:
+            return str(condition_values[0]).lower() == str(condition_values[1]).lower()
         return condition_values[0] == condition_values[1]
     elif condition_operator == "Fn::Not":
         return not parse_condition(condition_values[0], resources_map, condition_map)


Test patch:
diff --git a/tests/test_cloudformation/test_cloudformation_stack_crud_boto3.py b/tests/test_cloudformation/test_cloudformation_stack_crud_boto3.py
--- a/tests/test_cloudformation/test_cloudformation_stack_crud_boto3.py
+++ b/tests/test_cloudformation/test_cloudformation_stack_crud_boto3.py
@@ -113,6 +113,26 @@
           Value: Name tag for tests
 """
 
+dummy_yaml_template_with_equals = """---
+AWSTemplateFormatVersion: 2010-09-09
+Description: Stack with yaml template
+Conditions:
+  maybe:
+    Fn::Equals: [!Ref enabled, true]
+Parameters:
+  enabled:
+    Type: String
+    AllowedValues:
+     - true
+     - false
+Resources:
+  VPC1:
+    Type: AWS::EC2::VPC
+    Condition: maybe
+    Properties:
+      CidrBlock: 192.168.0.0/16
+"""
+
 dummy_template_yaml_with_ref = """---
 AWSTemplateFormatVersion: 2010-09-09
 Description: Stack1 with yaml template
@@ -1851,6 +1871,42 @@ def test_cloudformation_params_conditions_and_resources_are_distinct():
     ]
 
 
+@mock_cloudformation
+@mock_ec2
+def test_cloudformation_conditions_yaml_equals():
+    cf = boto3.client("cloudformation", region_name="us-east-1")
+    cf.create_stack(
+        StackName="teststack2",
+        TemplateBody=dummy_yaml_template_with_equals,
+        Parameters=[{"ParameterKey": "enabled", "ParameterValue": "true"}],
+    )
+    resources = cf.list_stack_resources(StackName="teststack2")[
+        "StackResourceSummaries"
+    ]
+    assert [
+        resource for resource in resources if resource["LogicalResourceId"] == "VPC1"
+    ]
+
+
+@mock_cloudformation
+@mock_ec2
+def test_cloudformation_conditions_yaml_equals_shortform():
+    _template = dummy_yaml_template_with_equals
+    _template = _template.replace("Fn::Equals:", "!Equals")
+    cf = boto3.client("cloudformation", region_name="us-east-1")
+    cf.create_stack(
+        StackName="teststack2",
+        TemplateBody=_template,
+        Parameters=[{"ParameterKey": "enabled", "ParameterValue": "true"}],
+    )
+    resources = cf.list_stack_resources(StackName="teststack2")[
+        "StackResourceSummaries"
+    ]
+    assert [
+        resource for resource in resources if resource["LogicalResourceId"] == "VPC1"
+    ]
+
+
 @mock_cloudformation
 def test_stack_tags():
     tags = [{"Key": "foo", "Value": "bar"}, {"Key": "baz", "Value": "bleh"}]