diff --git a/rfcs/sls-alternative-syntax.md b/rfcs/sls-alternative-syntax.md new file mode 100644 index 00000000000..f0183787c7e --- /dev/null +++ b/rfcs/sls-alternative-syntax.md @@ -0,0 +1,185 @@ +- Feature Name: Batch mode SLS syntax +- Start Date: November 17, 2018. + +# Summary +[summary]: #summary + +Enhance SLS syntax with simpler way of writing "things". + +# Motivation +[motivation]: #motivation + +Writing SLS can be not always easy and friendly. Especially when you +want to do various operations on the same object. Main complain is +heard that in order to do something _again_ or run the same function +e.g. on the same file, one would need to create another ID for it. + +The following example will get content of `/etc/-release`, +based on `os_family` grain, if it is e.g. `RedHat`: + +```yaml +rhelrelease: + cmd.run: + - name: cat /etc/redhat-release + - onlyif: test -f /etc/redhat-release + +centosrelease: + cmd.run: + - name: cat /etc/centos-release + - onlyif: test -f /etc/centos-release +``` + +This will return a structure with an empty `rhelrelease` and content +of the release file under `centosrelease`. From API point of view, one +would need to check both keys in the structure. + +Of course, it is possible to write it a bit better then that, +something like: + +```jinja +{% if grains['os_family'] == 'RedHat' %} +{% set releasefile = 'centos' if grains['os'] == 'CentOS' else 'redhat' %} + +release: + cmd.run: + - name: cat /etc/{{ releasefile }}-release + - onlyif: test -f /etc/{{ releasefile }}-release +{%endif%} +``` + +It is still gets more complicated if older opensuse or SLES machines +also needs to be looked up. And yet this SLS is not finished as even +more careful logic needed to be added in order to handle cases where +no OS is detected. At this point SLS is no longer simple and +declarative, but looks more like programming. + +We can make it simpler. For example as follows: + + +```jinja +release: + cmd: + {% for releasefile in ['redhat', 'centos', 'SuSE', 'sles'] %} + - run: + - name: cat /etc/{{releasefile}}-release + - onlyif: test -f /etc/{{releasefile}}-release + {% endfor %} +``` + +In the example above, the for-loop will just generate four times same +statement to call `cmd.run` function, reusing already built-in check +on `onlyif`. That said, no more need to figure out what OS name +belongs to what family etc. + +The example above only made for show-case to let you get the idea. + +# Design +[design]: #detailed-design + +## How batch-mode syntax is used? + +As simple as _not_ specifying particular function. Typically, in Salt +it is done this way: + +```yaml +/etc/some.conf: + somemodule.some_function: # <--- here! + - params... +``` + +The simple syntax will be invoked as so: + +```yaml +/etc/some.conf: + somemodule: # <--- just like that, no function + - some_function: + - params ... +``` + +Of course, in single call this dosn't make much sense and is actually +one line more to write. However, if there is more one operation that +needed to be done, this makes a big difference. Since it is impossible +to have more than one same ID, you will be required to write different +IDs for the _same group_ of tasks, if we grouping them by the targeted +object. This results into something like this: + + +```yaml +do_something_with_my_config: + somemodule.some_function: + - name: /etc/some.conf + - params .... + +do_something_with_my_config_again: + somemodule.some_other_function: + - name: /etc/some.conf + - params .... + +do_something_with_my_config_and_over_again: + somemodule.some_third_function: + - name: /etc/some.conf + - params .... +``` + +With the batch-mode, the same above can be re-written in much more +efficient way as follows: + +```yaml +/etc/some.conf: + somemodule: + - some_function + - params .... + - some_other_function + - params .... + - some_third_function + - params .... +``` + +## How does it work? + +It works in three steps: + +1. During state compile, compiler finds that there is no end function + defined, then it injects `__call__` function name into the + state. Essentially, referring to the example above, state will be + simply altered to `somemodule.__call__`, but transparently for the + user. LazyLoader does not allow any functions that starts with `_` + as they are considered private/hidden. The `__call__` is an + exception but it should be still impossible to invoke it from the + module directly. + +2. When LazyLoader loads a module, it will inject a generic function + named `__call__` at the module level. If such function is already + defined inside the module, LazyLoader will not inject that function + therefore. + +3. The `__call__` function takes a list of sets structure, as shown above, + assuming that the key of the set is the function name of the parent + element `somemodule`, which is a module name, and essentially + performs the following: + +```python +ret = [] +for function_name in functions: + args, kwargs = functions[function_name]['args'], functions[function_name]['kwargs'] + ret.append(getattr(this_module_instance, function_name)(*args, **kwargs)) +``` + +## What needs to be changed in existing modules? + +Nothing. Also no changes to any future modules, yet to be written. If +module has non-traditional way of exposing methods, e.g. implemented +as class or functions made dynamically, in this case `__call__` should +be made separately for that. However, since this is advanced way of +making modules, developer already knows what he is doing in this case. + +Or "lazy" implementation, if one wants to just turn this feature off: + +```python + +__call__ = lambda (*a, **k): raise SaltNotSupportedError() +``` + +## Impact on existing ecosystem + +No impact. Performance and default syntax should stay unchanged.