Python Mercenaries
in
t
g
p
b
f

Runtime Rendering with Salt

An alternative to slots and more

written by Jason Traub on 2023-10-19

Disclaimer

the endraw tags are missing a curly brace intentionally to avoid browser rendering of example text.

Introduction 💡

{% raw -%} Last month, while attempting to use slots, I learned of a cool trick in salt that allows you to render Jinja at runtime as input to several functions. I refer to this (probably not well known) capability as runtime rendering.

To be clear, I'm not referring the default behavior of salt state rendering, which involves a Jinja render, then YAML render, prior to highstate execution. By "runtime rendering", I'm referring to a Jinja render that occurs within a running highstate execution. This offers flexibility that is not available via other standard salt methods.

Demonstration 🔬👨

To demonstrate this capability, consider the following examples.

Example 1

write_a_random_number_to_file_at_runtime:
  cmd.run:
    - name: shuf -i 0-999 -n1 > /tmp/random-number

{% raw -%}
use_runtime_random_number_to_create_a_directory:
  cmd.run:
    - name: mkdir -p /tmp/random-dirs/{{ salt.cmd.run("cat /tmp/random-number") }}
    - template: jinja
{% endraw -%

This state file uses a shell command to generate a random number and write it to a file, /tmp/random-number. The second block then utilizes runtime rendering to retrieve the contents of this file to create a subdirectory of the same value. By escaping the Jinja pre-render by encapsulating the block with raw tags, the Jinja contained herein is passed to the function as a string. Combining this with cmd.run's ability to render its name argument as Jinja (when specifying such with the template keyword argument) we achieve a runtime render, which is used to influence further state logic.

Example 2

Runtime rendering is also possible with file.managed. Notice how we can do much more sophisticated logic than is possible with slots.

ensure_{{ pillar['user'] }}_user_is_present:
  user.present:
    - name: {{ pillar['user'] }}
    - password: {{ pillar['password'] }}

update_file_contents_with_{{ pillar['user'] }}_user_uid:
  file.managed:
    - name: /tmp/uid_render
    - template: jinja
{%- raw %}
    - contents: |
        {%- set user = pillar['user'] %}
        {%- set uid = salt.cmd.run("id -u " + user) %}
        The {{ user }} user's uid value is {{ uid }}
        {{ uid }} + 2 = {{ uid|int + 2 }}
{% endraw -%

This second example uses raw tags again, in a similar fashion, but this time leverages file.managed's template keyword argument to runtime render the contents argument. Notice how there is Jinja content (e.g. update_file_contents_with_{{ pillar['user'] }}_user_uid ) that we want rendered prior to highstate execution, and therefore is not enclosed in raw tags. Within the raw tags, we want runtime rendering, and here we see a more complex use of Jinja logic. With runtime rendering, we still have the full power of Jinja plus salt (functions, pillar, grains, etc.) at our disposal.

This is powerful 💪, and can potentially solve a class of problems otherwise unsolvable in salt without resorting to sub-optimal solutions.

Potential Rebuttals 😠

"But wait!", you may say. "This isn't that groundbreaking, I can already solve this by other means. You're just not writing your state in the correct way!"

These specific examples can be refactored without the use of runtime rendering to achieve a similar effect, yes; but, that is not the point here. The fact is there exists a set of problems where a runtime render is the only way to achieve an idempotent state configuration with a single highstate.

In example 1, you could generate the random number in the Jinja pre-render and then simply use that as a variable in the follow up state blocks. But what if your state needed setup logic to happen first before you could retrieve that random number? For example, imagine you need to install a certificate before retrieving a pseudo-random API token. Without runtime rendering, you'd either need to put all the pre-setup logic in Jinja, run multiple states, write a custom module, or resort to limited and unintuitive solutions such as slots or thorium. These solutions, at best, are sub-optimal, and deviate from what we are really trying to achieve: a single idempotent highstate execution to configure a fleet of servers.

In example 2, likewise, you could choose the uid, defined as a Jinja variable, and use that throughout your state, without the need for runtime rendering. However, again, this is not really desired. We want the system to decide what user id is assigned, which cannot be known ahead of time, and then use that to influence further logic. This is an ability that really ought to exist natively in salt, but outside of this revelation of runtime rendering, really does not, unless resorting to one of the other sub-optimal solutions mentioned above.

Salt Enhancement Proposals 🧂

A couple of SEPs (salt enhancement proposals) have been proposed seeking to improve the native capabilities of salt in this regard. One such SEP, SEP16: Delayed rendering, authored by our very own Joe Nix of Terminal Labs, does a great job of outlining the problem at hand, and proposes a feature coined delayed rendering, which calls for allowing the user to optionally delay parts of the Jinja state render to be able to use data from a running state execution, thereby achieving a similar effect as the examples above. Another very interesting SEP is SEP25: Add runtime registers to salt which proposes a feature similar to that of ansible's registering [of] variables at runtime. Again, this proposal aims to achieve a similar effect as seen in the examples presented above.

Future Development 🚀

Going forward, I think the fact that runtime rendering exists at all is an important finding, but it ought to exist more generally and natively featured in salt. Perhaps the existing template functionality of cmd.run and file.managed can be extended to all function arguments? However, as powerful as this finding is, it still is somewhat inferior to both of the aforementioned SEPs since there is no data retention. Runtime rendering can solve this class of problems by first retrieving the result of the prior execution in the rendering, but that may not always be possible. Both of the above SEPs propose storing the result of previous execution block(s), and allowing for further rendering with it, thus are probably better and more general and flexible solutions.

In terms of implementation complexity, it seems relatively easy to add runtime register as proposed in SEP25 to salt, since salt already includes a mechanism for data retention between state executions with the __context__ dunder variable. Delayed rendering, as proposed in SEP16, could perhaps be even more powerful, but may require more sophisticated feature development.

Conclusion 📖

If any of these proposals pique your interest, I encourage you to get involved in the community. For now, we'll have to settle for runtime rendering 😃

{% endraw -%} Could you see yourself benefiting from runtime rendering? Do you have thoughts on the the above discussion? If so, please leave a comment below 👇 We'd love to hear from you!


« Previous | Runtime Rendering with Salt | Next »