• Home
  • Posts

Recently we had a challenge to make a very complex deployment, defined with the SaltStack, more usable. The goal was to find a way to pass additional parameter to the state.apply command call to control if we are doing a full deploy or a fast deploy. Long story short, we ended up reading the source code of the Salt and yes, the solution was found.

What made things even more complicated for us: we need to catch the additional parameter value inside the pillar’s Jinja, and that pillar is not directly attached to the minion, but is being read with import command from another pillar.

Here is the list of all wrong approaches we tried and failed (and why) with the working solution in the end.

  1. Pass the additional pillar via cmd: state.apply app.deploy pillar='{fastdeploy: True}'. The first thing that comes to a mind. Works great if we have the ability to process conditions in the state’s Jinja code. But doesn’t work if you need to process conditions in the pillar’s Jinja code, as pillar passed via args are not available yet during processing the pillar inside sls.
  2. Use the localconfig parameter like state.apply app.deploy localconfig=config.yaml. Something that looks very close according to the docs. But it doesn’t, this parameter doesn’t really changes anything at all and reading the source code proved that.
  3. Use the pillarenv parameter. Just to complete the picture. Not really our case.
  4. Use Pillar Stack. Theoretically it is possible, this feature allows to read this kind of additional pillar in pillar. But that approach doesn’t directly gives the ability to pass additional parameter to the state.apply call.
  5. Get the argv. Luckily state.apply ignores additional parameter=value it doesn’t know. So you can call state.apply app.deploy fastdeploy=True and there will be no error. And you can catch this inside the pillar’s Jinja: either with {% set fastdeploy = "fastdeploy=True" in opts.get("argv", []) or "fastdeploy=true" in opts.get("argv", []) %}, or with {% set fastdeploy = "fastdeploy=True" in salt['config.get']('argv') or "fastdeploy=true" in salt['config.get']('argv') %}. That approach looked very promising but failed as well. Those argv values are available during pillar value evaluation, so you can, for example, pass the PHP version and use it in the pillar’s Jinja code. But argv value is not yet available when pillar page is being constructed with the usage import keyword.
  6. Use the previous approach in the first pillar attached via the pillar’s top sls for the minion. Set the flag somewhere (a tmp file maybe). Read the flag value in the next pillar attached to the minion via pillar’s top sls. Workable, but ugly.
  7. Finally, use minion_opts:grains: in the roster file to pass custom grains to the minion. As we are using salt-ssh to call the deploy, we have a roster file (/etc/salt/roster). Moreover we are using Docker and salt-ssh call is usually prefixed with aux helper command like ./ salt-ssh --wipe minion state.apply app.deploy. We have modified inside the Docker and our accounting code that generates the roster file. This allows us to call the deploy like ./ salt-ssh --wipe minion state.apply app.deploy grains="{fastdeploy: True}". And then you can get it inside pillar’s Jinja with {% if grains.get("fastdeploy", False) %}. And it works during the construction of the pillar code with import.

Also, as we are using yq to process grains= additional parameter, we can pass complex grains like grains='{a: b, b: {c: d, e: f}, fastdeploy: True}, the same way that passing additional pillars works (salt’s builtin feature).

Share this post