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_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_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_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_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_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_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_get_output(client, config): runner = Runner(client, config) assert runner.get_output('TestOutputKey') == 'TestOutputValue'
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_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'