def test_default_service_role_is_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', 'service-role': 'arn:aws:iam::123456789:role/my-role2', 'tags': { 'any-tag': 'any-tag-value' }, 'parameters': { 'any-parameter': 'any-value' } } } } ) self.assertEqual('arn:aws:iam::123456789:role/my-role2', config.stacks["any-stack"].service_role)
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 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 create_change_set(config, parameter, debug, confirm, yes, context, dry_run): 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, transform_context=context) StackActionHandler(config, dry_run).create_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 delete(config, 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) 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/KCOM-Enterprise/cfn-square/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 __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_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_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_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_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_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 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 get_stack_action_handler(domain, hosted_zone, 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 + ".", 'targetHostedZoneName': hosted_zone, '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 test_raises_exception_if_no_stacks_key(self): with self.assertRaises(NoConfigException): Config(config_dict={'region': 'eu-west-1'})
def test_validate_raises_exception_if_only_cli_params_given(self): with self.assertRaises(InvalidConfigException): Config(cli_params="foo")
def test_config_accepts_unicode_values(self): config_dict = {u"region": u"eu-west-1", u"stacks": { u"some-stack": {u"template-url": u"some-template.yml"}}} config = Config(config_dict=config_dict) self.assertEqual(config.region, "eu-west-1")
def test_config_reads_config_from_example_json_file(self, getcwd_mock): getcwd_mock.return_value = os.path.dirname(os.path.realpath(__file__)) config = Config("../../resources/example-stack-config.json") self.assertEqual(config.region, "eu-west-1") self.assertEqual(list(config.stacks.keys()), ["my-stack"])