예제 #1
0
class TestProviderInteractiveMode(unittest.TestCase):
    def setUp(self):
        region = "us-east-1"
        self.session = get_session(region=region)
        self.provider = Provider(self.session,
                                 interactive=True,
                                 recreate_failed=True)
        self.stubber = Stubber(self.provider.cloudformation)

    def test_successful_init(self):
        replacements = True
        p = Provider(self.session,
                     interactive=True,
                     replacements_only=replacements)
        self.assertEqual(p.replacements_only, replacements)

    @patch("stacker.providers.aws.default.ask_for_approval")
    def test_update_stack_execute_success_no_stack_policy(
            self, patched_approval):
        stack_name = "my-fake-stack"

        self.stubber.add_response("create_change_set", {
            'Id': 'CHANGESETID',
            'StackId': 'STACKID'
        })
        changes = []
        changes.append(generate_change())

        self.stubber.add_response(
            "describe_change_set",
            generate_change_set_response(
                status="CREATE_COMPLETE",
                execution_status="AVAILABLE",
                changes=changes,
            ))

        self.stubber.add_response("execute_change_set", {})

        with self.stubber:
            self.provider.update_stack(
                fqn=stack_name,
                template=Template(url="http://fake.template.url.com/"),
                old_parameters=[],
                parameters=[],
                tags=[])

        patched_approval.assert_called_with(full_changeset=changes,
                                            params_diff=[],
                                            include_verbose=True,
                                            fqn=stack_name)

        self.assertEqual(patched_approval.call_count, 1)

    @patch("stacker.providers.aws.default.ask_for_approval")
    def test_update_stack_execute_success_with_stack_policy(
            self, patched_approval):
        stack_name = "my-fake-stack"

        self.stubber.add_response("create_change_set", {
            'Id': 'CHANGESETID',
            'StackId': 'STACKID'
        })
        changes = []
        changes.append(generate_change())

        self.stubber.add_response(
            "describe_change_set",
            generate_change_set_response(
                status="CREATE_COMPLETE",
                execution_status="AVAILABLE",
                changes=changes,
            ))

        self.stubber.add_response("set_stack_policy", {})

        self.stubber.add_response("execute_change_set", {})

        with self.stubber:
            self.provider.update_stack(
                fqn=stack_name,
                template=Template(url="http://fake.template.url.com/"),
                old_parameters=[],
                parameters=[],
                tags=[],
                stack_policy=Template(body="{}"),
            )

        patched_approval.assert_called_with(full_changeset=changes,
                                            params_diff=[],
                                            include_verbose=True,
                                            fqn=stack_name)

        self.assertEqual(patched_approval.call_count, 1)

    def test_select_update_method(self):
        for i in [[{
                'force_interactive': False,
                'force_change_set': False
        }, self.provider.interactive_update_stack],
                  [{
                      'force_interactive': True,
                      'force_change_set': False
                  }, self.provider.interactive_update_stack],
                  [{
                      'force_interactive': False,
                      'force_change_set': True
                  }, self.provider.interactive_update_stack],
                  [{
                      'force_interactive': True,
                      'force_change_set': True
                  }, self.provider.interactive_update_stack]]:
            self.assertEquals(self.provider.select_update_method(**i[0]), i[1])

    @patch('stacker.providers.aws.default.output_full_changeset')
    @patch('stacker.providers.aws.default.output_summary')
    def test_get_stack_changes_interactive(self, mock_output_summary,
                                           mock_output_full_cs):
        stack_name = "MockStack"
        mock_stack = generate_stack_object(stack_name)

        self.stubber.add_response(
            'describe_stacks',
            {'Stacks': [generate_describe_stacks_stack(stack_name)]})
        self.stubber.add_response('get_template',
                                  generate_get_template('cfn_template.yaml'))
        self.stubber.add_response("create_change_set", {
            'Id': 'CHANGESETID',
            'StackId': stack_name
        })
        changes = []
        changes.append(generate_change())

        self.stubber.add_response(
            "describe_change_set",
            generate_change_set_response(
                status="CREATE_COMPLETE",
                execution_status="AVAILABLE",
                changes=changes,
            ))
        self.stubber.add_response("delete_change_set", {})
        self.stubber.add_response(
            'describe_stacks',
            {'Stacks': [generate_describe_stacks_stack(stack_name)]})

        with self.stubber:
            self.provider.get_stack_changes(
                stack=mock_stack,
                template=Template(url="http://fake.template.url.com/"),
                parameters=[],
                tags=[])

        mock_output_summary.assert_called_with(stack_name,
                                               'changes',
                                               changes, [],
                                               replacements_only=False)
        mock_output_full_cs.assert_called_with(full_changeset=changes,
                                               params_diff=[],
                                               fqn=stack_name)
예제 #2
0
class TestProviderInteractiveMode(unittest.TestCase):
    def setUp(self):
        region = "us-east-1"
        self.session = get_session(region=region)
        self.provider = Provider(
            self.session, interactive=True, recreate_failed=True)
        self.stubber = Stubber(self.provider.cloudformation)

    def test_successful_init(self):
        replacements = True
        p = Provider(self.session, interactive=True,
                     replacements_only=replacements)
        self.assertEqual(p.replacements_only, replacements)

    @patch("stacker.providers.aws.default.ask_for_approval")
    def test_update_stack_execute_success_no_stack_policy(self,
                                                          patched_approval):
        stack_name = "my-fake-stack"

        self.stubber.add_response(
            "create_change_set",
            {'Id': 'CHANGESETID', 'StackId': 'STACKID'}
        )
        changes = []
        changes.append(generate_change())

        self.stubber.add_response(
            "describe_change_set",
            generate_change_set_response(
                status="CREATE_COMPLETE", execution_status="AVAILABLE",
                changes=changes,
            )
        )

        self.stubber.add_response("execute_change_set", {})

        with self.stubber:
            self.provider.update_stack(
                fqn=stack_name,
                template=Template(url="http://fake.template.url.com/"),
                old_parameters=[],
                parameters=[], tags=[]
            )

        patched_approval.assert_called_with(full_changeset=changes,
                                            params_diff=[],
                                            include_verbose=True)

        self.assertEqual(patched_approval.call_count, 1)

    @patch("stacker.providers.aws.default.ask_for_approval")
    def test_update_stack_execute_success_with_stack_policy(self,
                                                            patched_approval):
        stack_name = "my-fake-stack"

        self.stubber.add_response(
            "create_change_set",
            {'Id': 'CHANGESETID', 'StackId': 'STACKID'}
        )
        changes = []
        changes.append(generate_change())

        self.stubber.add_response(
            "describe_change_set",
            generate_change_set_response(
                status="CREATE_COMPLETE", execution_status="AVAILABLE",
                changes=changes,
            )
        )

        self.stubber.add_response("set_stack_policy", {})

        self.stubber.add_response("execute_change_set", {})

        with self.stubber:
            self.provider.update_stack(
                fqn=stack_name,
                template=Template(url="http://fake.template.url.com/"),
                old_parameters=[],
                parameters=[], tags=[],
                stack_policy=Template(body="{}"),
            )

        patched_approval.assert_called_with(full_changeset=changes,
                                            params_diff=[],
                                            include_verbose=True)

        self.assertEqual(patched_approval.call_count, 1)

    def test_select_update_method(self):
        for i in [[{'force_interactive': False,
                    'force_change_set': False},
                   self.provider.interactive_update_stack],
                  [{'force_interactive': True,
                    'force_change_set': False},
                   self.provider.interactive_update_stack],
                  [{'force_interactive': False,
                    'force_change_set': True},
                   self.provider.interactive_update_stack],
                  [{'force_interactive': True,
                    'force_change_set': True},
                   self.provider.interactive_update_stack]]:
            self.assertEquals(
                self.provider.select_update_method(**i[0]),
                i[1]
            )
예제 #3
0
class TestProviderDefaultMode(unittest.TestCase):
    def setUp(self):
        region = "us-east-1"
        self.session = get_session(region=region)
        self.provider = Provider(self.session,
                                 region=region,
                                 recreate_failed=False)
        self.stubber = Stubber(self.provider.cloudformation)

    def test_get_stack_stack_does_not_exist(self):
        stack_name = "MockStack"
        self.stubber.add_client_error(
            "describe_stacks",
            service_error_code="ValidationError",
            service_message="Stack with id %s does not exist" % stack_name,
            expected_params={"StackName": stack_name})

        with self.assertRaises(exceptions.StackDoesNotExist):
            with self.stubber:
                self.provider.get_stack(stack_name)

    def test_get_stack_stack_exists(self):
        stack_name = "MockStack"
        stack_response = {
            "Stacks": [generate_describe_stacks_stack(stack_name)]
        }
        self.stubber.add_response("describe_stacks",
                                  stack_response,
                                  expected_params={"StackName": stack_name})

        with self.stubber:
            response = self.provider.get_stack(stack_name)

        self.assertEqual(response["StackName"], stack_name)

    def test_select_update_method(self):
        for i in [[{
                'force_interactive': True,
                'force_change_set': False
        }, self.provider.interactive_update_stack],
                  [{
                      'force_interactive': False,
                      'force_change_set': False
                  }, self.provider.default_update_stack],
                  [{
                      'force_interactive': False,
                      'force_change_set': True
                  }, self.provider.noninteractive_changeset_update],
                  [{
                      'force_interactive': True,
                      'force_change_set': True
                  }, self.provider.interactive_update_stack]]:
            self.assertEquals(self.provider.select_update_method(**i[0]), i[1])

    def test_prepare_stack_for_update_completed(self):
        stack_name = "MockStack"
        stack = generate_describe_stacks_stack(stack_name,
                                               stack_status="UPDATE_COMPLETE")

        with self.stubber:
            self.assertTrue(self.provider.prepare_stack_for_update(stack, []))

    def test_prepare_stack_for_update_in_progress(self):
        stack_name = "MockStack"
        stack = generate_describe_stacks_stack(
            stack_name, stack_status="UPDATE_IN_PROGRESS")

        with self.assertRaises(exceptions.StackUpdateBadStatus) as raised:
            with self.stubber:
                self.provider.prepare_stack_for_update(stack, [])

            self.assertIn('in-progress', str(raised.exception))

    def test_prepare_stack_for_update_non_recreatable(self):
        stack_name = "MockStack"
        stack = generate_describe_stacks_stack(
            stack_name, stack_status="REVIEW_IN_PROGRESS")

        with self.assertRaises(exceptions.StackUpdateBadStatus) as raised:
            with self.stubber:
                self.provider.prepare_stack_for_update(stack, [])

        self.assertIn('Unsupported state', str(raised.exception))

    def test_prepare_stack_for_update_disallowed(self):
        stack_name = "MockStack"
        stack = generate_describe_stacks_stack(
            stack_name, stack_status="ROLLBACK_COMPLETE")

        with self.assertRaises(exceptions.StackUpdateBadStatus) as raised:
            with self.stubber:
                self.provider.prepare_stack_for_update(stack, [])

        self.assertIn('re-creation is disabled', str(raised.exception))
        # Ensure we point out to the user how to enable re-creation
        self.assertIn('--recreate-failed', str(raised.exception))

    def test_prepare_stack_for_update_bad_tags(self):
        stack_name = "MockStack"
        stack = generate_describe_stacks_stack(
            stack_name, stack_status="ROLLBACK_COMPLETE")

        self.provider.recreate_failed = True

        with self.assertRaises(exceptions.StackUpdateBadStatus) as raised:
            with self.stubber:
                self.provider.prepare_stack_for_update(stack,
                                                       tags=[{
                                                           'Key':
                                                           'stacker_namespace',
                                                           'Value': 'test'
                                                       }])

        self.assertIn('tags differ', str(raised.exception).lower())

    def test_prepare_stack_for_update_recreate(self):
        stack_name = "MockStack"
        stack = generate_describe_stacks_stack(
            stack_name, stack_status="ROLLBACK_COMPLETE")

        self.stubber.add_response("delete_stack", {},
                                  expected_params={"StackName": stack_name})

        self.provider.recreate_failed = True

        with self.stubber:
            self.assertFalse(self.provider.prepare_stack_for_update(stack, []))

    def test_noninteractive_changeset_update_no_stack_policy(self):
        stack_name = "MockStack"

        self.stubber.add_response("create_change_set", {
            'Id': 'CHANGESETID',
            'StackId': 'STACKID'
        })
        changes = []
        changes.append(generate_change())

        self.stubber.add_response(
            "describe_change_set",
            generate_change_set_response(
                status="CREATE_COMPLETE",
                execution_status="AVAILABLE",
                changes=changes,
            ))

        self.stubber.add_response("execute_change_set", {})

        with self.stubber:
            self.provider.noninteractive_changeset_update(
                fqn=stack_name,
                template=Template(url="http://fake.template.url.com/"),
                old_parameters=[],
                parameters=[],
                stack_policy=None,
                tags=[],
            )

    def test_noninteractive_changeset_update_with_stack_policy(self):
        stack_name = "MockStack"

        self.stubber.add_response("create_change_set", {
            'Id': 'CHANGESETID',
            'StackId': 'STACKID'
        })
        changes = []
        changes.append(generate_change())

        self.stubber.add_response(
            "describe_change_set",
            generate_change_set_response(
                status="CREATE_COMPLETE",
                execution_status="AVAILABLE",
                changes=changes,
            ))

        self.stubber.add_response("set_stack_policy", {})

        self.stubber.add_response("execute_change_set", {})

        with self.stubber:
            self.provider.noninteractive_changeset_update(
                fqn=stack_name,
                template=Template(url="http://fake.template.url.com/"),
                old_parameters=[],
                parameters=[],
                stack_policy=Template(body="{}"),
                tags=[],
            )

    @patch('stacker.providers.aws.default.output_full_changeset')
    def test_get_stack_changes_update(self, mock_output_full_cs):
        stack_name = "MockStack"
        mock_stack = generate_stack_object(stack_name)

        self.stubber.add_response(
            'describe_stacks',
            {'Stacks': [generate_describe_stacks_stack(stack_name)]})
        self.stubber.add_response('get_template',
                                  generate_get_template('cfn_template.yaml'))
        self.stubber.add_response("create_change_set", {
            'Id': 'CHANGESETID',
            'StackId': stack_name
        })
        changes = []
        changes.append(generate_change())

        self.stubber.add_response(
            "describe_change_set",
            generate_change_set_response(
                status="CREATE_COMPLETE",
                execution_status="AVAILABLE",
                changes=changes,
            ))
        self.stubber.add_response("delete_change_set", {})
        self.stubber.add_response(
            'describe_stacks',
            {'Stacks': [generate_describe_stacks_stack(stack_name)]})

        with self.stubber:
            result = self.provider.get_stack_changes(
                stack=mock_stack,
                template=Template(url="http://fake.template.url.com/"),
                parameters=[],
                tags=[])

        mock_output_full_cs.assert_called_with(full_changeset=changes,
                                               params_diff=[],
                                               fqn=stack_name,
                                               answer='y')
        expected_outputs = {
            'FakeOutput':
            '<inferred-change: MockStack.FakeOutput={}>'.format(
                str({"Ref": "FakeResource"}))
        }
        self.assertEqual(self.provider.get_outputs(stack_name),
                         expected_outputs)
        self.assertEqual(result, expected_outputs)

    @patch('stacker.providers.aws.default.output_full_changeset')
    def test_get_stack_changes_create(self, mock_output_full_cs):
        stack_name = "MockStack"
        mock_stack = generate_stack_object(stack_name)

        self.stubber.add_response(
            'describe_stacks', {
                'Stacks': [
                    generate_describe_stacks_stack(
                        stack_name, stack_status='REVIEW_IN_PROGRESS')
                ]
            })
        self.stubber.add_response("create_change_set", {
            'Id': 'CHANGESETID',
            'StackId': stack_name
        })
        changes = []
        changes.append(generate_change())

        self.stubber.add_response(
            "describe_change_set",
            generate_change_set_response(
                status="CREATE_COMPLETE",
                execution_status="AVAILABLE",
                changes=changes,
            ))
        self.stubber.add_response("delete_change_set", {})
        self.stubber.add_response(
            'describe_stacks', {
                'Stacks': [
                    generate_describe_stacks_stack(
                        stack_name, stack_status='REVIEW_IN_PROGRESS')
                ]
            })
        self.stubber.add_response(
            'describe_stacks', {
                'Stacks': [
                    generate_describe_stacks_stack(
                        stack_name, stack_status='REVIEW_IN_PROGRESS')
                ]
            })

        self.stubber.add_response("delete_stack", {})

        with self.stubber:
            self.provider.get_stack_changes(
                stack=mock_stack,
                template=Template(url="http://fake.template.url.com/"),
                parameters=[],
                tags=[])

        mock_output_full_cs.assert_called_with(full_changeset=changes,
                                               params_diff=[],
                                               fqn=stack_name,
                                               answer='y')

    def test_tail_stack_retry_on_missing_stack(self):
        stack_name = "SlowToCreateStack"
        stack = MagicMock(spec=Stack)
        stack.fqn = "my-namespace-{}".format(stack_name)

        default.TAIL_RETRY_SLEEP = .01

        # Ensure the stack never appears before we run out of retries
        for i in range(MAX_TAIL_RETRIES + 5):
            self.stubber.add_client_error(
                "describe_stack_events",
                service_error_code="ValidationError",
                service_message="Stack [{}] does not exist".format(stack_name),
                http_status_code=400,
                response_meta={"attempt": i + 1},
            )

        with self.stubber:
            try:
                self.provider.tail_stack(stack, threading.Event())
            except ClientError as exc:
                self.assertEqual(exc.response["ResponseMetadata"]["attempt"],
                                 MAX_TAIL_RETRIES)

    def test_tail_stack_retry_on_missing_stack_eventual_success(self):
        stack_name = "SlowToCreateStack"
        stack = MagicMock(spec=Stack)
        stack.fqn = "my-namespace-{}".format(stack_name)

        default.TAIL_RETRY_SLEEP = .01
        default.GET_EVENTS_SLEEP = .01

        rcvd_events = []

        def mock_log_func(e):
            rcvd_events.append(e)

        def valid_event_response(stack, event_id):
            return {
                "StackEvents": [
                    {
                        "StackId": stack.fqn + "12345",
                        "EventId": event_id,
                        "StackName": stack.fqn,
                        "Timestamp": datetime.now()
                    },
                ]
            }

        # Ensure the stack never appears before we run out of retries
        for i in range(3):
            self.stubber.add_client_error(
                "describe_stack_events",
                service_error_code="ValidationError",
                service_message="Stack [{}] does not exist".format(stack_name),
                http_status_code=400,
                response_meta={"attempt": i + 1},
            )

        self.stubber.add_response("describe_stack_events",
                                  valid_event_response(stack, "InitialEvents"))

        self.stubber.add_response("describe_stack_events",
                                  valid_event_response(stack, "Event1"))

        with self.stubber:
            try:
                self.provider.tail_stack(stack,
                                         threading.Event(),
                                         log_func=mock_log_func)
            except UnStubbedResponseError:
                # Eventually we run out of responses - could not happen in
                # regular execution
                # normally this would just be dealt with when the threads were
                # shutdown, but doing so here is a little difficult because
                # we can't control the `tail_stack` loop
                pass

        self.assertEqual(rcvd_events[0]["EventId"], "Event1")
예제 #4
0
class TestProviderDefaultMode(unittest.TestCase):
    def setUp(self):
        region = "us-east-1"
        self.session = get_session(region=region)
        self.provider = Provider(
            self.session, region=region, recreate_failed=False)
        self.stubber = Stubber(self.provider.cloudformation)

    def test_get_stack_stack_does_not_exist(self):
        stack_name = "MockStack"
        self.stubber.add_client_error(
            "describe_stacks",
            service_error_code="ValidationError",
            service_message="Stack with id %s does not exist" % stack_name,
            expected_params={"StackName": stack_name}
        )

        with self.assertRaises(exceptions.StackDoesNotExist):
            with self.stubber:
                self.provider.get_stack(stack_name)

    def test_get_stack_stack_exists(self):
        stack_name = "MockStack"
        stack_response = {
            "Stacks": [generate_describe_stacks_stack(stack_name)]
        }
        self.stubber.add_response(
            "describe_stacks",
            stack_response,
            expected_params={"StackName": stack_name}
        )

        with self.stubber:
            response = self.provider.get_stack(stack_name)

        self.assertEqual(response["StackName"], stack_name)

    def test_select_update_method(self):
        for i in [[{'force_interactive': True,
                    'force_change_set': False},
                   self.provider.interactive_update_stack],
                  [{'force_interactive': False,
                    'force_change_set': False},
                   self.provider.default_update_stack],
                  [{'force_interactive': False,
                    'force_change_set': True},
                   self.provider.noninteractive_changeset_update],
                  [{'force_interactive': True,
                    'force_change_set': True},
                   self.provider.interactive_update_stack]]:
            self.assertEquals(
                self.provider.select_update_method(**i[0]),
                i[1]
            )

    def test_prepare_stack_for_update_completed(self):
        stack_name = "MockStack"
        stack = generate_describe_stacks_stack(
            stack_name, stack_status="UPDATE_COMPLETE")

        with self.stubber:
            self.assertTrue(
                self.provider.prepare_stack_for_update(stack, []))

    def test_prepare_stack_for_update_in_progress(self):
        stack_name = "MockStack"
        stack = generate_describe_stacks_stack(
            stack_name, stack_status="UPDATE_IN_PROGRESS")

        with self.assertRaises(exceptions.StackUpdateBadStatus) as raised:
            with self.stubber:
                self.provider.prepare_stack_for_update(stack, [])

            self.assertIn('in-progress', str(raised.exception))

    def test_prepare_stack_for_update_non_recreatable(self):
        stack_name = "MockStack"
        stack = generate_describe_stacks_stack(
            stack_name, stack_status="REVIEW_IN_PROGRESS")

        with self.assertRaises(exceptions.StackUpdateBadStatus) as raised:
            with self.stubber:
                self.provider.prepare_stack_for_update(stack, [])

        self.assertIn('Unsupported state', str(raised.exception))

    def test_prepare_stack_for_update_disallowed(self):
        stack_name = "MockStack"
        stack = generate_describe_stacks_stack(
            stack_name, stack_status="ROLLBACK_COMPLETE")

        with self.assertRaises(exceptions.StackUpdateBadStatus) as raised:
            with self.stubber:
                self.provider.prepare_stack_for_update(stack, [])

        self.assertIn('re-creation is disabled', str(raised.exception))
        # Ensure we point out to the user how to enable re-creation
        self.assertIn('--recreate-failed', str(raised.exception))

    def test_prepare_stack_for_update_bad_tags(self):
        stack_name = "MockStack"
        stack = generate_describe_stacks_stack(
            stack_name, stack_status="ROLLBACK_COMPLETE")

        self.provider.recreate_failed = True

        with self.assertRaises(exceptions.StackUpdateBadStatus) as raised:
            with self.stubber:
                self.provider.prepare_stack_for_update(
                    stack,
                    tags=[{'Key': 'stacker_namespace', 'Value': 'test'}])

        self.assertIn('tags differ', str(raised.exception).lower())

    def test_prepare_stack_for_update_recreate(self):
        stack_name = "MockStack"
        stack = generate_describe_stacks_stack(
            stack_name, stack_status="ROLLBACK_COMPLETE")

        self.stubber.add_response(
            "delete_stack",
            {},
            expected_params={"StackName": stack_name}
        )

        self.provider.recreate_failed = True

        with self.stubber:
            self.assertFalse(
                self.provider.prepare_stack_for_update(stack, []))

    def test_noninteractive_changeset_update_no_stack_policy(self):
        stack_name = "MockStack"

        self.stubber.add_response(
            "create_change_set",
            {'Id': 'CHANGESETID', 'StackId': 'STACKID'}
        )
        changes = []
        changes.append(generate_change())

        self.stubber.add_response(
            "describe_change_set",
            generate_change_set_response(
                status="CREATE_COMPLETE", execution_status="AVAILABLE",
                changes=changes,
            )
        )

        self.stubber.add_response("execute_change_set", {})

        with self.stubber:
            self.provider.noninteractive_changeset_update(
                fqn=stack_name,
                template=Template(url="http://fake.template.url.com/"),
                old_parameters=[],
                parameters=[], stack_policy=None, tags=[],
            )

    def test_noninteractive_changeset_update_with_stack_policy(self):
        stack_name = "MockStack"

        self.stubber.add_response(
            "create_change_set",
            {'Id': 'CHANGESETID', 'StackId': 'STACKID'}
        )
        changes = []
        changes.append(generate_change())

        self.stubber.add_response(
            "describe_change_set",
            generate_change_set_response(
                status="CREATE_COMPLETE", execution_status="AVAILABLE",
                changes=changes,
            )
        )

        self.stubber.add_response("set_stack_policy", {})

        self.stubber.add_response("execute_change_set", {})

        with self.stubber:
            self.provider.noninteractive_changeset_update(
                fqn=stack_name,
                template=Template(url="http://fake.template.url.com/"),
                old_parameters=[],
                parameters=[], stack_policy=Template(body="{}"), tags=[],
            )

    def test_tail_stack_retry_on_missing_stack(self):
        stack_name = "SlowToCreateStack"
        stack = MagicMock(spec=Stack)
        stack.fqn = "my-namespace-{}".format(stack_name)

        default.TAIL_RETRY_SLEEP = .01

        # Ensure the stack never appears before we run out of retries
        for i in range(MAX_TAIL_RETRIES + 5):
            self.stubber.add_client_error(
                "describe_stack_events",
                service_error_code="ValidationError",
                service_message="Stack [{}] does not exist".format(stack_name),
                http_status_code=400,
                response_meta={"attempt": i + 1},
            )

        with self.stubber:
            try:
                self.provider.tail_stack(stack, threading.Event())
            except ClientError as exc:
                self.assertEqual(
                    exc.response["ResponseMetadata"]["attempt"],
                    MAX_TAIL_RETRIES
                )

    def test_tail_stack_retry_on_missing_stack_eventual_success(self):
        stack_name = "SlowToCreateStack"
        stack = MagicMock(spec=Stack)
        stack.fqn = "my-namespace-{}".format(stack_name)

        default.TAIL_RETRY_SLEEP = .01
        default.GET_EVENTS_SLEEP = .01

        rcvd_events = []

        def mock_log_func(e):
            rcvd_events.append(e)

        def valid_event_response(stack, event_id):
            return {
                "StackEvents": [
                    {
                        "StackId": stack.fqn + "12345",
                        "EventId": event_id,
                        "StackName": stack.fqn,
                        "Timestamp": datetime.now()
                    },
                ]
            }

        # Ensure the stack never appears before we run out of retries
        for i in range(3):
            self.stubber.add_client_error(
                "describe_stack_events",
                service_error_code="ValidationError",
                service_message="Stack [{}] does not exist".format(stack_name),
                http_status_code=400,
                response_meta={"attempt": i + 1},
            )

        self.stubber.add_response(
            "describe_stack_events",
            valid_event_response(stack, "InitialEvents")
        )

        self.stubber.add_response(
            "describe_stack_events",
            valid_event_response(stack, "Event1")
        )

        with self.stubber:
            try:
                self.provider.tail_stack(stack, threading.Event(),
                                         log_func=mock_log_func)
            except UnStubbedResponseError:
                # Eventually we run out of responses - could not happen in
                # regular execution
                # normally this would just be dealt with when the threads were
                # shutdown, but doing so here is a little difficult because
                # we can't control the `tail_stack` loop
                pass

        self.assertEqual(rcvd_events[0]["EventId"], "Event1")