def test_execute_plan_locked(self): """Test execute plan locked. Locked stacks still need to have their requires evaluated when they're being created. """ vpc = Stack(definition=generate_definition("vpc", 1), context=self.context) bastion = Stack( definition=generate_definition("bastion", 1, requires=[vpc.name]), locked=True, context=self.context, ) calls = [] def fn(stack, status=None): calls.append(stack.fqn) return COMPLETE graph = Graph.from_steps([Step(vpc, fn), Step(bastion, fn)]) plan = Plan(description="Test", graph=graph) plan.execute(walk) self.assertEqual(calls, ["namespace-vpc.1", "namespace-bastion.1"])
def test_execute_plan_filtered(self): """Test execute plan filtered.""" vpc = Stack(definition=generate_definition("vpc", 1), context=self.context) db = Stack( definition=generate_definition("db", 1, requires=[vpc.name]), context=self.context, ) app = Stack( definition=generate_definition("app", 1, requires=[db.name]), context=self.context, ) calls = [] def fn(stack, status=None): calls.append(stack.fqn) return COMPLETE context = mock.MagicMock() context.persistent_graph_locked = False context.stack_names = ["db.1"] graph = Graph.from_steps([Step(vpc, fn), Step(db, fn), Step(app, fn)]) plan = Plan(context=context, description="Test", graph=graph) plan.execute(walk) self.assertEqual(calls, ["namespace-vpc.1", "namespace-db.1"])
def test_execute_plan_failed(self): """Test execute plan failed.""" vpc = Stack(definition=generate_definition("vpc", 1), context=self.context) bastion = Stack( definition=generate_definition("bastion", 1, requires=[vpc.name]), context=self.context, ) db = Stack(definition=generate_definition("db", 1), context=self.context) calls = [] def fn(stack, status=None): calls.append(stack.fqn) if stack.name == vpc_step.name: return FAILED return COMPLETE vpc_step = Step(vpc, fn) bastion_step = Step(bastion, fn) db_step = Step(db, fn) graph = Graph.from_steps([vpc_step, bastion_step, db_step]) plan = Plan(description="Test", graph=graph) with self.assertRaises(PlanFailed): plan.execute(walk) calls.sort() self.assertEqual(calls, ["namespace-db.1", "namespace-vpc.1"])
def test_execute_plan_exception(self): """Test execute plan exception.""" vpc = Stack(definition=generate_definition("vpc", 1), context=self.context) bastion = Stack( definition=generate_definition("bastion", 1, requires=[vpc.name]), context=self.context, ) calls = [] def fn(stack, status=None): calls.append(stack.fqn) if stack.name == vpc_step.name: raise ValueError("Boom") return COMPLETE vpc_step = Step(vpc, fn) bastion_step = Step(bastion, fn) graph = Graph.from_steps([vpc_step, bastion_step]) plan = Plan(description="Test", graph=graph) with self.assertRaises(PlanFailed): plan.execute(walk) self.assertEqual(calls, ["namespace-vpc.1"]) self.assertEqual(vpc_step.status, FAILED)
def test_execute_plan_cancelled(self): """Test execute plan cancelled.""" vpc = Stack(definition=generate_definition("vpc", 1), context=self.context) bastion = Stack( definition=generate_definition("bastion", 1, requires=[vpc.name]), context=self.context, ) calls = [] def fn(stack, status=None): calls.append(stack.fqn) if stack.fqn == vpc_step.name: raise CancelExecution return COMPLETE vpc_step = Step(vpc, fn) bastion_step = Step(bastion, fn) graph = Graph.from_steps([vpc_step, bastion_step]) plan = Plan(description="Test", graph=graph) plan.execute(walk) self.assertEqual(calls, ["namespace-vpc.1", "namespace-bastion.1"])
def test_execute_plan_skipped(self): """Test execute plan skipped.""" vpc = Stack(definition=generate_definition('vpc', 1), context=self.context) bastion = Stack(definition=generate_definition('bastion', 1, requires=[vpc.name]), context=self.context) calls = [] def fn(stack, status=None): calls.append(stack.fqn) if stack.fqn == vpc_step.name: return SKIPPED return COMPLETE vpc_step = Step(vpc, fn) bastion_step = Step(bastion, fn) graph = Graph.from_steps([vpc_step, bastion_step]) plan = Plan(description="Test", graph=graph) plan.execute(walk) self.assertEqual(calls, ['namespace-vpc.1', 'namespace-bastion.1'])
def test_execute_plan_no_persist(self): """Test execute plan with no persistent graph.""" context = Context(config=self.config) context.put_persistent_graph = mock.MagicMock() vpc = Stack(definition=generate_definition("vpc", 1), context=context) bastion = Stack( definition=generate_definition("bastion", 1, requires=[vpc.name]), context=context, ) calls = [] def _launch_stack(stack, status=None): calls.append(stack.fqn) return COMPLETE graph = Graph.from_steps( [Step(vpc, _launch_stack), Step(bastion, _launch_stack)]) plan = Plan(description="Test", graph=graph, context=context) plan.execute(walk) self.assertEqual(calls, ["namespace-vpc.1", "namespace-bastion.1"]) context.put_persistent_graph.assert_not_called()
def test_output_handler(self) -> None: """Test output handler.""" stack = Stack(definition=generate_definition("vpc", 1), context=self.context) stack.set_outputs({"SomeOutput": "Test Output"}) self.context.get_stack.return_value = stack value = OutputLookup.handle("stack-name::SomeOutput", context=self.context) self.assertEqual(value, "Test Output") self.assertEqual(self.context.get_stack.call_count, 1) args = self.context.get_stack.call_args self.assertEqual(args[0][0], "stack-name")
def test_plan_targeted(self): """Test plan targeted.""" context = Context(config=self.config) vpc = Stack(definition=generate_definition("vpc", 1), context=context) bastion = Stack( definition=generate_definition("bastion", 1, requires=[vpc.name]), context=context, ) context.stack_names = [vpc.name] graph = Graph.from_steps([Step(vpc, fn=None), Step(bastion, fn=None)]) plan = Plan(description="Test", graph=graph, context=context) self.assertEqual({vpc.name: set()}, plan.graph.to_dict())
def test_plan_reverse(self) -> None: """Test plan reverse.""" vpc = Stack(definition=generate_definition("vpc", 1), context=self.context) bastion = Stack( definition=generate_definition("bastion", 1, requires=[vpc.name]), context=self.context, ) graph = Graph.from_steps([Step(vpc, fn=None), Step(bastion, fn=None)]) plan = Plan(description="Test", graph=graph, reverse=True) # order is different between python2/3 so can't compare dicts result_graph_dict = plan.graph.to_dict() self.assertEqual(set(), result_graph_dict.get("bastion.1")) self.assertEqual(set(["bastion.1"]), result_graph_dict.get("vpc.1"))
def test_plan(self) -> None: """Test plan.""" vpc = Stack(definition=generate_definition("vpc", 1), context=self.context) bastion = Stack( definition=generate_definition("bastion", 1, requires=[vpc.name]), context=self.context, ) graph = Graph.from_steps([Step(vpc, fn=None), Step(bastion, fn=None)]) plan = Plan(description="Test", graph=graph) self.assertEqual( plan.graph.to_dict(), {"bastion.1": set(["vpc.1"]), "vpc.1": set([])} )
def test_variable_resolve_simple_lookup(self): """Test variable resolve simple lookup.""" stack = Stack(definition=generate_definition("vpc", 1), context=self.context) stack.set_outputs({ "FakeOutput": "resolved", "FakeOutput2": "resolved2", }) self.context.get_stack.return_value = stack var = Variable("Param1", "${output fakeStack::FakeOutput}") var.resolve(self.context, self.provider) self.assertTrue(var.resolved) self.assertEqual(var.value, "resolved")
def test_stack_cfn_parameters(self) -> None: """Test stack cfn parameters.""" definition = generate_definition( base_name="vpc", stack_id=1, variables={"Param1": "${output fakeStack::FakeOutput}"}, ) stack = Stack(definition=definition, context=self.context) # pylint: disable=protected-access stack._blueprint = MagicMock() stack._blueprint.parameter_values = { "Param2": "Some Resolved Value", } param = stack.parameter_values["Param2"] self.assertEqual(param, "Some Resolved Value")
def test_plan(self): """Test plan.""" vpc = Stack(definition=generate_definition('vpc', 1), context=self.context) bastion = Stack(definition=generate_definition('bastion', 1, requires=[vpc.name]), context=self.context) graph = Graph.from_steps([Step(vpc, fn=None), Step(bastion, fn=None)]) plan = Plan(description="Test", graph=graph) self.assertEqual(plan.graph.to_dict(), { 'bastion.1': set(['vpc.1']), 'vpc.1': set([]) })
def from_stack_name(cls, stack_name, context, requires=None, fn=None, watch_func=None): """Create a step using only a stack name. Args: stack_name (str): Name of a CloudFormation stack. context (:class:`runway.cfngin.context.Context`): Context object. Required to initialize a "fake" :class:`runway.cfngin.stack.Stack`. requires (List[str]): Stacks that this stack depends on. fn (Callable): The function to run to execute the step. This function will be ran multiple times until the step is "done". watch_func (Callable): an optional function that will be called to "tail" the step action. Returns: :class:`Step` """ # pylint: disable=import-outside-toplevel from runway.cfngin.config import Stack as StackConfig from runway.cfngin.stack import Stack stack_def = StackConfig({ 'name': stack_name, 'requires': requires or [] }) stack = Stack(stack_def, context) return cls(stack, fn=fn, watch_func=watch_func)
def test_stack_requires(self): """Test stack requires.""" definition = generate_definition( base_name="vpc", stack_id=1, variables={ "Var1": "${noop fakeStack3::FakeOutput}", "Var2": ("some.template.value:${output fakeStack2::FakeOutput}:" "${output fakeStack::FakeOutput}"), "Var3": "${output fakeStack::FakeOutput}," "${output fakeStack2::FakeOutput}", }, requires=["fakeStack"], ) stack = Stack(definition=definition, context=self.context) self.assertEqual(len(stack.requires), 2) self.assertIn( "fakeStack", stack.requires, ) self.assertIn( "fakeStack2", stack.requires, )
def test_execute_plan(self) -> None: """Test execute plan.""" context = CfnginContext(config=self.config) context.put_persistent_graph = mock.MagicMock() vpc = Stack(definition=generate_definition("vpc", 1), context=context) bastion = Stack( definition=generate_definition("bastion", 1, requires=[vpc.name]), context=context, ) removed = Stack( definition=generate_definition("removed", 1, requires=[]), context=context ) context._persistent_graph = Graph.from_steps([Step(removed)]) calls: List[str] = [] def _launch_stack(stack: Stack, status: Optional[Status] = None) -> Status: calls.append(stack.fqn) return COMPLETE def _destroy_stack(stack: Stack, status: Optional[Status] = None) -> Status: calls.append(stack.fqn) return COMPLETE graph = Graph.from_steps( [ Step(removed, fn=_destroy_stack), Step(vpc, fn=_launch_stack), Step(bastion, fn=_launch_stack), ] ) plan = Plan(description="Test", graph=graph, context=context) plan.context._persistent_graph_lock_code = plan.lock_code # type: ignore plan.execute(walk) # the order these are appended changes between python2/3 self.assertIn("namespace-vpc.1", calls) self.assertIn("namespace-bastion.1", calls) self.assertIn("namespace-removed.1", calls) context.put_persistent_graph.assert_called() # order is different between python2/3 so can't compare dicts result_graph_dict = context.persistent_graph.to_dict() # type: ignore self.assertEqual(2, len(result_graph_dict)) self.assertEqual(set(), result_graph_dict.get("vpc.1")) self.assertEqual(set(["vpc.1"]), result_graph_dict.get("bastion.1")) self.assertIsNone(result_graph_dict.get("namespace-removed.1"))
def test_stack_tags_extra(self): """Test stack tags extra.""" self.config.tags = {"environment": "prod"} definition = generate_definition(base_name="vpc", stack_id=1, tags={"app": "graph"}) stack = Stack(definition=definition, context=self.context) self.assertEqual(stack.tags, {"environment": "prod", "app": "graph"})
def test_stack_tags_override(self): """Test stack tags override.""" self.config.tags = {"environment": "prod"} definition = generate_definition(base_name="vpc", stack_id=1, tags={"environment": "stage"}) stack = Stack(definition=definition, context=self.context) self.assertEqual(stack.tags, {"environment": "stage"})
def test_execute_plan(self): """Test execute plan.""" context = Context(config=self.config) context.put_persistent_graph = mock.MagicMock() vpc = Stack(definition=generate_definition('vpc', 1), context=context) bastion = Stack(definition=generate_definition('bastion', 1, requires=[vpc.name]), context=context) removed = Stack(definition=generate_definition('removed', 1, requires=[]), context=context) context._persistent_graph = Graph.from_steps([removed]) calls = [] def _launch_stack(stack, status=None): calls.append(stack.fqn) return COMPLETE def _destroy_stack(stack, status=None): calls.append(stack.fqn) return COMPLETE graph = Graph.from_steps([ Step(removed, _destroy_stack), Step(vpc, _launch_stack), Step(bastion, _launch_stack) ]) plan = Plan(description="Test", graph=graph, context=context) plan.context._persistent_graph_lock_code = plan.lock_code plan.execute(walk) # the order these are appended changes between python2/3 self.assertIn('namespace-vpc.1', calls) self.assertIn('namespace-bastion.1', calls) self.assertIn('namespace-removed.1', calls) context.put_persistent_graph.assert_called() # order is different between python2/3 so can't compare dicts result_graph_dict = context.persistent_graph.to_dict() self.assertEqual(2, len(result_graph_dict)) self.assertEqual(set(), result_graph_dict.get('vpc.1')) self.assertEqual(set(['vpc.1']), result_graph_dict.get('bastion.1')) self.assertIsNone(result_graph_dict.get('namespace-removed.1'))
def test_stack_requires_circular_ref(self): """Test stack requires circular ref.""" definition = generate_definition( base_name="vpc", stack_id=1, variables={"Var1": "${output vpc.1::FakeOutput}"}, ) stack = Stack(definition=definition, context=self.context) with self.assertRaises(ValueError): stack.requires # pylint: disable=pointless-statement
def setUp(self): """Run before tests.""" self.sd = {"name": "test"} # pylint: disable=invalid-name self.config = Config({"namespace": "namespace"}) self.context = Context(config=self.config) self.stack = Stack( definition=generate_definition("vpc", 1), context=self.context, ) register_lookup_handler("noop", lambda **kwargs: "test")
def test_build_graph_cyclic_dependencies(self): """Test build graph cyclic dependencies.""" vpc = Stack(definition=generate_definition('vpc', 1), context=self.context) db = Stack(definition=generate_definition('db', 1, requires=['app.1']), context=self.context) app = Stack(definition=generate_definition('app', 1, requires=['db.1']), context=self.context) with self.assertRaises(GraphError) as expected: Graph.from_steps( [Step(vpc, None), Step(db, None), Step(app, None)]) message = ("Error detected when adding 'db.1' " "as a dependency of 'app.1': graph is " "not acyclic") self.assertEqual(str(expected.exception), message)
def test_variable_resolve_multiple_lookups_string(self): """Test variable resolve multiple lookups string.""" var = Variable( "Param1", "url://${output fakeStack::FakeOutput}@" "${output fakeStack::FakeOutput2}", ) stack = Stack(definition=generate_definition("vpc", 1), context=self.context) stack.set_outputs({ "FakeOutput": "resolved", "FakeOutput2": "resolved2", }) self.context.get_stack.return_value = stack var.resolve(self.context, self.provider) self.assertTrue(var.resolved) self.assertEqual(var.value, "url://resolved@resolved2")
def test_variable_resolve_nested_lookup(self): """Test variable resolve nested lookup.""" stack = Stack(definition=generate_definition("vpc", 1), context=self.context) stack.set_outputs({ "FakeOutput": "resolved", "FakeOutput2": "resolved2", }) def mock_handler(value, context, provider, **kwargs): return "looked up: {}".format(value) register_lookup_handler("lookup", mock_handler) self.context.get_stack.return_value = stack var = Variable( "Param1", "${lookup ${lookup ${output fakeStack::FakeOutput}}}", ) var.resolve(self.context, self.provider) self.assertTrue(var.resolved) self.assertEqual(var.value, "looked up: looked up: resolved")
def test_build_graph_missing_dependency(self): """Test build graph missing dependency.""" bastion = Stack(definition=generate_definition('bastion', 1, requires=['vpc.1']), context=self.context) with self.assertRaises(GraphError) as expected: Graph.from_steps([Step(bastion, None)]) message_starts = ("Error detected when adding 'vpc.1' " "as a dependency of 'bastion.1':") message_contains = "dependent node vpc.1 does not exist" self.assertTrue(str(expected.exception).startswith(message_starts)) self.assertTrue(message_contains in str(expected.exception))
def test_execute_plan_skipped(self) -> None: """Test execute plan skipped.""" vpc = Stack(definition=generate_definition("vpc", 1), context=self.context) bastion = Stack( definition=generate_definition("bastion", 1, requires=[vpc.name]), context=self.context, ) calls: List[str] = [] def fn(stack: Stack, status: Optional[Status] = None) -> Status: calls.append(stack.fqn) if stack.fqn == vpc_step.name: return SKIPPED return COMPLETE vpc_step = Step(vpc, fn=fn) bastion_step = Step(bastion, fn=fn) graph = Graph.from_steps([vpc_step, bastion_step]) plan = Plan(description="Test", graph=graph) plan.execute(walk) self.assertEqual(calls, ["namespace-vpc.1", "namespace-bastion.1"])
def setUp(self) -> None: """Run before tests.""" self.sd = {"name": "test"} # pylint: disable=invalid-name self.config = CfnginConfig.parse_obj({"namespace": "namespace"}) self.context = CfnginContext(config=self.config) self.stack = Stack(definition=generate_definition("vpc", 1), context=self.context) class FakeLookup(LookupHandler): """False Lookup.""" # pylint: disable=arguments-differ,unused-argument @classmethod def handle(cls, value: str, *__args: Any, **__kwargs: Any) -> str: # type: ignore """Perform the lookup.""" return "test" register_lookup_handler("noop", FakeLookup)
def test_dump(self) -> None: """Test dump.""" requires: List[str] = [] steps: List[Step] = [] for i in range(5): overrides = { "variables": { "PublicSubnets": "1", "SshKeyName": "1", "PrivateSubnets": "1", "Random": "${noop something}", }, "requires": requires, } stack = Stack( definition=generate_definition("vpc", i, **overrides), context=self.context, ) requires = [stack.name] steps += [Step(stack)] graph = Graph.from_steps(steps) plan = Plan(description="Test", graph=graph) tmp_dir = tempfile.mkdtemp() try: plan.dump(directory=tmp_dir, context=self.context) for step in plan.steps: template_path = os.path.join( tmp_dir, stack_template_key_name(step.stack.blueprint) # type: ignore ) self.assertTrue(os.path.isfile(template_path)) finally: shutil.rmtree(tmp_dir)
def test_stack_tags_default(self) -> None: """Test stack tags default.""" self.config.tags = {"environment": "prod"} definition = generate_definition(base_name="vpc", stack_id=1) stack = Stack(definition=definition, context=self.context) self.assertEqual(stack.tags, {"environment": "prod"})