Repository: aws-cloudformation/cfn-lint

Issue:
E0002 Unknown exception while processing rule E3020: expected string or bytes-like object
*cfn-lint version: (`cfn-lint 0.26.3`)*

*Description of issue.*

When running cfn-lint with a template that uses a macro the Route53 RecordSet rule E3020 assumed that A records will be strings.

For example:
```
  DomainNameReg:
    Type: AWS::Route53::RecordSet
    Properties:
      HostedZoneName:
        Fn::Sub: ${DnsEnvironmentDomain}.
      Name:
        Fn::Sub: ${DnsSubdomain}.${DnsEnvironmentDomain}
      Type: A
      TTL: '300'
      ResourceRecords:
        - Macro: {Ref: Param}
          Ref: MacroReplacedValue
```

The problem isn't that the template is flagged for violation of E3020, the problem is that the code fails and gives E0002.

When Adding `-D` the traceback I get is:
```
E0002 Unknown exception while processing rule E3020: Traceback (most recent call last):
  File "/Users/jpatrin/.cache/pre-commit/repovfpiuzdp/py_env-python3.6/lib/python3.6/site-packages/cfnlint/rules/__init__.py", line 203, in run_check
    return check(*args)
  File "/Users/jpatrin/.cache/pre-commit/repovfpiuzdp/py_env-python3.6/lib/python3.6/site-packages/cfnlint/rules/__init__.py", line 89, in wrapper
    results = match_function(self, filename, cfn, *args, **kwargs)
  File "/Users/jpatrin/.cache/pre-commit/repovfpiuzdp/py_env-python3.6/lib/python3.6/site-packages/cfnlint/rules/__init__.py", line 116, in matchall
    return self.match(cfn)  # pylint: disable=E1102
  File "/Users/jpatrin/.cache/pre-commit/repovfpiuzdp/py_env-python3.6/lib/python3.6/site-packages/cfnlint/rules/resources/route53/RecordSet.py", line 274, in match
    matches.extend(self.check_recordset(path, props, cfn))
  File "/Users/jpatrin/.cache/pre-commit/repovfpiuzdp/py_env-python3.6/lib/python3.6/site-packages/cfnlint/rules/resources/route53/RecordSet.py", line 201, in check_recordset
    check_value=self.check_a_record,
  File "/Users/jpatrin/.cache/pre-commit/repovfpiuzdp/py_env-python3.6/lib/python3.6/site-packages/cfnlint/__init__.py", line 613, in check_value
    value=value, path=new_path[:] + child_path, **kwargs))
  File "/Users/jpatrin/.cache/pre-commit/repovfpiuzdp/py_env-python3.6/lib/python3.6/site-packages/cfnlint/rules/resources/route53/RecordSet.py", line 52, in check_a_record
    if not re.match(REGEX_IPV4, value):
  File "/Users/jpatrin/.cache/pre-commit/repovfpiuzdp/py_env-python3.6/lib/python3.6/re.py", line 172, in match
    return _compile(pattern, flags).match(string)
TypeError: expected string or bytes-like object
```

Most of the other checks in the `RecordSet.py` seem to have an `if not isinstance(value, dict)` check, that would fix this problem as well.

Again, I'm not complaining about my non-standard template causing a valid error to be raised, that's expected and I can ignore those errors easily, as I have done in many other places.


Discussion:

Resolving patch:
diff --git a/src/cfnlint/rules/resources/route53/RecordSet.py b/src/cfnlint/rules/resources/route53/RecordSet.py
--- a/src/cfnlint/rules/resources/route53/RecordSet.py
+++ b/src/cfnlint/rules/resources/route53/RecordSet.py
@@ -3,6 +3,7 @@
 SPDX-License-Identifier: MIT-0
 """
 import re
+import six
 from cfnlint.rules import CloudFormationLintRule
 from cfnlint.rules import RuleMatch
 from cfnlint.helpers import REGEX_IPV4, REGEX_IPV6, REGEX_ALPHANUMERIC
@@ -49,9 +50,10 @@ def check_a_record(self, value, path):
         matches = []
 
         # Check if a valid IPv4 address is specified
-        if not re.match(REGEX_IPV4, value):
-            message = 'A record ({}) is not a valid IPv4 address'
-            matches.append(RuleMatch(path, message.format(value)))
+        if isinstance(value, six.string_types):
+            if not re.match(REGEX_IPV4, value):
+                message = 'A record ({}) is not a valid IPv4 address'
+                matches.append(RuleMatch(path, message.format(value)))
 
         return matches
 
@@ -59,7 +61,7 @@ def check_aaaa_record(self, value, path):
         """Check AAAA record Configuration"""
         matches = []
 
-        if not isinstance(value, dict):
+        if isinstance(value, six.string_types):
             # Check if a valid IPv4 address is specified
             if not re.match(REGEX_IPV6, value):
                 message = 'AAAA record ({}) is not a valid IPv6 address'
@@ -71,7 +73,7 @@ def check_caa_record(self, value, path):
         """Check CAA record Configuration"""
         matches = []
 
-        if not isinstance(value, dict):
+        if isinstance(value, six.string_types):
             # Split the record up to the mandatory settings (flags tag "value")
             items = value.split(' ', 2)
             # Check if the 3 settings are given.
@@ -82,7 +84,8 @@ def check_caa_record(self, value, path):
                 # Check the flag value
                 if not items[0].isdigit():
                     message = 'CAA record flag setting ({}) should be of type Integer.'
-                    extra_args = {'actual_type': type(items[0]).__name__, 'expected_type': int.__name__}
+                    extra_args = {'actual_type': type(
+                        items[0]).__name__, 'expected_type': int.__name__}
                     matches.append(RuleMatch(path, message.format(items[0]), **extra_args))
                 else:
                     if int(items[0]) not in [0, 128]:
@@ -119,7 +122,7 @@ def check_mx_record(self, value, path):
         """Check MX record Configuration"""
         matches = []
 
-        if not isinstance(value, dict):
+        if isinstance(value, six.string_types):
             # Split the record up to the mandatory settings (priority domainname)
             items = value.split(' ')
 
@@ -131,7 +134,8 @@ def check_mx_record(self, value, path):
                 # Check the priority value
                 if not items[0].isdigit():
                     message = 'MX record priority setting ({}) should be of type Integer.'
-                    extra_args = {'actual_type': type(items[0]).__name__, 'expected_type': int.__name__}
+                    extra_args = {'actual_type': type(
+                        items[0]).__name__, 'expected_type': int.__name__}
                     matches.append(RuleMatch(path, message.format(items[0], value), **extra_args))
                 else:
                     if not 0 <= int(items[0]) <= 65535:
@@ -148,7 +152,7 @@ def check_ns_record(self, value, path):
         """Check NS record Configuration"""
         matches = []
 
-        if not isinstance(value, dict):
+        if isinstance(value, six.string_types):
             if not re.match(self.REGEX_DOMAINNAME, value):
                 message = 'NS record ({}) does not contain a valid domain name'
                 matches.append(RuleMatch(path, message.format(value)))
@@ -159,7 +163,7 @@ def check_ptr_record(self, value, path):
         """Check PTR record Configuration"""
         matches = []
 
-        if not isinstance(value, dict):
+        if isinstance(value, six.string_types):
             if not re.match(self.REGEX_DOMAINNAME, value):
                 message = 'PTR record ({}) does not contain a valid domain name'
                 matches.append(RuleMatch(path, message.format(value)))


Test patch: