Skip to content
This repository was archived by the owner on Sep 26, 2025. It is now read-only.

Commit 42e1932

Browse files
committed
feat(map): update to v5 map.jinja
The v5 `map.jinja` is a generic and configurable system to load configuration values, exposed as the `mapdata` variable, from different places: - YAML files and templates from the fileserver for non-secret data - pillars or SDB are preferred for secret data - grains or `config.get` The `map.jinja` optional sources are configured with compound targeting like syntax `[<TYPE>[:<OPTION>[:<DELIMITER>]]@]<KEY>` with the following default ordered sources: - `Y:G@osarch`: YAML file and Jinja template named after `osarch` grain - `Y:G@os_family`: YAML file and Jinja template named after `os_family` grain - `Y:G@os` YAML file and Jinja template named after `os` grain - `Y:G@osfinger` YAML file and Jinja template named after `osfinger` grain - `C@{{ tplroot ~ ':lookup' }}`: dict retrieved with `salt["config.get"]` - `C@{{ tplroot }}`: dict retrieved with `salt["config.get"]` - `Y:G@id`: YAML file and Jinja template named after `osarch` grain This is done by two new libraries: - `libmatchers.jinja` provides the `parse_matchers` macro to parse strings looking like compound matchers, for example `Y:G@osarch` - `libmapstack.jinja` provides the `mapstack` macro to load configuration from different sources described by matchers Post-processing of `mapdata` variable can be done in a `parameters/post-map.jinja`. The v5 `map.jinja` is documented in `docs/map.jinja.rst`. BREAKING CHANGE: `map.jinja` now exports a generic `mapdata` variable BREAKING CHANGE: The per grain parameter values are now under `TEMPLATE/parameters/`
1 parent 41d222e commit 42e1932

39 files changed

+1492
-236
lines changed

TEMPLATE/_mapdata/init.sls

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3,10 +3,10 @@
33
---
44
{#- Get the `tplroot` from `tpldir` #}
55
{%- set tplroot = tpldir.split("/")[0] %}
6-
{%- from tplroot ~ "/map.jinja" import TEMPLATE with context %}
6+
{%- from tplroot ~ "/map.jinja" import mapdata with context %}
77
88
{%- set _mapdata = {
9-
"values": TEMPLATE,
9+
"values": mapdata,
1010
} %}
1111
{%- do salt["log.debug"]("### MAP.JINJA DUMP ###\n" ~ _mapdata | yaml(False)) %}
1212

TEMPLATE/config/clean.sls

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@
44
{#- Get the `tplroot` from `tpldir` #}
55
{%- set tplroot = tpldir.split('/')[0] %}
66
{%- set sls_service_clean = tplroot ~ '.service.clean' %}
7-
{%- from tplroot ~ "/map.jinja" import TEMPLATE with context %}
7+
{%- from tplroot ~ "/map.jinja" import mapdata as TEMPLATE with context %}
88
99
include:
1010
- {{ sls_service_clean }}

TEMPLATE/config/file.sls

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@
44
{#- Get the `tplroot` from `tpldir` #}
55
{%- set tplroot = tpldir.split('/')[0] %}
66
{%- set sls_package_install = tplroot ~ '.package.install' %}
7-
{%- from tplroot ~ "/map.jinja" import TEMPLATE with context %}
7+
{%- from tplroot ~ "/map.jinja" import mapdata as TEMPLATE with context %}
88
{%- from tplroot ~ "/libtofs.jinja" import files_switch with context %}
99
1010
include:

TEMPLATE/libmapstack.jinja

Lines changed: 310 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,310 @@
1+
{#- -*- coding: utf-8 -*- #}
2+
{#- vim: ft=jinja #}
3+
4+
{#- Get the `tplroot` from `tpldir` #}
5+
{%- set tplroot = tpldir.split("/")[0] %}
6+
{%- from tplroot ~ "/libmatchers.jinja" import parse_matchers, query_map %}
7+
8+
{%- set _default_config_dirs = [
9+
"parameters/",
10+
tplroot ~ "/parameters"
11+
] %}
12+
13+
{%- macro mapstack(
14+
matchers,
15+
defaults=None,
16+
dirs=_default_config_dirs,
17+
log_prefix="libmapstack: "
18+
) %}
19+
{#-
20+
Load configuration in the order of `matchers` and merge
21+
successively the values with `defaults`.
22+
23+
The `matchers` are processed using `libmatchers.jinja` to select
24+
the configuration sources from where the values are loaded.
25+
26+
Parameters:
27+
28+
- `matchers`: list of matchers in the form
29+
`[<TYPE>[:<OPTION>[:<DELIMITER>]]@]<QUERY>`
30+
31+
- `defaults`: dictionary of default values to start the merging,
32+
they are considered built-ins. It must conform to the same
33+
layout as the YAML files: a mandatory `values` key and two
34+
optional `strategy` and `merge_lists` keys.
35+
36+
- `dirs`: list of directory where to look-up the configuration
37+
file matching the matchers, by default a global `salt://parameters/`
38+
and a per formula `salt://<tplroot>/parameters`
39+
40+
- `log_prefix`: prefix used in the log outputs, by default it is
41+
`libmapstack: `
42+
43+
Example: On a Debian system with `roles=["nginx/server", "telegraf"]`
44+
45+
{%- set settings = mapstack(
46+
matchers=[
47+
"Y:G@os_family",
48+
"I@" ~ tplroot,
49+
"Y:C@roles",
50+
],
51+
dirs=["defaults", tplroot ~ "/parameters"],
52+
)
53+
| load_yaml %}
54+
55+
This will merge the values:
56+
57+
- starting with the default empty dictionary `{}` (no
58+
`defaults` parameter)
59+
60+
- from the YAML files
61+
62+
- `salt://defaults/os_family/Debian.yaml`
63+
64+
- `salt://{{ tplroot }}/parameters/os_family/Debian.yaml`
65+
66+
- from the pillar `salt["pillar.get"](tplroot)`
67+
68+
- from the `nginx/server` YAML files:
69+
70+
- `salt://defaults/roles/nginx/server.yaml`
71+
72+
- `salt://{{ tplroot }}/parameters/roles/nginx/server.yaml`
73+
74+
- from the `telegraf` YAML files:
75+
76+
- `salt://defaults/roles/telegraf.yaml`
77+
78+
- `salt://{{ tplroot }}/parameters/roles/telegraf.yaml`
79+
80+
Each YAML file and the `defaults` parameters must conform to the
81+
following layout:
82+
83+
- a mandatory `values` key to store the configuration values
84+
85+
- two optional keys to configure the use of `salt.slsutil.merge`
86+
87+
- an optional `strategy` key to configure the merging
88+
strategy, for example `strategy: 'recurse'`, the default is
89+
`smart`
90+
91+
- an optional `merge_lists` key to configure if lists should
92+
be merged or overridden for the `recurse` and `overwrite`
93+
strategies, for example `merge_lists: 'true'`
94+
#}
95+
{%- set stack = defaults | default({"values": {} }, boolean=True) %}
96+
97+
{#- Build configuration file names based on matchers #}
98+
{%- set config_get_strategy = salt["config.get"](tplroot ~ ":strategy", None) %}
99+
{%- set matchers = parse_matchers(
100+
matchers,
101+
config_get_strategy=config_get_strategy,
102+
log_prefix=log_prefix
103+
)
104+
| load_yaml %}
105+
106+
{%- do salt["log.debug"](
107+
log_prefix
108+
~ "built-in configuration:\n"
109+
~ {"values": defaults | traverse("values")}
110+
| yaml(False)
111+
) %}
112+
113+
{%- for param_dir in dirs %}
114+
{%- for matcher in matchers %}
115+
{#- `slsutil.merge` options from #}
116+
{#- 1. the `value` #}
117+
{#- 2. the `defaults` #}
118+
{#- 3. the built-in #}
119+
{%- set strategy = matcher.value
120+
| traverse(
121+
"strategy",
122+
defaults
123+
| traverse(
124+
"strategy",
125+
"smart"
126+
)
127+
) %}
128+
{%- set merge_lists = matcher.value
129+
| traverse(
130+
"merge_lists",
131+
defaults
132+
| traverse(
133+
"merge_lists",
134+
False
135+
)
136+
)
137+
| to_bool %}
138+
139+
{%- if matcher.type in query_map.keys() %}
140+
{#- No value is an empty list, must be a dict for `stack.update` #}
141+
{%- set normalized_value = matcher.value | default({}, boolean=True) %}
142+
143+
{#- Merge in `mapdata.<query>` instead of directly in `mapdata` #}
144+
{%- set is_sub_key = matcher.option | default(False) == "SUB" %}
145+
{%- if is_sub_key %}
146+
{#- Merge values with `mapdata.<key>`, `<key>` and `<key>:lookup` are merged together #}
147+
{%- set value = { matcher.query | regex_replace(":lookup$", ""): normalized_value } %}
148+
{%- else %}
149+
{%- set value = normalized_value %}
150+
{%- endif %}
151+
152+
{%- do salt["log.debug"](
153+
log_prefix
154+
~ "merge "
155+
~ "sub key " * is_sub_key
156+
~ "'"
157+
~ matcher.query
158+
~ "' retrieved with '"
159+
~ matcher.query_method
160+
~ "', merge: strategy='"
161+
~ strategy
162+
~ "', lists='"
163+
~ merge_lists
164+
~ "':\n"
165+
~ value
166+
| yaml(False)
167+
) %}
168+
169+
{%- do stack.update(
170+
{
171+
"values": salt["slsutil.merge"](
172+
stack["values"],
173+
value,
174+
strategy=strategy,
175+
merge_lists=merge_lists,
176+
)
177+
}
178+
) %}
179+
180+
{%- else %}
181+
{#- Load YAML file matching the grain/pillar/... #}
182+
{#- Fallback to use the source name as a direct filename #}
183+
184+
{%- if matcher.value | length == 0 %}
185+
{#- Mangle `matcher.value` to use it as literal path #}
186+
{%- set query_parts = matcher.query.split("/") %}
187+
{%- set yaml_dirname = query_parts[0:-1] | join("/") %}
188+
{%- set yaml_names = query_parts[-1] %}
189+
{%- else %}
190+
{%- set yaml_dirname = matcher.query %}
191+
{%- set yaml_names = matcher.value %}
192+
{%- endif %}
193+
194+
{#- Some configuration return list #}
195+
{%- if yaml_names is string %}
196+
{%- set yaml_names = [yaml_names] %}
197+
{%- endif %}
198+
199+
{#- Try to load a `.yaml.jinja` file for each `.yaml` file #}
200+
{%- set all_yaml_names = [] %}
201+
{%- for name in yaml_names %}
202+
{%- set extension = name.rpartition(".")[2] %}
203+
{%- if extension not in ["yaml", "jinja"] %}
204+
{%- do all_yaml_names.extend([name ~ ".yaml", name ~ ".yaml.jinja"]) %}
205+
{%- elif extension == "yaml" %}
206+
{%- do all_yaml_names.extend([name, name ~ ".jinja"]) %}
207+
{%- else %}
208+
{%- do all_yaml_names.append(name) %}
209+
{%- endif %}
210+
{%- endfor %}
211+
212+
{#- `yaml_dirname` can be an empty string with literal path like `myconf.yaml` #}
213+
{%- set yaml_dir = [
214+
param_dir,
215+
yaml_dirname
216+
]
217+
| select
218+
| join("/") %}
219+
220+
{%- for yaml_name in all_yaml_names %}
221+
{%- set yaml_filename = [
222+
yaml_dir.rstrip("/"),
223+
yaml_name
224+
]
225+
| select
226+
| join("/") %}
227+
228+
{%- do salt["log.debug"](
229+
log_prefix
230+
~ "load configuration values from "
231+
~ yaml_filename
232+
) %}
233+
{%- load_yaml as yaml_values %}
234+
{%- include yaml_filename ignore missing %}
235+
{%- endload %}
236+
237+
{%- if yaml_values %}
238+
{%- do salt["log.debug"](
239+
log_prefix
240+
~ "loaded configuration values from "
241+
~ yaml_filename
242+
~ ":\n"
243+
~ yaml_values
244+
| yaml(False)
245+
) %}
246+
247+
{#- `slsutil.merge` options from #}
248+
{#- 1. the `value` #}
249+
{#- 2. the `defaults` #}
250+
{#- 3. the built-in #}
251+
{%- set strategy = yaml_values
252+
| traverse(
253+
"strategy",
254+
defaults
255+
| traverse(
256+
"strategy",
257+
"smart"
258+
)
259+
) %}
260+
{%- set merge_lists = yaml_values
261+
| traverse(
262+
"merge_lists",
263+
defaults
264+
| traverse(
265+
"merge_lists",
266+
False
267+
)
268+
)
269+
| to_bool %}
270+
{%- do stack.update(
271+
{
272+
"values": salt["slsutil.merge"](
273+
stack["values"],
274+
yaml_values
275+
| traverse("values", {}),
276+
strategy=strategy,
277+
merge_lists=merge_lists,
278+
)
279+
}
280+
) %}
281+
{%- do salt["log.debug"](
282+
log_prefix
283+
~ "merged configuration values from "
284+
~ yaml_filename
285+
~ ", merge: strategy='"
286+
~ strategy
287+
~ "', merge_lists='"
288+
~ merge_lists
289+
~ "':\n"
290+
~ {"values": stack["values"]}
291+
| yaml(False)
292+
) %}
293+
{%- endif %}
294+
{%- endfor %}
295+
{%- endif %}
296+
{%- endfor %}
297+
{%- endfor %}
298+
299+
{%- do salt["log.debug"](
300+
log_prefix
301+
~ "final configuration values:\n"
302+
~ {"values": stack["values"]}
303+
| yaml(False)
304+
) %}
305+
306+
{#- Output stack as YAML, caller should use with something like #}
307+
{#- `{%- set config = mapstack(matchers=["foo"]) | load_yaml %}` #}
308+
{{ stack | yaml }}
309+
310+
{%- endmacro %}

0 commit comments

Comments
 (0)