def test_get_stack_order_raises_exception_on_cyclic_dependency(self): stacks = { 'app1': StackConfig({ 'template-url': 'horst.yml', 'parameters': { 'a': '|Ref|app2.id' } }), 'app2': StackConfig({ 'template-url': 'horst.yml', 'parameters': { 'a': '|Ref|app3.id' } }), 'app3': StackConfig({ 'template-url': 'horst.yml', 'parameters': { 'a': '|Ref|app1.id' } }) } with self.assertRaises(CyclicDependencyException): DependencyResolver.get_stack_order(stacks)
def delete_stacks(self): existing_stacks = self.cfn.get_stack_names() stacks = self.config.stacks stack_processing_order = DependencyResolver().get_stack_order(stacks) stack_processing_order.reverse() self.logger.info( "Will delete stacks in the following order: {0}".format( ", ".join(stack_processing_order))) for stack_name in stack_processing_order: stack_config = self.config.stacks.get(stack_name) if stack_name in existing_stacks: stack = CloudFormationStack( None, None, stack_name, None, None, service_role=stack_config.service_role) self.cfn.validate_stack_is_ready_for_action(stack) self.cfn.delete_stack(stack) else: self.logger.info( "Stack {0} is already deleted".format(stack_name))
def test_get_stack_order_raises_exception_on_cyclic_dependency(self): stacks = { "app1": StackConfig({"template-url": "horst.yml", "parameters": {"a": "|Ref|app2.id"}}), "app2": StackConfig({"template-url": "horst.yml", "parameters": {"a": "|Ref|app3.id"}}), "app3": StackConfig({"template-url": "horst.yml", "parameters": {"a": "|Ref|app1.id"}}), } with self.assertRaises(CyclicDependencyException): DependencyResolver.get_stack_order(stacks)
def test_get_stack_order_raises_exception_on_cyclic_dependency(self): stacks = { 'app1': StackConfig({'template-url': 'horst.yml', 'parameters': {'a': '|Ref|app2.id'}}), 'app2': StackConfig({'template-url': 'horst.yml', 'parameters': {'a': '|Ref|app3.id'}}), 'app3': StackConfig({'template-url': 'horst.yml', 'parameters': {'a': '|Ref|app1.id'}}) } with self.assertRaises(CyclicDependencyException): DependencyResolver.get_stack_order(stacks)
def _transform_value(value, suffix, managed_stack_names): result = value if DependencyResolver.is_parameter_reference(value): stack_name, output_name = DependencyResolver.parse_stack_reference_value( value) if stack_name in managed_stack_names: result = "|ref|{0}{1}.{2}".format(stack_name, suffix, output_name) return result
def resolve_parameter_values(self, stack_name, stack_config, cli_parameters=None): resolved_parameters = {} stack_outputs = self.cfn.get_stacks_outputs() for key, value in stack_config.parameters.items(): if isinstance(value, list): self.logger.debug("List parameter found for {0}".format(key)) for i, item in enumerate(value): if DependencyResolver.is_parameter_reference(item): referenced_stack, output_name = DependencyResolver.parse_stack_reference_value(item) value[i] = str(self.get_output_value(stack_outputs, referenced_stack, output_name)) value_string = self.convert_list_to_string(value) resolved_parameters[key] = value_string elif isinstance(value, str): if DependencyResolver.is_parameter_reference(value): referenced_stack, output_name = DependencyResolver.parse_stack_reference_value(value) resolved_parameters[key] = str(self.get_output_value(stack_outputs, referenced_stack, output_name)) elif self.is_keep_value(value): resolved_parameters[key] = str(self.get_latest_value(key, value, stack_name)) elif self.is_taupage_ami_reference(value): resolved_parameters[key] = str(self.ec2.get_latest_taupage_image_id()) elif self.is_kms(value): resolved_parameters[key] = str(self.kms.decrypt(value.split('|', 2)[2])) elif self.is_file(value): url = value.split('|', 2)[2] resolved_parameters[key] = FileLoader.get_file(url, stack_config.working_dir) else: resolved_parameters[key] = value elif isinstance(value, bool): resolved_parameters[key] = str(value).lower() elif isinstance(value, (int, float)): resolved_parameters[key] = str(value) else: raise NotImplementedError("Cannot handle {0} type for key: {1}".format(type(value), key)) if cli_parameters: return self.update_parameters_with_cli_parameters(resolved_parameters, cli_parameters, stack_name) else: return resolved_parameters
def delete_stacks(self): existing_stacks = self.cfn.get_stack_names() stacks = self.config.stacks stack_processing_order = DependencyResolver().get_stack_order(stacks) stack_processing_order.reverse() self.logger.info("Will delete stacks in the following order: {0}".format(", ".join(stack_processing_order))) for stack_name in stack_processing_order: if stack_name in existing_stacks: stack = CloudFormationStack(None, None, stack_name, None, None) self.cfn.validate_stack_is_ready_for_action(stack) self.cfn.delete_stack(stack) else: self.logger.info("Stack {0} is already deleted".format(stack_name))
def test_get_stack_order_includes_independent_stacks(self): stacks = { 'default-sg': StackConfig({'template-url': 'horst.yml'}), 'app1': StackConfig({ 'template-url': 'horst.yml', 'parameters': { 'a': '|Ref|vpc.id', 'b': '|Ref|default-sg.id' } }), 'app2': StackConfig({ 'template-url': 'horst.yml', 'parameters': { 'a': '|Ref|vpc.id', 'b': '|Ref|default-sg.id', 'c': 'Ref::app1.id' } }), 'vpc': StackConfig({ 'template-url': 'horst.yml', 'parameters': { 'logBucketName': 'is24-cloudtrail-logs', 'includeGlobalServices': False } }) } result = 4 self.assertEqual(result, len(DependencyResolver.get_stack_order(stacks)))
def test_filter_unmanaged_stacks(self): stacks = ['a', 'b', 'c'] managed_stacks = ['a', 'c'] self.assertListEqual( managed_stacks, DependencyResolver.filter_unmanaged_stacks(managed_stacks, stacks))
def resolve_parameter_value(self, key, value, stack_name, stack_config, stack_outputs): if isinstance(value, list): self.logger.debug("List parameter found for {0}".format(key)) for i, item in enumerate(value): value[i] = self.resolve_parameter_value( key, item, stack_name, stack_config, stack_outputs) return self.convert_list_to_string(value) elif isinstance(value, string_types): if DependencyResolver.is_parameter_reference(value): referenced_stack, output_name = DependencyResolver.parse_stack_reference_value( value) return str( self.get_output_value(stack_outputs, referenced_stack, output_name)) elif self.is_keep_value(value): return str(self.get_latest_value(key, value, stack_name)) elif self.is_taupage_ami_reference(value): return str(self.ec2.get_latest_taupage_image_id()) elif self.is_kms(value): return self.handle_kms_value(value) elif self.is_ssm(value): return self.handle_ssm_value(value) elif self.is_file(value): return self.handle_file_value(value, stack_config.working_dir) else: return value elif isinstance(value, bool): return str(value).lower() elif isinstance(value, (int, float)): return str(value) elif not value: raise CfnSphereException( "Parameter {0} does not seem to have a value".format(key)) else: raise NotImplementedError( "Cannot handle {0} type for key: {1}".format(type(value), key))
def test_get_stack_order_accepts_stacks_without_parameters_key(self): stacks = {'default-sg': {}, 'app1': None, 'app2': {}, 'vpc': {}, } result = 4 self.assertEqual(result, len(DependencyResolver.get_stack_order(stacks)))
def resolve_parameter_values(self, parameters_dict, stack_name): parameters = {} for key, value in parameters_dict.items(): if isinstance(value, list): self.logger.debug("List parameter found for {0}".format(key)) for i, item in enumerate(value): if DependencyResolver.is_parameter_reference(item): referenced_stack, output_name = DependencyResolver.parse_stack_reference_value(item) value[i] = str(self.get_output_value(referenced_stack + '.' + output_name)) value_string = self.convert_list_to_string(value) parameters[key] = value_string elif isinstance(value, str): if DependencyResolver.is_parameter_reference(value): referenced_stack, output_name = DependencyResolver.parse_stack_reference_value(value) parameters[key] = str(self.get_output_value(referenced_stack + '.' + output_name)) elif self.is_keep_value(value): parameters[key] = str(self.get_latest_value(key, value, stack_name)) elif self.is_taupage_ami_reference(value): parameters[key] = str(self.ec2.get_latest_taupage_image_id()) elif self.is_kms(value): parameters[key] = str(self.kms.decrypt(value.split('|', 2)[2])) else: parameters[key] = value elif isinstance(value, bool): parameters[key] = str(value).lower() elif isinstance(value, (int, float)): parameters[key] = str(value) else: raise NotImplementedError("Cannot handle {0} value for key: {1}".format(type(value), key)) return parameters
def resolve_parameter_values(self, parameters_dict, stack_name): parameters = {} for key, value in parameters_dict.items(): if isinstance(value, list): self.logger.debug("List parameter found for {0}".format(key)) value_string = self.convert_list_to_string(value) parameters[key] = value_string elif isinstance(value, str): if DependencyResolver.is_parameter_reference(value): referenced_stack, output_name = DependencyResolver.parse_stack_reference_value(value) parameters[key] = str(self.get_output_value(referenced_stack + '.' + output_name)) elif self.is_keep_value(value): parameters[key] = str(self.get_latest_value(key, value, stack_name)) elif self.is_taupage_ami_reference(value): parameters[key] = str(self.ec2.get_latest_taupage_image_id()) elif self.is_kms(value): parameters[key] = str(self.kms.decrypt(value.split('|', 2)[2])) else: parameters[key] = value elif isinstance(value, bool): parameters[key] = str(value).lower() elif isinstance(value, int): parameters[key] = str(value) elif isinstance(value, float): parameters[key] = str(value) else: raise NotImplementedError("Cannot handle {0} value for key: {1}".format(type(value), key)) return parameters
def test_get_stack_order_returns_a_valid_order_from_ref_in_list(self): stacks = {'default-sg': StackConfig({'template-url': 'horst.yml', 'parameters': {'a': ['|Ref|vpc.id']}}), 'app1': StackConfig( {'template-url': 'horst.yml', 'parameters': {'a': ['|Ref|vpc.id'], 'b': ['|Ref|default-sg.id']}}), 'app2': StackConfig({'template-url': 'horst.yml', 'parameters': {'a': ['|Ref|vpc.id'], 'b': ['|Ref|default-sg.id'], 'c': ['|Ref|app1.id']}}), 'vpc': StackConfig({'template-url': 'horst.yml', 'parameters': {'logBucketName': 'is24-cloudtrail-logs', 'includeGlobalServices': False}}) } expected = ['vpc', 'default-sg', 'app1', 'app2'] self.assertEqual(expected, DependencyResolver.get_stack_order(stacks))
def test_get_stack_order_includes_independent_stacks(self): stacks = {'default-sg': StackConfig({'template-url': 'horst.yml'}), 'app1': StackConfig( {'template-url': 'horst.yml', 'parameters': {'a': '|Ref|vpc.id', 'b': '|Ref|default-sg.id'}}), 'app2': StackConfig({'template-url': 'horst.yml', 'parameters': {'a': '|Ref|vpc.id', 'b': '|Ref|default-sg.id', 'c': 'Ref::app1.id'}}), 'vpc': StackConfig({'template-url': 'horst.yml', 'parameters': {'logBucketName': 'is24-cloudtrail-logs', 'includeGlobalServices': False}}) } result = 4 self.assertEqual(result, len(DependencyResolver.get_stack_order(stacks)))
def test_get_stack_order_returns_a_valid_order(self): stacks = {'default-sg': StackConfig({'template-url': 'horst.yml', 'parameters': {'a': '|Ref|vpc.id'}}), 'app1': StackConfig( {'template-url': 'horst.yml', 'parameters': {'a': '|Ref|vpc.id', 'b': '|Ref|default-sg.id'}}), 'app2': StackConfig({'template-url': 'horst.yml', 'parameters': {'a': '|Ref|vpc.id', 'b': '|Ref|default-sg.id', 'c': '|Ref|app1.id'}}), 'vpc': StackConfig({'template-url': 'horst.yml', 'parameters': {'logBucketName': 'is24-cloudtrail-logs', 'includeGlobalServices': False}}) } expected = ['vpc', 'default-sg', 'app1', 'app2'] self.assertEqual(expected, DependencyResolver.get_stack_order(stacks))
def create_or_update_stacks(self): existing_stacks = self.cfn.get_stack_names() desired_stacks = self.config.stacks stack_processing_order = DependencyResolver().get_stack_order( desired_stacks) if len(stack_processing_order) > 1: self.logger.info( "Will process stacks in the following order: {0}".format( ", ".join(stack_processing_order))) for stack_name in stack_processing_order: stack_config = self.config.stacks.get(stack_name) if stack_config.stack_policy_url: self.logger.info("Using stack policy from {0}".format( stack_config.stack_policy_url)) stack_policy = FileLoader.get_yaml_or_json_file( stack_config.stack_policy_url, stack_config.working_dir) else: stack_policy = None template = TemplateHandler.get_template(stack_config.template_url, stack_config.working_dir) parameters = self.parameter_resolver.resolve_parameter_values( stack_name, stack_config, self.cli_parameters) stack = CloudFormationStack( template=template, parameters=parameters, tags=stack_config.tags, name=stack_name, region=self.config.region, timeout=stack_config.timeout, service_role=stack_config.service_role, stack_policy=stack_policy, failure_action=stack_config.failure_action) if stack_name in existing_stacks: self.cfn.validate_stack_is_ready_for_action(stack) self.cfn.update_stack(stack) else: self.cfn.create_stack(stack) CustomResourceHandler.process_post_resources(stack)
def test_get_stack_order_returns_a_valid_order(self): stacks = { "default-sg": StackConfig({"template-url": "horst.yml", "parameters": {"a": "|Ref|vpc.id"}}), "app1": StackConfig( {"template-url": "horst.yml", "parameters": {"a": "|Ref|vpc.id", "b": "|Ref|default-sg.id"}} ), "app2": StackConfig( { "template-url": "horst.yml", "parameters": {"a": "|Ref|vpc.id", "b": "|Ref|default-sg.id", "c": "|Ref|app1.id"}, } ), "vpc": StackConfig( { "template-url": "horst.yml", "parameters": {"logBucketName": "is24-cloudtrail-logs", "includeGlobalServices": False}, } ), } expected = ["vpc", "default-sg", "app1", "app2"] self.assertEqual(expected, DependencyResolver.get_stack_order(stacks))
def test_get_stack_order_includes_independent_stacks(self): stacks = { "default-sg": StackConfig({"template-url": "horst.yml"}), "app1": StackConfig( {"template-url": "horst.yml", "parameters": {"a": "|Ref|vpc.id", "b": "|Ref|default-sg.id"}} ), "app2": StackConfig( { "template-url": "horst.yml", "parameters": {"a": "|Ref|vpc.id", "b": "|Ref|default-sg.id", "c": "Ref::app1.id"}, } ), "vpc": StackConfig( { "template-url": "horst.yml", "parameters": {"logBucketName": "is24-cloudtrail-logs", "includeGlobalServices": False}, } ), } result = 4 self.assertEqual(result, len(DependencyResolver.get_stack_order(stacks)))
def create_or_update_stacks(self): existing_stacks = self.cfn.get_stack_names() desired_stacks = self.config.stacks stack_processing_order = DependencyResolver().get_stack_order( desired_stacks) if len(stack_processing_order) > 1: self.logger.info( "Will process stacks in the following order: {0}".format( ", ".join(stack_processing_order))) for stack_name in stack_processing_order: stack_config = self.config.stacks.get(stack_name) raw_template = FileLoader.get_file_from_url( stack_config.template_url, stack_config.working_dir) template = CloudFormationTemplateTransformer.transform_template( raw_template) parameters = self.parameter_resolver.resolve_parameter_values( stack_config.parameters, stack_name) stack = CloudFormationStack(template=template, parameters=parameters, tags=(stack_config.tags), name=stack_name, region=self.region, timeout=stack_config.timeout) if stack_name in existing_stacks: self.cfn.validate_stack_is_ready_for_action(stack) self.cfn.update_stack(stack) else: self.cfn.create_stack(stack) CustomResourceHandler.process_post_resources(stack)
def test_is_parameter_reference_returns_true_for_lowercase_ref(self): self.assertTrue( DependencyResolver.is_parameter_reference("|ref|vpc.id"))
def test_parse_stack_reference_raises_exception_on_empty_reference(self): with self.assertRaises(CfnSphereException): DependencyResolver.parse_stack_reference_value('|ref|')
def test_get_stack_order_accepts_stacks_without_parameters_key(self): stacks = {"default-sg": {}, "app1": None, "app2": {}, "vpc": {}} result = 4 self.assertEqual(result, len(DependencyResolver.get_stack_order(stacks)))
def test_parse_stack_reference_value_returns_stack_and_output_name_tuple(self): self.assertEqual(("stack", "output"), DependencyResolver.parse_stack_reference_value("|ref|stack.output"))
def resolve_parameter_values(self, stack_name, stack_config, cli_parameters=None): resolved_parameters = {} stack_outputs = self.cfn.get_stacks_outputs() for key, value in stack_config.parameters.items(): if isinstance(value, list): self.logger.debug("List parameter found for {0}".format(key)) for i, item in enumerate(value): if DependencyResolver.is_parameter_reference(item): referenced_stack, output_name = DependencyResolver.parse_stack_reference_value( item) value[i] = str( self.get_output_value(stack_outputs, referenced_stack, output_name)) value_string = self.convert_list_to_string(value) resolved_parameters[key] = value_string elif isinstance(value, str): if DependencyResolver.is_parameter_reference(value): referenced_stack, output_name = DependencyResolver.parse_stack_reference_value( value) resolved_parameters[key] = str( self.get_output_value(stack_outputs, referenced_stack, output_name)) elif self.is_keep_value(value): resolved_parameters[key] = str( self.get_latest_value(key, value, stack_name)) elif self.is_taupage_ami_reference(value): resolved_parameters[key] = str( self.ec2.get_latest_taupage_image_id()) elif self.is_kms(value): resolved_parameters[key] = str( self.kms.decrypt(value.split('|', 2)[2])) elif self.is_file(value): resolved_parameters[key] = self.handle_file_value( value, stack_config.working_dir) else: resolved_parameters[key] = value elif isinstance(value, bool): resolved_parameters[key] = str(value).lower() elif isinstance(value, (int, float)): resolved_parameters[key] = str(value) else: raise NotImplementedError( "Cannot handle {0} type for key: {1}".format( type(value), key)) if cli_parameters: return self.update_parameters_with_cli_parameters( resolved_parameters, cli_parameters, stack_name) else: return resolved_parameters
def test_filter_unmanaged_stacks(self): stacks = ["a", "b", "c"] managed_stacks = ["a", "c"] self.assertListEqual(managed_stacks, DependencyResolver.filter_unmanaged_stacks(managed_stacks, stacks))
def test_filter_unmanaged_stacks(self): stacks = ['a', 'b', 'c'] managed_stacks = ['a', 'c'] self.assertListEqual(managed_stacks, DependencyResolver.filter_unmanaged_stacks(managed_stacks, stacks))
def test_parse_stack_reference_value_returns_none_for_non_reference(self): self.assertEqual((None, None), DependencyResolver.parse_stack_reference_value('foo'))
def test_is_parameter_reference_returns_true_for_uppercase_ref(self): self.assertTrue(DependencyResolver.is_parameter_reference("|Ref|vpc.id"))
def test_parse_stack_reference_value_returns_stack_and_output_name_tuple(self): self.assertEqual(('stack', 'output'), DependencyResolver.parse_stack_reference_value('|ref|stack.output'))
def test_parse_stack_reference_raises_exception_on_missing_dot(self): with self.assertRaises(CfnSphereException): DependencyResolver.parse_stack_reference_value('|ref|foo')
def test_is_parameter_reference_returns_false_for_single_separator(self): self.assertFalse( DependencyResolver.is_parameter_reference("Ref|vpc.id"))
def test_is_parameter_reference_returns_false_for_boolean_values(self): self.assertFalse(DependencyResolver.is_parameter_reference(True))
def test_parse_stack_reference_value_returns_stack_and_output_name_tuple( self): self.assertEqual(('stack', 'output'), DependencyResolver.parse_stack_reference_value( '|ref|stack.output'))
def test_is_parameter_reference_returns_false_for_single_separator(self): self.assertFalse(DependencyResolver.is_parameter_reference("Ref|vpc.id"))
def test_is_parameter_reference_returns_false_for_simple_string(self): self.assertFalse(DependencyResolver.is_parameter_reference("vpc.id"))
def test_is_parameter_reference_returns_true_on_empty_reference(self): self.assertTrue(DependencyResolver.is_parameter_reference('|ref|'))