Skip to content

Commit

Permalink
fixes saltstack#62508 add ifelse Jinja function as found in CFEngine
Browse files Browse the repository at this point in the history
  • Loading branch information
nicholasmhughes committed Aug 22, 2022
1 parent d5dec79 commit 1d8e1af
Show file tree
Hide file tree
Showing 6 changed files with 147 additions and 0 deletions.
1 change: 1 addition & 0 deletions changelog/62508.added
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
Add ifelse Jinja function as found in CFEngine
33 changes: 33 additions & 0 deletions doc/topics/jinja/index.rst
Original file line number Diff line number Diff line change
Expand Up @@ -2299,6 +2299,39 @@ will be rendered as:
unique = ['foo', 'bar']
Global Functions
================

Salt Project extends `builtin global functions`_ with these custom global functions:

.. jinja_ref:: ifelse

``ifelse``
----------

Evaluate each pair of arguments up to the last one as a (matcher, value)
tuple, returning ``value`` if matched. If none match, returns the last
argument.

The ``ifelse`` function is like a multi-level if-else statement. It was
inspired by CFEngine's ``ifelse`` function which in turn was inspired by
Oracle's ``DECODE`` function. It must have an odd number of arguments (from
1 to N). The last argument is the default value, like the ``else`` clause in
standard programming languages. Every pair of arguments before the last one
are evaluated as a pair. If the first one evaluates true then the second one
is returned, as if you had used the first one in a compound match
expression.

This is essentially another way to express the ``match.filter_by`` functionality
in way that's familiar to CFEngine or Oracle users. Consider using
``match.filter_by`` unless this function fits your workflow.

.. code-block:: jinja
{{ ifelse('foo*', 'fooval', 'bar*', 'barval', 'defaultval', minion_id='bar03') }}
.. _`builtin global functions`: https://jinja.palletsprojects.com/en/2.11.x/templates/#builtin-globals

Jinja in Files
==============

Expand Down
55 changes: 55 additions & 0 deletions salt/modules/match.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@
import salt.loader
from salt.defaults import DEFAULT_TARGET_DELIM
from salt.exceptions import SaltException
from salt.utils.decorators.jinja import jinja_global

__func_alias__ = {"list_": "list"}

Expand Down Expand Up @@ -396,3 +397,57 @@ def search_by(lookup, tgt_type="compound", minion_id=None):
matches.append(key)

return matches or None


@jinja_global("ifelse")
def ifelse(
*args,
tgt_type="compound",
minion_id=None,
merge=None,
merge_lists=False,
):
"""
.. versionadded:: 3006
Evaluate each pair of arguments up to the last one as a (matcher, value)
tuple, returning ``value`` if matched. If none match, returns the last
argument.
The ``ifelse`` function is like a multi-level if-else statement. It was
inspired by CFEngine's ``ifelse`` function which in turn was inspired by
Oracle's ``DECODE`` function. It must have an odd number of arguments (from
1 to N). The last argument is the default value, like the ``else`` clause in
standard programming languages. Every pair of arguments before the last one
are evaluated as a pair. If the first one evaluates true then the second one
is returned, as if you had used the first one in a compound match
expression.
This is essentially another way to express the ``filter_by`` functionality
in way that's familiar to CFEngine or Oracle users. Consider using
``filter_by`` unless this function fits your workflow.
CLI Example:
.. code-block:: bash
salt '*' match.ifelse 'foo*' 'Foo!' 'bar*' 'Bar!' minion_id=bar03
"""
if len(args) % 2 == 0:
raise SaltException("The ifelse function must have an odd number of arguments!")
elif len(args) == 1:
return args[0]

default_key = "SALT_IFELSE_FUNCTION_DEFAULT"

lookup = dict(zip(args[::2], args[1::2]))
lookup.update({default_key: args[-1]})

return filter_by(
lookup=lookup,
tgt_type=tgt_type,
minion_id=minion_id,
merge=merge,
merge_lists=merge_lists,
default=default_key,
)
1 change: 1 addition & 0 deletions salt/utils/templates.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@
import jinja2.ext
import jinja2.sandbox

import salt.modules.match
import salt.utils.data
import salt.utils.dateutils
import salt.utils.files
Expand Down
37 changes: 37 additions & 0 deletions tests/pytests/unit/modules/test_match.py
Original file line number Diff line number Diff line change
Expand Up @@ -253,3 +253,40 @@ def test_list_match_different_minion_id():

# passing minion_id, should return True
assert match.list_("bar02,bar04", "bar04")


def test_ifelse():
"""
Tests if ifelse returns the correct value.
"""
lookup = [
"foo*",
{"key1": "fooval1", "key2": "fooval2"},
"bar*",
{"key1": "barval1", "key2": "barval2"},
]
default = {"key1": "default1", "key2": "default2"}

# even args
with pytest.raises(SaltException):
match.ifelse("matcher", "value")
# only default provided
assert match.ifelse(default, minion_id="foo03") == {
"key1": "default1",
"key2": "default2",
}
# match foo
assert match.ifelse(*lookup, default, minion_id="foo03") == {
"key1": "fooval1",
"key2": "fooval2",
}
# match bar
assert match.ifelse(*lookup, default, minion_id="bar03") == {
"key1": "barval1",
"key2": "barval2",
}
# no match
assert match.ifelse(*lookup, default, minion_id="baz03") == {
"key1": "default1",
"key2": "default2",
}
20 changes: 20 additions & 0 deletions tests/pytests/unit/utils/jinja/test_custom_extensions.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@
import salt.loader

# dateutils is needed so that the strftime jinja filter is loaded
import salt.modules.match as match
import salt.utils.dateutils # pylint: disable=unused-import
import salt.utils.files
import salt.utils.json
Expand Down Expand Up @@ -56,6 +57,11 @@ def minion_opts(tmp_path):
return _opts


@pytest.fixture()
def configure_loader_modules(minion_opts):
return {match: {"__opts__": minion_opts}}


@pytest.fixture
def local_salt():
return {}
Expand Down Expand Up @@ -1234,3 +1240,17 @@ def test_random_shuffle(minion_opts, local_salt):
dict(opts=minion_opts, saltenv="test", salt=local_salt),
)
assert rendered == "['four', 'two', 'three', 'one']"


def test_ifelse(minion_opts, local_salt):
"""
Test the `ifelse` Jinja global function.
"""
rendered = render_jinja_tmpl(
"{{ ifelse('default') }}\n"
"{{ ifelse('foo*', 'fooval', 'bar*', 'barval', 'default', minion_id='foo03') }}\n"
"{{ ifelse('foo*', 'fooval', 'bar*', 'barval', 'default', minion_id='bar03') }}\n"
"{{ ifelse('foo*', 'fooval', 'bar*', 'barval', 'default', minion_id='baz03') }}",
dict(opts=minion_opts, saltenv="test", salt=local_salt),
)
assert rendered == ("default\n" "fooval\n" "barval\n" "default")

0 comments on commit 1d8e1af

Please sign in to comment.