Repository: aws-cloudformation/cfn-lint

Issue:
Length limit check on Route53 TXT records doesn't allow multiple values
*cfn-lint version: (`cfn-lint --version`)* master

*Description of issue.*

The length limit check on TXT records is not quite right. Multiple values of up to 255 characters *are* allowed, separated by spaces.

This valid template is thus categorized as invalid:

```
$ cat test.yml
Resources:
  Example:
    Type: AWS::Route53::RecordSet
    Properties:
      HostedZoneId: abc123
      Name: example.com.
      Type: TXT
      TTL: '14400'
      ResourceRecords:
      # 255 "a" characters within appropriate quotes, then a "b"
      - '"aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" "b"'
$ cfn-lint test.yml
E3020 The length of the TXT record (261) exceeds the limit (255)
test.yml:9:7
```

Verified it's valid by creating an equivalent record on the console.


Discussion:
FWIW this doesn't seem to be documented: https://docs.aws.amazon.com/Route53/latest/DeveloperGuide/ResourceRecordTypes.html#TXTFormat
I followed the documentation with creating the rules... If you've validated it in the console we need to fix this. 

Do you create a PR @adamchainz or do you want me to change it? It's also wise to create a PR at the documentation (they actually process those I can tell based on experience 😉 )

I've had a stab at this but don't have any more time right now. One of the tests is unexpectedly passing. Maybe I'll pick it up later or someone else can:

```patch
diff --git a/src/cfnlint/rules/resources/route53/RecordSet.py b/src/cfnlint/rules/resources/route53/RecordSet.py
index 4b1f5f3..7330769 100644
--- a/src/cfnlint/rules/resources/route53/RecordSet.py
+++ b/src/cfnlint/rules/resources/route53/RecordSet.py
@@ -45,6 +45,7 @@ class RecordSet(CloudFormationLintRule):
     ]
 
     REGEX_CNAME = re.compile(r'^(([a-zA-Z0-9]|[a-zA-Z0-9][a-zA-Z0-9\-]*[a-zA-Z0-9])\.)*([A-Za-z0-9]|[A-Za-z0-9][A-Za-z0-9\-]*[A-Za-z0-9])(.)$')
+    REGEX_TXT = re.compile(r'^("[^"]{1,255}" *)*"[^"]{1,255}"$')
 
     def check_a_record(self, path, recordset):
         """Check A record Configuration"""
@@ -150,13 +151,14 @@ class RecordSet(CloudFormationLintRule):
         for index, record in enumerate(resource_records):
             tree = path[:] + ['ResourceRecords', index]
 
-            if not isinstance(record, dict):
-                if not record.startswith('"') or not record.endswith('"'):
-                    message = 'TXT record ({}) has to be enclosed in double quotation marks (")'
-                    matches.append(RuleMatch(tree, message.format(record)))
-                elif len(record) > 257:  # 2 extra characters for start and end double quotation marks
-                    message = 'The length of the TXT record ({}) exceeds the limit (255)'
-                    matches.append(RuleMatch(tree, message.format(len(record))))
+            if not isinstance(record, dict) and not re.match(self.REGEX_TXT, record):
+                matches.append(RuleMatch(
+                    tree,
+                    (
+                        'TXT record is not structured as one or more items up to 255 characters '
+                        + 'enclosed in double quotation marks.'
+                    ),
+                ))
 
         return matches
 
diff --git a/test/fixtures/templates/bad/route53.yaml b/test/fixtures/templates/bad/route53.yaml
index e64f824..0596363 100644
--- a/test/fixtures/templates/bad/route53.yaml
+++ b/test/fixtures/templates/bad/route53.yaml
@@ -21,8 +21,8 @@ Resources:
       Type: "TXT"
       TTL: "300"
       ResourceRecords:
-        # 256 "a" characters - too long
-        - '"aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"'
+        - '"aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"'  # 256 "a" characters - too long
+        - '"a""bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb"'  # 256 "b" characters in a second record - too long
         - "\"MS=ms123123\""
         - "test2" # No quotation
   MyARecordSet:
diff --git a/test/fixtures/templates/good/route53.yaml b/test/fixtures/templates/good/route53.yaml
index 9b2266c..f112c53 100644
--- a/test/fixtures/templates/good/route53.yaml
+++ b/test/fixtures/templates/good/route53.yaml
@@ -33,8 +33,9 @@ Resources:
       TTL: "300"
       ResourceRecords:
         - "\"MS=ms123123\""
+        - "\"aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa\""  # 255 "a" characters
         # 255 "a" characters + quotes
-        - "\"aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa\""
+        - "\"aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa\"\"bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb\""  # 255 "a" characters and 255 "b" characters
   MyARecordSet:
     Type: "AWS::Route53::RecordSet"
     Properties:

```

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
@@ -17,9 +17,9 @@
 import re
 from cfnlint import CloudFormationLintRule
 from cfnlint import RuleMatch
-
 from cfnlint.helpers import REGEX_IPV4, REGEX_IPV6, REGEX_ALPHANUMERIC
 
+
 class RecordSet(CloudFormationLintRule):
     """Check Route53 Recordset Configuration"""
     id = 'E3020'
@@ -45,6 +45,7 @@ class RecordSet(CloudFormationLintRule):
     ]
 
     REGEX_CNAME = re.compile(r'^(([a-zA-Z0-9]|[a-zA-Z0-9][a-zA-Z0-9\-]*[a-zA-Z0-9])\.)*([A-Za-z0-9]|[A-Za-z0-9][A-Za-z0-9\-]*[A-Za-z0-9])(.)$')
+    REGEX_TXT = re.compile(r'^("[^"]{1,255}" *)*"[^"]{1,255}"$')
 
     def check_a_record(self, path, recordset):
         """Check A record Configuration"""
@@ -150,13 +151,15 @@ def check_txt_record(self, path, recordset):
         for index, record in enumerate(resource_records):
             tree = path[:] + ['ResourceRecords', index]
 
-            if not isinstance(record, dict):
-                if not record.startswith('"') or not record.endswith('"'):
-                    message = 'TXT record ({}) has to be enclosed in double quotation marks (")'
-                    matches.append(RuleMatch(tree, message.format(record)))
-                elif len(record) > 257:  # 2 extra characters for start and end double quotation marks
-                    message = 'The length of the TXT record ({}) exceeds the limit (255)'
-                    matches.append(RuleMatch(tree, message.format(len(record))))
+            if not isinstance(record, dict) and not re.match(self.REGEX_TXT, record):
+                message = 'TXT record is not structured as one or more items up to 255 characters ' \
+                          'enclosed in double quotation marks at {0}'
+                matches.append(RuleMatch(
+                    tree,
+                    (
+                        message.format('/'.join(map(str, tree)))
+                    ),
+                ))
 
         return matches
 


Test patch:
diff --git a/test/fixtures/templates/bad/route53.yaml b/test/fixtures/templates/bad/route53.yaml
--- a/test/fixtures/templates/bad/route53.yaml
+++ b/test/fixtures/templates/bad/route53.yaml
@@ -21,8 +21,8 @@ Resources:
       Type: "TXT"
       TTL: "300"
       ResourceRecords:
-        # 256 "a" characters - too long
-        - '"aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"'
+        - '"aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"'  # 256 "a" characters - too long
+        - '"a""bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb"'  # 256 "b" characters in a second record - too long
         - "\"MS=ms123123\""
         - "test2" # No quotation
   MyARecordSet:
diff --git a/test/fixtures/templates/good/route53.yaml b/test/fixtures/templates/good/route53.yaml
--- a/test/fixtures/templates/good/route53.yaml
+++ b/test/fixtures/templates/good/route53.yaml
@@ -33,8 +33,10 @@ Resources:
       TTL: "300"
       ResourceRecords:
         - "\"MS=ms123123\""
+        - "\"aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa\""  # 255 "a" characters
         # 255 "a" characters + quotes
-        - "\"aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa\""
+        - "\"aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa\"\"bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb\""  # 255 "a" characters and 255 "b" characters
+        -  "\"aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa\"\"bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb\" \"ccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccc\""  # 255 "a" characters and 255 "b" characters and 255 "c" characters with a space between b and c
   MyARecordSet:
     Type: "AWS::Route53::RecordSet"
     Properties:
diff --git a/test/rules/resources/route53/test_recordsets.py b/test/rules/resources/route53/test_recordsets.py
--- a/test/rules/resources/route53/test_recordsets.py
+++ b/test/rules/resources/route53/test_recordsets.py
@@ -34,4 +34,4 @@ def test_file_positive(self):
 
     def test_file_negative_alias(self):
         """Test failure"""
-        self.helper_file_negative('fixtures/templates/bad/route53.yaml', 24)
+        self.helper_file_negative('fixtures/templates/bad/route53.yaml', 25)