Repository: getmoto/moto

Issue:
Error when updating Cloudformation stack containing unsupported resources
When updating a Cloudformation stack which contains a resource which contains a resource which isn't supported by the moto implementation then a `TypeError` is generated, complaining about not being able to unpack a `NoneType` object.

The error is thrown at the bottom of this block: https://github.com/spulec/moto/blob/ed861ec/moto/cloudformation/parsing.py#L500-L511

And this line actually generates the error: https://github.com/spulec/moto/blob/ed861ec/moto/cloudformation/parsing.py#L306-L307

The return site expects a tuple and tries to unpack it but as the resource isn't supported the  `parse_resource` function returns `None`: https://github.com/spulec/moto/blob/ed861ec/moto/cloudformation/parsing.py#L264-L270

It looks like the logic needs to be updated to catch that case, which the corresponding create function does: https://github.com/spulec/moto/blob/ed861ec/moto/cloudformation/parsing.py#L294-L296


Discussion:
Thanks; setting this as a bug.

Resolving patch:
diff --git a/moto/cloudformation/parsing.py b/moto/cloudformation/parsing.py
--- a/moto/cloudformation/parsing.py
+++ b/moto/cloudformation/parsing.py
@@ -335,9 +335,12 @@ def parse_and_create_resource(logical_id, resource_json, resources_map, region_n
 
 
 def parse_and_update_resource(logical_id, resource_json, resources_map, region_name):
-    resource_class, resource_json, new_resource_name = parse_resource_and_generate_name(
+    resource_tuple = parse_resource_and_generate_name(
         logical_id, resource_json, resources_map
     )
+    if not resource_tuple:
+        return None
+    resource_class, resource_json, new_resource_name = resource_tuple
     original_resource = resources_map[logical_id]
     if not hasattr(
         resource_class.update_from_cloudformation_json, "__isabstractmethod__"


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
@@ -1,3 +1,4 @@
+import copy
 import json
 from collections import OrderedDict
 from datetime import datetime, timedelta
@@ -239,6 +240,14 @@
     },
 }
 
+dummy_unknown_template = {
+    "AWSTemplateFormatVersion": "2010-09-09",
+    "Description": "Stack 1",
+    "Resources": {
+        "UnknownResource": {"Type": "AWS::Cloud9::EnvironmentEC2", "Properties": {}},
+    },
+}
+
 dummy_template_json = json.dumps(dummy_template)
 dummy_template_special_chars_in_description_json = json.dumps(
     dummy_template_special_chars_in_description
@@ -249,6 +258,7 @@
 dummy_output_template_json = json.dumps(dummy_output_template)
 dummy_import_template_json = json.dumps(dummy_import_template)
 dummy_redrive_template_json = json.dumps(dummy_redrive_template)
+dummy_unknown_template_json = json.dumps(dummy_unknown_template)
 
 
 @mock_cloudformation
@@ -2112,6 +2122,42 @@ def test_create_stack_lambda_and_dynamodb():
     resource_types.should.contain("AWS::Lambda::EventSourceMapping")
 
 
+@mock_cloudformation
+@mock_ec2
+def test_create_and_update_stack_with_unknown_resource():
+    cf_conn = boto3.client("cloudformation", region_name="us-east-1")
+    # Creating a stack with an unknown resource should throw a warning
+    expected_err = "Tried to parse AWS::Cloud9::EnvironmentEC2 but it's not supported by moto's CloudFormation implementation"
+    if settings.TEST_SERVER_MODE:
+        # Can't verify warnings in ServerMode though
+        cf_conn.create_stack(
+            StackName="test_stack", TemplateBody=dummy_unknown_template_json
+        )
+    else:
+        with pytest.warns(UserWarning, match=expected_err):
+            cf_conn.create_stack(
+                StackName="test_stack", TemplateBody=dummy_unknown_template_json
+            )
+
+    # The stack should exist though
+    stacks = cf_conn.describe_stacks()["Stacks"]
+    stacks.should.have.length_of(1)
+    stacks[0].should.have.key("StackName").equal("test_stack")
+
+    # Updating an unknown resource should throw a warning, but not fail
+    new_template = copy.deepcopy(dummy_unknown_template)
+    new_template["Resources"]["UnknownResource"]["Properties"]["Sth"] = "other"
+    if settings.TEST_SERVER_MODE:
+        cf_conn.update_stack(
+            StackName="test_stack", TemplateBody=json.dumps(new_template)
+        )
+    else:
+        with pytest.warns(UserWarning, match=expected_err):
+            cf_conn.update_stack(
+                StackName="test_stack", TemplateBody=json.dumps(new_template)
+            )
+
+
 def get_role_name():
     with mock_iam():
         iam = boto3.client("iam", region_name="us-east-1")