diff --git a/stacker/plan2.py b/stacker/plan2.py index b614a2870..6971326e3 100644 --- a/stacker/plan2.py +++ b/stacker/plan2.py @@ -59,14 +59,18 @@ def run(self): self.status_changed_func() while not self.done: - status = self.fn(self.stack, status=self.status) - self.set_status(status) + self._run_once() finally: if watcher and watcher.is_alive(): watcher.terminate() watcher.join() return self.ok + def _run_once(self): + status = self.fn(self.stack, status=self.status) + self.set_status(status) + return status + @property def name(self): return self.stack.fqn diff --git a/stacker/tests/actions/test_build.py b/stacker/tests/actions/test_build.py index e7055c4db..79a9e1e82 100644 --- a/stacker/tests/actions/test_build.py +++ b/stacker/tests/actions/test_build.py @@ -11,7 +11,17 @@ ) from stacker.blueprints.variables.types import CFNString from stacker.context import Context, Config +from stacker.exceptions import StackDidNotChange, StackDoesNotExist from stacker.providers.base import BaseProvider +from stacker.providers.aws.default import Provider +from stacker.status import ( + NotSubmittedStatus, + COMPLETE, + PENDING, + SKIPPED, + SUBMITTED, + FAILED +) def mock_stack(parameters): @@ -154,6 +164,164 @@ def test_should_submit(self): self.assertEqual(build.should_submit(mock_stack), t.result) +class TestLaunchStack(TestBuildAction): + def setUp(self): + self.context = self._get_context() + self.provider = Provider(None, interactive=False, + recreate_failed=False) + self.build_action = build.Action(self.context, provider=self.provider) + + self.stack = mock.MagicMock() + self.stack.name = 'vpc' + self.stack.fqn = 'vpc' + self.stack.blueprint.rendered = '{}' + self.stack.locked = False + self.stack_status = None + + plan = self.build_action._generate_plan() + self.step = plan.steps[0] + self.step.stack = self.stack + + def patch_object(*args, **kwargs): + m = mock.patch.object(*args, **kwargs) + self.addCleanup(m.stop) + m.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, + 'Tags': []} + + 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.build_action, "s3_stack_push") + + def _advance(self, new_provider_status, expected_status, expected_reason): + self.stack_status = new_provider_status + status = self.step._run_once() + self.assertEqual(status, expected_status) + self.assertEqual(status.reason, expected_reason) + + def test_launch_stack_disabled(self): + self.assertEqual(self.step.status, PENDING) + + self.stack.enabled = False + self._advance(None, NotSubmittedStatus(), "disabled") + + def test_launch_stack_create(self): + # initial status should be PENDING + self.assertEqual(self.step.status, PENDING) + + # initial run should return SUBMITTED since we've passed off to CF + self._advance(None, SUBMITTED, "creating new stack") + + # status should stay as SUBMITTED when the stack becomes available + self._advance('CREATE_IN_PROGRESS', SUBMITTED, "creating new stack") + + # status should become COMPLETE once the stack finishes + self._advance('CREATE_COMPLETE', COMPLETE, "creating new stack") + + def test_launch_stack_create_rollback(self): + # initial status should be PENDING + self.assertEqual(self.step.status, PENDING) + + # initial run should return SUBMITTED since we've passed off to CF + self._advance(None, SUBMITTED, "creating new stack") + + # provider should now return the CF stack since it exists + self._advance("CREATE_IN_PROGRESS", SUBMITTED, + "creating new stack") + + # rollback should be noticed + self._advance("ROLLBACK_IN_PROGRESS", SUBMITTED, + "rolling back new stack") + + # rollback should not be added twice to the reason + self._advance("ROLLBACK_IN_PROGRESS", SUBMITTED, + "rolling back new stack") + + # rollback should finish with failure + self._advance("ROLLBACK_COMPLETE", FAILED, + "rolled back new stack") + + def test_launch_stack_recreate(self): + self.provider.recreate_failed = True + + # initial status should be PENDING + self.assertEqual(self.step.status, PENDING) + + # first action with an existing failed stack should be deleting it + self._advance("ROLLBACK_COMPLETE", SUBMITTED, + "destroying stack for re-creation") + + # status should stay as submitted during deletion + self._advance("DELETE_IN_PROGRESS", SUBMITTED, + "destroying stack for re-creation") + + # deletion being complete must trigger re-creation + self._advance("DELETE_COMPLETE", SUBMITTED, + "re-creating stack") + + # re-creation should continue as SUBMITTED + self._advance("CREATE_IN_PROGRESS", SUBMITTED, + "re-creating stack") + + # re-creation should finish with success + self._advance("CREATE_COMPLETE", COMPLETE, + "re-creating stack") + + def test_launch_stack_update_skipped(self): + # initial status should be PENDING + self.assertEqual(self.step.status, PENDING) + + # start the upgrade, that will be skipped + self.provider.update_stack.side_effect = StackDidNotChange + self._advance("CREATE_COMPLETE", SKIPPED, + "nochange") + + def test_launch_stack_update_rollback(self): + # initial status should be PENDING + self.assertEqual(self.step.status, PENDING) + + # initial run should return SUBMITTED since we've passed off to CF + self._advance("CREATE_COMPLETE", SUBMITTED, + "updating existing stack") + + # update should continue as SUBMITTED + self._advance("UPDATE_IN_PROGRESS", SUBMITTED, + "updating existing stack") + + # rollback should be noticed + self._advance("UPDATE_ROLLBACK_IN_PROGRESS", SUBMITTED, + "rolling back update") + + # rollback should finish with failure + self._advance("UPDATE_ROLLBACK_COMPLETE", FAILED, + "rolled back update") + + def test_launch_stack_update_success(self): + # initial status should be PENDING + self.assertEqual(self.step.status, PENDING) + + # initial run should return SUBMITTED since we've passed off to CF + self._advance("CREATE_COMPLETE", SUBMITTED, + "updating existing stack") + + # update should continue as SUBMITTED + self._advance("UPDATE_IN_PROGRESS", SUBMITTED, + "updating existing stack") + + # update should finish with sucess + self._advance("UPDATE_COMPLETE", COMPLETE, + "updating existing stack") + + class TestFunctions(unittest.TestCase): """ test module level functions """