Skip to content

Commit

Permalink
Add filter renderer returning partial datasets
Browse files Browse the repository at this point in the history
The new `filter` renderer only returns matching data from a loaded
template (e.g. YAML). This top-level dict is matched again a
customizable grain or pillar key, only the value of the first matching
key is returned.
  • Loading branch information
jgraichen committed Feb 9, 2021
1 parent a40db01 commit 1dbb693
Show file tree
Hide file tree
Showing 4 changed files with 187 additions and 1 deletion.
3 changes: 2 additions & 1 deletion .pylintrc
Original file line number Diff line number Diff line change
Expand Up @@ -14,4 +14,5 @@ good-names=f,k,v
additional-builtins=
__salt__,
__opts__,
__grains__
__grains__,
__pillar__
2 changes: 2 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,8 @@ The format is based on [Keep a Changelog](http://keepachangelog.com/en/1.0.0/)
and this project adheres to [Semantic Versioning](http://semver.org/spec/v2.0.0.html).

## [Unreleased]
### Added
- `filter` renderer returning only a matching subset from a dataset (e.g. YAML)

## [1.4.0] - 2021-01-29
### Added
Expand Down
116 changes: 116 additions & 0 deletions salt_tower/renderers/filter.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,116 @@
# -*- coding: utf-8 -*-
# pylint: disable=missing-docstring
"""
The ``filter`` renderers takes a dataset and only returns the first matching
value. It can match globs on grains and pillar values.
The input must be a parsed dictionary, for example from the YAML renderer. The
first key is uses as a pattern to match the value from grains or pillar. Shell
like globs are supported as the ``fnmatch`` function is used to check the
patterns.
The ``default`` option can provide a string used as the value if the grain or
pillar key does not exist.
Example: Default matching uses ``minion_id``:
.. code-block:: yaml
#!yaml | filter
minion-1:
some:
data: for minion-1
other-minion:
some:
data: for other minion
minions*:
some:
data: for mulitple minions matching key
Example: Matching using the ``os_family`` grain
.. code-block:: yaml
#!yaml | filter grain=os_family default='default value'
Debian:
package_name: docker.io
default value:
package_name: docker-ce
"""

import logging
import shlex

from fnmatch import fnmatch

from salt.exceptions import TemplateError

try:
from salt.utils.data import traverse_dict_and_list
except ImportError:
from salt.utils import traverse_dict_and_list


VALID_SELECTORS = ("grain", "pillar")
LOG = logging.getLogger(__name__)


def render(source, _saltenv, _sls, argline=None, **kwargs):
if not isinstance(source, dict):
raise TypeError(f"Source must be a dict, not {type(source)}")

selector = "grain"
default = None
key = "id"

if argline:
for arg in shlex.split(argline):
try:
(option, value) = arg.split("=", 2)
except ValueError:
option, value = arg, None

if option in VALID_SELECTORS:
if not value:
raise TemplateError(f"Selector {option!r} needs a value")
selector = option
key = value

elif option == "default":
if not value:
raise TemplateError(f"Option {option!r} needs a value")
default = value

else:
raise TemplateError(f"Unknown option {option!r}")

if selector == "grain":
value = traverse_dict_and_list(__grains__, key, default)

elif selector == "pillar":
context = kwargs.get("context", {})
if "pillar" in context:
value = traverse_dict_and_list(context["pillar"], key, default)
else:
value = traverse_dict_and_list(__pillar__, key, default)

if not value:
LOG.debug("Skipping blank filter value: %r", value)
return {}

# Matching only works on strings
value = str(value)

for pattern in source:
if fnmatch(value, pattern):
return source[pattern]

LOG.debug("No pattern matched value: %r", value)

return {}
67 changes: 67 additions & 0 deletions test/renderers/test_filter.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
# -*- coding: utf-8 -*-
# pylint: disable=missing-docstring
# pylint: disable=redefined-outer-name


def test_render(render):
template = """
#!yaml | filter grain=id
test_*:
key: 1
test_master:
key: 2
something else:
key: 3
"""

assert render(template) == {"key": 1}


def test_render_default_minion_id(render):
template = """
#!yaml | filter
test_master:
key: 1
something else:
key: 2
"""

assert render(template) == {"key": 1}


def test_render_grain(render):
template = """
#!yaml | filter grain=os_family
Debian:
key: 1
something else:
key: 2
"""

assert render(template) == {"key": 1}


def test_render_pillar(render):
template = """
#!yaml | filter pillar=some:key
value:
match: True
"""

context = {"pillar": {"some": {"key": "value"}}}

assert render(template, context=context) == {"match": True}


def test_render_default(render):
template = """
#!yaml | filter pillar=some:key default='matching value'
'*value':
match: True
"""

assert render(template) == {"match": True}

0 comments on commit 1dbb693

Please sign in to comment.