示例#1
0
 def setUp(self):
     """Run before tests."""
     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)
示例#2
0
 def test_get_delete_failed_status_reason(self, mocker: MockerFixture) -> None:
     """Test get_delete_failed_status_reason."""
     mock_get_event_by_resource_status = mocker.patch.object(
         Provider,
         "get_event_by_resource_status",
         side_effect=[{"ResourceStatusReason": "reason"}, {}],
     )
     obj = Provider(MagicMock())
     assert obj.get_delete_failed_status_reason("test") == "reason"
     mock_get_event_by_resource_status.assert_called_once_with(
         "test", "DELETE_FAILED", chronological=True
     )
     assert not obj.get_delete_failed_status_reason("test")
示例#3
0
 def test_ensure_cfn_bucket_does_not_exist_us_west(self):
     """Test ensure cfn bucket does not exist us west."""
     session = get_session("us-west-1")
     provider = Provider(session)
     action = BaseAction(
         context=mock_context("mynamespace"),
         provider_builder=MockProviderBuilder(provider, region="us-west-1"),
     )
     stubber = Stubber(action.s3_conn)
     stubber.add_client_error(
         "head_bucket",
         service_error_code="NoSuchBucket",
         service_message="Not Found",
         http_status_code=404,
     )
     stubber.add_response(
         "create_bucket",
         service_response={},
         expected_params={
             "Bucket": ANY,
             "CreateBucketConfiguration": {
                 "LocationConstraint": "us-west-1"
             },
         },
     )
     with stubber:
         action.ensure_cfn_bucket()
示例#4
0
    def test_stack_template_url(self):
        """Test stack template url."""
        context = mock_context("mynamespace")
        blueprint = MockBlueprint(name="myblueprint", context=context)

        region = "us-east-1"
        endpoint = "https://example.com"
        session = get_session(region)
        provider = Provider(session)
        action = BaseAction(
            context=context,
            provider_builder=MockProviderBuilder(provider, region=region),
        )

        with patch(
                "runway.cfngin.actions.base.get_s3_endpoint",
                autospec=True,
                return_value=endpoint,
        ):
            self.assertEqual(
                action.stack_template_url(blueprint),
                "%s/%s/stack_templates/%s/%s-%s.json" % (
                    endpoint,
                    "stacker-mynamespace",
                    "mynamespace-myblueprint",
                    "myblueprint",
                    MOCK_VERSION,
                ),
            )
示例#5
0
    def setUp(self):
        """Run before tests."""
        self.region = "us-east-1"
        self.session = get_session(self.region)
        self.provider = Provider(self.session)

        self.config_no_persist = {
            "stacks": [{
                "name": "stack1"
            }, {
                "name": "stack2",
                "requires": ["stack1"]
            }]
        }

        self.config_persist = {
            "persistent_graph_key":
            "test.json",
            "stacks": [{
                "name": "stack1"
            }, {
                "name": "stack2",
                "requires": ["stack1"]
            }],
        }
示例#6
0
 def test_successful_init(self):
     """Test successful init."""
     replacements = True
     provider = Provider(self.session,
                         interactive=True,
                         replacements_only=replacements)
     self.assertEqual(provider.replacements_only, replacements)
示例#7
0
 def test_is_stack_destroy_possible(self, expected: bool, status: str) -> None:
     """Test is_stack_destroy_possible."""
     assert (
         Provider(MagicMock()).is_stack_destroy_possible(
             generate_describe_stacks_stack("test", stack_status=status)  # type: ignore
         )
         is expected
     )
示例#8
0
    def setUp(self) -> None:
        """Run before tests."""
        self.context = self._get_context()
        self.session = get_session(region=None)
        self.provider = Provider(self.session,
                                 interactive=False,
                                 recreate_failed=False)
        provider_builder = MockProviderBuilder(provider=self.provider)
        self.deploy_action = deploy.Action(
            self.context,
            provider_builder=provider_builder,
            cancel=MockThreadingEvent(),  # type: ignore
        )

        self.stack = MagicMock()
        self.stack.region = None
        self.stack.name = "vpc"
        self.stack.fqn = "vpc"
        self.stack.blueprint.rendered = "{}"
        self.stack.locked = False
        self.stack_status = None

        plan = cast(
            Plan, self.deploy_action._Action__generate_plan())  # type: ignore
        self.step = plan.steps[0]
        self.step.stack = self.stack

        def patch_object(*args: Any, **kwargs: Any) -> None:
            mock_object = patch.object(*args, **kwargs)
            self.addCleanup(mock_object.stop)
            mock_object.start()

        def get_stack(name: str, *_args: Any,
                      **_kwargs: Any) -> Dict[str, Any]:
            if name != self.stack.name or not self.stack_status:
                raise StackDoesNotExist(name)

            return {
                "StackName": self.stack.name,
                "StackStatus": self.stack_status,
                "Outputs": [],
                "Tags": [],
            }

        def get_events(name: str, *_args: Any,
                       **_kwargs: Any) -> List[Dict[str, str]]:
            return [{
                "ResourceStatus": "ROLLBACK_IN_PROGRESS",
                "ResourceStatusReason": "CFN fail",
            }]

        patch_object(self.provider, "get_stack", side_effect=get_stack)
        patch_object(self.provider, "update_stack")
        patch_object(self.provider, "create_stack")
        patch_object(self.provider, "destroy_stack")
        patch_object(self.provider, "get_events", side_effect=get_events)

        patch_object(self.deploy_action, "s3_stack_push")
示例#9
0
    def setUp(self):
        """Run before tests."""
        self.context = self._get_context()
        self.session = get_session(region=None)
        self.provider = Provider(self.session,
                                 interactive=False,
                                 recreate_failed=False)
        provider_builder = MockProviderBuilder(self.provider)
        self.build_action = build.Action(self.context,
                                         provider_builder=provider_builder,
                                         cancel=MockThreadingEvent())

        self.stack = MagicMock()
        self.stack.region = None
        self.stack.name = 'vpc'
        self.stack.fqn = 'vpc'
        self.stack.blueprint.rendered = '{}'
        self.stack.locked = False
        self.stack_status = None

        plan = self.build_action._Action__generate_plan()
        self.step = plan.steps[0]
        self.step.stack = self.stack

        def patch_object(*args, **kwargs):
            mock_object = patch.object(*args, **kwargs)
            self.addCleanup(mock_object.stop)
            mock_object.start()

        def get_stack(name, *args, **kwargs):
            if name != self.stack.name or not self.stack_status:
                raise StackDoesNotExist(name)

            return {
                'StackName': self.stack.name,
                'StackStatus': self.stack_status,
                'Outputs': [],
                'Tags': []
            }

        def get_events(name, *args, **kwargs):
            return [{
                'ResourceStatus': 'ROLLBACK_IN_PROGRESS',
                'ResourceStatusReason': 'CFN fail'
            }]

        patch_object(self.provider, 'get_stack', side_effect=get_stack)
        patch_object(self.provider, 'update_stack')
        patch_object(self.provider, 'create_stack')
        patch_object(self.provider, 'destroy_stack')
        patch_object(self.provider, 'get_events', side_effect=get_events)

        patch_object(self.build_action, "s3_stack_push")
示例#10
0
    def test_get_event_by_resource_status(self, mocker: MockerFixture) -> None:
        """Test get_event_by_resource_status."""
        events = [
            {"StackName": "0"},
            {"StackName": "1", "ResourceStatus": "no match"},
            {"StackName": "2", "ResourceStatus": "match"},
            {"StackName": "3", "ResourceStatus": "match"},
        ]
        mock_get_events = mocker.patch.object(
            Provider, "get_events", return_value=events
        )
        obj = Provider(MagicMock())

        result = obj.get_event_by_resource_status("test", "match")
        assert result
        assert result["StackName"] == "2"
        mock_get_events.assert_called_once_with("test", chronological=True)

        assert not obj.get_event_by_resource_status(
            "test", "missing", chronological=False
        )
        mock_get_events.assert_called_with("test", chronological=False)
示例#11
0
 def test_ensure_cfn_bucket_exists(self):
     """Test ensure cfn bucket exists."""
     session = get_session("us-east-1")
     provider = Provider(session)
     action = BaseAction(
         context=mock_context("mynamespace"),
         provider_builder=MockProviderBuilder(provider),
     )
     stubber = Stubber(action.s3_conn)
     stubber.add_response("head_bucket",
                          service_response={},
                          expected_params={"Bucket": ANY})
     with stubber:
         action.ensure_cfn_bucket()
示例#12
0
 def test_get_rollback_status_reason(self, mocker: MockerFixture) -> None:
     """Test get_rollback_status_reason."""
     mock_get_event_by_resource_status = mocker.patch.object(
         Provider,
         "get_event_by_resource_status",
         side_effect=[
             {"ResourceStatusReason": "reason0"},
             {},
             {"ResourceStatusReason": "reason2"},
             {},
             {},
         ],
     )
     obj = Provider(MagicMock())
     assert obj.get_rollback_status_reason("test") == "reason0"
     mock_get_event_by_resource_status.assert_called_once_with(
         "test", "UPDATE_ROLLBACK_IN_PROGRESS", chronological=False
     )
     assert obj.get_rollback_status_reason("test") == "reason2"
     mock_get_event_by_resource_status.assert_called_with(
         "test", "ROLLBACK_IN_PROGRESS", chronological=True
     )
     assert not obj.get_rollback_status_reason("test")
示例#13
0
 def test_ensure_cfn_forbidden(self):
     """Test ensure cfn forbidden."""
     session = get_session("us-west-1")
     provider = Provider(session)
     action = BaseAction(context=mock_context("mynamespace"),
                         provider_builder=MockProviderBuilder(provider))
     stubber = Stubber(action.s3_conn)
     stubber.add_client_error(
         "head_bucket",
         service_error_code="AccessDenied",
         service_message="Forbidden",
         http_status_code=403,
     )
     with stubber:
         with self.assertRaises(botocore.exceptions.ClientError):
             action.ensure_cfn_bucket()
示例#14
0
 def test_ensure_cfn_bucket_does_not_exist_us_east(self) -> None:
     """Test ensure cfn bucket does not exist us east."""
     session = get_session("us-east-1")
     provider = Provider(session)
     action = BaseAction(
         context=mock_context("mynamespace"),
         provider_builder=MockProviderBuilder(provider=provider),
     )
     stubber = Stubber(action.s3_conn)
     stubber.add_client_error(
         "head_bucket",
         service_error_code="NoSuchBucket",
         service_message="Not Found",
         http_status_code=404,
     )
     stubber.add_response("create_bucket",
                          service_response={},
                          expected_params={"Bucket": ANY})
     with stubber:
         action.ensure_cfn_bucket()
示例#15
0
    def test_diff_stack_validationerror_template_too_large(
        self,
        caplog: LogCaptureFixture,
        cfngin_context: MockCFNginContext,
        monkeypatch: MonkeyPatch,
    ) -> None:
        """Test _diff_stack ValidationError - template too large."""
        caplog.set_level(logging.ERROR)

        cfngin_context.add_stubber("cloudformation")
        cfngin_context.config.cfngin_bucket = ""
        expected = SkippedStatus("cfngin_bucket: existing bucket required")
        provider = Provider(cfngin_context.get_session())  # type: ignore
        mock_get_stack_changes = MagicMock(
            side_effect=ClientError(
                {
                    "Error": {
                        "Code": "ValidationError",
                        "Message": "length less than or equal to",
                    }
                },
                "create_change_set",
            )
        )
        monkeypatch.setattr(provider, "get_stack_changes", mock_get_stack_changes)
        stack = MagicMock()
        stack.region = cfngin_context.env.aws_region
        stack.name = "test-stack"
        stack.fqn = "test-stack"
        stack.blueprint.rendered = "{}"
        stack.locked = False
        stack.status = None

        result = Action(
            context=cfngin_context,
            provider_builder=MockProviderBuilder(provider=provider),
            cancel=MockThreadingEvent(),  # type: ignore
        )._diff_stack(stack)
        mock_get_stack_changes.assert_called_once()
        assert result == expected
示例#16
0
    def setUp(self):
        """Run before tests."""
        self.region = 'us-east-1'
        self.session = get_session(self.region)
        self.provider = Provider(self.session)

        self.config_no_persist = {
            'stacks': [
                {'name': 'stack1'},
                {'name': 'stack2',
                 'requires': ['stack1']}
            ]
        }

        self.config_persist = {
            'persistent_graph_key': 'test.json',
            'stacks': [
                {'name': 'stack1'},
                {'name': 'stack2',
                 'requires': ['stack1']}
            ]
        }
示例#17
0
 def test_get_stack_status_reason(self) -> None:
     """Test get_stack_status_reason."""
     stack_details = generate_describe_stacks_stack("test")
     assert Provider.get_stack_status_reason(stack_details) is None
     stack_details["StackStatusReason"] = "reason"
     assert Provider.get_stack_status_reason(stack_details) == "reason"
示例#18
0
class TestProviderDefaultMode(unittest.TestCase):
    """Tests for runway.cfngin.providers.aws.default default mode."""

    def setUp(self) -> None:
        """Run before tests."""
        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_create_stack_no_changeset(self) -> None:
        """Test create_stack, no changeset, template url."""
        stack_name = "fake_stack"
        template = Template(url="http://fake.template.url.com/")
        parameters: List[Any] = []
        tags: List[Any] = []

        expected_args = generate_cloudformation_args(
            stack_name, parameters, tags, template
        )
        expected_args["EnableTerminationProtection"] = False
        expected_args["TimeoutInMinutes"] = 60

        self.stubber.add_response(
            "create_stack", {"StackId": stack_name}, expected_args
        )

        with self.stubber:
            self.provider.create_stack(
                stack_name, template, parameters, tags, timeout=60
            )
        self.stubber.assert_no_pending_responses()

    @patch("runway.cfngin.providers.aws.default.Provider.update_termination_protection")
    @patch("runway.cfngin.providers.aws.default.create_change_set")
    def test_create_stack_with_changeset(
        self, patched_create_change_set: MagicMock, patched_update_term: MagicMock
    ) -> None:
        """Test create_stack, force changeset, termination protection."""
        stack_name = "fake_stack"
        template_path = Path("./tests/unit/cfngin/fixtures/cfn_template.yaml")
        template = Template(body=template_path.read_text())
        parameters: List[Any] = []
        tags: List[Any] = []

        changeset_id = "CHANGESETID"

        patched_create_change_set.return_value = ([], changeset_id)

        self.stubber.add_response(
            "execute_change_set", {}, {"ChangeSetName": changeset_id}
        )

        with self.stubber:
            self.provider.create_stack(
                stack_name,
                template,
                parameters,
                tags,
                force_change_set=True,
                termination_protection=True,
            )
        self.stubber.assert_no_pending_responses()

        patched_create_change_set.assert_called_once_with(
            self.provider.cloudformation,
            stack_name,
            template,
            parameters,
            tags,
            "CREATE",
            service_role=self.provider.service_role,
        )
        patched_update_term.assert_called_once_with(stack_name, True)

    def test_destroy_stack(self) -> None:
        """Test destroy stack."""
        stack = {"StackName": "MockStack"}

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

        with self.stubber:
            self.assertIsNone(self.provider.destroy_stack(stack))  # type: ignore
            self.stubber.assert_no_pending_responses()

    def test_get_stack_stack_does_not_exist(self) -> None:
        """Test get stack stack does not exist."""
        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) -> None:
        """Test get stack stack exists."""
        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_destroy_method(self) -> None:
        """Test select destroy method."""
        for i in [
            [{"force_interactive": False}, self.provider.noninteractive_destroy_stack],
            [{"force_interactive": True}, self.provider.interactive_destroy_stack],
        ]:
            self.assertEqual(
                self.provider.select_destroy_method(**i[0]), i[1]  # type: ignore
            )

    def test_select_update_method(self) -> None:
        """Test select update method."""
        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.assertEqual(
                self.provider.select_update_method(**i[0]), i[1]  # type: ignore
            )

    def test_prepare_stack_for_update_completed(self) -> None:
        """Test prepare stack for update completed."""
        with self.stubber:
            stack_name = "MockStack"
            stack = generate_describe_stacks_stack(
                stack_name, stack_status="UPDATE_COMPLETE"
            )

            self.assertTrue(self.provider.prepare_stack_for_update(stack, []))

    def test_prepare_stack_for_update_in_progress(self) -> None:
        """Test prepare stack for update in progress."""
        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) -> None:
        """Test prepare stack for update non recreatable."""
        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) -> None:
        """Test prepare stack for update disallowed."""
        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) -> None:
        """Test prepare stack for update bad tags."""
        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": "cfngin_namespace", "Value": "test"}]
                )

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

    def test_prepare_stack_for_update_recreate(self) -> None:
        """Test prepare stack for update recreate."""
        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) -> None:
        """Test noninteractive changeset update no stack policy."""
        self.stubber.add_response(
            "create_change_set", {"Id": "CHANGESETID", "StackId": "STACKID"}
        )
        changes = [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:
            stack_name = "MockStack"
            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) -> None:
        """Test noninteractive changeset update with stack policy."""
        self.stubber.add_response(
            "create_change_set", {"Id": "CHANGESETID", "StackId": "STACKID"}
        )
        changes = [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:
            stack_name = "MockStack"
            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_noninteractive_destroy_stack_termination_protected(self) -> None:
        """Test noninteractive_destroy_stack with termination protection."""
        self.stubber.add_client_error("delete_stack")

        with self.stubber, self.assertRaises(ClientError):
            self.provider.noninteractive_destroy_stack("fake-stack")
        self.stubber.assert_no_pending_responses()

    @patch("runway.cfngin.providers.aws.default.output_full_changeset")
    def test_get_stack_changes_update(self, mock_output_full_cs: MagicMock) -> None:
        """Test get stack changes update."""
        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 = [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("runway.cfngin.providers.aws.default.output_full_changeset")
    def test_get_stack_changes_create(self, mock_output_full_cs: MagicMock) -> None:
        """Test get stack changes create."""
        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 = [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) -> None:
        """Test tail stack retry on missing stack."""
        stack_name = "SlowToCreateStack"
        stack = MagicMock(spec=Stack)
        stack.fqn = "my-namespace-{}".format(stack_name)

        default.TAIL_RETRY_SLEEP = 0.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) -> None:
        """Test tail stack retry on missing stack eventual success."""
        stack_name = "SlowToCreateStack"
        stack = MagicMock(spec=Stack)
        stack.fqn = "my-namespace-{}".format(stack_name)

        default.TAIL_RETRY_SLEEP = 0.01
        default.GET_EVENTS_SLEEP = 0.01

        received_events: List[Any] = []

        def mock_log_func(event: Any) -> None:
            received_events.append(event)

        def valid_event_response(stack: Stack, event_id: str) -> Dict[str, Any]:
            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(received_events[0]["EventId"], "Event1")

    def test_update_termination_protection(self) -> None:
        """Test update_termination_protection."""
        stack_name = "fake-stack"
        test_cases = [
            MutableMap(aws=False, defined=True, expected=True),
            MutableMap(aws=True, defined=False, expected=False),
            MutableMap(aws=True, defined=True, expected=None),
            MutableMap(aws=False, defined=False, expected=None),
        ]

        for test in test_cases:
            self.stubber.add_response(
                "describe_stacks",
                {
                    "Stacks": [
                        generate_describe_stacks_stack(
                            stack_name, termination_protection=test["aws"]
                        )
                    ]
                },
                {"StackName": stack_name},
            )
            if isinstance(test["expected"], bool):
                self.stubber.add_response(
                    "update_termination_protection",
                    {"StackId": stack_name},
                    {
                        "EnableTerminationProtection": test["expected"],
                        "StackName": stack_name,
                    },
                )
            with self.stubber:
                self.provider.update_termination_protection(stack_name, test["defined"])
            self.stubber.assert_no_pending_responses()
示例#19
0
 def setUp(self) -> None:
     """Run before tests."""
     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)
示例#20
0
class TestProviderInteractiveMode(unittest.TestCase):
    """Tests for runway.cfngin.providers.aws.default interactive mode."""

    def setUp(self) -> None:
        """Run before tests."""
        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)

    @patch("runway.cfngin.ui.get_raw_input")
    def test_interactive_destroy_stack(self, patched_input: MagicMock) -> None:
        """Test interactive_destroy_stack."""
        stack_name = "fake-stack"
        stack = {"StackName": stack_name}
        patched_input.return_value = "y"

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

        with self.stubber:
            self.assertIsNone(self.provider.interactive_destroy_stack(stack_name))
            self.stubber.assert_no_pending_responses()

    @patch("runway.cfngin.providers.aws.default.Provider.update_termination_protection")
    @patch("runway.cfngin.ui.get_raw_input")
    def test_interactive_destroy_stack_termination_protected(
        self, patched_input: MagicMock, patched_update_term: MagicMock
    ) -> None:
        """Test interactive_destroy_stack with termination protection."""
        stack_name = "fake-stack"
        stack = {"StackName": stack_name}
        patched_input.return_value = "y"

        self.stubber.add_client_error(
            "delete_stack", service_message="TerminationProtection"
        )
        self.stubber.add_response("delete_stack", {}, stack)

        with self.stubber:
            self.provider.interactive_destroy_stack(stack_name, approval="y")
        self.stubber.assert_no_pending_responses()
        patched_input.assert_called_once()
        patched_update_term.assert_called_once_with(stack_name, False)

    @patch("runway.cfngin.ui.get_raw_input")
    def test_destroy_stack_canceled(self, patched_input: MagicMock) -> None:
        """Test destroy stack canceled."""
        patched_input.return_value = "n"

        with self.assertRaises(exceptions.CancelExecution):
            stack = {"StackName": "MockStack"}
            self.provider.destroy_stack(stack)  # type: ignore

    def test_successful_init(self) -> None:
        """Test successful init."""
        replacements = True
        provider = Provider(
            self.session, interactive=True, replacements_only=replacements
        )
        self.assertEqual(provider.replacements_only, replacements)

    @patch("runway.cfngin.providers.aws.default.Provider.update_termination_protection")
    @patch("runway.cfngin.providers.aws.default.ask_for_approval")
    def test_update_stack_execute_success_no_stack_policy(
        self, patched_approval: MagicMock, patched_update_term: MagicMock
    ) -> None:
        """Test update stack execute success no stack policy."""
        stack_name = "my-fake-stack"

        self.stubber.add_response(
            "create_change_set", {"Id": "CHANGESETID", "StackId": "STACKID"}
        )
        changes = [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_once_with(
            full_changeset=changes, params_diff=[], include_verbose=True, fqn=stack_name
        )
        patched_update_term.assert_called_once_with(stack_name, False)

    @patch("runway.cfngin.providers.aws.default.Provider.update_termination_protection")
    @patch("runway.cfngin.providers.aws.default.ask_for_approval")
    def test_update_stack_execute_success_with_stack_policy(
        self, patched_approval: MagicMock, patched_update_term: MagicMock
    ) -> None:
        """Test update stack execute success with stack policy."""
        stack_name = "my-fake-stack"

        self.stubber.add_response(
            "create_change_set", {"Id": "CHANGESETID", "StackId": "STACKID"}
        )
        changes = [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_once_with(
            full_changeset=changes, params_diff=[], include_verbose=True, fqn=stack_name
        )
        patched_update_term.assert_called_once_with(stack_name, False)

    def test_select_destroy_method(self) -> None:
        """Test select destroy method."""
        for i in [
            [{"force_interactive": False}, self.provider.interactive_destroy_stack],
            [{"force_interactive": True}, self.provider.interactive_destroy_stack],
        ]:
            self.assertEqual(
                self.provider.select_destroy_method(**i[0]), i[1]  # type: ignore
            )

    def test_select_update_method(self) -> None:
        """Test select update method."""
        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.assertEqual(
                self.provider.select_update_method(**i[0]), i[1]  # type: ignore
            )

    @patch("runway.cfngin.providers.aws.default.output_full_changeset")
    @patch("runway.cfngin.providers.aws.default.output_summary")
    def test_get_stack_changes_interactive(
        self, mock_output_summary: MagicMock, mock_output_full_cs: MagicMock
    ) -> None:
        """Test get stack changes interactive."""
        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 = [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
        )
示例#21
0
class TestProviderInteractiveMode(unittest.TestCase):
    """Tests for runway.cfngin.providers.aws.default interactive mode."""
    def setUp(self):
        """Run before tests."""
        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)

    @patch('runway.cfngin.ui.get_raw_input')
    def test_destroy_stack(self, patched_input):
        """Test destroy stack."""
        stack = {'StackName': 'MockStack'}
        patched_input.return_value = 'y'

        self.stubber.add_response('delete_stack', {}, stack)

        with self.stubber:
            self.assertIsNone(self.provider.destroy_stack(stack))
            self.stubber.assert_no_pending_responses()

    @patch('runway.cfngin.ui.get_raw_input')
    def test_destroy_stack_canceled(self, patched_input):
        """Test destroy stack canceled."""
        stack = {'StackName': 'MockStack'}
        patched_input.return_value = 'n'

        with self.assertRaises(exceptions.CancelExecution):
            self.provider.destroy_stack(stack)

    def test_successful_init(self):
        """Test successful init."""
        replacements = True
        provider = Provider(self.session,
                            interactive=True,
                            replacements_only=replacements)
        self.assertEqual(provider.replacements_only, replacements)

    @patch("runway.cfngin.providers.aws.default.ask_for_approval")
    def test_update_stack_execute_success_no_stack_policy(
            self, patched_approval):
        """Test update stack execute success no stack policy."""
        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("runway.cfngin.providers.aws.default.ask_for_approval")
    def test_update_stack_execute_success_with_stack_policy(
            self, patched_approval):
        """Test update stack execute success with stack policy."""
        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_destroy_method(self):
        """Test select destroy method."""
        for i in [[{
                'force_interactive': False
        }, self.provider.interactive_destroy_stack],
                  [{
                      'force_interactive': True
                  }, self.provider.interactive_destroy_stack]]:
            self.assertEqual(self.provider.select_destroy_method(**i[0]), i[1])

    def test_select_update_method(self):
        """Test select update method."""
        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.assertEqual(self.provider.select_update_method(**i[0]), i[1])

    @patch('runway.cfngin.providers.aws.default.output_full_changeset')
    @patch('runway.cfngin.providers.aws.default.output_summary')
    def test_get_stack_changes_interactive(self, mock_output_summary,
                                           mock_output_full_cs):
        """Test get stack changes interactive."""
        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)
示例#22
0
class TestProviderDefaultMode(unittest.TestCase):
    """Tests for runway.cfngin.providers.aws.default default mode."""
    def setUp(self):
        """Run before tests."""
        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_destroy_stack(self):
        """Test destroy stack."""
        stack = {'StackName': 'MockStack'}

        self.stubber.add_response('delete_stack', {}, stack)

        with self.stubber:
            self.assertIsNone(self.provider.destroy_stack(stack))
            self.stubber.assert_no_pending_responses()

    def test_get_stack_stack_does_not_exist(self):
        """Test get stack stack does not exist."""
        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):
        """Test get stack stack exists."""
        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_destroy_method(self):
        """Test select destroy method."""
        for i in [[{
                'force_interactive': False
        }, self.provider.noninteractive_destroy_stack],
                  [{
                      'force_interactive': True
                  }, self.provider.interactive_destroy_stack]]:
            self.assertEqual(self.provider.select_destroy_method(**i[0]), i[1])

    def test_select_update_method(self):
        """Test select update method."""
        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.assertEqual(self.provider.select_update_method(**i[0]), i[1])

    def test_prepare_stack_for_update_completed(self):
        """Test prepare stack for update completed."""
        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):
        """Test prepare stack for update in progress."""
        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):
        """Test prepare stack for update non recreatable."""
        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):
        """Test prepare stack for update disallowed."""
        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):
        """Test prepare stack for update bad tags."""
        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':
                                                           'cfngin_namespace',
                                                           'Value': 'test'
                                                       }])

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

    def test_prepare_stack_for_update_recreate(self):
        """Test prepare stack for update recreate."""
        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):
        """Test noninteractive changeset update no stack policy."""
        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):
        """Test noninteractive changeset update with stack policy."""
        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('runway.cfngin.providers.aws.default.output_full_changeset')
    def test_get_stack_changes_update(self, mock_output_full_cs):
        """Test get stack changes update."""
        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('runway.cfngin.providers.aws.default.output_full_changeset')
    def test_get_stack_changes_create(self, mock_output_full_cs):
        """Test get stack changes create."""
        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):
        """Test tail stack retry on missing stack."""
        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):
        """Test tail stack retry on missing stack eventual success."""
        stack_name = "SlowToCreateStack"
        stack = MagicMock(spec=Stack)
        stack.fqn = "my-namespace-{}".format(stack_name)

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

        received_events = []

        def mock_log_func(event):
            received_events.append(event)

        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(received_events[0]["EventId"], "Event1")