Repository: cloudtools/troposphere (python)

Issue:
SSM Document Property "Content" also allows plain Strings
Good evening everyone,

This is my first issue report. My apologies for any mistakes I make submitting this.
I'm using troposphere 4.0.2 as it is tagged on GitHub.

https://github.com/cloudtools/troposphere/blob/4.0.2/troposphere/ssm.py
In line 115  the propery "Content" is define as a "dict", but according to the AWS Documentation, it also accepts palin strings.

https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-resource-ssm-document.html#cfn-ssm-document-content

I temporarely changed the code and simply used "YAML as a string" inside a JSON CloudFormation Template.
Everything worked as it should and the pre-formatted YAML shows up as the contents of SSM Document.

I'm unclear about how this should be fixed correctly. I simply added the type in the one-line diff below:

```diff
--- ssm.py	2022-06-10 18:28:07.450170572 +0000
+++ ssm.py.new	2022-06-10 18:28:22.186197102 +0000
@@ -112,7 +112,7 @@
 
     props: PropsDictType = {
         "Attachments": ([AttachmentsSource], False),
-        "Content": (dict, True),
+        "Content": ((dict, str), True),
         "DocumentFormat": (str, False),
         "DocumentType": (str, False),
         "Name": (str, False),
```

Pull request:
Allow json/yaml strings for SSM Document.Content property
Fixes #2053

@pmau could you review and verify this fixes your use case?

Patch:
diff --git a/troposphere/ssm.py b/troposphere/ssm.py
index 2a2a11dd..dd223c43 100644
--- a/troposphere/ssm.py
+++ b/troposphere/ssm.py
@@ -14,6 +14,7 @@ from .validators.ssm import (
     notification_type,
     operating_system,
     task_type,
+    validate_document_content,
     validate_json_checker,
     validate_s3_bucket_name,
 )
@@ -112,7 +113,7 @@ class Document(AWSObject):
 
     props: PropsDictType = {
         "Attachments": ([AttachmentsSource], False),
-        "Content": (dict, True),
+        "Content": (validate_document_content, True),
         "DocumentFormat": (str, False),
         "DocumentType": (str, False),
         "Name": (str, False),
diff --git a/troposphere/validators/ssm.py b/troposphere/validators/ssm.py
index c6f76ab3..0381cdbb 100644
--- a/troposphere/validators/ssm.py
+++ b/troposphere/validators/ssm.py
@@ -7,6 +7,38 @@
 from . import json_checker, s3_bucket_name
 
 
+def validate_document_content(x):
+    """
+    Property: Document.Content
+    """
+
+    def check_json(x):
+        import json
+
+        try:
+            json.loads(x)
+            return True
+        except json.decoder.JSONDecodeError:
+            return False
+
+    def check_yaml(x):
+        import yaml
+
+        try:
+            yaml.safe_load(x)
+            return True
+        except yaml.composer.ComposerError:
+            return False
+
+    if isinstance(x, dict):
+        return x
+
+    if check_json(x) or check_yaml(x):
+        return x
+
+    raise ValueError("Content must be one of dict or json/yaml string")
+
+
 def validate_json_checker(x):
     """
     Property: MaintenanceWindowLambdaParameters.Payload


Test patch:
diff --git a/tests/test_ssm.py b/tests/test_ssm.py
new file mode 100644
index 00000000..edb16eed
--- /dev/null
+++ b/tests/test_ssm.py
@@ -0,0 +1,19 @@
+import unittest
+
+from troposphere.ssm import Document
+
+
+class TestQueue(unittest.TestCase):
+    def test_Document(self):
+        # dict
+        Document("title", Content={"foo": "bar"}).validate()
+        # Valid yaml and json
+        Document("title", Content='{"foo": "bar"}').validate()
+        Document("title", Content="{foo: bar}").validate()
+        # Invalid json/yaml
+        with self.assertRaises(ValueError):
+            Document("title", Content="*foo").validate()
+
+
+if __name__ == "__main__":
+    unittest.main()