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 #359] Support use-previous-parameters #1476

Closed
Show file tree
Hide file tree
Changes from all commits
Commits
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
20 changes: 20 additions & 0 deletions docs/_source/docs/stack_config.rst
Original file line number Diff line number Diff line change
Expand Up @@ -289,6 +289,13 @@ values/resolvers. Lists of values/resolvers will be formatted into an AWS
compatible comma separated string e.g. \ ``value1,value2,value3``. Lists can
contain a mixture of values and resolvers.

A parameter can also be configured to use the previous value. You can do so by
making the value a dictionary. The values supported in the dictionary are
``initial_value`` and ``use_previous_value``. When creating stacks, setting
``initial_value`` is required, but can be left out for stack updates. The value
set at ``initial_value`` value will only be used during creation, or when
setting ``use_previous_value`` to false.

Syntax:

.. code-block:: yaml
Expand All @@ -305,6 +312,14 @@ Syntax:
<parameter5_name>:
- !<resolver_name> <resolver_value>
- "value1"
<parameter6_name>:
initial_value: "value"
use_previous_value: <boolean>
<parameter7_name>:
initial_value:
- "value1"
- !<resolver_name> <resolver_value>
use_previous_value: <boolean>

Example:

Expand All @@ -320,6 +335,11 @@ Example:
- "sg-12345678"
- !stack_output security-groups.yaml::BaseSecurityGroupId
- !file_contents /file/with/security_group_id.txt
security_group_whitelist:
initial_value:
- "127.0.0.0/24"
- "127.0.1.0/24"
use_previous_value: true

parameters_inheritance
~~~~~~~~~~~~~~~~~~~~~~~~
Expand Down
8 changes: 8 additions & 0 deletions sceptre/exceptions.py
Original file line number Diff line number Diff line change
Expand Up @@ -191,6 +191,14 @@ class InvalidAWSCredentialsError(SceptreException):
pass


class InvalidParameterError(SceptreException):
"""
Error raised when parameters are invalid.
"""

pass


class TemplateHandlerNotFoundError(SceptreException):
"""
Error raised when a Template Handler of a certain type is not found
Expand Down
39 changes: 35 additions & 4 deletions sceptre/plan/actions.py
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@
StackDoesNotExistError,
UnknownStackChangeSetStatusError,
UnknownStackStatusError,
InvalidParameterError,
)
from sceptre.helpers import extract_datetime_from_aws_response_headers
from sceptre.hooks import add_stack_hooks, add_stack_hooks_with_aliases
Expand Down Expand Up @@ -71,7 +72,7 @@ def create(self):
self.logger.info("%s - Creating Stack", self.stack.name)
create_stack_kwargs = {
"StackName": self.stack.external_name,
"Parameters": self._format_parameters(self.stack.parameters),
"Parameters": self._format_parameters(self.stack.parameters, create=True),
"Capabilities": [
"CAPABILITY_IAM",
"CAPABILITY_NAMED_IAM",
Expand Down Expand Up @@ -759,22 +760,52 @@ def get_status(self):
except StackDoesNotExistError:
return "PENDING"

def _format_parameters(self, parameters):
def _format_parameters(self, parameters, create=False):
"""
Converts CloudFormation parameters to the format used by Boto3.

:param parameters: A dictionary of parameters.
:type parameters: dict
:param create: Flags if this is a stack create or update operation.
:type parameters: bool

:returns: A list of the formatted parameters.
:rtype: list
"""
formatted_parameters = []
for name, value in parameters.items():
if value is None:
continue

formatted_parameter = dict(ParameterKey=name)

if isinstance(value, list):
value = ",".join(value)
formatted_parameters.append({"ParameterKey": name, "ParameterValue": value})
formatted_parameter["ParameterValue"] = ",".join(value)
elif isinstance(value, dict):
initial_value = value.get("initial_value")
use_previous_value = value.get("use_previous_value", False)
if not isinstance(use_previous_value, bool):
raise InvalidParameterError(
"'use_previous_value' must be a boolean"
)
if create or not use_previous_value:
if initial_value is None:
raise InvalidParameterError(
"'initial_value' is required when creating a new "
"stack or when 'use_previous_value' is false"
)
elif isinstance(initial_value, list):
formatted_parameter["ParameterValue"] = ",".join(initial_value)
else:
formatted_parameter["ParameterValue"] = value.get(
"initial_value"
)
else:
formatted_parameter["UsePreviousValue"] = use_previous_value
else:
formatted_parameter["ParameterValue"] = value

formatted_parameters.append(formatted_parameter)

return formatted_parameters

Expand Down
75 changes: 75 additions & 0 deletions tests/test_actions.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@
StackDoesNotExistError,
UnknownStackChangeSetStatusError,
UnknownStackStatusError,
InvalidParameterError,
)
from sceptre.plan.actions import StackActions
from sceptre.stack import Stack
Expand Down Expand Up @@ -860,6 +861,16 @@ def test_format_parameters_with_none_values(self):
)
assert sorted_formatted_parameters == []

def test_format_parameters_with_empty_dict_value(self):
parameter = {"key": dict()}
with pytest.raises(InvalidParameterError):
self.actions._format_parameters(parameter)

def test_format_parameters_with_non_bool_previous_value(self):
parameter = {"key": dict(use_previous_value="fosho")}
with pytest.raises(InvalidParameterError):
self.actions._format_parameters(parameter)

def test_format_parameters_with_none_and_string_values(self):
parameters = {"key1": "value1", "key2": None, "key3": "value3"}
formatted_parameters = self.actions._format_parameters(parameters)
Expand Down Expand Up @@ -933,6 +944,70 @@ def test_format_parameters_with_none_list_and_string_values(self):
{"ParameterKey": "key2", "ParameterValue": "value4"},
]

def test_format_parameters_with_string_and_dict_values(self):
parameters = {"key1": "value1", "key2": {"initial_value": "value2"}}
formatted_parameters = self.actions._format_parameters(parameters)
sorted_formatted_parameters = sorted(
formatted_parameters, key=lambda x: x["ParameterKey"]
)
assert sorted_formatted_parameters == [
{"ParameterKey": "key1", "ParameterValue": "value1"},
{"ParameterKey": "key2", "ParameterValue": "value2"},
]

def test_format_parameters_with_dict_string_and_list_values(self):
parameters = {
"key1": {"initial_value": ["value1", "value2"]},
"key2": {"initial_value": "value3"},
}
formatted_parameters = self.actions._format_parameters(parameters)
sorted_formatted_parameters = sorted(
formatted_parameters, key=lambda x: x["ParameterKey"]
)
assert sorted_formatted_parameters == [
{"ParameterKey": "key1", "ParameterValue": "value1,value2"},
{"ParameterKey": "key2", "ParameterValue": "value3"},
]

def test_format_parameters_with_string_and_previous_values(self):
parameters = {
"key1": "value1",
"key2": {"use_previous_value": True},
"key3": {"initial_value": "value3", "use_previous_value": True},
}
formatted_parameters = self.actions._format_parameters(parameters)
sorted_formatted_parameters = sorted(
formatted_parameters, key=lambda x: x["ParameterKey"]
)
assert sorted_formatted_parameters == [
{"ParameterKey": "key1", "ParameterValue": "value1"},
{"ParameterKey": "key2", "UsePreviousValue": True},
{"ParameterKey": "key3", "UsePreviousValue": True},
]

def test_format_parameters_with_create_and_previous_without_initial_values(self):
parameters = {
"key1": "value1",
"key2": {"use_previous_value": True},
"key3": {"initial_value": "value3", "use_previous_value": True},
}
with pytest.raises(InvalidParameterError):
self.actions._format_parameters(parameters, create=True)

def test_format_parameters_with_create_and_previous_values(self):
parameters = {
"key1": "value1",
"key2": {"initial_value": "value2", "use_previous_value": True},
}
formatted_parameters = self.actions._format_parameters(parameters, create=True)
sorted_formatted_parameters = sorted(
formatted_parameters, key=lambda x: x["ParameterKey"]
)
assert sorted_formatted_parameters == [
{"ParameterKey": "key1", "ParameterValue": "value1"},
{"ParameterKey": "key2", "ParameterValue": "value2"},
]

@patch("sceptre.plan.actions.StackActions.describe")
def test_get_status_with_created_stack(self, mock_describe):
mock_describe.return_value = {"Stacks": [{"StackStatus": "CREATE_COMPLETE"}]}
Expand Down