Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add ifelse function as found in CFEngine #62509

Merged
merged 5 commits into from
Sep 21, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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
35 changes: 35 additions & 0 deletions doc/topics/jinja/index.rst
Original file line number Diff line number Diff line change
Expand Up @@ -2299,6 +2299,41 @@ 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. Boolean values can also be used as the first item in a pair, as it
will be translated to a match that will always match ("*") or never match
("SALT_IFELSE_MATCH_NOTHING") a target system.

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
64 changes: 64 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,66 @@ 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. Boolean values can also be used as the first item in a pair,
as it will be translated to a match that will always match ("*") or never
match ("SALT_IFELSE_MATCH_NOTHING") a target system.

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"

keys = list(args[::2])
for idx, key in enumerate(keys):
if key is True:
keys[idx] = "*"
elif key is False:
keys[idx] = "SALT_IFELSE_MATCH_NOTHING"

lookup = dict(zip(keys, 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
42 changes: 42 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,45 @@ 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",
}
# boolean matchers
assert (
match.ifelse(False, "nuh uhn", True, "this is true", "default value")
== "this is true"
)
21 changes: 21 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,18 @@ 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(False, 'fooval', True, 'barval', 'default', minion_id='foo03') }}\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" "barval\n" "default")