Krikri::TemplateActionPlugin
Inherits Krikri::ActionPlugin < Reference < Object
Constants
{% for (key, value) in dict.items() %} - the idiomatic real-
Jinja2 way to iterate a dict's key/value pairs (mysql_hardening's
own hardening.cnf.j2 writes it exactly this way). Only the parens
around the loop variables need stripping - the vendored crinja
fork's .items() is a real method on Hash values (see
lib/crinja/src/runtime/python_hash_methods.cr), so .items()
itself is left alone and evaluated for real rather than textually
stripped. Was NOT always true: an earlier FOR_ITEMS_METHOD regex
used to strip .items() out entirely and rely on Crinja's bare
{% for k, v in dict %} already yielding (key, value) pairs - a
real deviation from Python/Jinja2 (where a bare dict for-loop
iterates keys only) that happened to produce the right pairs, but
silently broke .items() | sort (jtyr.nsswitch's own nsswitch.
conf.j2: {% for key, val in nsswitch_config.items() | sort %})
by sorting the raw dict instead of its item tuples. Removed once
.items() support landed in the fork - verified live against
both jtyr.nsswitch (.items() | sort) and jtyr.motd (.items()
alone, item.motd.j2), byte-for-byte identical to real Ansible.
Rewrites Jinja2 inline conditional expressions {{ A if C else B }}
into the Crinja-parseable {{ C | ternary(A, B) }} form. This is
real Jinja2 (used by dev-sec os_hardening), which Crinja 0.9.0 cannot
parse. Only {{ }} expression blocks are touched; {% %} statement
blocks are left as-is.
Regex-based (not manual char indexing): matches a {{ ... }} block
and rewrites an inline A if C else B ternary within it. Each
match keeps the if / else as the top-level separator, so an
operand that is itself a parenthesized ternary ((B if C2 else D))
is handled naturally by the nested ( ... ) captures.
The else ... branch is optional - real Jinja2 permits {{ A if C }} on its own (renders as empty/Undefined when C is false;
konstruktoid-hardening's sshd_config.j2 does this throughout, e.g.
{{ 'Ciphers ' ~ sshd_ciphers | join(',') if sshd_ciphers }} to
omit the whole config line entirely when the list is empty).
#rewrite_ternary_expr treats a missing else branch as ''.
[^}\n] (not just [^}]) in every lazy segment below is load-
bearing, not cosmetic: a real inline ternary is always written on
one line, but [^}]*? alone also matches newlines, so on a large
template with sparse/mismatched {/} (mrlesmithjr.netdata's own
5934-line netdata.conf.j2 - only 26 {{ and 37 bare } total in
the whole file, zero of them an actual ternary) each of the 26
{{ candidates would lazily scan for the next literal }
ACROSS THE REST OF THE FILE, and the nested optional else group
multiplies that against every already-scanned position - PCRE2's
JIT match-time stack (a separate resource from its compile-time
stack, and unrelated to true catastrophic backtracking) overflows
("Regex match error: JIT stack limit reached"), crashing the
template: task outright on a file that never needed rewriting at
all. Excluding \n bounds every candidate scan to a single line,
matching how these templates are actually written and eliminating
the cross-file scan entirely.
Method-call .join( form that real Jinja2 permits but Crinja's
parser rejects: {{ "SEP".join(LIST) }} is the standard sep.join(list)
idiom (dev-sec os_hardening's securetty template uses it). Rewritten
into the equivalent LIST | join("SEP") filter, which Crinja supports.
$1 = the sep string literal, $2 = the list expression being joined.
[^"\\] excludes \n too (not just cosmetic - see INLINE_TERNARY's
own comment above for the general shape of this bug): a template
with an ODD number of " characters total has no valid second
quote to close a literal at all, so the unbounded (?:[^"\\]|\\.)*
tries every possible split of the REST OF THE FILE between its two
alternatives looking for one - mrlesmithjr.netdata's own 5934-line
netdata.conf.j2 has exactly one stray " in the whole file, and
this pattern alone (not INLINE_TERNARY, initially suspected first)
was the one that actually overflowed PCRE2's JIT match-time stack.
Real quoted string literals here are always single-line.
Method-call .split(...) - real Python's own str.split() method
(not a Jinja2 filter - Jinja2 exposes native object methods
directly), rejected by Crinja's parser the same way .items()
was. dev-sec apache_hardening's own httpd.conf.j2 uses it to pick
the minor version out of an already-parsed apache_version
string: {% if apache_version.split('.')[1] == '4' %}. Rewritten
to VAR | split(ARGS) (see jinja_filters.cr's own :split filter).
$1 = the dotted/bracketed variable expression being split
(apache_version, _apache_version.stdout, ...), $2 = the raw
argument text (possibly empty, for Python's own no-arg whitespace-
split form), $3 = an optional trailing literal numeric index
(apache_version.split('.')[1] - split()'s result is almost
always indexed immediately). Crinja can parse (EXPR).1 (dot-
numeric indexing on a parenthesized expression) but NOT (EXPR)[1]
(bracket indexing on one) - confirmed by direct testing, not
assumed - so a captured trailing [N] is rewritten to .N rather
than carried through as-is; #rewrite_inline_ternaries's own gsub
call is responsible for making that substitution (see below).
Group 1 alternates .ident/[...] suffixes in any order - real
bug found benchmarking githubixx.ansible_role_wireguard's own
wg.conf.j2: the old pattern only allowed dot-segments before any
bracket-index suffix (foo.bar[0]), so hostvars[host]. wireguard_address.split('/')[0] (a bracket, THEN more dots) never
matched starting from "hostvars" at all - the regex engine instead
found a match starting mid-expression, from "wireguard_address"
alone, silently leaving the "hostvars[host]." prefix untouched and
producing the syntactically invalid "hostvars[host].(wireguard_
address | split('/', 0))" (a stray .( Crinja's own parser
rejects outright: "Expected IDENTIFIER, got LEFT_PAREN").
A {% if EXPR %}/{% elif EXPR %} statement tag - $1/$4 are the
optional whitespace-control markers, either the trim - or the
keep + (both preserved as-is on rewrite - the + must survive
so the vendored Crinja fork's native + handling still sees it;
the + forms used to be pre-stripped out of the whole template,
see #render_template's comment for why that's gone), $2 the
keyword, $3 the condition. Used to find the same real-Jinja2
infix in/not in operator Crinja can't parse (see
#rewrite_in_expr) when it's used directly in a statement condition
rather than nested inside an inline ternary's own condition
(already handled separately, since that one lives inside a {{ }}
block). Deliberately does NOT match {% for %} - for x in list
is valid Crinja syntax on its own and must never be touched.
Instance methods
Execute action on controller Returns: modified params to send to remote plugin, or nil if action failed