Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[Resolve #1318] Implement dump template and write output files #1325

Merged
merged 39 commits into from
May 14, 2023
Merged
Show file tree
Hide file tree
Changes from 29 commits
Commits
Show all changes
39 commits
Select commit Hold shift + click to select a range
4d1e5cc
Write rendered template to a temp file
alex-harvey-z3q Apr 10, 2023
c9ebb5a
[Resolves #1318] Part 1
alex-harvey-z3q Apr 15, 2023
97d680f
more
alex-harvey-z3q Apr 15, 2023
8ad7b63
Merge branch 'alexharvey/add-some-debugging' into merge-branch
alex-harvey-z3q Apr 15, 2023
3005b14
version
alex-harvey-z3q Apr 15, 2023
acb399b
Revert "version"
alex-harvey-z3q Apr 15, 2023
a3bf4b7
Revert deprecation
alex-harvey-z3q Apr 15, 2023
c1ca8ed
Revert "Revert "version""
alex-harvey-z3q Apr 15, 2023
bfa4675
more
alex-harvey-z3q Apr 15, 2023
fc2be81
Revert "Write rendered template to a temp file"
alex-harvey-z3q Apr 15, 2023
8905933
Revert "Revert "Revert "version"""
alex-harvey-z3q Apr 15, 2023
7c1d2a1
more
alex-harvey-z3q Apr 15, 2023
901c5cf
more
alex-harvey-z3q Apr 15, 2023
8d1bbdd
Merge branch 'alexharvey/1318-part-1' into alexharvey/1318-part-2
alex-harvey-z3q Apr 15, 2023
f9533bd
more
alex-harvey-z3q Apr 15, 2023
7919be0
Merge branch 'alexharvey/1318-part-1' into alexharvey/1318-part-2
alex-harvey-z3q Apr 15, 2023
efb2090
Restore CONTRIBUTING.md
alex-harvey-z3q Apr 18, 2023
2ec7239
Remove venv.sh
alex-harvey-z3q Apr 18, 2023
7899dad
Merge pull request #3 from alexharv074/alexharvey/1318-part-2
alex-harvey-z3q Apr 18, 2023
c7cf881
more
alex-harvey-z3q Apr 20, 2023
b95a914
Deprecation
alex-harvey-z3q Apr 21, 2023
1b7a8d2
docs
alex-harvey-z3q Apr 21, 2023
a9a9076
Add dump all
alex-harvey-z3q Apr 21, 2023
d21f7dd
various fixes
alex-harvey-z3q Apr 21, 2023
7e8629b
Add info messages
alex-harvey-z3q Apr 21, 2023
323e3e8
Update sceptre/plan/actions.py
alex-harvey-z3q Apr 27, 2023
e755dbf
Fix names
alex-harvey-z3q Apr 27, 2023
22a0516
Update sceptre/cli/template.py
alex-harvey-z3q Apr 27, 2023
5bbfb6f
fail_if_not_removed
alex-harvey-z3q Apr 27, 2023
119162d
more
alex-harvey-z3q Apr 28, 2023
c1b3068
Merge branch 'master' into alexharvey/1318-part-1
alex-harvey-z3q May 2, 2023
94dcaf1
Correction to deprecation
alex-harvey-z3q May 2, 2023
3e9682e
Add to-file
alex-harvey-z3q May 2, 2023
e5321c0
Correction in steps
alex-harvey-z3q May 3, 2023
64004ff
Merge branch 'master' into alexharvey/1318-part-1
zaro0508 May 4, 2023
d08f091
Respone to reviews
alex-harvey-z3q May 7, 2023
8a771e6
Fix integration tests
alex-harvey-z3q May 13, 2023
08862a8
More renaming for dump name
alex-harvey-z3q May 13, 2023
f51e267
Linter
alex-harvey-z3q May 14, 2023
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions docs/_source/docs/resolvers.rst
Original file line number Diff line number Diff line change
Expand Up @@ -476,7 +476,7 @@ have not yet been deployed. During normal deployment operations (using the ``lau
ensure that order is followed, so everything works as expected.

But there are other commands that will not actually deploy dependencies of a stack config before
operating on that Stack Config. These commands include ``generate``, ``validate``, and ``diff``.
operating on that Stack Config. These commands include ``dump template``, ``validate``, and ``diff``.
If you have used resolvers to reverence other stacks, it is possible that a resolver might not be able
to be resolved when performing that command's operations and will trigger an error. This is not likely
to happen when you have only used resolvers in a stack's ``parameters``, but it is much more likely
Expand All @@ -491,7 +491,7 @@ A few examples...
and you run the ``diff`` command before other_stack.yaml has been deployed, the diff output will
show the value of that parameter to be ``"{ !StackOutput(other_stack.yaml::OutputName) }"``.
* If you have a ``sceptre_user_data`` value used in a Jinja template referencing
``!stack_output other_stack.yaml::OutputName`` and you run the ``generate`` command, the generated
``!stack_output other_stack.yaml::OutputName`` and you run the ``dump template`` command, the generated
template will replace that value with ``"StackOutputotherstackyamlOutputName"``. This isn't as
"pretty" as the sort of placeholder used for stack parameters, but the use of sceptre_user_data is
broader, so it placeholder values can only be alphanumeric to reduce chances of it breaking the
Expand Down
84 changes: 74 additions & 10 deletions sceptre/cli/dump.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,10 @@
import click

from sceptre.context import SceptreContext
from sceptre.cli.helpers import catch_exceptions, write
from sceptre.cli.helpers import catch_exceptions, dump_to_file
from sceptre.plan.plan import SceptrePlan
from sceptre.helpers import null_context
from sceptre.resolvers.placeholders import use_resolver_placeholders_on_error

logger = logging.getLogger(__name__)

Expand Down Expand Up @@ -41,15 +43,77 @@ def dump_config(ctx, path):

output = []
for stack, config in responses.items():
if config is None:
logger.warning(f"{stack.external_name} does not exist")
else:
output.append({stack.external_name: config})
output.append({stack.external_name: config})

output_format = "json" if context.output_format == "json" else "yaml"

if len(output) == 1:
write(output[0][stack.external_name], output_format)
else:
for config in output:
write(config, output_format)
for config in output:
dump_to_file(config, output_format, type="config")


@dump_group.command(name="template")
jfalkenstein marked this conversation as resolved.
Show resolved Hide resolved
@click.argument("path")
@click.option(
"-n",
"--no-placeholders",
is_flag=True,
help="If True, no placeholder values will be supplied for resolvers that cannot be resolved.",
)
jfalkenstein marked this conversation as resolved.
Show resolved Hide resolved
@click.pass_context
@catch_exceptions
def dump_template(ctx, no_placeholders, path):
alex-harvey-z3q marked this conversation as resolved.
Show resolved Hide resolved
"""
Prints the template used for stack in PATH.
\f

:param path: Path to execute the command on.
:type path: str
"""
context = SceptreContext(
command_path=path,
command_params=ctx.params,
project_path=ctx.obj.get("project_path"),
user_variables=ctx.obj.get("user_variables"),
options=ctx.obj.get("options"),
output_format=ctx.obj.get("output_format"),
ignore_dependencies=ctx.obj.get("ignore_dependencies"),
)

plan = SceptrePlan(context)

execution_context = (
null_context() if no_placeholders else use_resolver_placeholders_on_error()
)
with execution_context:
responses = plan.dump_template()

output = []
for stack, template in responses.items():
output.append({stack.external_name: template})

output_format = "json" if context.output_format == "json" else "yaml"

for template in output:
dump_to_file(template, output_format)


@dump_group.command(name="all")
@click.argument("path")
@click.option(
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is missing the --to-file option. is this intentional?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

not intentional, fixed.

"-n",
"--no-placeholders",
is_flag=True,
help="If True, no placeholder values will be supplied for resolvers that cannot be resolved.",
)
@click.pass_context
@catch_exceptions
def dump_all(ctx, no_placeholders, path):
"""
Dumps both the rendered (post-Jinja) Stack Configs and the template used for stack in PATH.
\f

:param path: Path to execute the command on.
:type path: str
"""
ctx.invoke(dump_config, path=path)
ctx.invoke(dump_template, no_placeholders=no_placeholders, path=path)
41 changes: 37 additions & 4 deletions sceptre/cli/helpers.py
Original file line number Diff line number Diff line change
@@ -1,8 +1,12 @@
import logging
import sys

from itertools import cycle
from functools import partial, wraps

from typing import Any, Optional
from pathlib import Path

import json
import click
import six
Expand All @@ -16,6 +20,8 @@
from sceptre.stack_status import StackStatus
from sceptre.stack_status_colourer import StackStatusColourer

logger = logging.getLogger(__name__)


def catch_exceptions(func):
"""
Expand Down Expand Up @@ -66,18 +72,21 @@ def confirmation(command, ignore, command_path, change_set=None):
click.confirm(msg, abort=True)


def write(var, output_format="json", no_colour=True):
def write(
var: Any,
output_format: str = "json",
no_colour: bool = True,
file_path: Optional[Path] = None,
) -> None:
"""
Writes ``var`` to stdout. If output_format is set to "json" or "yaml",
write ``var`` as a JSON or YAML string.

:param var: The object to print
:type var: object
:param output_format: The format to print the output as. Allowed values: \
"text", "json", "yaml"
:type output_format: str
:param no_colour: Whether to colour stack statuses
:type no_colour: bool
:param file_path: Optional path to a file to save the output
"""
output = var

Expand All @@ -93,6 +102,13 @@ def write(var, output_format="json", no_colour=True):

click.echo(output)
alex-harvey-z3q marked this conversation as resolved.
Show resolved Hide resolved
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I find it odd that we still echo if a file_path is specified.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ok how about now? (I'm not sure whether people want the text to go to both the screen and file if file_path is specified).


if file_path:
dir_path = file_path.parent
alex-harvey-z3q marked this conversation as resolved.
Show resolved Hide resolved
dir_path.mkdir(parents=True, exist_ok=True)

with open(file_path, "w") as f:
f.write(output)


def _generate_json(stream):
encoder = CustomJsonEncoder(indent=4)
Expand Down Expand Up @@ -173,6 +189,23 @@ def _generate_text(stream):
return stream


def dump_to_file(
template: dict, output_format: str, template_type: str = "template"
) -> None:
"""
Helper to write templates and configs used in dump commands.

:param template: the template.
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You seem to have a very specific understanding of what this "template" ought to be: A dict with a single key, which is the stack name, and a single value, which is the template dict that needs to be written.

It's not super obvious from reading this code that this is supposed to work this way. Again, as an inner, nested function, it was fine. But now that you've moved it out to be at the level of "helper framework", it's not a simple or well-described interface. In fact, it's a misnomer to call it the "template", when in reality it's something more and different than just the "template". And I don't see why it should be the interface at all. Instead, I think that this function should take a stack_name and a template_dict without this single-key dictionary structure business going on. It'd be easier to follow and reuse that way.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You'll have to elaborate ....

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Your template variable here isn't actually a template. In other words, your variable naming lies. For a helper function that's meant to stand on its own, the interface must be clear, simple, and functional. But you're making a lot of assumptions here about what template is.

It should take a stack_name and a template_dict parameter instead of a dict that happens to have a key that is the stack name and a value that is the actual template dict. Your code would be simpler, more readable, and easier to interact with if you did that.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm not convinced that a variable named template_dict of type dict is clearer than template: dict and to my mind the whole comes out more rather than less messy afterwards. Perhaps we are better off without this helper at all and I revert to how it was before?

Copy link
Contributor Author

@alex-harvey-z3q alex-harvey-z3q Apr 28, 2023

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

For example on the caller side:

    for template in output:
        dump_to_file(list(template.keys())[0], template, output_format)

We don't want duplication of list(template.keys())[0] all over the place for the sake of helping the reader of that helper function since most people are going to be reading the CLI functions; not the helpers.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

My point is that you're calling the dict a "template", but it's actually a dict with a single key indicating the stack name and a its value is the actual template. In other words, your variable name is wrong and the interface of the function is more challenging than it ought to be.

Rather than assuming this arbitrary structure that is a mouthful to describe, I'm just saying take two parameters instead of this complex dict structure that is isn't well-typed.

:param output_format: The format to print the output as. Allowed values: \
:param template_type: either template or config used only in the file name.
"""
stack_name = list(template.keys())[0]
file_path = Path(".dump") / stack_name / f"{template_type}.{output_format}"
logger.info(f"{stack_name} dumping {template_type} to {file_path}")
write(template[stack_name], output_format, no_colour=True, file_path=file_path)
logger.info(f"{stack_name} dump to {file_path} complete.")


def setup_vars(var_file, var, merge_vars, debug, no_colour):
"""
Handle --var-file and --var arguments before
Expand Down
5 changes: 4 additions & 1 deletion sceptre/cli/template.py
Original file line number Diff line number Diff line change
@@ -1,8 +1,10 @@
import logging
import webbrowser

import click

from deprecation import deprecated

from sceptre import __version__
from sceptre.cli.helpers import catch_exceptions, write
from sceptre.context import SceptreContext
from sceptre.helpers import null_context
Expand Down Expand Up @@ -65,6 +67,7 @@ def validate_command(ctx, no_placeholders, path):
@click.argument("path")
@click.pass_context
@catch_exceptions
@deprecated("4.2.0", "5.0.0", __version__, "Use dump template instead.")
def generate_command(ctx, no_placeholders, path):
"""
Prints the template used for stack in PATH.
Expand Down
23 changes: 17 additions & 6 deletions sceptre/plan/actions.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,15 +12,17 @@
import time
import typing
import urllib
from datetime import datetime, timedelta
from os import path
from typing import Dict, Optional, Tuple, Union

import botocore

from datetime import datetime, timedelta
from dateutil.tz import tzutc
from os import path
from deprecation import deprecated

from sceptre import __version__
from sceptre.config.reader import ConfigReader
from sceptre.connection_manager import ConnectionManager

from sceptre.exceptions import (
CannotUpdateFailedStackError,
ProtectedStackError,
Expand All @@ -33,6 +35,8 @@
from sceptre.stack import Stack
from sceptre.stack_status import StackChangeSetStatus, StackStatus

from typing import Dict, Optional, Tuple, Union

if typing.TYPE_CHECKING:
from sceptre.diffing.stack_differ import StackDiff, StackDiffer

Expand Down Expand Up @@ -624,12 +628,12 @@ def _convert_to_url(self, summaries):

return new_summaries

@add_stack_hooks
@deprecated("4.2.0", "5.0.0", __version__, "Use dump template instead.")
def generate(self):
"""
Returns the Template for the Stack
"""
return self.stack.template.body
return self.dump_template()

@add_stack_hooks
def validate(self):
Expand Down Expand Up @@ -1152,3 +1156,10 @@ def dump_config(self, config_reader: ConfigReader):
"""
stack_path = normalise_path(self.stack.name + ".yaml")
return config_reader.read(stack_path)

@add_stack_hooks
def dump_template(self):
"""
Returns the Template for the Stack
"""
return self.stack.template.body
jfalkenstein marked this conversation as resolved.
Show resolved Hide resolved
14 changes: 14 additions & 0 deletions sceptre/plan/plan.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,8 +8,10 @@
"""
import functools
import itertools

from os import path, walk
from typing import Dict, List, Set, Callable, Iterable, Optional
from deprecation import deprecated

from sceptre.config.graph import StackGraph
from sceptre.config.reader import ConfigReader
Expand All @@ -19,6 +21,7 @@
from sceptre.helpers import sceptreise_path
from sceptre.plan.executor import SceptrePlanExecutor
from sceptre.stack import Stack
from sceptre import __version__


def require_resolved(func) -> Callable:
Expand Down Expand Up @@ -440,3 +443,14 @@ def dump_config(self, *args):
"""
self.resolve(command=self.dump_config.__name__)
return self._execute(self.config_reader, *args)

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You ought to deprecate generate above as well.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

done

@deprecated("4.2.0", "5.0.0", __version__, "Use dump template instead.")
def dump_template(self, *args):
"""
Returns a generated Template for a given Stack

:returns: A dictionary of Stacks and their template body.
:rtype: dict
"""
self.resolve(command=self.dump_template.__name__)
return self._execute(*args)
8 changes: 4 additions & 4 deletions sceptre/stack.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,9 +8,9 @@
"""

import logging
from typing import List, Any, Optional

import deprecation
from typing import List, Any, Optional
from deprecation import deprecated

from sceptre import __version__
from sceptre.connection_manager import ConnectionManager
Expand Down Expand Up @@ -383,7 +383,7 @@ def template(self):
return self._template

@property
@deprecation.deprecated(
@deprecated(
"4.0.0", "5.0.0", __version__, "Use the template Stack Config key instead."
)
def template_path(self) -> str:
Expand All @@ -393,7 +393,7 @@ def template_path(self) -> str:
return self.template_handler_config["path"]

@template_path.setter
@deprecation.deprecated(
@deprecated(
"4.0.0", "5.0.0", __version__, "Use the template Stack Config key instead."
)
def template_path(self, value: str):
Expand Down
9 changes: 5 additions & 4 deletions tests/test_connection_manager.py
Original file line number Diff line number Diff line change
@@ -1,11 +1,12 @@
# -*- coding: utf-8 -*-
import warnings
import pytest

from collections import defaultdict
from typing import Union
from unittest.mock import Mock, patch, sentinel, create_autospec
from deprecation import fail_if_not_removed

import deprecation
import pytest
from boto3.session import Session
from botocore.exceptions import ClientError
from moto import mock_s3
Expand Down Expand Up @@ -805,11 +806,11 @@ def test_create_session_environment_variables__include_system_envs_false__does_n
}
assert expected == result

@deprecation.fail_if_not_removed
@fail_if_not_removed
def test_iam_role__is_removed_on_removal_version(self):
self.connection_manager.iam_role

@deprecation.fail_if_not_removed
@fail_if_not_removed
def test_iam_role_session_duration__is_removed_on_removal_version(self):
self.connection_manager.iam_role_session_duration

Expand Down
8 changes: 8 additions & 0 deletions tests/test_plan.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
import pytest
from unittest.mock import MagicMock, patch, sentinel

from deprecation import fail_if_not_removed

from sceptre.context import SceptreContext
from sceptre.stack import Stack
from sceptre.config.reader import ConfigReader
Expand Down Expand Up @@ -61,3 +63,9 @@ def test_command_not_found_error_raised(self):
plan = MagicMock(spec=SceptrePlan)
plan.context = self.mock_context
plan.invalid_command()

@fail_if_not_removed
def test_generate_removed(self):
plan = MagicMock(spec=SceptrePlan)
plan.context = self.mock_context
plan.generate("test-attribute")
Loading