sy substitute --help
sy-substitute
Substitutes templates using structured data. The idea is to build a tree of data that is used to substitute in various
templates, using multiple inputs and outputs.That way, secrets (like credentials) can be extracted from the vault just
once and used wherever needed without them touching disk.Liquid is used as template engine, and it's possible to refer
to and inherit from other templates by their file-stem. Read more on their website at https://shopify.github.io/liquid .
USAGE:
sy substitute [FLAGS] [OPTIONS] [--] [template-spec]...
FLAGS:
-h, --help
Prints help information
--no-stdin
If set, we will not try to read structured data from standard input. This may be required in some situations
where we are blockingly reading from a standard input which is attached to a pseudo-terminal.
-v, --validate
If set, the instantiated template will be parsed as YAML or JSON. If both of them are invalid, the command
will fail.
OPTIONS:
-d, --data=<data>
Structured data in YAML or JSON format to use when instantiating/substituting the template. If set,
everything from standard input is interpreted as template.
-e, --engine=<name>
The choice of engine used for the substitution. Valid values are 'handlebars' and 'liquid'. 'liquid', the
default, is coming with batteries included and very good at handling
one template at a time.
'handlebars' supports referencing other templates using partials, which is useful for
sharing of common functionality. [default: liquid] [possible values: handlebars, liquid]
--partial=<template>...
A file to be read as partial template, whose name will be the its file stem. It can then be included from
another template, and thus act as a function call.
--replace=<find-this:replace-with-that>...
A simple find & replace for values for the string data to be placed into the template. The word to find is
the first specified argument, the second one is the word to replace it with, e.g. -r=foo:bar.
-s, --separator=<separator>
The string to use to separate multiple documents that are written to the same stream. This can be useful to
output a multi-document YAML file from multiple input templates to stdout if the separator is '---'. The
separator is also used when writing multiple templates into the same file, like in 'a:out b:out'. [default:
]
ARGS:
<template-spec>...
Identifies the how to map template files to output. The syntax is '<src>:<dst>'. <src> and <dst> are a
relative or absolute paths to the source templates or destination files respectively. If <src> is
unspecified, the template will be read from stdin, e.g. ':output'. Only one spec can read from stdin. If
<dst> is unspecified, the substituted template will be output to stdout, e.g 'input.hbs:' or 'input.hbs'.
Multiple templates are separated by the '--separator' accordingly. This is particularly useful for YAML
files,where the separator should be `$'---\n'`
You can also use this alias: sub.
Control your output
template-specs
are the bread and butter of this substitution engine. They allow to not
only specify the input templates, like ./some-file.tpl
, but also set the output location.
By default, this is standard ouptut, but can easily be some-file.yml
, as in ./some-file.tpl:out/some-file.yml
.
You can have any amount of template specs, which allows them to use the same, possibly expensive, data-model.
Separating YAML Documents
At first sight, it might not be so useful to output multiple templates to standard output. Some formats are built just for that usecase, provided you separate the documents correctly.
If there are multiple YAML files for instance, you can separate them like this:
echo 'value: 42' \
| sy substitute --separator=$'---\n' <(echo 'first: {{value}}') <(echo 'second: {{value}}')
first: 42
---
second: 42
Also note the explicit newline in the separator, which might call for special syntax depending on which shell you use.
Validating YAML or JSON Documents
In the example above, how great would it be to protect ourselves from accidentially creating invalid YAML or JSON documents?
Fortunately, sheesy
has got you covered with the --validate
flag.
echo 'value: 42' \
| sy substitute --validate <(echo '{"first":"{{value}}}')
error: Validation of template output at 'stream' failed. It's neither valid YAML, nor JSON
Caused by:
1: while scanning a quoted scalar, found unexpected end of stream at line 1 column 10
Protecting against 'special' values
When generating structured data files, like YAML or JSON, even with a valid template you
are very vulnerable to the values contained in the data-model. Some passwords for instance
may contain characters which break your output. Even though --validate
can tell you right away,
how can you make this work without pre-processing your data?
--replace
to the rescure. The following example fails to validate as the password was
now changed to contain a special character in the JSON context:
echo 'password: xyz"abc' \
| sy substitute --validate <(echo '{"pw":"{{password}}"}')
error: Validation of template output at 'stream' failed. It's neither valid YAML, nor JSON
Caused by:
1: while parsing a flow mapping, did not find expected ',' or '}' at line 1 column 12
Here is how it looks like without validation:
echo 'password: xyz"abc' \
| sy substitute <(echo '{"pw":"{{password}}"}')
{"pw":"xyz"abc"}
You can fix it by replacing all violating characters with the respective escaped version:
echo 'password: xyz"abc' \
| sy substitute --replace='":\"' --validate <(echo '{"pw":"{{password}}"}')
{"pw":"xyz\"abc"}
How to use multi-file data in your templates
You have probably seen this coming from a mile away, but this is a great opportunity for a shameless plug to advertise sy merge
.
sy merge
allows to merge multiple files together to become one, and even some additional processing to it.
That way you can use the combined data as model during template substitution.
sy merge --at=team team.yml --at=project project.yml --at=env --environment \
| sy substitute kubernetes-manifest.yaml.tpl
apiVersion: v1
data:
game.properties: |
enemies=aliens
lives=3
enemies.cheat=true
ui.properties: |
color.good=purple
color.bad=yellow
kind: ConfigMap
metadata:
name: game-config
namespace: default
labels:
team: awesomenessies
department: finance
project: fantasti-project
kind: AI-research
Templates from STDIN ? Sure thing...
By default, we read the data model from stdin and expect all templates to be provided
by template-spec
. However, sometimes doing exactly the opposite might be what you need.
In this case, just use the -d
flag to feed the data model, which automatically turns
standard input into expecting the template.
echo '{{greeting | capitalize}} {{name}}' | sy substitute -d <(echo '{"greeting":"hello", "name":"Hans"}')
Hello Hans
Meet the engines
The substitution can be performed by various engines, each with their distinct advantages and disadvantages.
This section sums up their highlights.
Liquid (default)
The Liquid
template engine was originally created for web-shops and is both easy to use as well as fully-featured.
It’s main benefit is its various filters, which can be used to put something into uppercase ({{ “something” | uppercase }}
), or to encode text into base64 ({{ “text” | base64 }}
).
There are a few filters which have been added for convenience:
- base64
- Converts anything into its base64 representation.
- No arguments are supported.
Handlebars
The first optional template engine is handlebars
. Compared to Liquid
, it’s rather bare-bone and does not support any filters. The filtering syntax also makes chained filters more cumbersome.
However, it allows you to use partials, which are good to model something like multiple sites, which share a header and a footer. The shared portions are filled with data that contextually originates in the page that uses them.
For example, in an invocation like this you can declare headers and footers without rendering them, and then output multiple pages that use it.
Here is the content of the files used:
cat data.json
{
"title" : "Main Heading",
"parent" : "base0"
}
cat base0.hbs
<html>
<head>{{title}}</head>
<body>
<div><h1>Derived from base0.hbs</h1></div>
{{~> page}}
</body>
</html>
cat template.hbs
{{#*inline "page"}}
<p>Rendered in partial, parent is {{parent}}</p>
{{/inline}}
{{~> (parent)~}}
When using these in substitution, this is the output:
sy substitute --engine=handlebars -d data.json base0.hbs:/dev/null template.hbs
<html>
<head>Main Heading</head>
<body>
<div><h1>Derived from base0.hbs</h1></div>
<p>Rendered in partial, parent is base0</p>
</body>
</html>
The perceived disadvantage of having close to zero available filters would have to be compensated using a processing program which takes the data, and adds all the variations that you would need in your templates:
./data-processor < data.json | sy substitute template.tpl
The normal title: Main Heading
The capitalized title: main heading
The data-processor
in the example just adds transformed values for all fields it sees:
./data-processor < data.json
{
"title" : "Main Heading",
"title_lowercase" : "main heading",
}