def test_chained_commands(config): args = ['-p', 'dev', '-r', 'us-east-1', 'build-lambda', '-s', 'src/', '-o', '.', '--runtime', 'python3.8', 'upload', '-b', 'bucket', '-k', 'key', 'deploy', '-e', 'env', '-c', 'config.yml'] arg_config = Config({ 'Region': 'us-east-1', 'Parameters': { 'LambdaBucket': 'bucket', 'LambdaKey': 'key' } }) with patch('stackmanager.packager.build_lambda') as build_lambda: build_lambda.return_value = 'src.zip' with patch('stackmanager.cli.create_uploader') as create_uploader: with patch('stackmanager.cli.load_config') as load_config: load_config.return_value = config with patch('stackmanager.cli.create_runner') as create_runner: runner = CliRunner() result = runner.invoke(cli, args) assert result.exit_code == 0 build_lambda.assert_called_once_with('src/', '.', 'python3.8', None) create_uploader.assert_called_once_with('dev', 'us-east-1') create_uploader.return_value.upload.assert_called_once_with('src.zip', 'bucket', 'key') load_config.assert_called_once_with('config.yml', arg_config, 'env', Template=None, Parameters=(), PreviousParameters=(), ChangeSetName=None, ExistingChanges='ALLOW', AutoApply=False) create_runner.assert_called_once_with('dev', config) create_runner.return_value.deploy.assert_called_once()
def test_auto_apply(): config = Config({ 'Environment': 'dev', 'Region': 'us-east-1', 'AutoApply': True }) assert config.auto_apply is True
def test_termination_protection_true(): config = Config({ 'Environment': 'dev', 'Region': 'us-east-1', 'TerminationProtection': True }) assert config.termination_protection is True
def test_change_set_id(): config = Config({ 'Environment': 'dev', 'Region': 'us-east-1', 'ChangeSetId': 'abc123' }) assert config.change_set_id == 'abc123'
def test_change_set_name(): config = Config({ 'Environment': 'dev', 'Region': 'us-east-1', 'ChangeSetName': 'TestChangeSet' }) assert config.change_set_name == 'TestChangeSet'
def test_validate_config_missing_template(): config = Config({ 'Environment': 'dev', 'Region': 'us-east-1', 'StackName': 'Name' }) with pytest.raises(ValidationError, match='Template not set'): config.validate(check_template=True)
def test_validate_config_with_template_url(): config = Config({ 'Environment': 'dev', 'Region': 'us-east-1', 'StackName': 'Name', 'Template': 'https://s3.amazonaws.com/notarealurl' }) config.validate(check_template=True)
def test_validate_config_with_template_file(): config = Config({ 'Environment': 'dev', 'Region': 'us-east-1', 'StackName': 'Name', # Uses this file as the template file as it's just an exists check 'Template': __file__ }) config.validate(check_template=True)
def test_status(config): with patch('stackmanager.cli.load_config') as load_config: load_config.return_value = config with patch('stackmanager.cli.create_runner') as create_runner: cli_runner = CliRunner() result = cli_runner.invoke(cli, ['-p', 'dev', 'status', '-r', 'us-east-1', '-e', 'env', '-c', 'config.yml']) assert result.exit_code == 0 load_config.assert_called_once_with('config.yml', Config({'Region': 'us-east-1'}), 'env', False) create_runner.assert_called_once_with('dev', config) create_runner.return_value.status.assert_called_once_with(7)
def dev_config(all_config): config = Config({ 'Environment': 'dev', 'Region': 'us-east-1', 'Parameters': { 'This': 'TheOther', 'Extra': '/Company/{{ Region }}/{{ Environment }}/Extra' }, 'Capabilities': ['CAPABILITIES_NAMED_IAM'] }) config.set_parent(all_config) return config
def test_reject_change_set_name(config): with patch('stackmanager.cli.load_config') as load_config: load_config.return_value = config with patch('stackmanager.cli.create_runner') as create_runner: cli_runner = CliRunner() result = cli_runner.invoke(cli, ['-r', 'us-east-1', 'reject', '-p', 'dev', '-e', 'env', '-c', 'config.yml', '--change-set-name', 'testchangeset']) assert result.exit_code == 0 load_config.assert_called_once_with('config.yml', Config({'Region': 'us-east-1'}), 'env', False, ChangeSetName='testchangeset') create_runner.assert_called_once_with('dev', config) create_runner.return_value.reject_change_set.assert_called_once()
def test_loader_prod(): config = load_config(config_file(), Config({'Region': 'us-east-2'}), 'prod', False) assert config.environment == 'prod' assert config.region == 'us-east-2' assert config.template == 'integration/template.yaml' assert config.parameters == { 'Environment': 'prod', 'SSMKey': '/Company/p/us-east-2/Key', 'Domain': 'prod.example.com', 'KeyId': 'guid4' } assert config.tags == {'Application': 'Example', 'Environment': 'prod'} assert config.capabilities == ['CAPABILITY_NAMED_IAM']
def test_get_output(config): with patch('stackmanager.cli.load_config') as load_config: load_config.return_value = config with patch('stackmanager.cli.create_runner') as create_runner: create_runner.return_value.configure_mock(**{'get_output.return_value': 'TestOutputValue'}) cli_runner = CliRunner() result = cli_runner.invoke(cli, ['-p', 'dev', 'get-output', '-r', 'us-east-1', '-e', 'env', '-c', 'config.yml', '-o', 'TestOutputKey']) assert result.exit_code == 0 load_config.assert_called_once_with('config.yml', Config({'Region': 'us-east-1'}), 'env', False) create_runner.assert_called_once_with('dev', config) create_runner.return_value.get_output.assert_called_once_with('TestOutputKey') assert result.output == 'TestOutputValue\n'
def create_arg_config(environment, region, template, parameters): """ Create a Configuration from the command line arguments, used as top of hierarchy to optionally override template and parameters. :param str environment: Environment :param str region: Region to deploy :param str template: Override value for Template from command line :param list parameters: Override values for Parameters from the command line :return: Argument Config """ raw_config = {'Environment': environment, 'Region': region} if template: raw_config['Template'] = template if parameters: raw_config['Parameters'] = dict(parameters) return Config(raw_config)
def test_deploy(config): with patch('stackmanager.cli.load_config') as load_config: load_config.return_value = config with patch('stackmanager.cli.create_runner') as create_runner: cli_runner = CliRunner() result = cli_runner.invoke(cli, ['deploy', '-p', 'dev', '-r', 'us-east-1', '-e', 'env', '-c', 'config.yml', '-t', 'template.yml', '--parameter', 'foo', 'bar', '--parameter-use-previous', 'keep', '--change-set-name', 'testchangeset', '--auto-apply']) assert result.exit_code == 0 load_config.assert_called_once_with('config.yml', Config({'Region': 'us-east-1'}), 'env', Template='template.yml', Parameters=(('foo', 'bar'),), PreviousParameters=('keep',), ChangeSetName='testchangeset', ExistingChanges='ALLOW', AutoApply=True) create_runner.assert_called_once_with('dev', config) create_runner.return_value.deploy.assert_called_once()
def dev_config(all_config): config = Config({ 'Environment': 'dev', 'Region': 'us-east-1', 'Parameters': { 'Extra': '/Company/{{ Region }}/{{ Environment }}/Extra', 'Override': 'dev-value' }, 'Variables': { 'EnvironmentCode': 'd' }, 'Capabilities': ['CAPABILITIES_NAMED_IAM'], 'Tags': { 'CostCenter': 200 } }) config.parent = all_config return config
def all_config(): return Config({ 'Environment': 'all', 'StackName': '{{ Environment }}-ExampleStack', 'Template': 'template.yaml', 'Parameters': { 'Environment': '{{ Environment }}', 'Thing': '/Company/{{ Environment }}/{{ Region }}/Thing', 'This': 'That' }, 'Tags': { 'Team': 'Ops', 'TestFunction': '{{ Region|replace("us-", "u")|replace("east-","e")|replace("west-","w") }}' }, 'Capabilities': ['CAPABILITIES_IAM'] })
def load_config(config_file, environment, region, template, parameters): """ Build hierarchy of configurations by loading multi-document config file. There must be a matching config for the environment name and region. :param str config_file: Path to config file :param str environment: Environment being updated :param str region: Region for the Stack :param str template: Override value for Template from command line :param list parameters: Override values for Parameters from the command line :return: Top of Config hierarchy :raises validationException: If config file not found, matching environment not found in config or config is invalid """ arg_config = create_arg_config(environment, region, template, parameters) try: with open(config_file) as c: raw_configs = yaml.safe_load_all(c) all_config = None env_config = None for rc in raw_configs: config = Config(rc) if Config.is_all(config.environment): all_config = config elif config.environment == environment and config.region == region: env_config = config if not env_config: raise ValidationError( f'Environment {environment} for {region} not found in {config_file}' ) env_config.set_parent(all_config) arg_config.set_parent(env_config) # Validate before returning arg_config.validate() return arg_config except FileNotFoundError: raise ValidationError(f'Config file {config_file} not found')
def test_loader_dev_overrides(): previous_parameters = ('Previous', 'LambdaKey') override_parameters = (('SSMKey', '/Other/{{ Environment }}/Key'), ('Domain', 'notdev.example.com'), ('Extra', 'OverrideDefault'), ('LambdaKey', 'd/e/f')) arg_config = Config({}) arg_config.region = 'us-east-1' arg_config.add_parameters({ 'LambdaBucket': 'mybucket', 'LambdaKey': 'a/b/c' }) config = load_config(config_file(), arg_config, 'dev', False, Template='integration/config.yaml', Parameters=override_parameters, PreviousParameters=previous_parameters, ChangeSetName='TestChangeSet', AutoApply=True) assert config.environment == 'dev' assert config.region == 'us-east-1' assert config.template == 'integration/config.yaml' assert config.parameters == { 'Environment': 'dev', 'SSMKey': '/Other/dev/Key', 'Domain': 'notdev.example.com', 'KeyId': 'guid1', 'Extra': 'OverrideDefault', 'LambdaBucket': 'mybucket', 'LambdaKey': 'd/e/f', 'Previous': '<<UsePreviousValue>>' } assert config.change_set_name == 'TestChangeSet' assert config.auto_apply is True
def config(): return Config({ 'StackName': 'TestStack', 'Environment': 'test', 'Region': 'us-east-1', 'Parameters': { 'Param1': 'Value1', 'Param2': USE_PREVIOUS_VALUE }, 'Tags': { 'Tag1': 'Value1' }, 'Capabilities': ['CAPABILITY_IAM'], 'Template': os.path.join(os.path.dirname(__file__), 'template.yaml'), 'ExistingChanges': 'ALLOW', 'ChangeSetName': 'TestChangeSet' })
def create_changeset_runner(profile, region, change_set_id): """ Create a runner for processing a changeset using it's id. This first describes the changeset to get the stack name, and then the runner can be created with a dummy config. :param profile: AWS Profile from command line :param region: AWS Region from command line :param change_set_id: Change Set identifier :return: Runner instance """ session = boto3.Session(profile_name=profile, region_name=region) client = session.client('cloudformation') try: change_set = client.describe_change_set(ChangeSetName=change_set_id) config = Config({ 'Environment': 'unknown', 'Region': session.region_name, 'StackName': change_set['StackName'], 'ChangeSetName': change_set['ChangeSetName'] }) return create_runner(profile, config) except client.exceptions.ChangeSetNotFoundException: raise ValidationError(f'ChangeSet {change_set_id} not found')
def test_auto_apply_default(): config = Config({'Environment': 'dev', 'Region': 'us-east-1'}) assert config.auto_apply is False
def test_termination_protection_default(): config = Config({'Environment': 'dev', 'Region': 'us-east-1'}) assert config.termination_protection is True
def config(): return Config({})
def test_loader_missing_region(): with pytest.raises(ValidationError, match='Environment dev for us-west-1 not found in .*'): load_config(config_file(), Config({'Region': 'us-west-1'}), 'dev')
def __init__(self, profile, region): self.config = Config({}) self.config.region = region self.profile = profile self.zip_file = None