def test_status_does_not_exist(config, capsys): mock = MagicMock(**{'describe_stacks.side_effect': STACK_DOES_NOT_EXIST}) runner = Runner(mock, config) runner.status(7) captured = capsys.readouterr() assert captured.out == '\nStack: TestStack, Status: does not exist\n'
def test_apply_change_set(client, config, capsys, monkeypatch): # Prevent differences in format depending upon where this runs monkeypatch.setenv('STACKMANAGER_TIMEZONE', 'UTC') runner = Runner(client, config) runner.apply_change_set() client.describe_stack_events.assert_called_once_with(StackName='TestStack') client.get_waiter.assert_called_once_with('stack_update_complete') client.get_waiter().wait.assert_called_once_with(StackName='TestStack', WaiterConfig={ 'Delay': 10, 'MaxAttempts': 360 }) client.get_paginator.assert_called_once_with('describe_stack_events') client.get_paginator().paginate.assert_called_once_with( StackName='TestStack') client.update_termination_protection.assert_not_called() captured = capsys.readouterr() assert 'Executing ChangeSet TestChangeSet for TestStack' in captured.out assert 'ChangeSet TestChangeSet for TestStack successfully completed:' in captured.out assert 'Timestamp LogicalResourceId ResourceType ResourceStatus Reason' in captured.out assert '2020-01-01 13:23:11 TestStack AWS::CloudFormation::Stack UPDATE_IN_PROGRESS User Initiated' in captured.out assert '2020-01-01 13:24:53 Queue AWS::SQS::Queue CREATE_COMPLETE -' in captured.out assert '2020-01-01 13:25:01 TestStack AWS::CloudFormation::Stack UPDATE_COMPLETE -' in captured.out # We shouldn't have the previous events in the output assert 'AWS::CloudFormation::Stack CREATE_COMPLETE' not in captured.out
def test_delete_no_stack(config): attrs = {'describe_stacks.side_effect': STACK_DOES_NOT_EXIST} mock = MagicMock(**attrs) runner = Runner(mock, config) with pytest.raises(ValidationError, match='Stack TestStack not found'): runner.delete()
def test_delete_client_error(client, config): client.configure_mock( **{'delete_stack.side_effect': ClientError({}, 'delete_stack')}) runner = Runner(client, config) with pytest.raises( StackError, match= 'An error occurred \\(Unknown\\) when calling the delete_stack operation.*' ): runner.delete()
def test_print_stack_status_create_complete(client, config, monkeypatch, capsys): # Prevent differences in format depending upon where this runs monkeypatch.setenv('STACKMANAGER_TIMEZONE', 'UTC') runner = Runner(client, config) runner.print_stack_status() captured = capsys.readouterr() assert captured.out == '\nStack: TestStack, Status: CREATE_COMPLETE (2019-12-31 18:30:11)\n'
def test_reject(client, config, capsys): runner = Runner(client, config) runner.reject_change_set() client.describe_change_set.assert_called_once_with( StackName='TestStack', ChangeSetName='TestChangeSet') client.delete_change_set.assert_called_once_with( StackName='TestStack', ChangeSetName='TestChangeSet') captured = capsys.readouterr() assert 'Deleting ChangeSet TestChangeSet for TestStack' in captured.out
def test_apply_change_set_client_error(client, config): client.configure_mock(**{ 'execute_change_set.side_effect': ClientError({}, 'execute_change_set') }) runner = Runner(client, config) with pytest.raises( StackError, match= 'An error occurred \\(Unknown\\) when calling the execute_change_set .*' ): runner.apply_change_set()
def test_deploy_failed_only_existing_changes(client, config): config._config['ExistingChanges'] = 'FAILED_ONLY' runner = Runner(client, config) with pytest.raises( ValidationError, match= 'Creation of new ChangeSet not allowed when existing valid ChangeSets found' ): runner.deploy() client.list_change_sets.assert_called_once_with(StackName='TestStack')
def test_delete(client, config, capsys): runner = Runner(client, config) runner.delete() assert runner.stack is None client.describe_stack_events.assert_called_once_with(StackName='TestStack') client.delete_stack.assert_called_once_with(StackName='TestStack', RetainResources=[]) client.get_waiter.assert_called_once_with('stack_delete_complete') client.get_waiter().wait.assert_called_once_with(StackName='TestStack', WaiterConfig={ 'Delay': 10, 'MaxAttempts': 360 })
def test_deploy_rollback_complete(client, config): describe_stacks = { 'Stacks': [{ 'StackName': 'TestStack', 'StackStatus': StackStatus.ROLLBACK_COMPLETE.name, 'CreationTime': '2019-12-31T18:30:11.12345+0000' }] } client.configure_mock(**{'describe_stacks.return_value': describe_stacks}) runner = Runner(client, config) runner.deploy() client.delete_stack.assert_called_once_with(StackName='TestStack', RetainResources=[])
def test_load_stack_expired_token(config): ce = ClientError({'Error': {'Code': 'ExpiredToken'}}, 'describe_stacks') mock = MagicMock(**{'describe_stacks.side_effect': ce}) with pytest.raises(StackError, match='An error occurred \\(ExpiredToken\\).*'): Runner(mock, config)
def test_load_stack_does_not_exist(config): mock = MagicMock(**{'describe_stacks.side_effect': STACK_DOES_NOT_EXIST}) runner = Runner(mock, config) mock.describe_stacks.assert_called_once_with(StackName='TestStack') assert runner.stack is None assert runner.change_set_name == 'TestChangeSet'
def test_deploy_invalid_status(client, config): describe_stacks = { 'Stacks': [{ 'StackName': 'TestStack', 'StackStatus': StackStatus.UPDATE_IN_PROGRESS.name, 'CreationTime': '2019-12-31T18:30:11.12345+0000' }] } client.configure_mock(**{'describe_stacks.return_value': describe_stacks}) runner = Runner(client, config) with pytest.raises( ValidationError, match= 'Stack TestStack is not in a deployable status: UPDATE_IN_PROGRESS' ): runner.deploy()
def test_deploy_waiter_failed(client, config): # Configure waiter to throw WaiterError for some unknown other reasons waiter_error = WaiterError('change_set_create_complete', 'Other reason', { 'Status': 'FAILED', 'StatusReason': 'Some other reason' }) waiter_mock = MagicMock(**{'wait.side_effect': waiter_error}) client.configure_mock(**{'get_waiter.return_value': waiter_mock}) runner = Runner(client, config) with pytest.raises( StackError, match= 'ChangeSet creation failed - Status: FAILED, Reason: Some other reason' ): runner.deploy()
def test_status(client, config, capsys, monkeypatch): # Prevent differences in format depending upon where this runs monkeypatch.setenv('STACKMANAGER_TIMEZONE', 'UTC') before = datetime(2019, 12, 31, 0, 0, 0, 0, tzinfo=timezone.utc) now = datetime.now(tz=timezone.utc) runner = Runner(client, config) runner.status((now - before).days) captured = capsys.readouterr() assert 'Stack: TestStack, Status: CREATE_COMPLETE (2019-12-31 18:30:11)' in captured.out assert 'Existing ChangeSets:' in captured.out assert '2020-01-01 00:00:00: ExistingChangeSet (CREATE_COMPLETE)' in captured.out assert 'Events since 2019-12-31' in captured.out # Verify that the events are included assert '2020-01-01 11:58:20 Topic AWS::SNS::Topic CREATE_COMPLETE -' in captured.out
def test_deploy(client, config, capsys, monkeypatch): # Prevent differences in format depending upon where this runs monkeypatch.setenv('STACKMANAGER_TIMEZONE', 'UTC') runner = Runner(client, config) runner.deploy() client.list_change_sets.assert_called_once_with(StackName='TestStack') client.create_change_set.assert_called_once_with( StackName='TestStack', ChangeSetName='TestChangeSet', ChangeSetType='UPDATE', Parameters=[{ 'ParameterKey': 'Param1', 'ParameterValue': 'Value1' }, { 'ParameterKey': 'Param2', 'UsePreviousValue': True }], Tags=[{ 'Key': 'Tag1', 'Value': 'Value1' }], Capabilities=['CAPABILITY_IAM'], TemplateBody='AWSTemplateFormatVersion : "2010-09-09"') client.get_waiter.assert_called_once_with('change_set_create_complete') client.get_waiter().wait.assert_called_once_with( StackName='TestStack', ChangeSetName='TestChangeSet', WaiterConfig={ 'Delay': 5, 'MaxAttempts': 120 }) client.describe_change_set.assert_called_once_with( ChangeSetName='TestChangeSet', StackName='TestStack') captured = capsys.readouterr() assert 'Stack: TestStack, Status: CREATE_COMPLETE' in captured.out assert 'Existing ChangeSets:\n 2020-01-01 00:00:00: ExistingChangeSet (CREATE_COMPLETE)' in captured.out assert 'Creating ChangeSet TestChangeSet' in captured.out assert 'Action LogicalResourceId ResourceType Replacement' in captured.out assert 'Add Queue AWS::SQS::Queue -' in captured.out assert 'ChangeSet TestChangeSet is ready to run' in captured.out
def test_deploy_no_changes(client, config, capsys): # Configure waiter to throw WaiterError for FAILURE due to no changes waiter_error = WaiterError('change_set_create_complete', 'No Changes', { 'Status': 'FAILED', 'StatusReason': 'No updates are to be performed' }) waiter_mock = MagicMock(**{'wait.side_effect': waiter_error}) client.configure_mock(**{'get_waiter.return_value': waiter_mock}) runner = Runner(client, config) runner.deploy() client.list_change_sets.assert_called_once_with(StackName='TestStack') client.create_change_set.assert_called_once() client.get_waiter.assert_called_once_with('change_set_create_complete') client.describe_change_set.assert_not_called() client.update_termination_protection.assert_not_called() captured = capsys.readouterr() assert 'No changes to Stack TestStack' in captured.out
def test_print_stack_status_pending(config, monkeypatch, capsys): # Prevent differences in format depending upon where this runs monkeypatch.setenv('STACKMANAGER_TIMEZONE', 'UTC') describe_stacks = { 'Stacks': [{ 'StackName': 'TestStack', 'StackStatus': StackStatus.REVIEW_IN_PROGRESS.name, 'CreationTime': datetime(2019, 12, 31, 18, 29, 53, 64136, tzinfo=timezone.utc) }] } mock = MagicMock(**{'describe_stacks.return_value': describe_stacks}) runner = Runner(mock, config) runner.print_stack_status() captured = capsys.readouterr() assert captured.out == '\nStack: TestStack, Status: REVIEW_IN_PROGRESS (2019-12-31 18:29:53)\n'
def test_reject_delete_stack(client, config, capsys): # return no changesets client.configure_mock( **{'list_change_sets.return_value': { 'Summaries': [] }}) runner = Runner(client, config) # Update the status runner.stack['StackStatus'] = StackStatus.REVIEW_IN_PROGRESS.name runner.reject_change_set() client.describe_change_set.assert_called_once_with( StackName='TestStack', ChangeSetName='TestChangeSet') client.delete_change_set.assert_called_once_with( StackName='TestStack', ChangeSetName='TestChangeSet') client.list_change_sets.assert_called_once_with(StackName='TestStack') client.delete_stack.assert_called_once_with(StackName='TestStack') captured = capsys.readouterr() assert 'Deleting REVIEW_IN_PROGRESS Stack TestStack that has no remaining ChangeSets' in captured.out
def test_deploy_no_changes_enable_termination_protection( client, config, capsys): describe_stacks = { 'Stacks': [{ 'StackName': 'TestStack', 'StackStatus': StackStatus.CREATE_COMPLETE.name, 'CreationTime': '2019-12-31T18:30:11.12345+0000', 'EnableTerminationProtection': False }] } # Configure waiter to throw WaiterError for FAILURE due to no changes waiter_error = WaiterError('change_set_create_complete', 'No Changes', { 'Status': 'FAILED', 'StatusReason': 'No updates are to be performed' }) waiter_mock = MagicMock(**{'wait.side_effect': waiter_error}) client.configure_mock( **{ 'get_waiter.return_value': waiter_mock, 'describe_stacks.return_value': describe_stacks }) runner = Runner(client, config) runner.deploy() client.list_change_sets.assert_called_once_with(StackName='TestStack') client.create_change_set.assert_called_once() client.get_waiter.assert_called_once_with('change_set_create_complete') client.describe_change_set.assert_not_called() client.update_termination_protection.assert_called_once_with( StackName='TestStack', EnableTerminationProtection=True) captured = capsys.readouterr() assert 'No changes to Stack TestStack' in captured.out assert 'Enabled Termination Protection' in captured.out
def test_load_stack_pending(config): describe_stacks = { 'Stacks': [{ 'StackName': 'TestStack', 'StackStatus': StackStatus.REVIEW_IN_PROGRESS.name, 'CreationTime': datetime(2019, 12, 31, 18, 29, 53, 64136, tzinfo=timezone.utc) }] } mock = MagicMock(**{'describe_stacks.return_value': describe_stacks}) runner = Runner(mock, config) mock.describe_stacks.assert_called_once_with(StackName='TestStack') assert runner.stack is not None
def test_status_no_events(client, config, capsys): runner = Runner(client, config) runner.status(7) captured = capsys.readouterr() assert 'No events' in captured.out
def test_delete_waiter_error(client, config, capsys, monkeypatch): # Prevent differences in format depending upon where this runs monkeypatch.setenv('STACKMANAGER_TIMEZONE', 'UTC') # Configure waiter waiter_error = WaiterError('stack_delete_complete', 'Delete Failed', { 'Status': 'FAILED', 'StatusReason': 'Delete failed' }) waiter_mock = MagicMock(**{'wait.side_effect': waiter_error}) # Override Stack events stack_events = { 'StackEvents': [{ 'Timestamp': datetime(2020, 1, 1, 13, 35, 11, 0, tzinfo=timezone.utc), 'LogicalResourceId': 'Topic', 'ResourceType': 'AWS::SNS::Topic', 'ResourceStatus': 'DELETE_FAILED', 'ResourceStatusReason': 'Something went wrong' }, { 'Timestamp': datetime(2020, 1, 1, 12, 0, 0, 0, tzinfo=timezone.utc), 'LogicalResourceId': 'TestStack', 'ResourceType': 'AWS::CloudFormation::Stack', 'ResourceStatus': 'CREATE_COMPLETE' }, { 'Timestamp': datetime(2020, 1, 1, 11, 58, 20, 12436, tzinfo=timezone.utc), 'LogicalResourceId': 'Topic', 'ResourceType': 'AWS::SNS::Topic', 'ResourceStatus': 'CREATE_COMPLETE' }] } paginator_mock = MagicMock(**{'paginate.return_value': [stack_events]}) client.configure_mock( **{ 'get_waiter.return_value': waiter_mock, 'get_paginator.return_value': paginator_mock }) runner = Runner(client, config) with pytest.raises( StackError, match='Waiter stack_delete_complete failed: Delete Failed'): runner.delete() captured = capsys.readouterr() assert 'Deletion of Stack TestStack failed:' in captured.err assert '2020-01-01 13:35:11 Topic AWS::SNS::Topic DELETE_FAILED Something went wrong' \ in captured.out
def test_get_output(client, config): runner = Runner(client, config) assert runner.get_output('TestOutputKey') == 'TestOutputValue'
def test_get_output_not_found(client, config): runner = Runner(client, config) with pytest.raises(ValidationError, match='Output OtherKey not found'): runner.get_output('OtherKey')
def test_load_stack(client, config): runner = Runner(client, config) client.describe_stacks.assert_called_once_with(StackName='TestStack') assert runner.stack is not None assert runner.change_set_name == 'TestChangeSet'