-
Notifications
You must be signed in to change notification settings - Fork 311
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 #1252] Supporting resolvers in Hook and Resolver arguments, with new !substitute, !join, !split, and !select resolvers! #1313
Conversation
sceptre/hooks/__init__.py
Outdated
self.argument = argument | ||
self.stack = stack | ||
|
||
def setup(self): | ||
""" | ||
setup is a method that may be overwritten by inheriting classes. Allows | ||
hooks to run so initalisation steps when config is first read. | ||
""" | ||
pass # pragma: no cover | ||
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This is now defined in the base class
def clone(self, stack: "Stack") -> "Hook": | ||
""" | ||
Produces a "fresh" copy of the Hook, with the specified stack. | ||
|
||
:param stack: The stack to set on the cloned resolver | ||
""" | ||
clone = type(self)(self.argument, stack) | ||
return clone | ||
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This is now defined on the base class
|
||
if TYPE_CHECKING: | ||
from sceptre.stack import Stack | ||
|
||
|
||
class Hook(abc.ABC): | ||
class Hook(CustomYamlTagBase, metaclass=abc.ABCMeta): |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The big change here is that, since there's very little that differentiates a hook from a resolver, other than the "resolve" vs "run" public method, they now share a common base class. A lot of the functionality on Hooks has now been moved up into that base class since it was shared with Resolvers.
@@ -84,7 +57,7 @@ def __set__(self, instance: "Stack", value): | |||
""" | |||
|
|||
def setup(attr, key, value: Hook): | |||
attr[key] = clone = value.clone(instance) | |||
attr[key] = clone = value.clone_for_stack(instance) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
clone_for_stack
(as you'll see below) is a recursive clone operation that allows for cloning resolvers inside of the arguments.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think this is great and will be a boon to users!
Looking at how resolvers work, though, I'm wondering if a more functional approach might drastically improve the code. There's a lot of mutation and behavior that, while correct, is not obvious and might be harder to reason about or troubleshoot.
It feels like resolvers (and maybe hooks?) could be evaluated as a series of transformations that act on data and their invocation is basically an abstract syntax tree.
The correct reply to the above is, of course, "Put your PR where your mouth is Dan." :)
docs/_source/docs/resolvers.rst
Outdated
This resolver allows you to select a specific index of a list of items. This is great for combining | ||
with the ``!split`` resolver to obtain part of a string. This function works almost the same as | ||
CloudFormation's ``!Select`` intrinsic function, **except you can use this with negative indexes to | ||
select with a reverse index**. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
What about "except you can use this with negative indices to select from the end of the list." ?
docs/_source/docs/resolvers.rst
Outdated
@@ -181,6 +248,32 @@ Example: | |||
parameters: | |||
VpcIdParameter: !stack_output_external prj-network-vpc::VpcIdOutput prod | |||
|
|||
|
|||
substitute |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I don't have super-strong feelings about this, but why "substitute" and not "sub" as in CF? I might argue in favor of "sub" for consistency with CF.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I don't know. I guess I just don't like the abbreviation. It doesn't seem as clear as substitute
. @zaro0508 , thoughts on this?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
i agree with @dboitnot. Since you are making references to cloudformation intrinsic functions I prefer sub
for consistency.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I agree "substitute" has more intrinsic clarity, but since most of our users are going to be pretty familiar with CF and sub
I think we could avoid the mental dichotomy by sticking to CF's nomenclature. But I would like @zaro0508's thoughts as well.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I've changed this to sub
Key = Union[str, int] | ||
|
||
|
||
def delete_keys_from_containers(keys_to_delete: List[Tuple[Container, Key]]): |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I'm glad that this has been extracted but I'm a bit queasy about this function mutating collections it doesn't own. I got side-tracked by work but I feel this could be simplified (in the future) using list comprehensions and/or filter
. But then I wasn't able to dig enough into how it's used to propose a refactor.
On the subject of in-place mutation...
|
I'm really looking forward to this PR being merged |
docs/_source/docs/resolvers.rst
Outdated
~~~~~~~~~~ | ||
|
||
This resolver allows you to create a string using Python string format syntax. This functions as a | ||
great way to combine together a number of resolver outputs into a single string. This functions very |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
very/is very
docs/_source/docs/resolvers.rst
Outdated
@@ -181,6 +248,32 @@ Example: | |||
parameters: | |||
VpcIdParameter: !stack_output_external prj-network-vpc::VpcIdOutput prod | |||
|
|||
|
|||
substitute |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
i agree with @dboitnot. Since you are making references to cloudformation intrinsic functions I prefer sub
for consistency.
notifications: !split | ||
- ";" | ||
- !stack_output my/sns/topics.yaml::SemicolonDelimitedArns |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
do you think it would make it easier for users to understand if these examples just used direct values? something like this..
notifications: !split
- ";"
- "note1; note2; note3"
result: the list ["note1", "note2", "note3"] is passed into the notifications
parameter
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Actually, I don't really. !sub, !join, !select, and !split are ONLY useful when you have resolvers to string together; They're clumsy if you don't. It's better to use Jinja to do these things, if you can. But if you have resolvers whose resolved values you need to manipulate/interpolate/select from, they're really the only way to do that.
As such, If we don't show how these resolvers can be used with other resolvers, we're not really showing how to use them well.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
¿Por qué no los dos? :)
notifications: !split
- ";"
- "note1; note2; note3"
Resolves to:
notifications: [ "note1", "note2", "note3" ]
As a more practical example, the following will split a semicolon-delimited list exported by another stack:
notifications: !split
- ";"
- !stack_output my/sns/topics.yaml::SemicolonDelimitedArns
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@dboitnot, sure, that works. But If you wanted to split a simple string, you'd just go like this:
notifications: {{ "note1; note2; note3".split(";") }}
I fear that if we show people examples of using these resolvers in cases where you don't actually need to use them (and where you really shouldn't use them), it will be more confusing. The only times you need to use these resolvers is when you need to combine/manipulate the outputs of other resolvers. I actually added documentation to that effect to make it absolutely clear.
|
||
parameters: | ||
ConnectionString: !sub | ||
- "postgres://{username}:{password}@{hostname}:{port}/{database}" |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
can the sub reference vars outside of it's scope? Can you set these to build-in vars like account id/region/etc.. What about references to vars from config.yaml If possible it might help users if there was an example or two of those use cases.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
!sub
needs a second argument that defines the variable values. It's not the same as CloudFormation's sub that lets you just resolve values without defining them. It's just a glorified str.format()
invocation. It doesn't have a "scope" outside its own arguments. If you want to resolve something from the Stack scope, you could combine !sub
and Jinja syntax together, like this:
!sub
# Note the double-curly braces is JINJA syntax to access variables within Jinja scope
- "postgres://{{ var.username }}:{password}@{hostname}:{port}/{{ database }}"
- password: !ssm /my/ssm/password
hostname: !stack_output my/database/stack.yaml::HostName
port: !stack_output my/database/stack.yaml::Port
Notice how, if it's accessible within Jinja, you don't actually need to provide that to !sub
as a variable, because Jinja interpolation will happen BEFORE the yaml is rendered. I'm going to look into clarifying this in the docs, since I doubt you'll be the first one confused about this.
tests/test_resolvers/test_select.py
Outdated
return self.argument | ||
|
||
|
||
class TestSplit: |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
shouldn't this be TestSelect:
?
Also would it make sense to have a test to select on a negative index values?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yes!
setup.py
Outdated
"stack_attr = sceptre.resolvers.stack_attr:StackAttr", | ||
"sub = sceptre.resolvers.substitute:Sub", |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
shouldn't this be sub = sceptre.resolvers.sub:Sub
?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yikes! Yes! That's probably why my tests are failing.
return self.argument | ||
|
||
|
||
class TestSelect: |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I need to implement better error handling around missing indexes/keys.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Pydantic has changed my life:
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
generally LGTM. just a few minor fixes and should be good to go.
tests/test_resolvers/test_split.py
Outdated
[ | ||
pytest.param("just a string", id="just a string"), | ||
pytest.param([123, "something"], id="first item is not string"), | ||
pytest.param(["something", 123], id="first item is not string"), |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
first/second
@@ -224,7 +226,7 @@ Assume a Sceptre `copy` hook that calls the `cp command`_: | |||
- !copy | |||
options: "-r" | |||
source: "from_dir" | |||
destination: "to_dir" | |||
destination: !stack_output my/other/stack::CopyDestination |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
i think the before_update and after_update examples are wrong originally. shouldn't they be like this instead?
before_update:
- !copy [ {"options":"-r", "source": "from_dir", "destination": "to_dir"} ]
after_update: !copy
- options: "-r"
source: "from_dir"
destination: !stack_output my/other/stack::CopyDestination
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
No. !copy
in this imaginary example takes a dict as an argument, not a list. However, after_update
and before_update
should be lists. So it's correct in the docs, not in your suggestion.
This pull request brings the ability for Hooks and Resolvers to have resolvers in their arguments and have those resolvers automatically resolve when
self.argument
is accessed. When a resolver or hook issetup
, it will also setup all nested resolvers in its argument. And when it is attached to a Stack, it and its argument will be recursively cloned so that no resolver is associated with a different stack. This will let resolvers be "inherited" from parent stack configurations.PR Checklist
[Resolve #issue-number]
.make test
) are passing.pre-commit run --all-files
).and description in grammatically correct, complete sentences.
Approver/Reviewer Checklist
Other Information
Guide to writing a good commit