diff options
author | Jenkins <jenkins@review.openstack.org> | 2016-08-25 00:39:02 +0000 |
---|---|---|
committer | Gerrit Code Review <review@openstack.org> | 2016-08-25 00:39:02 +0000 |
commit | 74506411d5ea5c8fdd590b6d2fedad48426d074c (patch) | |
tree | 98f404ecbfee466b5b43732605fa7835d973f7e9 | |
parent | d86ea51bbddddd7ea5f0c2313a4fe42fe36bc336 (diff) | |
parent | 59476f63bc1d5da584d5d49cae44caa630a8efae (diff) | |
download | heat-74506411d5ea5c8fdd590b6d2fedad48426d074c.tar.gz |
Merge "Support condition for resource"
-rw-r--r-- | doc/source/template_guide/hot_spec.rst | 30 | ||||
-rw-r--r-- | heat/common/exception.py | 4 | ||||
-rw-r--r-- | heat/engine/cfn/template.py | 49 | ||||
-rw-r--r-- | heat/engine/hot/template.py | 35 | ||||
-rw-r--r-- | heat/engine/rsrc_defn.py | 16 | ||||
-rw-r--r-- | heat/engine/stack.py | 3 | ||||
-rw-r--r-- | heat/engine/template.py | 14 | ||||
-rw-r--r-- | heat/engine/template_common.py | 33 | ||||
-rw-r--r-- | heat/tests/test_stack_update.py | 56 | ||||
-rw-r--r-- | heat/tests/test_template.py | 44 | ||||
-rw-r--r-- | heat_integrationtests/functional/test_conditions.py | 131 |
11 files changed, 395 insertions, 20 deletions
diff --git a/doc/source/template_guide/hot_spec.rst b/doc/source/template_guide/hot_spec.rst index ff9a1e243..e45a0a3a0 100644 --- a/doc/source/template_guide/hot_spec.rst +++ b/doc/source/template_guide/hot_spec.rst @@ -635,6 +635,7 @@ the following syntax update_policy: <update policy> deletion_policy: <deletion policy> external_id: <external resource ID> + condition: <condition name> resource ID A resource ID which must be unique within the ``resources`` section of the @@ -682,6 +683,13 @@ external_id update that attribute. Also resource won't be deleted by heat when stack is deleted. +condition + Condition for the resource. Which decides whether to create the + resource or not. + This attribute is optional. + + Note: Support ``condition`` for resource is added in the Newton version. + Depending on the type of resource, the resource block might include more resource specific data. @@ -825,6 +833,28 @@ An example of conditions section definition cd2: {get_param: param1} cd3: {equals: [{get_param: param2}, "yes"]} +The example below shows how to associate condition with resources + +.. code-block:: yaml + + parameters: + env_type: + default: test + type: string + conditions: + create_prod_res: {equals : [{get_param: env_type}, "prod"] + resources: + volume: + type: OS::Cinder::Volume + condition: create_prod_res + properties: + size: 1 + +The 'create_prod_res' condition evaluates to true if the 'env_type' +parameter is equal to 'prod'. In the above sample template, the 'volume' +resource is associated with the 'create_prod_res' condition. Therefore, +the 'volume' resource is created only if the 'env_type' is equal to 'prod'. + .. _hot_spec_intrinsic_functions: diff --git a/heat/common/exception.py b/heat/common/exception.py index 836f3ac26..5ef7fe0f7 100644 --- a/heat/common/exception.py +++ b/heat/common/exception.py @@ -138,6 +138,10 @@ class InvalidConditionDefinition(HeatException): "invalid: %(definition)s") +class InvalidConditionReference(HeatException): + msg_fmt = _('Invalid condition "%(cd)s" (in %(path)s)') + + class ImmutableParameterModified(HeatException): msg_fmt = _("The following parameters are immutable and may not be " "updated: %(keys)s") diff --git a/heat/engine/cfn/template.py b/heat/engine/cfn/template.py index ff69f672f..3ec72b35c 100644 --- a/heat/engine/cfn/template.py +++ b/heat/engine/cfn/template.py @@ -50,6 +50,7 @@ class CfnTemplateBase(template_common.CommonTemplate): 'DeletionPolicy', 'UpdatePolicy', 'Description', ) + extra_rsrc_defn = () functions = { 'Fn::FindInMap': cfn_funcs.FindInMap, 'Fn::GetAZs': cfn_funcs.GetAZs, @@ -66,6 +67,13 @@ class CfnTemplateBase(template_common.CommonTemplate): 'Snapshot': rsrc_defn.ResourceDefinition.SNAPSHOT } + HOT_TO_CFN_RES_ATTRS = {'type': RES_TYPE, + 'properties': RES_PROPERTIES, + 'metadata': RES_METADATA, + 'depends_on': RES_DEPENDS_ON, + 'deletion_policy': RES_DELETION_POLICY, + 'update_policy': RES_UPDATE_POLICY} + def __getitem__(self, section): """Get the relevant section in the template.""" if section not in self.SECTIONS: @@ -130,25 +138,24 @@ class CfnTemplateBase(template_common.CommonTemplate): 'description': data.get(self.RES_DESCRIPTION) or '' } + for key in self.extra_rsrc_defn: + kwargs[key.lower()] = data.get(key) + defn = rsrc_defn.ResourceDefinition(name, **kwargs) return name, defn - return dict(rsrc_defn_item(name, data) - for name, data in resources.items()) + return dict( + rsrc_defn_item(name, data) + for name, data in resources.items() if self.get_res_condition( + stack, data, name)) def add_resource(self, definition, name=None): if name is None: name = definition.name hot_tmpl = definition.render_hot() - HOT_TO_CFN_ATTRS = {'type': self.RES_TYPE, - 'properties': self.RES_PROPERTIES, - 'metadata': self.RES_METADATA, - 'depends_on': self.RES_DEPENDS_ON, - 'deletion_policy': self.RES_DELETION_POLICY, - 'update_policy': self.RES_UPDATE_POLICY} - - cfn_tmpl = dict((HOT_TO_CFN_ATTRS[k], v) for k, v in hot_tmpl.items()) + cfn_tmpl = dict((self.HOT_TO_CFN_RES_ATTRS[k], v) + for k, v in hot_tmpl.items()) if len(cfn_tmpl.get(self.RES_DEPENDS_ON, [])) == 1: cfn_tmpl[self.RES_DEPENDS_ON] = cfn_tmpl[self.RES_DEPENDS_ON][0] @@ -160,9 +167,16 @@ class CfnTemplateBase(template_common.CommonTemplate): class CfnTemplate(CfnTemplateBase): + CONDITION = 'Condition' CONDITIONS = 'Conditions' SECTIONS = CfnTemplateBase.SECTIONS + (CONDITIONS,) + RES_CONDITION = CONDITION + _RESOURCE_KEYS = CfnTemplateBase._RESOURCE_KEYS + (RES_CONDITION,) + HOT_TO_CFN_RES_ATTRS = CfnTemplateBase.HOT_TO_CFN_RES_ATTRS + HOT_TO_CFN_RES_ATTRS.update({'condition': RES_CONDITION}) + + extra_rsrc_defn = CfnTemplateBase.extra_rsrc_defn + (RES_CONDITION,) condition_functions = { 'Fn::Equals': hot_funcs.Equals, 'Ref': cfn_funcs.ParamRef, @@ -175,10 +189,25 @@ class CfnTemplate(CfnTemplateBase): self._parser_condition_functions = dict( (n, function.Invalid) for n in self.functions) self._parser_condition_functions.update(self.condition_functions) + self.merge_sections = [self.PARAMETERS, self.CONDITIONS] def get_condition_definitions(self): return self[self.CONDITIONS] + def has_condition_section(self, snippet): + if snippet and self.CONDITION in snippet: + return True + + return False + + def validate_resource_definition(self, name, data): + super(CfnTemplate, self).validate_resource_definition(name, data) + + self.validate_resource_key_type( + self.RES_CONDITION, + (six.string_types, bool), + 'string or boolean', self._RESOURCE_KEYS, name, data) + class HeatTemplate(CfnTemplateBase): functions = { diff --git a/heat/engine/hot/template.py b/heat/engine/hot/template.py index 2ff931efe..a028511eb 100644 --- a/heat/engine/hot/template.py +++ b/heat/engine/hot/template.py @@ -220,9 +220,15 @@ class HOTemplate20130523(template_common.CommonTemplate): def resource_definitions(self, stack): resources = self.t.get(self.RESOURCES) or {} - parsed_resources = self.parse(stack, resources) - return dict((name, self.rsrc_defn_from_snippet(name, data)) - for name, data in parsed_resources.items()) + + def rsrc_defn_from_snippet(name, snippet): + data = self.parse(stack, snippet) + return self.rsrc_defn_from_snippet(name, data) + + return dict( + (name, rsrc_defn_from_snippet(name, data)) + for name, data in resources.items() if self.get_res_condition( + stack, data, name)) @classmethod def rsrc_defn_from_snippet(cls, name, data): @@ -380,6 +386,9 @@ class HOTemplate20160408(HOTemplate20151015): class HOTemplate20161014(HOTemplate20160408): + + CONDITION = 'condition' + RES_CONDITION = CONDITION CONDITIONS = 'conditions' SECTIONS = HOTemplate20160408.SECTIONS + (CONDITIONS,) @@ -387,12 +396,19 @@ class HOTemplate20161014(HOTemplate20160408): _CFN_TO_HOT_SECTIONS = HOTemplate20160408._CFN_TO_HOT_SECTIONS _CFN_TO_HOT_SECTIONS.update({ cfn_template.CfnTemplate.CONDITIONS: CONDITIONS}) + _RESOURCE_KEYS = HOTemplate20160408._RESOURCE_KEYS _EXT_KEY = (RES_EXTERNAL_ID,) = ('external_id',) _RESOURCE_KEYS += _EXT_KEY + _RESOURCE_KEYS += (RES_CONDITION,) + _RESOURCE_HOT_TO_CFN_ATTRS = HOTemplate20160408._RESOURCE_HOT_TO_CFN_ATTRS _RESOURCE_HOT_TO_CFN_ATTRS.update({RES_EXTERNAL_ID: None}) - extra_rsrc_defn = HOTemplate20160408.extra_rsrc_defn + (RES_EXTERNAL_ID,) + _RESOURCE_HOT_TO_CFN_ATTRS.update( + {CONDITION: cfn_template.CfnTemplate.CONDITION}) + + extra_rsrc_defn = HOTemplate20160408.extra_rsrc_defn + ( + RES_EXTERNAL_ID, RES_CONDITION,) deletion_policies = { 'Delete': rsrc_defn.ResourceDefinition.DELETE, @@ -458,6 +474,7 @@ class HOTemplate20161014(HOTemplate20160408): else: self._parser_condition_functions[n] = f self._parser_condition_functions.update(self.condition_functions) + self.merge_sections = [self.PARAMETERS, self.CONDITIONS] def get_condition_definitions(self): return self[self.CONDITIONS] @@ -470,3 +487,13 @@ class HOTemplate20161014(HOTemplate20160408): self.RES_EXTERNAL_ID, (six.string_types, function.Function), 'string', self._RESOURCE_KEYS, name, data) + self.validate_resource_key_type( + self.RES_CONDITION, + (six.string_types, bool), + 'string or boolean', self._RESOURCE_KEYS, name, data) + + def has_condition_section(self, snippet): + if snippet and self.CONDITION in snippet: + return True + + return False diff --git a/heat/engine/rsrc_defn.py b/heat/engine/rsrc_defn.py index 4e304e190..9b0d9f5cd 100644 --- a/heat/engine/rsrc_defn.py +++ b/heat/engine/rsrc_defn.py @@ -70,7 +70,7 @@ class ResourceDefinitionCore(object): def __init__(self, name, resource_type, properties=None, metadata=None, depends=None, deletion_policy=None, update_policy=None, - description=None, external_id=None): + description=None, external_id=None, condition=None): """Initialise with the parsed definition of a resource. Any intrinsic functions present in any of the sections should have been @@ -85,6 +85,7 @@ class ResourceDefinitionCore(object): :param update_policy: A dictionary of supplied update policies :param description: A string describing the resource :param external_id: A uuid of an external resource + :param condition: A condition name associated with the resource """ self.name = name self.resource_type = resource_type @@ -95,6 +96,7 @@ class ResourceDefinitionCore(object): self._deletion_policy = deletion_policy self._update_policy = update_policy self._external_id = external_id + self._condition = condition self._hash = hash(self.resource_type) self._rendering = None @@ -132,6 +134,10 @@ class ResourceDefinitionCore(object): self._hash ^= _hash_data(external_id) self._deletion_policy = self.RETAIN + if condition is not None: + assert isinstance(condition, six.string_types) + self._hash ^= hash(condition) + def freeze(self, **overrides): """Return a frozen resource definition, with all functions resolved. @@ -155,7 +161,7 @@ class ResourceDefinitionCore(object): args = ('name', 'resource_type', '_properties', '_metadata', '_depends', '_deletion_policy', '_update_policy', - 'description', '_external_id') + 'description', '_external_id', '_condition') defn = type(self)(**dict(arg_item(a) for a in args)) defn._frozen = True @@ -180,7 +186,8 @@ class ResourceDefinitionCore(object): depends=reparse_snippet(self._depends), deletion_policy=reparse_snippet(self._deletion_policy), update_policy=reparse_snippet(self._update_policy), - external_id=reparse_snippet(self._external_id)) + external_id=reparse_snippet(self._external_id), + condition=self._condition) def dep_attrs(self, resource_name): """Iterate over attributes of a given resource that this references. @@ -272,6 +279,7 @@ class ResourceDefinitionCore(object): 'update_policy': '_update_policy', 'depends_on': '_depends', 'external_id': '_external_id', + 'condition': '_condition' } def rawattrs(): @@ -336,7 +344,7 @@ class ResourceDefinitionCore(object): return '='.join([arg_name, repr(getattr(self, '_%s' % arg_name))]) args = ('properties', 'metadata', 'depends', - 'deletion_policy', 'update_policy') + 'deletion_policy', 'update_policy', 'condition') data = { 'classname': type(self).__name__, 'name': repr(self.name), diff --git a/heat/engine/stack.py b/heat/engine/stack.py index b8a074a02..8c6085b08 100644 --- a/heat/engine/stack.py +++ b/heat/engine/stack.py @@ -1456,6 +1456,7 @@ class Stack(collections.Mapping): updater = scheduler.TaskRunner(update_task) self.parameters = newstack.parameters + self.t._conditions = newstack.t.conditions(newstack) self.t.files = newstack.t.files self.t.env = newstack.t.env self.disable_rollback = newstack.disable_rollback @@ -1519,8 +1520,10 @@ class Stack(collections.Mapping): # and new stack resources, we should have user params of both. existing_params.load(newstack.t.env.user_env_as_dict()) self.t.env = existing_params + self.t.merge_snippets(newstack.t) self.t.store(self.context) backup_stack.t.env = existing_params + backup_stack.t.merge_snippets(newstack.t) backup_stack.t.store(self.context) self.store() diff --git a/heat/engine/template.py b/heat/engine/template.py index 663693d8c..b245e7e71 100644 --- a/heat/engine/template.py +++ b/heat/engine/template.py @@ -117,6 +117,8 @@ class Template(collections.Mapping): self.files = files or {} self.maps = self[self.MAPPINGS] self.env = env or environment.Environment({}) + self._conditions = None + self.merge_sections = [self.PARAMETERS] self.version = get_version(self.t, _template_classes.keys()) self.t_digest = None @@ -125,6 +127,14 @@ class Template(collections.Mapping): return Template(copy.deepcopy(self.t, memo), files=self.files, env=self.env) + def merge_snippets(self, other): + for s in self.merge_sections: + if s not in other.t: + continue + if s not in self.t: + self.t[s] = {} + self.t[s].update(other.t[s]) + @classmethod def load(cls, context, template_id, t=None): """Retrieve a Template with the given ID from the database.""" @@ -216,6 +226,10 @@ class Template(collections.Mapping): """Check conditions section.""" pass + def conditions(self, stack): + """Return a dictionary of resolved conditions.""" + return {} + @abc.abstractmethod def resource_definitions(self, stack): """Return a dictionary of ResourceDefinition objects.""" diff --git a/heat/engine/template_common.py b/heat/engine/template_common.py index 9d8ac9295..add12346b 100644 --- a/heat/engine/template_common.py +++ b/heat/engine/template_common.py @@ -103,4 +103,37 @@ class CommonTemplate(template.Template): return result def get_condition_definitions(self): + """Return the condition definitions of template.""" return {} + + def has_condition_section(self, snippet): + return False + + def get_res_condition(self, stack, res_data, res_name): + """Return the value of condition referenced by resource.""" + + path = '' + if self.has_condition_section(res_data): + path = '.'.join([res_name, self.RES_CONDITION]) + + return self.get_condition(res_data, stack, path) + + def get_condition(self, snippet, stack, path=''): + # if specify condition return the resolved condition value, + # true or false if don't specify condition, return true + if self.has_condition_section(snippet): + cd_key = snippet[self.CONDITION] + cds = self.conditions(stack) + if cd_key not in cds: + raise exception.InvalidConditionReference( + cd=cd_key, path=path) + cd = cds[cd_key] + return cd + + return True + + def conditions(self, stack): + if self._conditions is None: + self._conditions = self.resolve_conditions(stack) + + return self._conditions diff --git a/heat/tests/test_stack_update.py b/heat/tests/test_stack_update.py index f414ec773..d78aeb207 100644 --- a/heat/tests/test_stack_update.py +++ b/heat/tests/test_stack_update.py @@ -1979,3 +1979,59 @@ class StackUpdateTest(common.HeatTestCase): # Bres in backup stack should have empty data. self.assertEqual({}, backup['Bres'].data()) self.assertEqual({'test': '42'}, self.stack['Bres'].data()) + + def test_update_failed_with_new_condition(self): + create_a_res = {'equals': [{'get_param': 'create_a_res'}, 'yes']} + create_b_res = {'equals': [{'get_param': 'create_b_res'}, 'yes']} + tmpl = { + 'heat_template_version': '2016-10-14', + 'parameters': { + 'create_a_res': { + 'type': 'string', + 'default': 'yes' + } + }, + 'conditions': { + 'create_a': create_a_res + }, + 'resources': { + 'Ares': {'type': 'GenericResourceType', + 'condition': 'create_a'} + } + } + + test_stack = stack.Stack(self.ctx, + 'test_update_failed_with_new_condition', + template.Template(tmpl)) + test_stack.store() + test_stack.create() + self.assertEqual((stack.Stack.CREATE, stack.Stack.COMPLETE), + test_stack.state) + self.assertIn('Ares', test_stack) + + update_tmpl = copy.deepcopy(tmpl) + update_tmpl['conditions']['create_b'] = create_b_res + update_tmpl['parameters']['create_b_res'] = { + 'type': 'string', + 'default': 'yes' + } + update_tmpl['resources']['Bres'] = { + 'type': 'OS::Heat::TestResource', + 'properties': { + 'value': 'res_b', + 'fail': True + } + } + + stack_with_new_resource = stack.Stack( + self.ctx, + 'test_update_with_new_res_cd', + template.Template(update_tmpl)) + test_stack.update(stack_with_new_resource) + self.assertEqual((stack.Stack.UPDATE, stack.Stack.FAILED), + test_stack.state) + self.assertIn('Bres', test_stack) + self.assertEqual((resource.Resource.CREATE, resource.Resource.FAILED), + test_stack['Bres'].state) + self.assertIn('create_b', test_stack.t.t['conditions']) + self.assertIn('create_b_res', test_stack.t.t['parameters']) diff --git a/heat/tests/test_template.py b/heat/tests/test_template.py index f9fd5f7c7..9f61523b9 100644 --- a/heat/tests/test_template.py +++ b/heat/tests/test_template.py @@ -119,10 +119,11 @@ class TemplatePluginFixture(fixtures.Fixture): class TestTemplatePluginManager(common.HeatTestCase): def test_template_NEW_good(self): class NewTemplate(template.Template): - SECTIONS = (VERSION, MAPPINGS, CONDITIONS) = ( + SECTIONS = (VERSION, MAPPINGS, CONDITIONS, PARAMETERS) = ( 'NEWTemplateFormatVersion', '__undefined__', - 'conditions') + 'conditions', + 'parameters') RESOURCES = 'thingies' def param_schemata(self, param_defaults=None): @@ -282,6 +283,26 @@ class TestTemplateConditionParser(common.HeatTestCase): def setUp(self): super(TestTemplateConditionParser, self).setUp() self.ctx = utils.dummy_context() + t = { + 'heat_template_version': '2016-10-14', + 'parameters': { + 'env_type': { + 'type': 'string', + 'default': 'test' + } + }, + 'conditions': { + 'prod_env': { + 'equals': [{'get_param': 'env_type'}, 'prod']}}, + 'resources': { + 'r1': { + 'type': 'GenericResourceType', + 'condition': 'prod_env' + } + } + } + + self.tmpl = template.Template(t) def test_conditions_with_non_supported_functions(self): t = { @@ -341,6 +362,25 @@ class TestTemplateConditionParser(common.HeatTestCase): self.assertIn('The definition of condition (prod_env) is invalid', six.text_type(ex)) + def test_get_res_condition_invalid(self): + tmpl = copy.deepcopy(self.tmpl) + # test condition name is invalid + tmpl.t['resources']['r1']['condition'] = 'invalid_cd' + stk = stack.Stack(self.ctx, 'test_res_invalid_condition', tmpl) + res_snippet = tmpl.t.get('resources')['r1'] + ex = self.assertRaises(exception.InvalidConditionReference, + tmpl.get_condition, + res_snippet, stk, 'r1.condition') + self.assertIn('Invalid condition "invalid_cd" (in r1.condition)', + six.text_type(ex)) + # test condition name is not string + tmpl.t['resources']['r1']['condition'] = 111 + ex = self.assertRaises(exception.InvalidConditionReference, + tmpl.get_condition, + res_snippet, stk, 'r1.condition') + self.assertIn('Invalid condition "111" (in r1.condition)', + six.text_type(ex)) + class TestTemplateValidate(common.HeatTestCase): diff --git a/heat_integrationtests/functional/test_conditions.py b/heat_integrationtests/functional/test_conditions.py new file mode 100644 index 000000000..d0f49a8d8 --- /dev/null +++ b/heat_integrationtests/functional/test_conditions.py @@ -0,0 +1,131 @@ +# Licensed under the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. You may obtain +# a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. + +from heat_integrationtests.functional import functional_base + + +cfn_template = ''' +AWSTemplateFormatVersion: 2010-09-09 +Parameters: + env_type: + Default: test + Type: String + AllowedValues: [prod, test] +Conditions: + Prod: {"Fn::Equals" : [{Ref: env_type}, "prod"]} +Resources: + test_res: + Type: OS::Heat::TestResource + Properties: + value: test_res + prod_res: + Type: OS::Heat::TestResource + Properties: + value: prod_res + Condition: Prod +''' + +hot_template = ''' +heat_template_version: 2016-10-14 +parameters: + env_type: + default: test + type: string + constraints: + - allowed_values: [prod, test] +conditions: + prod: {equals : [{get_param: env_type}, "prod"]} +resources: + test_res: + type: OS::Heat::TestResource + properties: + value: test_res + prod_res: + type: OS::Heat::TestResource + properties: + value: prod_res + condition: prod +''' + + +class CreateUpdateResConditionTest(functional_base.FunctionalTestsBase): + + def setUp(self): + super(CreateUpdateResConditionTest, self).setUp() + + def res_assert_for_prod(self, resources): + self.assertEqual(2, len(resources)) + res_names = [res.resource_name for res in resources] + self.assertIn('prod_res', res_names) + self.assertIn('test_res', res_names) + + def res_assert_for_test(self, resources): + self.assertEqual(1, len(resources)) + res_names = [res.resource_name for res in resources] + self.assertIn('test_res', res_names) + self.assertNotIn('prod_res', res_names) + + def test_stack_create_update_cfn_template_test_to_prod(self): + stack_identifier = self.stack_create(template=cfn_template) + resources = self.client.resources.list(stack_identifier) + self.res_assert_for_test(resources) + + parms = {'env_type': 'prod'} + self.update_stack(stack_identifier, + template=cfn_template, + parameters=parms) + + resources = self.client.resources.list(stack_identifier) + self.res_assert_for_prod(resources) + + def test_stack_create_update_cfn_template_prod_to_test(self): + parms = {'env_type': 'prod'} + stack_identifier = self.stack_create(template=cfn_template, + parameters=parms) + resources = self.client.resources.list(stack_identifier) + self.res_assert_for_prod(resources) + + parms = {'env_type': 'test'} + self.update_stack(stack_identifier, + template=cfn_template, + parameters=parms) + + resources = self.client.resources.list(stack_identifier) + self.res_assert_for_test(resources) + + def test_stack_create_update_hot_template_test_to_prod(self): + stack_identifier = self.stack_create(template=hot_template) + resources = self.client.resources.list(stack_identifier) + self.res_assert_for_test(resources) + + parms = {'env_type': 'prod'} + self.update_stack(stack_identifier, + template=hot_template, + parameters=parms) + + resources = self.client.resources.list(stack_identifier) + self.res_assert_for_prod(resources) + + def test_stack_create_update_hot_template_prod_to_test(self): + parms = {'env_type': 'prod'} + stack_identifier = self.stack_create(template=hot_template, + parameters=parms) + resources = self.client.resources.list(stack_identifier) + self.res_assert_for_prod(resources) + + parms = {'env_type': 'test'} + self.update_stack(stack_identifier, + template=hot_template, + parameters=parms) + + resources = self.client.resources.list(stack_identifier) + self.res_assert_for_test(resources) |