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)
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")
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()
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, ), )
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"] }], }
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)
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 )
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")
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")
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)
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()
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")
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()
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()
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
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']} ] }
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"
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()
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)
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 )
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)
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")