SLS Parameter Validation#

Parameter validation is feature essentially enables documentation and validation of params used in an SLS file. It is not at all related to params processing, as params processing only occurs during state sub-command execution - where actual param values are available. During validate phase we have no idea what params the given SLS uses. In fact, to extract those out of SLS is our goal.

Goal#

The goal of SLS parameter validation is to extract/document parameters being used in an SLS file (including any files referred using include statement) for each state defined in the SLS file. Further, an additional goal is to do this transparently without exposing the end-user to any of the idem internals.

Limitation#

When jinja processes any document it does not have any context as to what is the state that is being currently processed, since jinja is indifferent to sls syntax and only focuses on the piece of code it needs to handle. Not only that, the params may be getting used outside of any state, for initializing a jinja variables, like so:

{% set value = params.get('value') %}

state A:
    state.a.present:
        group: {{ value }}

state B:
    state.b.present:
        group: {{ value }}

The above limitations are the overbearing force behind this implementation.

Overview of the Process Involved#

The process follows these steps.

Step 1 Transformation#

We let Jinja process the document, but instead of sending a traditional dict object as params object, we send an object of type Parameters class as defined in idem/tool/parameter.py.

This class transforms the original string (for example)

{{ params.get('rg_name').get('your_rg', 'default') }}

into another base64 encoded string, which preserves the context (for example)

?? params.get('rg_name').get('your_rg', 'default') ?? ^^rg_name^^.~~your_rg ??

Here the first portion of the ?? string has the original expression, and the second portion helps identify the state inside to which the param is referred.

The parameters are converted to string with markers such as

?? params.get('value') ?? ^^value^^ ??

Since the ^^value^^ may come as a complex type such as JSON, and break in string format, base64 encoding is done to all the parameter values to ensure all the types fit into this marker string.

Then, the base64 encoded marker strings are decoded back to original form during the second pass when remapping is done below in Step 4.

The preceding example transforms as:

state A:
state.a.present:
    group: ?? params.get('value') ?? ^^value^^ ??

state B:
state.b.present:
    group: ?? params.get('value') ?? ^^value^^ ??

The original parameter context, as well as the original params.get() string, are well preserved in the transformed YAML.

Step 2 Extraction of Parameters#

This step involves extracting the parameters out of transformed YAML using relevant regular expressions.

For details, see idem/idem/validate/0001_find_params.py

Step 3 Tallying with Meta Section in SLS#

This step is simply giving warnings if a parameter used in any given state doesn’t have a corresponding definition in the meta section of the SLS. This is just for aiding SLS writers so that they can add a meta section if it is missing.

See idem/idem/validate/0010_validate_meta.py

Step 4 Remapping Transformed Strings Original Values#

This is simply mapping and base64 decoded transformed strings (base64 encoded). For example

?? params.get('rg_name').get('your_rg', 'default') ?? ^^rg_name^^.~~your_rg ??

To original string (for example)

{{ params.get('rg_name').get('your_rg', 'default') }}

See idem/idem/validate/0020_reverse_map.py

Sample Output#

For reference, here is the output of the validate sub-command on the above SLS:

{
    "high": {
        "state A": {
            "state.a.present": {
                "group": "{{ params.get('value') }}"
            },
            "__sls__": "test"
        },
        "state B": {
            "state.b.present": {
                "group": "{{ params.get('value') }}"
            },
            "__sls__": "test"
        }
    },
    "low": [
        {
            "state": "state.a.present",
            "name": "state A",
            "__sls__": "test",
            "__id__": "state A",
            "fun": "group",
            "order": 1
        },
        {
            "state": "state.b.present",
            "name": "state B",
            "__sls__": "test",
            "__id__": "state B",
            "fun": "group",
            "order": 1
        }
    ],
    "meta": {
        "SLS": {},
        "ID_DECS": {}
    },
    "parameters": {
        "GLOBAL": {
            "value": ""
        },
        "ID_DECS": {
            "test.state A": {
                "value": ""
            },
            "test.state B": {
                "value": ""
            }
        }
    },
    "warnings": {
        "GLOBAL": [],
        "ID_DECS": {
            "test.state A": {
                "params_meta_missing": [
                    "value"
                ]
            },
            "test.state B": {
                "params_meta_missing": [
                    "value"
                ]
            }
        }
    }
}

As shown above, two new sections are added in validate sub-command output: parameters and warnings

Some Additional Samples#

Here are examples with SLS and corresponding validation output.

Iterating over list items

{% for ruleId in params.get('ruleIds') %}
{{ ruleId }}:
securestate.rules_status.present:
    - abc: def
{% endfor %}
{
    "parameters": {
        "GLOBAL": {
            "ruleIds": ""
        },
        "ID_DECS": {
            "resource_group.{{ params.get('ruleIds') }}": {
                "ruleIds": ""
            }
        }
    }
}

Iterating over dict items

{% set ruleIds = params.get("ruleDict") %}
{% for key, value in ruleIds.items() %}
{{ key }}:
securestate.rules_status.present:
    - x: value-{{ value }}
{% endfor %}
{
    "parameters": {
        "GLOBAL": {
            "ruleDict": ""
        },
        "ID_DECS": {
            "resource_group.key-{{ params.get('ruleDict') }}": {
                "ruleDict": ""
            }
        }
    }
}

General use case of calling a function

{% set ruleIds = params.get("ruleIdsString") %}
{% for ruleId in ruleIds.split(',') %}
{{ ruleId }}:
securestate.rules_status.present:
    - x: y
{% endfor %}
{
    "parameters": {
        "GLOBAL": {
            "ruleIds": ""
        },
        "ID_DECS": {
            "resource_group.{{ params.get('ruleIdsString') }}": {
                "ruleIdsString": ""
            }
        }
    }
}