diff --git a/sceptre/stack.py b/sceptre/stack.py index 8a633096c..fa5639a8b 100644 --- a/sceptre/stack.py +++ b/sceptre/stack.py @@ -9,7 +9,7 @@ import logging -from typing import List, Any, Optional +from typing import List, Dict, Union, Any, Optional from deprecation import deprecated from sceptre import __version__ @@ -26,6 +26,7 @@ ResolvableValueProperty, RecursiveResolve, PlaceholderType, + Resolver, ) from sceptre.template import Template @@ -262,7 +263,7 @@ def __init__( ) self.s3_details = s3_details - self.parameters = parameters or {} + self.parameters = self._ensure_parameters(parameters or {}) self.sceptre_user_data = sceptre_user_data or {} self.notifications = notifications or [] @@ -275,6 +276,30 @@ def _ensure_boolean(self, config_name: str, value: Any) -> bool: ) return value + def _ensure_parameters( + self, parameters: Dict[str, Any] + ) -> Dict[str, Union[str, List[Union[str, Resolver]], Resolver]]: + """Ensure CloudFormation parameters are of valid types""" + + def is_valid(value: Any) -> bool: + return ( + isinstance(value, str) + or ( + isinstance(value, list) + and all( + isinstance(item, str) or isinstance(item, Resolver) + for item in value + ) + ) + or isinstance(value, Resolver) + ) + + if not all(is_valid(value) for value in parameters.values()): + raise InvalidConfigFileError( + f"{self.name}: Values for parameters must be strings, lists or resolvers, got {parameters}" + ) + return parameters + def __repr__(self): return ( "sceptre.stack.Stack(" diff --git a/tests/test_stack.py b/tests/test_stack.py index 7e843e47f..6f67e08cd 100644 --- a/tests/test_stack.py +++ b/tests/test_stack.py @@ -40,6 +40,11 @@ def stack_factory(**kwargs): return Stack(**call_kwargs) +class FakeResolver(Resolver): + def resolve(self): + return "Fake" + + class TestStack(object): def setup_method(self, test_method): self.stack = Stack( @@ -183,6 +188,46 @@ def test_init__non_boolean_obsolete_value__raises_invalid_config_file_error(self obsolete="true", ) + @pytest.mark.parametrize( + "parameters", + [ + {"someNum": 1}, + {"someBool": True}, + {"aBadList": [1, 2, 3]}, + {"aDict": {"foo": "bar"}}, + ], + ) + def test_init__invalid_parameters_raise_invalid_config_file_error(self, parameters): + with pytest.raises(InvalidConfigFileError): + Stack( + name="stack_name", + project_code="project_code", + template_handler_config={"type": "file"}, + region="region", + parameters=parameters, + ) + + @pytest.mark.parametrize( + "parameters", + [ + {"someNum": "1"}, + {"someBool": "true"}, + {"aList": ["aString", FakeResolver()]}, + {"aResolver": FakeResolver()}, + ], + ) + def test_init__valid_parameters_do_not_raise_invalid_config_file_error( + self, parameters + ): + stack = Stack( + name="stack_name", + project_code="project_code", + template_handler_config={"type": "file"}, + region="region", + parameters=parameters, + ) + assert isinstance(stack, Stack) + def test_stack_repr(self): assert ( self.stack.__repr__() == "sceptre.stack.Stack(" @@ -248,14 +293,10 @@ def resolve(self): def test_configuration_manager__sceptre_role_returns_value__returns_connection_manager_with_that_role( self, ): - class FakeResolver(Resolver): - def resolve(self): - return "role" - self.stack.sceptre_role = FakeResolver() connection_manager = self.stack.connection_manager - assert connection_manager.sceptre_role == "role" + assert connection_manager.sceptre_role == "Fake" @fail_if_not_removed def test_iam_role__is_removed_on_removal_version(self):