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

[Resolves #359] Implements use previous paramater feature #914

Closed
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 @@ -86,6 +86,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 @@ -102,6 +109,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 @@ -117,6 +132,11 @@ Example:
- "sg-12345678"
- !stack_output security-groups::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

protected
~~~~~~~~~
Expand Down
2 changes: 1 addition & 1 deletion docs/_source/docs/templates.rst
Original file line number Diff line number Diff line change
Expand Up @@ -72,7 +72,7 @@ Stack:

Template `dns-extras.j2`:

.. code-block:: yaml
.. code-block:: jinja

AWSTemplateFormatVersion: '2010-09-09'
Description: 'Add Route53 - CNAME and ALIAS records'
Expand Down
7 changes: 7 additions & 0 deletions sceptre/exceptions.py
Original file line number Diff line number Diff line change
Expand Up @@ -166,3 +166,10 @@ class InvalidAWSCredentialsError(SceptreException):
Error raised when AWS credentials are invalid.
"""
pass


class InvalidParameterError(SceptreException):
"""
Error raised when parameters are invalid.
"""
pass
30 changes: 23 additions & 7 deletions sceptre/plan/actions.py
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@
from sceptre.exceptions import UnknownStackChangeSetStatusError
from sceptre.exceptions import StackDoesNotExistError
from sceptre.exceptions import ProtectedStackError
from sceptre.exceptions import InvalidParameterError


class StackActions(object):
Expand Down Expand Up @@ -59,7 +60,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', 'CAPABILITY_AUTO_EXPAND'],
"NotificationARNs": self.stack.notifications,
"Tags": [
Expand Down Expand Up @@ -640,7 +641,7 @@ 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.

Expand All @@ -653,12 +654,27 @@ def _format_parameters(self, 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 is True or use_previous_value is False) and initial_value is None:
raise InvalidParameterError("'initial_value' is required when creating a new "
"stack or when 'use_previous_value' is false")
if create is True or use_previous_value is False:
if 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
88 changes: 87 additions & 1 deletion tests/test_actions.py
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@
from sceptre.exceptions import UnknownStackChangeSetStatusError
from sceptre.exceptions import StackDoesNotExistError
from sceptre.exceptions import ProtectedStackError
from sceptre.exceptions import InvalidParameterError


class TestStackActions(object):
Expand Down Expand Up @@ -703,7 +704,7 @@ def test_unlock_calls_set_stack_policy_with_policy(
"tests/fixtures/stack_policies/unlock.json"
)

def test_format_parameters_with_sting_values(self):
def test_format_parameters_with_string_values(self):
parameters = {
"key1": "value1",
"key2": "value2",
Expand Down Expand Up @@ -733,6 +734,20 @@ 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",
Expand Down Expand Up @@ -815,6 +830,77 @@ 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 = {
Expand Down