Skip to content

Commit

Permalink
Revert "Remove template_path config parameter (#1386)" (#1406)
Browse files Browse the repository at this point in the history
This reverts commit bc828dc.
This removal should go out on a major release however
We are not ready for a major release yet.  Temporarily
revert this change.
  • Loading branch information
zaro0508 committed Jan 11, 2024
1 parent 17835e4 commit 56d2111
Show file tree
Hide file tree
Showing 10 changed files with 139 additions and 27 deletions.
20 changes: 19 additions & 1 deletion docs/_source/docs/stack_config.rst
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ Structure
A Stack config file is a ``yaml`` object of key-value pairs configuring a
particular Stack. The available keys are listed below.

- `template`_ *(required)*
- `template_path`_ or `template`_ *(required)*
- `dependencies`_ *(optional)*
- `hooks`_ *(optional)*
- `ignore`_ *(optional)*
Expand All @@ -34,6 +34,23 @@ particular Stack. The available keys are listed below.
- `stack_tags`_ *(optional)*
- `stack_timeout`_ *(optional)*

It is not possible to define both `template_path`_ and `template`_. If you do so,
you will receive an error when deploying the stack.

template_path
~~~~~~~~~~~~~~~~~~~~~~~~
* Resolvable: No
* Can be inherited from StackGroup: No

The path to the CloudFormation, Jinja2 or Python template to build the Stack
from. The path can either be absolute or relative to the Sceptre Directory.
Sceptre treats the template as CloudFormation, Jinja2 or Python depending on
the template’s file extension. Note that the template filename may be different
from the Stack config filename.

.. warning::

This key is deprecated in favor of the `template`_ key. It will be removed in version 5.0.0.

template
~~~~~~~~
Expand Down Expand Up @@ -611,6 +628,7 @@ Examples
tag_1: value_1
tag_2: value_2
.. _template_path: #template-path
.. _template: #template
.. _dependencies: #dependencies
.. _hooks: #hooks
Expand Down
7 changes: 7 additions & 0 deletions docs/_source/docs/template_handlers.rst
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,10 @@ Template handlers can be used to resolve a ``template`` config block to a CloudF
fetch templates from S3, for example. Users can create their own template handlers to easily add support for other
template loading mechanisms. See `Custom Template Handlers`_ for more information.

.. warning::

The ``template_path`` key is deprecated in favor of the ``template`` key.

Available Template Handlers
---------------------------

Expand All @@ -14,6 +18,9 @@ file
Loads a template from the local file system. This handler supports templates with .json, .yaml, .template, .j2
and .py extensions. This is the default template handler type, specifying the ``file`` type is optional.

For backwards compatability, when a ``template_path`` key is specified in the Stack config, it is wired to
use the ``file`` template handler.

Syntax:

.. code-block:: yaml
Expand Down
27 changes: 13 additions & 14 deletions integration-tests/steps/templates.py
Original file line number Diff line number Diff line change
Expand Up @@ -23,15 +23,18 @@ def set_template_path(context, stack_name, template_name):
with open(os.path.join(config_path, stack_name + ".yaml")) as config_file:
stack_config = yaml.safe_load(config_file)

stack_config["template"]["type"] = stack_config["template"].get("type", "file")
template_handler_type = stack_config["template"]["type"]
if template_handler_type.lower() == "s3":
segments = stack_config["template"]["path"].split("/")
bucket = context.TEST_ARTIFACT_BUCKET_NAME
key = "/".join(segments[1:])
stack_config["template"]["path"] = f"{bucket}/{key}"
else:
stack_config["template"]["path"] = template_path
if "template_path" in stack_config:
stack_config["template_path"] = template_path
if "template" in stack_config:
stack_config["template"]["type"] = stack_config["template"].get("type", "file")
template_handler_type = stack_config["template"]["type"]
if template_handler_type.lower() == "s3":
segments = stack_config["template"]["path"].split("/")
bucket = context.TEST_ARTIFACT_BUCKET_NAME
key = "/".join(segments[1:])
stack_config["template"]["path"] = f"{bucket}/{key}"
else:
stack_config["template"]["path"] = template_path

with open(os.path.join(config_path, stack_name + ".yaml"), "w") as config_file:
yaml.safe_dump(stack_config, config_file, default_flow_style=False)
Expand Down Expand Up @@ -86,11 +89,7 @@ def step_impl(context, stack_name):
with open(os.path.join(config_path, stack_name + ".yaml")) as config_file:
stack_config = yaml.safe_load(config_file)

if (
"template" in stack_config
and "type" in stack_config["template"]
and stack_config["template"]["type"].lower() == "s3"
):
if "template" in stack_config and stack_config["template"]["type"].lower() == "s3":
segments = stack_config["template"]["path"].split("/")
bucket = segments[0]
key = "/".join(segments[1:])
Expand Down
5 changes: 4 additions & 1 deletion sceptre/config/reader.py
Original file line number Diff line number Diff line change
Expand Up @@ -63,6 +63,7 @@
"template_bucket_name": strategies.child_wins,
"template_key_value": strategies.child_wins,
"template": strategies.child_wins,
"template_path": strategies.child_wins,
"ignore": strategies.child_wins,
"obsolete": strategies.child_wins,
}
Expand All @@ -80,6 +81,7 @@
STACK_CONFIG_ATTRIBUTES = ConfigAttributes(
{},
{
"template_path",
"template",
"dependencies",
"hooks",
Expand Down Expand Up @@ -467,7 +469,7 @@ def _render(self, directory_path, basename, stack_group_config):
try:
config = yaml.safe_load(rendered_template)
except Exception as err:
message = f"Error parsing {abs_directory_path}{basename}: \n{err}"
message = f"Error parsing {abs_directory_path}{basename}:\n{err}"

if logging_level() == logging.DEBUG:
debug_file_path = write_debug_file(
Expand Down Expand Up @@ -587,6 +589,7 @@ def _construct_stack(self, rel_path, stack_group_config=None):
stack = Stack(
name=stack_name,
project_code=config["project_code"],
template_path=config.get("template_path"),
template_handler_config=config.get("template"),
region=config["region"],
template_bucket_name=config.get("template_bucket_name"),
Expand Down
39 changes: 37 additions & 2 deletions sceptre/stack.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,9 @@
import logging

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

from sceptre import __version__
from sceptre.connection_manager import ConnectionManager
from sceptre.exceptions import InvalidConfigFileError
from sceptre.helpers import (
Expand All @@ -36,9 +39,14 @@ class Stack:
:param project_code: A code which is prepended to the Stack names\
of all Stacks built by Sceptre.
:param template_path: The relative path to the CloudFormation, Jinja2,
or Python template to build the Stack from. If this is filled,
`template_handler_config` should not be filled. This field has been deprecated since
version 4.0.0 and will be removed in version 5.0.0.
:param template_handler_config: Configuration for a Template Handler that can resolve
its arguments to a template string. Should contain the `type` property to specify
the type of template handler to load.
the type of template handler to load. Conflicts with `template_path`.
:param region: The AWS region to build Stacks in.
Expand Down Expand Up @@ -161,6 +169,7 @@ def __init__(
name: str,
project_code: str,
region: str,
template_path: str = None,
template_handler_config: dict = None,
template_bucket_name: str = None,
template_key_prefix: str = None,
Expand Down Expand Up @@ -234,7 +243,15 @@ def __init__(
role_arn,
)
self.template_bucket_name = template_bucket_name
self.template_handler_config = template_handler_config
self._set_field_with_deprecated_alias(
"template_handler_config",
template_handler_config,
"template_path",
template_path,
required=True,
preferred_config_name="template",
)

self.s3_details = s3_details
self.parameters = parameters or {}
self.sceptre_user_data = sceptre_user_data or {}
Expand Down Expand Up @@ -292,6 +309,7 @@ def __eq__(self, stack):
self.name == stack.name
and self.external_name == stack.external_name
and self.project_code == stack.project_code
and self.template_path == stack.template_path
and self.region == stack.region
and self.template_key_prefix == stack.template_key_prefix
and self.required_version == stack.required_version
Expand Down Expand Up @@ -368,6 +386,23 @@ def template(self):
)
return self._template

@property
@deprecated(
"4.0.0", "5.0.0", __version__, "Use the template Stack Config key instead."
)
def template_path(self) -> str:
"""The path argument from the template_handler config. This field is deprecated as of v4.0.0
and will be removed in v5.0.0.
"""
return self.template_handler_config["path"]

@template_path.setter
@deprecated(
"4.0.0", "5.0.0", __version__, "Use the template Stack Config key instead."
)
def template_path(self, value: str):
self.template_handler_config = {"type": "file", "path": value}

def _set_field_with_deprecated_alias(
self,
preferred_attribute_name,
Expand Down
3 changes: 1 addition & 2 deletions tests/fixtures/config/account/stack-group/region/vpc.yaml
Original file line number Diff line number Diff line change
@@ -1,5 +1,4 @@
template:
path: path/to/template
template_path: path/to/template
parameters:
param1: val1
dependencies:
Expand Down
3 changes: 1 addition & 2 deletions tests/fixtures/config/top/level.yaml
Original file line number Diff line number Diff line change
@@ -1,2 +1 @@
template:
path: vpc.py
template_path: vpc.py
5 changes: 3 additions & 2 deletions tests/test_actions.py
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,7 @@ def setup_method(self, test_method):
self.stack = Stack(
name="prod/app/stack",
project_code=sentinel.project_code,
template_handler_config={"type": "file", "path": sentinel.path},
template_path=sentinel.template_path,
region=sentinel.region,
profile=sentinel.profile,
parameters={"key1": "val1"},
Expand Down Expand Up @@ -76,7 +76,7 @@ def test_template_loads_template(self, mock_Template):

mock_Template.assert_called_once_with(
name="prod/app/stack",
handler_config={"type": "file", "path": sentinel.path},
handler_config={"type": "file", "path": sentinel.template_path},
sceptre_user_data=sentinel.sceptre_user_data,
stack_group_config={},
connection_manager=self.stack.connection_manager,
Expand All @@ -93,6 +93,7 @@ def test_external_name_with_custom_stack_name(self):
stack = Stack(
name="stack_name",
project_code="project_code",
template_path="template_path",
region="region",
external_name="external_name",
)
Expand Down
3 changes: 2 additions & 1 deletion tests/test_config_reader.py
Original file line number Diff line number Diff line change
Expand Up @@ -287,6 +287,7 @@ def test_construct_stacks_constructs_stack(
mock_Stack.assert_any_call(
name="account/stack-group/region/vpc",
project_code="account_project_code",
template_path=None,
template_handler_config={"path": "path/to/template"},
region="region_region",
profile="account_profile",
Expand Down Expand Up @@ -453,7 +454,7 @@ def test_missing_attr(self, filepaths, del_key):
config = {
"project_code": "project_code",
"region": "region",
"path": rel_path,
"template_path": rel_path,
}
# Delete the mandatory key to be tested.
del config[del_key]
Expand Down
54 changes: 52 additions & 2 deletions tests/test_stack.py
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ def stack_factory(**kwargs):
"template_bucket_name": sentinel.template_bucket_name,
"template_key_prefix": sentinel.template_key_prefix,
"required_version": sentinel.required_version,
"template_path": sentinel.template_path,
"region": sentinel.region,
"profile": sentinel.profile,
"parameters": {"key1": "val1"},
Expand Down Expand Up @@ -47,6 +48,7 @@ def setup_method(self, test_method):
template_bucket_name=sentinel.template_bucket_name,
template_key_prefix=sentinel.template_key_prefix,
required_version=sentinel.required_version,
template_path=sentinel.template_path,
region=sentinel.region,
profile=sentinel.profile,
parameters={"key1": "val1"},
Expand All @@ -68,10 +70,47 @@ def setup_method(self, test_method):
)
self.stack._template = MagicMock(spec=Template)

def test_initialize_stack_with_template_path(self):
stack = Stack(
name="dev/stack/app",
project_code=sentinel.project_code,
template_path=sentinel.template_path,
template_bucket_name=sentinel.template_bucket_name,
template_key_prefix=sentinel.template_key_prefix,
required_version=sentinel.required_version,
region=sentinel.region,
external_name=sentinel.external_name,
)
assert stack.name == "dev/stack/app"
assert stack.project_code == sentinel.project_code
assert stack.template_bucket_name == sentinel.template_bucket_name
assert stack.template_key_prefix == sentinel.template_key_prefix
assert stack.required_version == sentinel.required_version
assert stack.external_name == sentinel.external_name
assert stack.hooks == {}
assert stack.parameters == {}
assert stack.sceptre_user_data == {}
assert stack.template_path == sentinel.template_path
assert stack.template_handler_config == {
"path": sentinel.template_path,
"type": "file",
}
assert stack.s3_details is None
assert stack._template is None
assert stack.protected is False
assert stack.sceptre_role is None
assert stack.role_arn is None
assert stack.dependencies == []
assert stack.tags == {}
assert stack.notifications == []
assert stack.on_failure is None
assert stack.disable_rollback is False
assert stack.stack_group_config == {}

def test_initialize_stack_with_template_handler(self):
expected_template_handler_config = {
"type": "file",
"path": sentinel.path,
"path": sentinel.template_path,
}
stack = Stack(
name="dev/stack/app",
Expand All @@ -92,6 +131,7 @@ def test_initialize_stack_with_template_handler(self):
assert stack.hooks == {}
assert stack.parameters == {}
assert stack.sceptre_user_data == {}
assert stack.template_path == sentinel.template_path
assert stack.template_handler_config == expected_template_handler_config
assert stack.s3_details is None
assert stack._template is None
Expand All @@ -105,6 +145,16 @@ def test_initialize_stack_with_template_handler(self):
assert stack.disable_rollback is False
assert stack.stack_group_config == {}

def test_raises_exception_if_path_and_handler_configured(self):
with pytest.raises(InvalidConfigFileError):
Stack(
name="stack_name",
project_code="project_code",
template_path="template_path",
template_handler_config={"type": "file"},
region="region",
)

def test_init__non_boolean_ignore_value__raises_invalid_config_file_error(self):
with pytest.raises(InvalidConfigFileError):
Stack(
Expand Down Expand Up @@ -138,7 +188,7 @@ def test_stack_repr(self):
self.stack.__repr__() == "sceptre.stack.Stack("
"name='dev/app/stack', "
"project_code=sentinel.project_code, "
"template_handler_config=None, "
"template_handler_config={'type': 'file', 'path': sentinel.template_path}, "
"region=sentinel.region, "
"template_bucket_name=sentinel.template_bucket_name, "
"template_key_prefix=sentinel.template_key_prefix, "
Expand Down

0 comments on commit 56d2111

Please sign in to comment.