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

[master] Port json_query Jinja filter from Ansible #55749

Merged
merged 1 commit into from
Dec 29, 2019
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
45 changes: 44 additions & 1 deletion doc/topics/jinja/index.rst
Original file line number Diff line number Diff line change
Expand Up @@ -1343,9 +1343,52 @@ Returns:

'default'


.. jinja_ref:: json_query

``json_query``
--------------

.. versionadded:: Neon

A port of Ansible ``json_query`` Jinja filter to make queries against JSON data using `JMESPath language`_.
Could be used to filter ``pillar`` data, ``yaml`` maps, and together with :jinja_ref:`http_query`.
Depends on the `jmespath`_ Python module.

Examples:

.. code-block:: jinja

Example 1: {{ [1, 2, 3, 4, [5, 6]] | json_query('[]') }}

Example 2: {{
{"machines": [
{"name": "a", "state": "running"},
{"name": "b", "state": "stopped"},
{"name": "c", "state": "running"}
]} | json_query("machines[?state=='running'].name") }}

Example 3: {{
{"services": [
{"name": "http", "host": "1.2.3.4", "port": 80},
{"name": "smtp", "host": "1.2.3.5", "port": 25},
{"name": "ssh", "host": "1.2.3.6", "port": 22},
]} | json_query("services[].port") }}

Returns:

.. code-block:: text

Example 1: [1, 2, 3, 4, 5, 6]

Example 2: ['a', 'c']

Example 3: [80, 25, 22]

.. _`builtin filters`: http://jinja.pocoo.org/docs/templates/#builtin-filters
.. _`timelib`: https://github.com/pediapress/timelib/

.. _`JMESPath language`: http://jmespath.org/
.. _`jmespath`: https://github.com/jmespath/jmespath.py

.. jinja_ref:: to_snake_case

Expand Down
17 changes: 16 additions & 1 deletion doc/topics/releases/neon.rst
Original file line number Diff line number Diff line change
Expand Up @@ -82,8 +82,11 @@ as well as managing keystore files.
-----END CERTIFICATE-----


Jinja enhancements
==================

Troubleshooting Jinja map files
===============================
-------------------------------

A new :py:func:`execution module <salt.modules.jinja>` for ``map.jinja`` troubleshooting
has been added.
Expand All @@ -109,6 +112,18 @@ The module can be also used to test ``json`` and ``yaml`` maps:
salt myminion jinja.import_json myformula/defaults.json


json_query filter
-----------------

A port of Ansible :jinja_ref:`json_query` Jinja filter has been added. It allows
making queries against JSON data using `JMESPath language`_. Could be used to
filter ``pillar`` data, ``yaml`` maps, and also useful with :jinja_ref:`http_query`.

Depends on the `jmespath`_ Python module.

.. _`JMESPath language`: http://jmespath.org/
.. _`jmespath`: https://github.com/jmespath/jmespath.py

Slot Syntax Updates
===================

Expand Down
1 change: 1 addition & 0 deletions requirements/static/darwin.in
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ croniter>=0.3.0,!=0.3.22
dnspython
docker
futures>=2.0; python_version < '3.0'
jmespath
jsonschema
junos-eznc
jxmlease
Expand Down
1 change: 1 addition & 0 deletions requirements/static/linux.in
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ docker
futures>=2.0; python_version < '3.0'
GitPython
hgtools
jmespath
jsonschema
junos-eznc
jxmlease
Expand Down
1 change: 1 addition & 0 deletions requirements/static/windows.in
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ dnspython
# We require docker < 3.0.0 because after that they also start locking their pywin32 requirement, actually
# pypiwin32, which after version 223 it just makes pywin32 a dep and installs nothing else
docker<3.0.0
jmespath
jsonschema
keyring==5.7.1
kubernetes<4.0
Expand Down
37 changes: 37 additions & 0 deletions salt/utils/data.py
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,11 @@
from salt.ext import six
from salt.ext.six.moves import range # pylint: disable=redefined-builtin

try:
import jmespath
except ImportError:
jmespath = None

log = logging.getLogger(__name__)


Expand Down Expand Up @@ -979,6 +984,38 @@ def stringify(data):
return ret


@jinja_filter('json_query')
def json_query(data, expr):
'''
Query data using JMESPath language (http://jmespath.org).

Requires the https://github.com/jmespath/jmespath.py library.

:param data: A complex data structure to query
:param expr: A JMESPath expression (query)
:returns: The query result

.. code-block:: jinja

{"services": [
{"name": "http", "host": "1.2.3.4", "port": 80},
{"name": "smtp", "host": "1.2.3.5", "port": 25},
{"name": "ssh", "host": "1.2.3.6", "port": 22},
]} | json_query("services[].port") }}

will be rendered as:

.. code-block:: text

[80, 25, 22]
'''
if jmespath is None:
err = 'json_query requires jmespath module installed'
log.error(err)
raise RuntimeError(err)
return jmespath.search(expr, data)


def _is_not_considered_falsey(value, ignore_types=()):
'''
Helper function for filter_falsey to determine if something is not to be
Expand Down
21 changes: 21 additions & 0 deletions tests/unit/utils/test_data.py
Original file line number Diff line number Diff line change
Expand Up @@ -628,6 +628,27 @@ def test_stringify(self):
['one', 'two', 'three', '4', '5']
)

def test_json_query(self):
# Raises exception if jmespath module is not found
with patch('salt.utils.data.jmespath', None):
self.assertRaisesRegex(
RuntimeError, 'requires jmespath',
salt.utils.data.json_query, {}, '@'
)

# Test search
user_groups = {
'user1': {'groups': ['group1', 'group2', 'group3']},
'user2': {'groups': ['group1', 'group2']},
'user3': {'groups': ['group3']},
}
expression = '*.groups[0]'
primary_groups = ['group1', 'group1', 'group3']
self.assertEqual(
sorted(salt.utils.data.json_query(user_groups, expression)),
primary_groups
)


class FilterFalseyTestCase(TestCase):
'''
Expand Down
10 changes: 10 additions & 0 deletions tests/unit/utils/test_jinja.py
Original file line number Diff line number Diff line change
Expand Up @@ -1451,6 +1451,16 @@ def test_base64_decode(self):
dict(opts=self.local_opts, saltenv='test', salt=self.local_salt))
self.assertEqual(rendered, 'random')

def test_json_query(self):
'''
Test the `json_query` Jinja filter.
'''
rendered = render_jinja_tmpl(
"{{ [1, 2, 3] | json_query('[1]')}}",
dict(opts=self.local_opts, saltenv='test', salt=self.local_salt)
)
self.assertEqual(rendered, '2')

# def test_print(self):
# env = Environment(extensions=[SerializerExtension])
# source = '{% import_yaml "toto.foo" as docu %}'
Expand Down