Allow in-place updates for all compatible types

We previously only allowed a resource to update in-place when the 'type'
field in the old and new templates matched. In an age of environment type
mappings, this is incorrect. Instead, allow a resource to be updated
in-place when the old and new resource plugins are of the same class, since
this is actually what determines whether handle_update is capable of
correctly updating the resource in-place.

There is a special-case for TemplateResource as we currently subclass it
for each template type, but all TemplateResource subclasses are in fact
capable of converting between each other via a stack update.

Change-Id: Iba4abf5efd9737ca6a17d8c91d5c54ab6d3f12e0
Co-Authored-By: Zane Bitter <zbitter@redhat.com>
Closes-Bug: #1508115
diff --git a/functional/test_create_update.py b/functional/test_create_update.py
index 153eb9f..1aef6b6 100644
--- a/functional/test_create_update.py
+++ b/functional/test_create_update.py
@@ -331,6 +331,105 @@
         self.assertEqual(nested_resources,
                          self.list_resources(nested_identifier))
 
+    def test_stack_update_alias_type(self):
+        env = {'resource_registry':
+               {'My::TestResource': 'OS::Heat::RandomString',
+                'My::TestResource2': 'OS::Heat::RandomString'}}
+        stack_identifier = self.stack_create(
+            template=self.provider_template,
+            environment=env
+        )
+        p_res = self.client.resources.get(stack_identifier, 'test1')
+        self.assertEqual('My::TestResource', p_res.resource_type)
+
+        initial_resources = {'test1': 'My::TestResource'}
+        self.assertEqual(initial_resources,
+                         self.list_resources(stack_identifier))
+        res = self.client.resources.get(stack_identifier, 'test1')
+        # Modify the type of the resource alias to My::TestResource2
+        tmpl_update = copy.deepcopy(self.provider_template)
+        tmpl_update['resources']['test1']['type'] = 'My::TestResource2'
+        self.update_stack(stack_identifier, tmpl_update, environment=env)
+        res_a = self.client.resources.get(stack_identifier, 'test1')
+        self.assertEqual(res.physical_resource_id, res_a.physical_resource_id)
+        self.assertEqual(res.attributes['value'], res_a.attributes['value'])
+
+    def test_stack_update_alias_changes(self):
+        env = {'resource_registry':
+               {'My::TestResource': 'OS::Heat::RandomString'}}
+        stack_identifier = self.stack_create(
+            template=self.provider_template,
+            environment=env
+        )
+        p_res = self.client.resources.get(stack_identifier, 'test1')
+        self.assertEqual('My::TestResource', p_res.resource_type)
+
+        initial_resources = {'test1': 'My::TestResource'}
+        self.assertEqual(initial_resources,
+                         self.list_resources(stack_identifier))
+        res = self.client.resources.get(stack_identifier, 'test1')
+        # Modify the resource alias to point to a different type
+        env = {'resource_registry':
+               {'My::TestResource': 'OS::Heat::TestResource'}}
+        self.update_stack(stack_identifier, template=self.provider_template,
+                          environment=env)
+        res_a = self.client.resources.get(stack_identifier, 'test1')
+        self.assertNotEqual(res.physical_resource_id,
+                            res_a.physical_resource_id)
+
+    def test_stack_update_provider_type(self):
+        template = _change_rsrc_properties(
+            test_template_one_resource, ['test1'],
+            {'value': 'test_provider_template'})
+        files = {'provider.template': json.dumps(template)}
+        env = {'resource_registry':
+               {'My::TestResource': 'provider.template',
+                'My::TestResource2': 'provider.template'}}
+        stack_identifier = self.stack_create(
+            template=self.provider_template,
+            files=files,
+            environment=env
+        )
+        p_res = self.client.resources.get(stack_identifier, 'test1')
+        self.assertEqual('My::TestResource', p_res.resource_type)
+
+        initial_resources = {'test1': 'My::TestResource'}
+        self.assertEqual(initial_resources,
+                         self.list_resources(stack_identifier))
+
+        # Prove the resource is backed by a nested stack, save the ID
+        nested_identifier = self.assert_resource_is_a_stack(stack_identifier,
+                                                            'test1')
+        nested_id = nested_identifier.split('/')[-1]
+
+        # Then check the expected resources are in the nested stack
+        nested_resources = {'test1': 'OS::Heat::TestResource'}
+        self.assertEqual(nested_resources,
+                         self.list_resources(nested_identifier))
+        n_res = self.client.resources.get(nested_identifier, 'test1')
+
+        # Modify the type of the provider resource to My::TestResource2
+        tmpl_update = copy.deepcopy(self.provider_template)
+        tmpl_update['resources']['test1']['type'] = 'My::TestResource2'
+        self.update_stack(stack_identifier, tmpl_update,
+                          environment=env, files=files)
+        p_res = self.client.resources.get(stack_identifier, 'test1')
+        self.assertEqual('My::TestResource2', p_res.resource_type)
+
+        # Parent resources should be unchanged and the nested stack
+        # should have been updated in-place without replacement
+        self.assertEqual({u'test1': u'My::TestResource2'},
+                         self.list_resources(stack_identifier))
+        rsrc = self.client.resources.get(stack_identifier, 'test1')
+        self.assertEqual(rsrc.physical_resource_id, nested_id)
+
+        # Then check the expected resources are in the nested stack
+        self.assertEqual(nested_resources,
+                         self.list_resources(nested_identifier))
+        n_res2 = self.client.resources.get(nested_identifier, 'test1')
+        self.assertEqual(n_res.physical_resource_id,
+                         n_res2.physical_resource_id)
+
     def test_stack_update_provider_group(self):
         """Test two-level nested update."""