Repository: localstack/localstack

Issue:
CloudFormation change-set fails if elements are unchanged
I can reproduce this with DynamoDB and S3. For example, this file as `template.json`:
```json
{
  "AWSTemplateFormatVersion": "2010-09-09",
  "Transform": "AWS::Serverless-2016-10-31",
  "Resources": {
    "S3Setup": {
      "Type": "AWS::S3::Bucket",
      "Properties": {
        "BucketName": "testbucket"
      }
    }
  }
}
```
Apply using
`awslocal cloudformation create-stack --stack-name local --template-body file://template.json` -> 200 OK
Immediately followed by
`awslocal cloudformation create-change-set --stack-name local --change-set-name nochanges --template-body file://template.json` -> Error
Errors happen if stack has multiple objects and any one has no changes, which makes it impossible to update the stack via cloudformation.

Here's a DynamoDB example for which it happens as well:
```json
{
  "AWSTemplateFormatVersion": "2010-09-09",
  "Transform": "AWS::Serverless-2016-10-31",
  "Resources": {
    "DynamoSample": {
      "Type": "AWS::DynamoDB::Table",
      "Properties": {
        "TableName": "ExampleTable",
        "AttributeDefinitions": [
          {
            "AttributeName": "name",
            "AttributeType": "S"
          }
        ],
        "KeySchema": [
          {
            "AttributeName": "name",
            "KeyType": "HASH"
          }
        ],
        "ProvisionedThroughput": {
          "ReadCapacityUnits": 1,
          "WriteCapacityUnits": 1
        }
      }
    }
  }
}
```



Discussion:

Resolving patch:
diff --git a/localstack/services/cloudformation/cloudformation_starter.py b/localstack/services/cloudformation/cloudformation_starter.py
--- a/localstack/services/cloudformation/cloudformation_starter.py
+++ b/localstack/services/cloudformation/cloudformation_starter.py
@@ -218,9 +218,8 @@ def _parse_and_create_resource(logical_id, resource_json, resources_map, region_
             # This resource is either not deployable or already exists. Check if it can be updated
             if not template_deployer.is_updateable(logical_id, resource_wrapped, stack_name):
                 LOG.debug('Resource %s need not be deployed: %s %s' % (logical_id, resource_json, bool(resource)))
-                # Note: leave this check here, otherwise breaks things (e.g., serverless lambda deploy fails)
-                if resource:
-                    return resource
+                # Return if this resource already exists and cannot be updated
+                return resource
 
         # fix resource ARNs, make sure to convert account IDs 000000000000 to 123456789012
         resource_json_arns_fixed = clone(json_safe(convert_objs_to_ids(resource_json)))
@@ -241,10 +240,6 @@ def _parse_and_create_resource(logical_id, resource_json, resources_map, region_
         update_resource_name(resource, resource_json)
         LOG.debug('Deploying CloudFormation resource (update=%s): %s' % (update, resource_json))
 
-        if not should_be_created:
-            # skip the parts below for update requests
-            return resource
-
         try:
             CURRENTLY_UPDATING_RESOURCES[resource_hash_key] = True
             deploy_func = template_deployer.update_resource if update else template_deployer.deploy_resource
@@ -252,6 +247,10 @@ def _parse_and_create_resource(logical_id, resource_json, resources_map, region_
         finally:
             CURRENTLY_UPDATING_RESOURCES[resource_hash_key] = False
 
+        if not should_be_created:
+            # skip the parts below for update requests
+            return resource
+
         def find_id(resource):
             """ Find ID of the given resource. """
             if not resource:


Test patch:
diff --git a/tests/integration/test_cloudformation.py b/tests/integration/test_cloudformation.py
--- a/tests/integration/test_cloudformation.py
+++ b/tests/integration/test_cloudformation.py
@@ -2,7 +2,7 @@
 import unittest
 from localstack.constants import TEST_AWS_ACCOUNT_ID
 from localstack.utils.aws import aws_stack
-from localstack.utils.common import load_file, retry
+from localstack.utils.common import load_file, retry, short_uid
 from localstack.utils.cloudformation import template_deployer
 from botocore.exceptions import ClientError
 from botocore.parsers import ResponseParserError
@@ -10,9 +10,15 @@
 THIS_FOLDER = os.path.dirname(os.path.realpath(__file__))
 TEST_TEMPLATE_1 = os.path.join(THIS_FOLDER, 'templates', 'template1.yaml')
 TEST_TEMPLATE_2 = os.path.join(THIS_FOLDER, 'templates', 'template2.yaml')
-
-TEST_STACK_NAME = 'test-cf-stack-1'
-TEST_STACK_NAME_2 = 'test-cf-stack-2'
+TEST_TEMPLATE_3 = """
+AWSTemplateFormatVersion: 2010-09-09
+Transform: AWS::Serverless-2016-10-31
+Resources:
+  S3Setup:
+    Type: AWS::S3::Bucket
+    Properties:
+      BucketName: test-%s
+""" % short_uid()
 
 
 def bucket_exists(name):
@@ -97,11 +103,12 @@ def test_apply_template(self):
         template = template_deployer.template_to_json(load_file(TEST_TEMPLATE_1))
 
         # deploy template
-        cloudformation.create_stack(StackName=TEST_STACK_NAME, TemplateBody=template)
+        stack_name = 'stack-%s' % short_uid()
+        cloudformation.create_stack(StackName=stack_name, TemplateBody=template)
 
         # wait for deployment to finish
         def check_stack():
-            stack = get_stack_details(TEST_STACK_NAME)
+            stack = get_stack_details(stack_name)
             self.assertEqual(stack['StackStatus'], 'CREATE_COMPLETE')
 
         retry(check_stack, retries=3, sleep=2)
@@ -115,7 +122,7 @@ def check_stack():
         # assert that stream has been created
         assert stream_exists('cf-test-stream-1')
         # assert that queue has been created
-        resource = describe_stack_resource(TEST_STACK_NAME, 'SQSQueueNoNameProperty')
+        resource = describe_stack_resource(stack_name, 'SQSQueueNoNameProperty')
         assert queue_exists(resource['PhysicalResourceId'])
 
         # assert that topic tags have been created
@@ -161,15 +168,16 @@ def test_validate_invalid_json_template_should_fail(self):
     def test_list_stack_resources_returns_queue_urls(self):
         cloudformation = aws_stack.connect_to_resource('cloudformation')
         template = template_deployer.template_to_json(load_file(TEST_TEMPLATE_2))
-        cloudformation.create_stack(StackName=TEST_STACK_NAME_2, TemplateBody=template)
+        stack_name = 'stack-%s' % short_uid()
+        cloudformation.create_stack(StackName=stack_name, TemplateBody=template)
 
         def check_stack():
-            stack = get_stack_details(TEST_STACK_NAME_2)
+            stack = get_stack_details(stack_name)
             self.assertEqual(stack['StackStatus'], 'CREATE_COMPLETE')
 
         retry(check_stack, retries=3, sleep=2)
 
-        stack_summaries = list_stack_resources(TEST_STACK_NAME_2)
+        stack_summaries = list_stack_resources(stack_name)
         queue_urls = get_queue_urls()
         topic_arns = get_topic_arns()
 
@@ -180,3 +188,16 @@ def check_stack():
         stack_topics = [r for r in stack_summaries if r['ResourceType'] == 'AWS::SNS::Topic']
         for resource in stack_topics:
             self.assertIn(resource['PhysicalResourceId'], topic_arns)
+
+    def test_create_change_set(self):
+        cloudformation = aws_stack.connect_to_service('cloudformation')
+
+        # deploy template
+        stack_name = 'stack-%s' % short_uid()
+        cloudformation.create_stack(StackName=stack_name, TemplateBody=TEST_TEMPLATE_3)
+
+        # create change set with the same template (no changes)
+        response = cloudformation.create_change_set(StackName=stack_name,
+            TemplateBody=TEST_TEMPLATE_3, ChangeSetName='nochanges')
+        self.assertIn(':%s:changeSet/nochanges/' % TEST_AWS_ACCOUNT_ID, response['Id'])
+        self.assertIn(':%s:stack/' % TEST_AWS_ACCOUNT_ID, response['StackId'])