def test_default_service_role_is_used_if_not_overwritten_by_stack_config( self): config = Config( config_dict={ 'region': 'eu-west-1', 'service-role': 'arn:aws:iam::123456789:role/my-role1', 'tags': { 'global-tag': 'global-tag-value' }, 'stacks': { 'any-stack': { 'timeout': 99, 'template-url': 'foo.json', 'tags': { 'any-tag': 'any-tag-value' }, 'parameters': { 'any-parameter': 'any-value' } } } }) self.assertEqual('arn:aws:iam::123456789:role/my-role1', config.stacks["any-stack"].service_role)
def test_config_reads_config_from_file(self, get_file_mock, getcwd_mock): getcwd_mock.return_value = "/home/user/something" get_file_mock.return_value = {"region": "eu-west-1", "stacks": { "some-stack": {"template-url": "some-template.yml"}}} Config("my-stacks/stacks.yml") get_file_mock.assert_called_once_with("my-stacks/stacks.yml", working_dir="/home/user/something")
def sync_stacks_with_parameters_overwrite(self, cli_params): config = Config(config_file=os.path.join(self.test_resources_dir, "stacks.yml"), cli_params=cli_params) stack_handler = StackActionHandler(config) self.logger.info("Syncing stacks") stack_handler.create_or_update_stacks()
def test_apply_stack_name_suffix_does_not_append_none_suffix(self): stacks = { "stack-d": StackConfig({"template-url": "some-url"}) } result = Config._apply_stack_name_suffix(stacks, None) self.assertEqual(result, stacks)
def delete(config, suffix, debug, confirm, yes): confirm = confirm or yes if debug: LOGGER.setLevel(logging.DEBUG) else: LOGGER.setLevel(logging.INFO) if not confirm: check_update_available() click.confirm( 'This action will delete all stacks in {0} from account: {1}\nAre you sure?' .format(config, get_first_account_alias_or_account_id()), abort=True) try: config = Config(config, stack_name_suffix=suffix) StackActionHandler(config).delete_stacks() except CfnSphereException as e: LOGGER.error(e) if debug: LOGGER.exception(e) sys.exit(1) except Exception as e: LOGGER.error("Failed with unexpected error") LOGGER.exception(e) LOGGER.info( "Please report at https://github.com/cfn-sphere/cfn-sphere/issues!" ) sys.exit(1)
def test_parse_cli_parameters(self): config = Config(cli_params=("stack1.p1=v1", "stack1.p2=v2"), config_dict={'region': 'eu-west-1', 'stacks': {'stack1': {'template-url': 'foo.json'}}}) self.assertTrue('p1' in config.cli_params['stack1']) self.assertTrue('p2' in config.cli_params['stack1']) self.assertTrue('v1' in config.cli_params['stack1'].values()) self.assertTrue('v2' in config.cli_params['stack1'].values())
def sync(config, parameter, suffix, debug, confirm, yes): confirm = confirm or yes if debug: LOGGER.setLevel(logging.DEBUG) boto3.set_stream_logger(name='boto3', level=logging.DEBUG) boto3.set_stream_logger(name='botocore', level=logging.DEBUG) else: LOGGER.setLevel(logging.INFO) if not confirm: check_update_available() click.confirm( 'This action will modify AWS infrastructure in account: {0}\nAre you sure?' .format(get_first_account_alias_or_account_id()), abort=True) try: config = Config(config_file=config, cli_params=parameter, stack_name_suffix=suffix) StackActionHandler(config).create_or_update_stacks() except CfnSphereException as e: LOGGER.error(e) if debug: LOGGER.exception(e) sys.exit(1) except Exception as e: LOGGER.error("Failed with unexpected error") LOGGER.exception(e) LOGGER.info( "Please report at https://github.com/cfn-sphere/cfn-sphere/issues!" ) sys.exit(1)
def test_config_properties_parsing(self): config = Config( config_dict={ 'region': 'eu-west-1', 'tags': { 'global-tag': 'global-tag-value' }, 'stacks': { 'any-stack': { 'timeout': 99, 'template-url': 'foo.json', 'tags': { 'any-tag': 'any-tag-value' }, 'parameters': { 'any-parameter': 'any-value' } } } }) self.assertEqual('eu-west-1', config.region) self.assertEqual(1, len(config.stacks.keys())) self.assertTrue(isinstance(config.stacks['any-stack'], StackConfig)) self.assertEqual('foo.json', config.stacks['any-stack'].template_url) self.assertDictContainsSubset( { 'any-tag': 'any-tag-value', 'global-tag': 'global-tag-value' }, config.stacks['any-stack'].tags) self.assertDictContainsSubset({'global-tag': 'global-tag-value'}, config.default_tags) self.assertDictContainsSubset({'any-parameter': 'any-value'}, config.stacks['any-stack'].parameters) self.assertEqual(99, config.stacks['any-stack'].timeout)
def get_stack_action_handler(domain, verification_token=None, dkim_tokens=None): verification_token = verification_token or "" dkim_tokens = dkim_tokens or ["", "", ""] return StackActionHandler(config=Config( config_dict={ 'region': REGION, 'stacks': { get_dns_stack_name(domain): { 'template-url': recordset_template.name, 'parameters': { 'dnsBaseName': domain + ".", 'dkimOne': dkim_tokens[0], 'dkimTwo': dkim_tokens[1], 'dkimThree': dkim_tokens[2], 'verifyTxt': verification_token } }, get_bucket_stack_name(domain): { 'template-url': ses_template.name, } } }))
def __init__(self): self.logger = logging.getLogger(__name__) self.logger.setLevel(logging.INFO) self.test_resources_dir = self._get_resources_dir() self.cfn_conn = boto3.client('cloudformation', region_name='eu-west-1') self.kms_conn = boto3.client('kms', region_name='eu-west-1') self.config = Config( config_file=os.path.join(self.test_resources_dir, "stacks.yml"))
def setUpClass(cls): test_resources_dir = get_resources_dir() cls.cfn_conn = cloudformation.connect_to_region("eu-west-1") cls.config = Config(config_file=os.path.join(test_resources_dir, "stacks.yml")) cls.stack_handler = StackActionHandler(cls.config) LOGGER.info("Syncing stacks") cls.stack_handler.create_or_update_stacks()
def test_apply_stack_name_suffix_does_not_modify_externally_referenced_stacks(self): stacks = { "stack-c": StackConfig({"template-url": "some-url", "parameters": {"a": 1, "b": "|ref|external_stack.a"}}) } result = Config._apply_stack_name_suffix(stacks, "-test") self.assertEqual(result["stack-c-test"].parameters["a"], 1) self.assertEqual(result["stack-c-test"].parameters["b"], "|ref|external_stack.a")
def test_raises_exception_if_no_region_key(self): with self.assertRaises(NoConfigException): Config(config_dict={ 'foo': '', 'stacks': { 'any-stack': { 'template': 'foo.json' } } })
def test_validate_raises_exception_for_empty_stack_config(self): with self.assertRaises(InvalidConfigException): Config( config_dict={ 'invalid-key': 'some-value', 'region': 'eu-west-1', 'stacks': { 'stack2': None } })
def test_apply_stack_name_suffix_applies_suffix_to_sublist_items(self): stacks = { "stack-a": StackConfig( {"template-url": "some-url", "parameters": {"alist": ["|ref|stack-b.a", "|ref|stack-b.b"]}}), "stack-b": StackConfig({"template-url": "some-url"}) } result = Config._apply_stack_name_suffix(stacks, "-test") self.assertEqual(result["stack-a-test"].parameters["alist"][0], "|ref|stack-b-test.a") self.assertEqual(result["stack-a-test"].parameters["alist"][1], "|ref|stack-b-test.b")
def test_apply_stack_name_suffix_appends_number_suffix_to_all_stacks(self): stacks = { "stack-a": StackConfig({"template-url": "some-url", "parameters": {"a": 1, "b": "|ref|stack-b.a"}}), "stack-b": StackConfig({"template-url": "some-url", "parameters": {"a": 1, "b": "foo"}}) } result = Config._apply_stack_name_suffix(stacks, 3) self.assertEqual(result["stack-a3"].parameters["a"], 1) self.assertEqual(result["stack-a3"].parameters["b"], "|ref|stack-b3.a") self.assertEqual(result["stack-b3"].parameters["a"], 1) self.assertEqual(result["stack-b3"].parameters["b"], "foo")
def test_validate_raises_exception_for_invalid_config_key(self): with self.assertRaises(InvalidConfigException): Config( config_dict={ 'invalid-key': 'some-value', 'region': 'eu-west-1', 'stacks': { 'stack2': { 'template-url': 'foo.json' } } })
def test_validate_raises_exception_for_cli_param_on_non_configured_stack( self): with self.assertRaises(InvalidConfigException): Config(cli_params=("stack1.p1=v1", ), config_dict={ 'region': 'eu-west-1', 'stacks': { 'stack2': { 'template-url': 'foo.json' } } })
def test_validate_passes_on_valid_service_role_value(self): with self.assertRaises(InvalidConfigException): Config( config_dict={ 'region': 'eu-west-q', 'service-role': 'some-role', 'stacks': { 'any-stack': { 'template': 'foo.json' } } })
def test_validate_raises_exception_on_invalid_service_role_value(self): with self.assertRaises(InvalidConfigException): Config( config_dict={ 'region': 'eu-west-q', 'service-role': 'arn:aws:iam::123456789:role/my-role', 'stacks': { 'any-stack': { 'template': 'foo.json' } } })
def test_parse_cli_parameters_accepts_spaces(self): self.assertDictEqual( { 'stack1': { 'p1': 'v1', 'p2': 'v2' }, 'stack2': { 'p1': 'v1' } }, Config._parse_cli_parameters( ("stack1.p1 = v1 ", "stack1.p2=v2", "stack2.p1=v1 ")))
def create_config_object(): config_dict = { 'region': 'region a', 'tags': {'key_a': 'value a'}, 'stacks': { 'stack_a': { 'template-url': 'template_a', 'parameters': 'any parameters' } } } return Config(config_dict=config_dict, cli_params=['stack_a.cli_parameter_a=cli_value_a'])
def test_a_stacks_timeout_is_set_if_not_configured(self): config = Config( config_dict={ 'region': 'eu-west-1', 'tags': { 'global-tag': 'global-tag-value' }, 'stacks': { 'any-stack': { 'template-url': 'foo.json', 'tags': { 'any-tag': 'any-tag-value' }, 'parameters': { 'any-parameter': 'any-value' } } } }) self.assertTrue(isinstance(config.stacks["any-stack"].timeout, int))
def test_a_stacks_service_role_is_none_if_not_configured(self): config = Config( config_dict={ 'region': 'eu-west-1', 'tags': { 'global-tag': 'global-tag-value' }, 'stacks': { 'any-stack': { 'timeout': 99, 'template-url': 'foo.json', 'tags': { 'any-tag': 'any-tag-value' }, 'parameters': { 'any-parameter': 'any-value' } } } }) self.assertIsNone(config.stacks["any-stack"].service_role)
def execute_change_set(change_set, debug, confirm, yes, region): confirm = confirm or yes if debug: LOGGER.setLevel(logging.DEBUG) boto3.set_stream_logger(name='boto3', level=logging.DEBUG) boto3.set_stream_logger(name='botocore', level=logging.DEBUG) else: LOGGER.setLevel(logging.INFO) if not confirm: check_update_available() click.confirm( 'This action will modify AWS infrastructure in account: {0}\nAre you sure?' .format(get_first_account_alias_or_account_id()), abort=True) try: matched = re.match(r'arn:aws:cloudformation:([^:]+):.*', change_set) if matched: LOGGER.info('ARN detected, setting region to {}'.format( matched.group(1))) region = matched.group(1) config_dict = {'change_set': change_set, 'region': str(region)} config = Config(config_dict=config_dict) StackActionHandler(config).execute_change_set() except CfnSphereException as e: LOGGER.error(e) if debug: LOGGER.exception(e) sys.exit(1) except Exception as e: LOGGER.error("Failed with unexpected error") LOGGER.exception(e) LOGGER.info( "Please report at https://github.com/KCOM-Enterprise/cfn-square/issues!" ) sys.exit(1)
def test_parse_cli_parameters_throws_exception_on_invalid_syntax(self): with self.assertRaises(CfnSphereException): Config._parse_cli_parameters(("foo", ))
def test_parse_cli_parameters_parses_single_int_parameter(self): self.assertDictEqual({'stack1': { 'p1': '2' }}, Config._parse_cli_parameters(("stack1.p1=2", )))
def test_parse_cli_parameters_accepts_list_of_strings(self): self.assertDictEqual({'stack1': { 'p1': 'v1,v2,v3' }}, Config._parse_cli_parameters(("stack1.p1=v1,v2,v3", )))
def test_parse_cli_parameters_throws_exception_on_invalid_syntax(self): with self.assertRaises(CfnSphereException): Config._parse_cli_parameters(("foo",))
def test_parse_cli_parameters_accepts_list_of_int(self): self.assertDictEqual({'stack1': {'p1': '1,2,3'}}, Config._parse_cli_parameters(("stack1.p1=1,2,3",)))
def test_parse_cli_parameters_accepts_list_of_strings(self): self.assertDictEqual({'stack1': {'p1': 'v1,v2,v3'}}, Config._parse_cli_parameters(("stack1.p1=v1,v2,v3",)))
def test_parse_cli_parameters_parses_single_int_parameter(self): self.assertDictEqual({'stack1': {'p1': '2'}}, Config._parse_cli_parameters(("stack1.p1=2",)))
def test_parse_cli_parameters_accepts_spaces(self): self.assertDictEqual({'stack1': {'p1': 'v1', 'p2': 'v2'}, 'stack2': {'p1': 'v1'}}, Config._parse_cli_parameters(("stack1.p1 = v1 ", "stack1.p2=v2", "stack2.p1=v1 ")))
def test_raises_exception_if_no_stacks_key(self): with self.assertRaises(NoConfigException): Config(config_dict={'region': 'eu-west-1'})
def test_parse_cli_parameters_accepts_list_of_int(self): self.assertDictEqual({'stack1': { 'p1': '1,2,3' }}, Config._parse_cli_parameters(("stack1.p1=1,2,3", )))