Repository: cloudtools/troposphere

Issue:
AWS Control Tower CloudFormation spec does not match docs
EnabledControl.EnabledControlParameter Value type according to docs is 'array, string, number, object, or boolean', but the spec simply states 'Json', which does not allow the values required.

https://docs.aws.amazon.com/AWSCloudFormation/latest/TemplateReference/aws-properties-controltower-enabledcontrol-enabledcontrolparameter.html#cfn-controltower-enabledcontrol-enabledcontrolparameter-value

EnabledBaseline.Parameter Value type according to docs is String, but the spec here also states 'Json', which results in an invalid CloudFormation template.

https://docs.aws.amazon.com/AWSCloudFormation/latest/TemplateReference/aws-properties-controltower-enabledbaseline-parameter.html#cfn-controltower-enabledbaseline-parameter-value

I haven't found how to tell AWS about this, but at least I know how to patch it in troposphere, so PR is incoming 😉


Discussion:

Patch:
diff --git a/examples/ControlTower.py b/examples/ControlTower.py
new file mode 100644
index 000000000..bb60525e1
--- /dev/null
+++ b/examples/ControlTower.py
@@ -0,0 +1,62 @@
+from troposphere import Template
+from troposphere.controltower import (
+    EnabledBaseline,
+    EnabledControl,
+    EnabledControlParameter,
+    Parameter,
+)
+
+template = Template()
+template.set_version("2010-09-09")
+
+parameters = [
+    Parameter(
+        Key="IdentityCenterEnabledBaselineArn",
+        Value="arn:aws:controltower:eu-west-1:123456789012:enabledbaseline/A1B2C3D4E5F6G7H8I",
+    ),
+]
+
+enabled_baseline = EnabledBaseline(
+    "DemoEnabledBaseline",
+    BaselineIdentifier="arn:aws:controltower:eu-west-1::baseline/A1B2C3D4E5F6G7H8I",
+    BaselineVersion="4.0",
+    Parameters=parameters,
+    TargetIdentifier="arn:aws:organizations::123456789012:ou/o-a1b2c3d4e5/ou-1234-a1b2c3d4",
+)
+template.add_resource(enabled_baseline)
+
+enabled_control_parameters = [
+    # array, string, number, object, or boolean
+    EnabledControlParameter(
+        Key="ArrayParameter",
+        Value=["string"],
+    ),
+    EnabledControlParameter(
+        Key="StringParameter",
+        Value="string",
+    ),
+    EnabledControlParameter(
+        Key="NumberParameter",
+        Value=42,
+    ),
+    EnabledControlParameter(
+        Key="ObjectParameter",
+        Value={
+            "key1": "value1",
+        },
+    ),
+    EnabledControlParameter(
+        Key="BooleanParameter",
+        Value=True,
+    ),
+]
+
+enabled_control = EnabledControl(
+    "DemoEnabledControl",
+    ControlIdentifier="arn:aws:controltower:us-east-2::control/EXAMPLE_NAME",
+    TargetIdentifier="arn:aws:organizations::01234567890:ou/o-EXAMPLE/ou-zzxx-zzx0zzz2",
+    Parameters=enabled_control_parameters,
+)
+template.add_resource(enabled_control)
+
+print(template.to_json())
diff --git a/scripts/gen.py b/scripts/gen.py
index 3cfbb2730..62fa95ef5 100755
--- a/scripts/gen.py
+++ b/scripts/gen.py
@@ -22,7 +22,7 @@
 # file. In other words, a type such as AWS::Batch::JobDefinition will be
 # put into the batch.py file.
 #
-# Since there are usually discrepencies in the docs or spec files plus the
+# Since there are usually discrepancies in the docs or spec files plus the
 # need for validation routines to be included there is additional processing
 # done to the Resource Specification file and validators substituted into
 # the code.
diff --git a/scripts/patches/controltower.py b/scripts/patches/controltower.py
new file mode 100644
index 000000000..98f5d5f7d
--- /dev/null
+++ b/scripts/patches/controltower.py
@@ -0,0 +1,16 @@
+patches = [
+    {
+        "op": "replace",
+        "path": "/PropertyTypes/AWS::ControlTower::EnabledBaseline.Parameter/Properties/Value/PrimitiveType",
+        "value": "String",
+    },
+    {
+        "op": "add",
+        "path": "/PropertyTypes/AWS::ControlTower::EnabledControl.EnabledControlParameter/Properties/Value/Type",
+        "value": "object",
+    },
+    {
+        "op": "remove",
+        "path": "/PropertyTypes/AWS::ControlTower::EnabledControl.EnabledControlParameter/Properties/Value/PrimitiveType",
+    },
+]
diff --git a/troposphere/controltower.py b/troposphere/controltower.py
index 3b7b1bf69..d6ff9c737 100644
--- a/troposphere/controltower.py
+++ b/troposphere/controltower.py
@@ -16,7 +16,7 @@ class Parameter(AWSProperty):
 
     props: PropsDictType = {
         "Key": (str, False),
-        "Value": (dict, False),
+        "Value": (str, False),
     }
 
 
@@ -43,7 +43,7 @@ class EnabledControlParameter(AWSProperty):
 
     props: PropsDictType = {
         "Key": (str, True),
-        "Value": (dict, True),
+        "Value": (object, True),
     }
 
 


Test patch:
diff --git a/tests/examples_output/ControlTower.template b/tests/examples_output/ControlTower.template
new file mode 100644
index 000000000..4ac64671c
--- /dev/null
+++ b/tests/examples_output/ControlTower.template
@@ -0,0 +1,52 @@
+{
+ "AWSTemplateFormatVersion": "2010-09-09",
+ "Resources": {
+  "DemoEnabledBaseline": {
+   "Properties": {
+    "BaselineIdentifier": "arn:aws:controltower:eu-west-1::baseline/A1B2C3D4E5F6G7H8I",
+    "BaselineVersion": "4.0",
+    "Parameters": [
+     {
+      "Key": "IdentityCenterEnabledBaselineArn",
+      "Value": "arn:aws:controltower:eu-west-1:123456789012:enabledbaseline/A1B2C3D4E5F6G7H8I"
+     }
+    ],
+    "TargetIdentifier": "arn:aws:organizations::123456789012:ou/o-a1b2c3d4e5/ou-1234-a1b2c3d4"
+   },
+   "Type": "AWS::ControlTower::EnabledBaseline"
+  },
+  "DemoEnabledControl": {
+   "Properties": {
+    "ControlIdentifier": "arn:aws:controltower:us-east-2::control/EXAMPLE_NAME",
+    "Parameters": [
+     {
+      "Key": "ArrayParameter",
+      "Value": [
+       "string"
+      ]
+     },
+     {
+      "Key": "StringParameter",
+      "Value": "string"
+     },
+     {
+      "Key": "NumberParameter",
+      "Value": 42
+     },
+     {
+      "Key": "ObjectParameter",
+      "Value": {
+       "key1": "value1"
+      }
+     },
+     {
+      "Key": "BooleanParameter",
+      "Value": true
+     }
+    ],
+    "TargetIdentifier": "arn:aws:organizations::01234567890:ou/o-EXAMPLE/ou-zzxx-zzx0zzz2"
+   },
+   "Type": "AWS::ControlTower::EnabledControl"
+  }
+ }
+}
diff --git a/tests/test_controltower.py b/tests/test_controltower.py
new file mode 100644
index 000000000..a887b9458
--- /dev/null
+++ b/tests/test_controltower.py
@@ -0,0 +1,22 @@
+import unittest
+
+from troposphere.controltower import Parameter
+
+
+class TestControltower(unittest.TestCase):
+    def test_Parameter(self):
+        Parameter(
+            Key="foo",
+            Value="bar",
+        ).to_dict()
+
+    def test_invalid_Parameter_Value_Dict(self):
+        with self.assertRaises(TypeError):
+            Parameter(
+                Key="foo",
+                Value={"foo": "bar"},
+            ).to_dict()
+
+
+if __name__ == "__main__":
+    unittest.main()