def test_get_stacks_correctly_calls_aws_api(self, cloudformation_mock): stacks = [Mock(spec=Stack), Mock(spec=Stack)] result = ResultSet() result.extend(stacks) result.next_token = None cloudformation_mock.connect_to_region.return_value.describe_stacks.return_value = result cfn = CloudFormation() self.assertListEqual(stacks, cfn.get_stacks())
def test_get_stacks_correctly_aggregates_paged_results(self, cloudformation_mock): stacks_1 = [Mock(spec=Stack), Mock(spec=Stack)] stacks_2 = [Mock(spec=Stack), Mock(spec=Stack)] result_1 = ResultSet() result_1.extend(stacks_1) result_1.next_token = "my-next-token" result_2 = ResultSet() result_2.extend(stacks_2) result_2.next_token = None cloudformation_mock.connect_to_region.return_value.describe_stacks.side_effect = [result_1, result_2] cfn = CloudFormation() self.assertListEqual(stacks_1 + stacks_2, cfn.get_stacks())
def test_update_stack_calls_cloudformation_api_properly(self, _, cloudformation_mock): stack = Mock(spec=CloudFormationStack) stack.name = "stack-name" stack.get_parameters_list.return_value = [('a', 'b')] stack.parameters = {} stack.template = Mock(spec=CloudFormationTemplate) stack.template.name = "template-name" stack.template.get_template_json.return_value = {'key': 'value'} stack.timeout = 42 cfn = CloudFormation() cfn.update_stack(stack) cloudformation_mock.return_value.update_stack.assert_called_once_with('stack-name', capabilities=['CAPABILITY_IAM'], parameters=[('a', 'b')], template_body={'key': 'value'})
def test_wait_for_stack_event_returns_on_update_complete(self, cloudformation_mock): timestamp = datetime.datetime.utcnow() template_mock = Mock(spec=CloudFormationTemplate) template_mock.url = "foo.yml" template_mock.get_template_body_dict.return_value = {} event = StackEvent() event.resource_type = "AWS::CloudFormation::Stack" event.resource_status = "UPDATE_COMPLETE" event.event_id = "123" event.timestamp = timestamp stack_events_mock = Mock() stack_events_mock.describe_stack_events.return_value = [event] cloudformation_mock.connect_to_region.return_value = stack_events_mock cfn = CloudFormation() cfn.wait_for_stack_events("foo", "UPDATE_COMPLETE", timestamp - timedelta(seconds=10), timeout=10)
def test_wait_for_stack_event_raises_exception_on_rollback(self, cloudformation_mock): timestamp = datetime.datetime.utcnow() template_mock = Mock(spec=CloudFormationTemplate) template_mock.url = "foo.yml" template_mock.get_template_body_dict.return_value = {} event = StackEvent() event.resource_type = "AWS::CloudFormation::Stack" event.resource_status = "ROLLBACK_COMPLETE" event.event_id = "123" event.timestamp = timestamp stack_events_mock = Mock() stack_events_mock.describe_stack_events.return_value = [event] cloudformation_mock.connect_to_region.return_value = stack_events_mock cfn = CloudFormation() with self.assertRaises(Exception): cfn.wait_for_stack_events("foo", ["UPDATE_COMPLETE"], timestamp - timedelta(seconds=10), timeout=10)
def test_wait_for_stack_event_returns_on_start_event_with_valid_timestamp(self, cloudformation_mock): timestamp = datetime.datetime.utcnow() template_mock = Mock(spec=CloudFormationTemplate) template_mock.url = "foo.yml" template_mock.get_template_body_dict.return_value = {} event = StackEvent() event.resource_type = "AWS::CloudFormation::Stack" event.resource_status = "UPDATE_IN_PROGRESS" event.event_id = "123" event.timestamp = timestamp stack_events_mock = Mock() stack_events_mock.describe_stack_events.return_value = [event] cloudformation_mock.connect_to_region.return_value = stack_events_mock cfn = CloudFormation() event = cfn.wait_for_stack_events("foo", "UPDATE_IN_PROGRESS", timestamp - timedelta(seconds=10), timeout=10) self.assertEqual(timestamp, event.timestamp)
class StackActionHandler(object): def __init__(self, config): self.logger = get_logger() self.config = config self.region = config.region self.cfn = CloudFormation(region=self.region) self.parameter_resolver = ParameterResolver(region=self.region) 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) template_url = stack_config.template_url working_dir = stack_config.working_dir template = CloudFormationTemplateLoader.get_template_from_url(template_url, working_dir) template = CloudFormationTemplateTransformer.transform_template(template) parameters = self.parameter_resolver.resolve_parameter_values(stack_config.parameters, stack_name) stack = CloudFormationStack(template, parameters, stack_name, self.region, stack_config.timeout) if stack_name in existing_stacks: self.cfn.validate_stack_is_ready_for_updates(stack_name) self.cfn.update_stack(stack) else: self.cfn.create_stack(stack) CustomResourceHandler.process_post_resources(stack)
def __init__(self, config): self.logger = get_logger() self.config = config self.region = config.region self.cfn = CloudFormation(region=self.region) self.parameter_resolver = ParameterResolver(region=self.region)
def __init__(self, region="eu-west-1"): self.logger = get_logger() self.cfn = CloudFormation(region) self.ec2 = Ec2Api(region)
class ParameterResolver(object): """ Resolves a given artifact identifier to the value of a stacks output. """ def __init__(self, region="eu-west-1"): self.logger = get_logger() self.cfn = CloudFormation(region) self.ec2 = Ec2Api(region) @staticmethod def convert_list_to_string(value): if not value: return "" value_string = "" for item in value: if value_string: value_string += ',' value_string += str(item) return value_string def get_stack_outputs(self): """ Get a list of all available stack outputs in format <stack-name>.<output>. :return: dict """ artifacts = {} stacks = self.cfn.get_stacks() for stack in stacks: for output in stack.outputs: key = stack.stack_name + '.' + output.key artifacts[key] = output.value return artifacts def get_output_value(self, key): """ Get value for a specific output key in format <stack-name>.<output>. :param key: str <stack-name>.<output> :return: str """ artifacts = self.get_stack_outputs() self.logger.debug("Looking up key: {0}".format(key)) self.logger.debug("Found artifacts: {0}".format(artifacts)) try: artifact = artifacts[key] return artifact except KeyError: raise ParameterResolverException("Could not get a valid value for {0}.".format(key)) @staticmethod def is_keep_value(value): return value.lower().startswith('|keeporuse|') @staticmethod def is_taupage_ami_reference(value): return value.lower() == '|latesttaupageami|' @staticmethod def get_default_from_keep_value(value): return value.split('|', 2)[2] def get_actual_value(self, key, value, stack_name): try: actual_stack_parameters = self.cfn.get_stack_parameters_dict(stack_name) actual_value = actual_stack_parameters.get(key, None) if actual_value: self.logger.info("Will keep '{0}' as actual value for {1}".format(actual_value, key)) return actual_value else: return self.get_default_from_keep_value(value) except BotoServerError: self.logger.info("Will use default value for {0}".format(key)) return self.get_default_from_keep_value(value) 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_actual_value(key, value, stack_name)) elif self.is_taupage_ami_reference(value): parameters[key] = str(self.ec2.get_latest_taupage_image_id()) 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