-
Notifications
You must be signed in to change notification settings - Fork 41
SEP: Expose utility functions to states, but not to CLI or template context #70
base: master
Are you sure you want to change the base?
Changes from 6 commits
910e742
9a9f3eb
591d0d5
05cc38f
46e1653
372497d
89c7078
f0fc71b
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change | ||||||||||||||||||||||||||||
---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
@@ -0,0 +1,133 @@ | ||||||||||||||||||||||||||||||
- Feature Name: Expose utility functions to states, but not to the CLI or template context | ||||||||||||||||||||||||||||||
- Start Date: 2023-06-06 | ||||||||||||||||||||||||||||||
- SEP Status: Draft | ||||||||||||||||||||||||||||||
- SEP PR: https://github.com/saltstack/salt-enhancement-proposals/pull/70 | ||||||||||||||||||||||||||||||
- Salt Issue: https://github.com/saltstack/salt/pull/64287#pullrequestreview-1451646261 | ||||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||||
# Summary | ||||||||||||||||||||||||||||||
[summary]: #summary | ||||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||||
Provide a means for the loader to access utility functions, but keep them from | ||||||||||||||||||||||||||||||
being added to the CLI or template context. | ||||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||||
# Motivation | ||||||||||||||||||||||||||||||
[motivation]: #motivation | ||||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||||
It is a policy of the Salt core team not to add any new utility functions to | ||||||||||||||||||||||||||||||
the `__salt__` loader (thus exposing them to the Salt CLI and the template | ||||||||||||||||||||||||||||||
context). The core team's proposed alternative, however, is to do an OS check | ||||||||||||||||||||||||||||||
in the state module, then a late import of the salt execution module, and | ||||||||||||||||||||||||||||||
finally to invoke the utility function directly. While this option technically | ||||||||||||||||||||||||||||||
works, it is unequivocally a bad idea, with several complications: | ||||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||||
1. The state modules were never supposed to include platform-specific code. I | ||||||||||||||||||||||||||||||
was even [advised to | ||||||||||||||||||||||||||||||
remove](https://github.com/saltstack/salt/pull/3019#issuecomment-11680999) | ||||||||||||||||||||||||||||||
grains checks from a state module some 10 years ago. | ||||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||||
2. Doing an OS check in a state module duplicates work already done in the | ||||||||||||||||||||||||||||||
execution module's `__virtual__` function. If the conditions in that | ||||||||||||||||||||||||||||||
`__virtual__` function are changed at a later date, all ad-hoc duplications | ||||||||||||||||||||||||||||||
of this logic in state modules must be located and updated. It is only a | ||||||||||||||||||||||||||||||
matter of when, not if, this will cause bugs to arise. | ||||||||||||||||||||||||||||||
Comment on lines
+28
to
+32
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. There are other ways to maintain consistency on There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Then name them? There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. It was named |
||||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||||
3. For execution modules which are virtual (e.g. `service`, `pkg`, etc.), | ||||||||||||||||||||||||||||||
additional care needs to be taken to craft these ad-hoc duplications, in | ||||||||||||||||||||||||||||||
order to catch minions which use the | ||||||||||||||||||||||||||||||
[providers](https://docs.saltproject.io/en/latest/ref/configuration/minion.html#providers) | ||||||||||||||||||||||||||||||
config option to manually assign a module as the virtual provider. If this | ||||||||||||||||||||||||||||||
very obscure and specific case isn't manually accounted for in one's ad-hoc | ||||||||||||||||||||||||||||||
platform checks, | ||||||||||||||||||||||||||||||
[providers](https://docs.saltproject.io/en/latest/ref/configuration/minion.html#providers) | ||||||||||||||||||||||||||||||
functionality is broken. | ||||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||||
In the past, complications `2` and `3` above were unnecessary, as we would | ||||||||||||||||||||||||||||||
simply add a function to the platform-specific execution module, and then use | ||||||||||||||||||||||||||||||
loader membership (e.g. `if "pkg.somefunc" in __salt__`) to determine whether | ||||||||||||||||||||||||||||||
or not to run them. The `pkg` state module is littered with such loader checks | ||||||||||||||||||||||||||||||
to handle the various idiosyncrasies of different package managers. This method | ||||||||||||||||||||||||||||||
has the benefit of automatically supporting minions which use | ||||||||||||||||||||||||||||||
[providers](https://docs.saltproject.io/en/latest/ref/configuration/minion.html#providers), | ||||||||||||||||||||||||||||||
and keeps duplicated platform checks out of state modules, at the expense of | ||||||||||||||||||||||||||||||
exposing dubiously-useful functions to the CLI and template context. | ||||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||||
# Design | ||||||||||||||||||||||||||||||
[design]: #detailed-design | ||||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||||
## Option 1: Augmentation of LazyLoader | ||||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||||
An optional module-level dunder could be added to remote-execution modules, to | ||||||||||||||||||||||||||||||
suppress availability of utility functions. For example: | ||||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||||
```python | ||||||||||||||||||||||||||||||
__utility_funcs__ = ('foo', 'bar') | ||||||||||||||||||||||||||||||
``` | ||||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||||
Any function name found in `__utility_funcs__` would be ignored by the loader | ||||||||||||||||||||||||||||||
by default, but they could be conditionally loaded by passing an argument when | ||||||||||||||||||||||||||||||
instantiating the `LazyLoader` class. This would allow `salt.loader.states()` | ||||||||||||||||||||||||||||||
to instantiate its own copy of the `minion_mods` loader that contains the | ||||||||||||||||||||||||||||||
utility functions, while the CLI and template context get a copy with them | ||||||||||||||||||||||||||||||
absent, preventing them from being exposed to end users. | ||||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||||
## Option 2: Move the utility functions to a new loader type | ||||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||||
This solution is far less elegant and more intrusive, but I'm including it as | ||||||||||||||||||||||||||||||
an alternative. It was my first idea for solving this problem, but I believe | ||||||||||||||||||||||||||||||
that [Option 1](#option-1-augmentation-of-lazyloader) is the superior solution. | ||||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||||
Utility functions would be moved to a different module, which would be in a | ||||||||||||||||||||||||||||||
directory processed by a loader instance. That loader would be added to the | ||||||||||||||||||||||||||||||
dunders which get packed onto the state modules. Meanwhile, the execution | ||||||||||||||||||||||||||||||
modules can import these utility functions directly. | ||||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||||
The obvious first choice would be to use `__utils__` for this purpose, but I | ||||||||||||||||||||||||||||||
noticed there is already [a SEP to remove the `__utils__` | ||||||||||||||||||||||||||||||
loader](https://github.com/saltstack/salt-enhancement-proposals/pull/66). So, | ||||||||||||||||||||||||||||||
a different directory then? Assuming this directory is called `foo`, then you | ||||||||||||||||||||||||||||||
would for example have a `salt/modules/aptpkg.py` and a `salt/foo/aptpkg.py`. | ||||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||||
Splitting the code into separate modules for remote-execution and utility | ||||||||||||||||||||||||||||||
functions would appear at first to result in a duplicated `__virtual__` | ||||||||||||||||||||||||||||||
function, but objects beginning and ending in double-underscores are not | ||||||||||||||||||||||||||||||
name-mangled and thus can be imported. So, the `__virtual__` can be defined in | ||||||||||||||||||||||||||||||
the utility module and imported into the remote-execution module. | ||||||||||||||||||||||||||||||
Comment on lines
+97
to
+98
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
Precisely, this is the way to avoid duplication. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. But it doesn't avoid duplication when it comes to invoking imported code in the state module. As I've mentioned both in the original Salt issue and in this SEP, you end up needing to duplicate the logic from the There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. You import the util module that implements the virtual logic, and then, on each of the module needing that check, you call that function. We're not arguing about adding import statements are we? There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. And we're also not arguing about adding a function call in each of the virtual functions right? There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. No and no. What is at issue here is the duplication of OS checks in state modules. Yes, it is a 10-year-old opinion. Pointing out that it's an old opinion is not a refutation of the opinion, it's just a non sequitur. There was a reason behind it, and that reason is that the loader is the right solution for running code specific to a given platform. There are cases where simply importing a function from another module is a viable solution. Simpler state modules for which the execution module counterpart is not a virtual module are a great candidate for simply importing a function and running it. But Since the very early days of Salt, the way we have accounted for that is to use the loader. If a specific piece of functionality is only applicable to a single platform, Salt just checks if that function is in the loader. I gave several examples of this technique in this post, only to be told that the way Salt has been doing this since time immemorial is wrong, and should be considered "technical debt that needs to be fixed". To be clear, the method required to "fix" this code in the if "pkg.check_db" not in __salt__: with this: import salt.modules.ebuildpkg
if not (
(salt.modules.ebuildpkg.HAS_PORTAGE and __grains__["os"] == "Gentoo")
or __opts__.get('providers', {}).get('pkg') == 'ebuildpkg'
): That is the alternative to a loader membership check. That is what you are arguing in favor of. You have to reproduce everything that the loader is already doing for you. Loader membership checks are the right way to handle these cases. This SEP is an attempt to find a way to balance this truth with the desire of the project not to further expose unnecessary functions to the loader. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. To be clear, you will always be able to do:
This was never in question, unless You can still create a utility module with all the required checks to reduce the verboseness of:
What I'm arguing for is not adding yet another complexity layer which will have to be maintained by the core team. Your counter-argument is that "virtual" state modules do a lot of checks, and switching that to use imported utilities will make it more complex. This is my point of view. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
You'll still need to reproduce all the work the loader is already doing to detect OS and use of
It's not "extra work". It's duplicating code, and then hoping that you remember to update that duplicated logic in the future if/when the code in the execution module drifts. Adding an attribute lookup to the loader is a less complex and more robust solution. Arguing that there are a lot more non-virtual modules than virtual modules is another non-sequitur. They won't have the attribute, and will act the same way as they did before. The The ratio of modules that have used
Am I unqualified to help maintain Salt? I was a core team member before I was employed by SaltStack, and I didn't lose my ability or inclination to contribute when I stopped being employed by SaltStack. EDIT: Additionally, who if not the core team will have to keep in sync all the code duplication that will be added if this SEP is not implemented? There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
Not quite, there's no duplication on the utils module.
Not if you just call a utils function in the virtual functions. You update logic in one place, it works on all places using that function.
Erik, seriously? Me, as paid maintainer of Salt, have the obvious obligation to fix any issues that come in. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
The issue is is not that the And the example I showed was really just a best-case scenario. The functions in Unless, of course, you have a solution for removing these OS and minion opts checks from the state module?
This is just moving the goalposts. There will be an additional maintenance burden to maintain the additional OS and opts checks in the state modules and keep them in sync. Let's assume your best-case scenario, in which the OS check exists in one place, within a utils module, where it is invoked by the execution module's By any measure, augmenting the loader is the less-complex solution. It doesn't require code duplication in the state modules. It automatically responds to changes in the execution modules. And it lets us continue to use loader membership to decide whether or not to run platform-specific code.
Any SEP will result in some future maintenance burden. In this case, not doing the SEP will entail its own, even uglier maintenance burden. The excuse that the core team will have an additional maintenance burden can be used to suppress any SEP. If that's the case, then what are we even doing here? There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
A proper utils functions can do that check, and if it needs access to
Depends on the perspective. Might be ugly, but less magical, and thus, more predictable, testable, maintainable.
I can't reject a SEP alone, and I'm not talking on behalf of the whole core team. |
||||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||||
## Alternatives | ||||||||||||||||||||||||||||||
[alternatives]: #alternatives | ||||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||||
This SEP already contains two potential implementations. The alternative to | ||||||||||||||||||||||||||||||
implementing this SEP would be to duplicate the logic from the `__virtual__` at | ||||||||||||||||||||||||||||||
every location where utility functions need to be used, which is an open | ||||||||||||||||||||||||||||||
invitation for the two (or more) copies of that logic to drift from one | ||||||||||||||||||||||||||||||
another. | ||||||||||||||||||||||||||||||
Comment on lines
+101
to
+107
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
Suggested change
This is not an alternative. I've pointed out how it should be done in the previous comment. |
||||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||||
## Unresolved questions | ||||||||||||||||||||||||||||||
[unresolved]: #unresolved-questions | ||||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||||
- For [Option 1](#option-1-augmentation-of-lazyloader), Salt once had a test | ||||||||||||||||||||||||||||||
which parsed all docstrings in every execution module, ensuring that CLI | ||||||||||||||||||||||||||||||
examples were included. I'm not sure if this test still exists, but utility | ||||||||||||||||||||||||||||||
functions would not need CLI examples and this test case (if still present) | ||||||||||||||||||||||||||||||
would need to be updated to account for this fact. | ||||||||||||||||||||||||||||||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
Suggested change
There isn't a test case, but there is a pre-commit hook. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. No longer a concern with my proposed implementation from #70 (comment), since the utility functions now remain prefixed with underscores. |
||||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||||
- Also for [Option 1](#option-1-augmentation-of-lazyloader), we may need to add | ||||||||||||||||||||||||||||||
something to prevent Sphinx from including utility functions in the docs. | ||||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||||
- As noted above for [Option | ||||||||||||||||||||||||||||||
2](#option-2-move-the-utility-functions-to-a-new-loader-type), with the fate | ||||||||||||||||||||||||||||||
of `__utils__` in question, the actual name of the hypothetical loader type | ||||||||||||||||||||||||||||||
is unknown. | ||||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||||
# Drawbacks | ||||||||||||||||||||||||||||||
[drawbacks]: #drawbacks | ||||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||||
- For [Option 1](#option-1-augmentation-of-lazyloader), this means that | ||||||||||||||||||||||||||||||
`states` loader instances would no longer be re-using the existing | ||||||||||||||||||||||||||||||
`minion_mods` loader instance that is used by the CLI and template context, | ||||||||||||||||||||||||||||||
and would need to generate its own to use as its `__salt__` dunder. The | ||||||||||||||||||||||||||||||
performance impact of this should be minimal, however. | ||||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||||
- For [Option 2](#option-2-move-the-utility-functions-to-a-new-loader-type), | ||||||||||||||||||||||||||||||
adding a new loader type is a non-trivial undertaking, so this would be a | ||||||||||||||||||||||||||||||
much less-attractive solution. |
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.
Let me start by pointing out that 10 years is a long time ago, things have changed. The main idea,
never supposed to include platform-specific code
is still valid, when possible.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.
When not possible, there are still ways to achieve it without code duplication.
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.
Then name them?
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.
It was named
#70 (comment)