Skip to content

Commit

Permalink
Fix #201 Add describe-env-dependencies command to expose the stack de…
Browse files Browse the repository at this point in the history
…pendencies within an environment

This commit also allows users to output their dependencies using the DOT language, which can be used
to graph the dependencies using programs like Graphwiz.
  • Loading branch information
cbosss committed Sep 29, 2017
1 parent d4ef366 commit ed42ceb
Show file tree
Hide file tree
Showing 5 changed files with 106 additions and 10 deletions.
8 changes: 8 additions & 0 deletions docs/docs/cli.md
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,7 @@ $ sceptre delete-stack
$ sceptre describe-change-set
$ sceptre describe-env
$ sceptre describe-env-resources
$ sceptre describe-env-dependencies
$ sceptre describe-stack-outputs
$ sceptre describe-stack-resources
$ sceptre execute-change-set
Expand Down Expand Up @@ -76,3 +77,10 @@ Note that Sceptre prepends the string `SCEPTRE_` to the name of the environment
$ env | grep SCEPTRE
SCEPTRE_<output_name>=<output_value>
```

## Visualizing Environment Dependencies

The `describe-env-dependencies` command can produce output in the DOT language. DOT can easily be converted to an image by making use of the dot command provided by GraphViz:
```shell
$ sceptre describe-env-dependencies ENVIRONMENT --dot | dot -Tpng > dependencies.png
```
46 changes: 46 additions & 0 deletions sceptre/cli.py
Original file line number Diff line number Diff line change
Expand Up @@ -545,6 +545,52 @@ def describe_env(ctx, environment):
write(responses, ctx.obj["output_format"], ctx.obj["no_colour"])


@cli.command(name="describe-env-dependencies")
@environment_options
@click.option(
"--dot", is_flag=True, help="Format output in the graphwiz dot language."
)
@click.option("--invert", is_flag=True, help="Invert the dependency graph.")
@click.pass_context
@catch_exceptions
def describe_env_dependencies(ctx, environment, dot, invert):
"""
Describes ENVIRONMENTs stack dependencies
"""
env = get_env(ctx.obj["sceptre_dir"], environment, ctx.obj["options"])

if not invert:
dependencies = env.get_launch_dependencies(environment)
else:
dependencies = env.get_delete_dependencies()

if dot:
dot_repr = _generate_dot_representation(dependencies)
write(dot_repr, "str")
else:
write(dependencies, ctx.obj["output_format"])


def _generate_dot_representation(dependencies):
"""
Converts a environment dependencies into DOT language.
:param dependencies: dictionary of stacks with their list of dependencies
:type dependencies: dict
:returns: Dot formatted dependency graph
:rtype: str
"""
lines = []
for stack, stack_dependencies in dependencies.items():
lines.append("\"{0}\";".format(stack))
for dependency in stack_dependencies:
lines.append("\"{0}\" -> \"{1}\";".format(dependency, stack))

blob = "\n".join(lines)
return "digraph stack_dependencies {{{dependencies}}}"\
.format(dependencies=blob)


@cli.command(name="set-stack-policy")
@stack_options
@click.option("--policy-file")
Expand Down
10 changes: 5 additions & 5 deletions sceptre/environment.py
Original file line number Diff line number Diff line change
Expand Up @@ -122,7 +122,7 @@ def launch(self):
self.logger.debug("Launching environment '%s'", self.path)
threading_events = self._get_threading_events()
stack_statuses = self._get_initial_statuses()
launch_dependencies = self._get_launch_dependencies(self.path)
launch_dependencies = self.get_launch_dependencies(self.path)

self._check_for_circular_dependencies(launch_dependencies)
self._build(
Expand All @@ -139,7 +139,7 @@ def delete(self):
self.logger.debug("Deleting environment '%s'", self.path)
threading_events = self._get_threading_events()
stack_statuses = self._get_initial_statuses()
delete_dependencies = self._get_delete_dependencies()
delete_dependencies = self.get_delete_dependencies()

self._check_for_circular_dependencies(delete_dependencies)
self._build(
Expand Down Expand Up @@ -284,7 +284,7 @@ def _get_initial_statuses(self):
}

@recurse_into_sub_environments
def _get_launch_dependencies(self, top_level_environment_path):
def get_launch_dependencies(self, top_level_environment_path):
"""
Returns a dict of each stack's launch dependencies.
Expand All @@ -308,15 +308,15 @@ def _get_launch_dependencies(self, top_level_environment_path):
}
return launch_dependencies

def _get_delete_dependencies(self):
def get_delete_dependencies(self):
"""
Returns a dict of each stack's delete dependencies.
:returns: A list of the stacks that a particular stack depends on
while deleting, keyed by that stack's name.
:rtype: dict
"""
launch_dependencies = self._get_launch_dependencies(self.path)
launch_dependencies = self.get_launch_dependencies(self.path)
delete_dependencies = {
stack_name: [] for stack_name in launch_dependencies
}
Expand Down
42 changes: 42 additions & 0 deletions tests/test_cli.py
Original file line number Diff line number Diff line change
Expand Up @@ -470,6 +470,48 @@ def test_describe_env(self, mock_get_env):
result = self.runner.invoke(cli, ["describe-env", "dev"])
assert result.output == "stack: status\n\n"

@patch("sceptre.cli.get_env")
def test_describe_env_dependencies(self, mock_get_env):
mock_Environment = Mock()
mock_Environment.get_launch_dependencies.return_value = {
"dev/vpc": []
}
mock_get_env.return_value = mock_Environment

result = self.runner.invoke(cli, ["describe-env-dependencies", "dev"])
assert result.output == "dev/vpc: []\n\n"

@patch("sceptre.cli.get_env")
def test_describe_env_dependencies_with_invert(self, mock_get_env):
mock_Environment = Mock()
mock_Environment.get_delete_dependencies.return_value = {
"dev/vpc": []
}
mock_get_env.return_value = mock_Environment

result = self.runner.invoke(
cli, ["describe-env-dependencies", "dev", "--invert"]
)
assert result.output == "dev/vpc: []\n\n"

@patch("sceptre.cli.get_env")
def test_describe_env_dependencies_with_dot(self, mock_get_env):
mock_Environment = Mock()
mock_Environment.get_launch_dependencies.return_value = {
"dev/vpc": [],
"dev/networking": ["dev/vpc"]
}
mock_get_env.return_value = mock_Environment

result = self.runner.invoke(
cli, ["describe-env-dependencies", "dev", "--dot"]
)
assert "digraph stack_dependencies {" in result.output
assert "\"dev/vpc\";" in result.output
assert "\"dev/networking\";" in result.output
assert "\"dev/vpc\" -> \"dev/networking\";" in result.output
assert "}" in result.output

@patch("sceptre.cli.os.getcwd")
@patch("sceptre.cli.get_env")
def test_set_stack_policy_with_file_flag(
Expand Down
10 changes: 5 additions & 5 deletions tests/test_environment.py
Original file line number Diff line number Diff line change
Expand Up @@ -133,7 +133,7 @@ def test_is_leaf_with_non_leaf_dir(self):

@patch("sceptre.environment.Environment._build")
@patch("sceptre.environment.Environment._check_for_circular_dependencies")
@patch("sceptre.environment.Environment._get_launch_dependencies")
@patch("sceptre.environment.Environment.get_launch_dependencies")
@patch("sceptre.environment.Environment._get_initial_statuses")
@patch("sceptre.environment.Environment._get_threading_events")
def test_launch_calls_build_with_correct_args(
Expand All @@ -158,7 +158,7 @@ def test_launch_calls_build_with_correct_args(

@patch("sceptre.environment.Environment._build")
@patch("sceptre.environment.Environment._check_for_circular_dependencies")
@patch("sceptre.environment.Environment._get_delete_dependencies")
@patch("sceptre.environment.Environment.get_delete_dependencies")
@patch("sceptre.environment.Environment._get_initial_statuses")
@patch("sceptre.environment.Environment._get_threading_events")
def test_delete_calls_build_with_correct_args(
Expand Down Expand Up @@ -369,23 +369,23 @@ def test_get_launch_dependencies(self):

self.environment.stacks = {"mock_stack": mock_stack}

response = self.environment._get_launch_dependencies("dev")
response = self.environment.get_launch_dependencies("dev")

# Note that "prod/sg" is filtered out, because it's not under the
# top level environment path "dev".
assert response == {
"dev/mock_stack": ["dev/vpc", "dev/subnets"]
}

@patch("sceptre.environment.Environment._get_launch_dependencies")
@patch("sceptre.environment.Environment.get_launch_dependencies")
def test_get_delete_dependencies(self, mock_get_launch_dependencies):
mock_get_launch_dependencies.return_value = {
"dev/mock_stack_1": [],
"dev/mock_stack_2": [],
"dev/mock_stack_3": ["dev/mock_stack_1", "dev/mock_stack_2"],
}

dependencies = self.environment._get_delete_dependencies()
dependencies = self.environment.get_delete_dependencies()
assert dependencies == {
"dev/mock_stack_1": ["dev/mock_stack_3"],
"dev/mock_stack_2": ["dev/mock_stack_3"],
Expand Down

0 comments on commit ed42ceb

Please sign in to comment.