Templates
September 11, 2023 ยท View on GitHub
Templates are written in Go's text/template.
Flat key/value caveats and considerations
Because the templates only see a flat list of key/value pairs, certain operations will behave differently than the CLI (notably getv itself).
Take the following yaml for example:
credentials:
username: foo
password: bar
Would be seen by inside the templates as:
/credentials/username: foo
/credentials/password: bar
In a template, getv (and similar value-based functions) can only "see" full keys (e.g. /credentials/username).
Asking a template for a partial key (e.g. /credentials) will fail.
Additional functions, like ls and lsdir can provide access to inspecting and ranging on sub-keys.
Wildcards
Some commands allow for wildcard key matching using *.
A * does not match /.
Multiple * are allowed such as /foo/*/bar/*.
Template Functions
add
Adds two int values.
$ clconf getv / --output go-template --template '{{add 1 3}}'
4
asJson
Converts the supplied value to properly encoded JSON.
$ clconf --pipe getv /foo --output go-template --template '{{asJson (getksvs "/*" "int")}}' <<EOF
foo:
- hip
- hop
- bar
- baz
EOF
["hip","hop","bar","baz"]
It's worth noting in this example that we use getksvs "/*" "int" to extract the values sorted by the int value of the keys.
If we didn't do this the array would be in random order because the data backing getv templates is represented as a map-backed key/value store and map iteration in go is random.
asJsonString
Converts the supplied value to a properly encoded JSON string.
For any value that is not currently of type string, fmt.Sprintf("%v", value) will be used to convert prior to encoding.
$ clconf --pipe getv / --output go-template --template '{{printf "{\"safeforjson\": %s}" (asJsonString (getv "/notsafeforjson"))}}' <<EOF
notsafeforjson: |
{"']
EOF
{"safeforjson": "{\"']\n"}
atoi
Alias for the strconv.Atoi function.
$ clconf getv / --output go-template --template '{{seq 1 (atoi "10")}}'
[1 2 3 4 5 6 7 8 9 10]
base
Alias for the path.Base function.
$ clconf getv / --output go-template --template '{{base "/foo/bar.txt"}}'
bar.txt
base64Decode
Returns the string representing the decoded base64 value.
$ clconf getv / --output go-template --template '{{base64Decode "VmFsdWU="}}'
Value
base64Encode
Returns a base64 encoded string of the value.
$ clconf getv / --output go-template --template '{{base64Encode "Value"}}'
VmFsdWU=
cget
Equivalent to get but the value will be decrypted.
cgets
Equivalent to gets but the value will be decrypted.
cgetv
Equivalent to getv but the value will be decrypted.
cgetvs
Equivalent to getvs but the value will be decrypted.
contains
Alias for strings.Contains
$ clconf getv / --output go-template --template '{{if contains "a long time ago" "time"}}{{"the world makes sense"}}{{end}}'
the world makes sense
datetime
Alias for time.Now
clconf getv / --output go-template --template '{{datetime}}'
2023-03-24 10:25:20.282129609 -0600 MDT m=+0.000769001
$ clconf getv / --output go-template --template '{{datetime.Format "Jan 2, 2006 at 3:04pm (MST)"}}'
Mar 24, 2023 at 10:25am (MDT)
See the time package for more usage: http://golang.org/pkg/time/
dir
Equivalent to path.Dir
$ clconf getv / --output go-template --template '{{dir "."}}'
.
$ clconf getv / --output go-template --template '{{dir "/foo/bar/bip.txt"}}'
/foo/bar
$ clconf getv / --output go-template --template '{{dir "/foo"}}'
/
$ clconf getv / --output go-template --template '{{dir ""}}'
.
div
Divides two int values.
$ clconf getv / --output go-template --template '{{div 4 2}}'
2
$ clconf getv / --output go-template --template '{{div 3 2}}'
1
escapeOsgi
Places a single \ prior to any ', ", \, = or space.
$ clconf --pipe getv --output go-template --template '{{escapeOsgi "foo=bar"}}' < /dev/null
foo\=bar
exists
Checks if the key exists. Return false if key is not found.
$ clconf --pipe getv / --output go-template --template '{{if exists "/foo/bar"}}exists{{else}}nope{{end}}' <<EOF
foo:
bar: bip
EOF
exists
$ clconf --pipe getv / --output go-template --template '{{if exists "/foo"}}exists{{else}}nope{{end}}' <<EOF
foo:
bar: bip
EOF
nope
fileExists
Checks if the file or directory at the specified filesystem path exists.
{{if fileExists "/etc/myConfig"}}
useConfig: /etc/myConfig
{{end}}
fqdn
Adds a domain to a hostname if not already qualified.
$ clconf --pipe getv --output go-template --template '{{fqdn "foo" "example.com"}}' < /dev/null
foo.example.com
$ clconf --pipe getv --output go-template --template '{{fqdn "foo.google.com" "example.com"}}' < /dev/null
foo.google.com
get
Returns the KVPair where key matches its argument. Returns an error if key is not found. Wildcards not supported.
$ clconf --pipe getv / --output go-template --template '{{with get "/foo/bar"}}k: {{.Key}}, v: {{.Value}}{{end}}' <<EOF
foo:
bar: bip
EOF
$ clconf --pipe getv / --output go-template --template '{{with get "/foo/*"}}k: {{.Key}}, v: {{.Value}}{{end}}' <<EOF
foo:
bar: bip
EOF
Error: template execute: execute template: template: cli:1:7: executing "cli" at <get "/foo/*">: error calling get: /foo/*: key does not exist
$ clconf --pipe getv / --output go-template --template '{{with get "/foo"}}k: {{.Key}}, v: {{.Value}}{{end}}' <<EOF
foo:
bar: bip
EOF
Error: template execute: execute template: template: cli:1:7: executing "cli" at <get "/foo">: error calling get: /foo: key does not exist
getenv
Wrapper for os.Getenv. Retrieves the value of the environment variable named by the key. It returns the value, which will be empty if the variable is not present. Optionally, you can give a default value that will be returned if the key is not present.
MYENV=foo clconf getv / --output go-template --template '[{{getenv "MYENV"}}]'
[foo]
$ MYENV= clconf getv / --output go-template --template '[{{getenv "MYENV"}}]'
[]
# Default used when defined and empty
$ MYENV= clconf getv / --output go-template --template '[{{getenv "MYENV" "adefault"}}]'
[adefault]
$ clconf getv / --output go-template --template '[{{getenv "MYENV" "adefault"}}]'
[adefault]
# Does not fail if not defined
$ clconf getv / --output go-template --template '[{{getenv "MYENV"}}]'
[]
getksvs
Returns all values, []string, where key matches its argument, sorted by key.
Specify optional argument int to sort the keys as integers.
Returns an error if key is not found.
Wildcards are allowed.
Preserve the order of list inputs:
$ clconf --pipe getv --output go-template --template '{{getksvs "/foo/*" "int"}}' <<EOF
foo:
- dog
- bird
- cat
EOF
[dog bird cat]
Sort by string keys:
$ clconf --pipe getv --output go-template --template '{{getksvs "/foo/*"}}' <<EOF
foo:
dog: woof
bird: tweet
cat: meow
EOF
[tweet meow woof]
$ clconf --pipe getv --output go-template --template '{{getksvs "/*/*"}}' <<EOF
foo:
dog: woof
bird: tweet
cat: meow
bar:
fox: ????
EOF
[tweet meow woof ????]
$ clconf --pipe getv --output go-template --template '{{getksvs "/foo/dog"}}' <<EOF
foo:
dog: woof
bird: tweet
cat: meow
EOF
[woof]
Caveat's and wildcard rules apply:
$ clconf --pipe getv --output go-template --template '{{getksvs "/*"}}' <<EOF
foo:
dog: woof
bird: tweet
cat: meow
EOF
[]
$ clconf --pipe getv --output go-template --template '{{getksvs "/foo"}}' <<EOF
foo:
dog: woof
bird: tweet
cat: meow
EOF
[]
getsvs
Returns all values, []string, where key matches its argument, sorted.
Optionally specify int to sort the values as integers. Returns an error if key is not found.
Wildcards optional.
$ clconf --pipe getv --output go-template --template '{{getsvs "/foo/*"}}' <<EOF
foo:
- dog
- bird
- cat
EOF
[bird cat dog]
$ clconf --pipe getv --output go-template --template '{{getsvs "/foo/*"}}' <<EOF
foo:
dog: woof
bird: tweet
cat: meow
EOF
[meow tweet woof]
$ clconf --pipe getv --output go-template --template '{{getsvs "/foo/dog"}}' <<EOF
foo:
dog: woof
bird: tweet
cat: meow
EOF
[woof]
$ clconf --pipe getv --output go-template --template '{{getsvs "/*/*"}}' <<EOF
foo:
dog: woof
bird: tweet
cat: meow
bar:
fox: ????
EOF
[???? meow tweet woof]
Caveat's and wildcard rules apply:
$ clconf --pipe getv --output go-template --template '{{getsvs "/*"}}' <<EOF
foo:
dog: woof
bird: tweet
cat: meow
EOF
[]
$ clconf --pipe getv --output go-template --template '{{getsvs "/foo"}}' <<EOF
foo:
dog: woof
bird: tweet
cat: meow
EOF
[]
gets
Returns all KVPair, []KVPair, where key matches its argument. Returns an error if key is not found. Wildcards optional.
$ clconf --pipe getv / --output go-template --template '{{range gets "/foo/*"}}k: {{.Key}}, v: {{.Value}}{{"\n"}}{{end}}' <<EOF
foo:
bar: bip
zip: zap
EOF
k: /foo/bar, v: bip
k: /foo/zip, v: zap
$ clconf --pipe getv / --output go-template --template '{{range gets "/foo/bar"}}k: {{.Key}}, v: {{.Value}}{{"\n"}}{{end}}' <<EOF
foo:
bar: bip
zip: zap
EOF
k: /foo/bar, v: bip
$ clconf --pipe getv / --output go-template --template '{{range gets "/*/*"}}k: {{.Key}}, v: {{.Value}}{{"\n"}}{{end}}' <<EOF
foo:
bar: bip
zip: zap
EOF
k: /foo/bar, v: bip
k: /foo/zip, v: zap
Caveat's and wildcard rules apply:
$ clconf --pipe getv / --output go-template --template '{{range gets "/*"}}k: {{.Key}}, v: {{.Value}}{{"\n"}}{{end}}' <<EOF
foo:
bar: bip
zip: zap
EOF
$ clconf --pipe getv / --output go-template --template '{{range gets "/foo"}}k: {{.Key}}, v: {{.Value}}{{"\n"}}{{end}}' <<EOF
foo:
bar: bip
zip: zap
EOF
getv
Returns the value as a string where key matches its argument or an optional default value. Returns an error if key is not found and no default value given. Wildcards not supported.
$ clconf --pipe getv / --output go-template --template '{{getv "/foo/bar"}}' <<EOF
foo:
bar: bip
zip: zap
EOF
bip
# With default value
$ clconf --pipe getv / --output go-template --template '{{getv "/foo/hip" "hop"}}' <<EOF
foo:
bar: bip
zip: zap
EOF
hop
# Missing, no default
$ clconf --pipe getv / --output go-template --template '{{getv "/foo/hip"}}' <<EOF
foo:
bar: bip
zip: zap
EOF
Error: template execute: execute template: template: cli:1:2: executing "cli" at <getv "/foo/hip">: error calling getv: /foo/hip: key does not exist
$ clconf --pipe getv / --output go-template --template '{{getv "/foo"}}' <<EOF
foo:
bar: bip
zip: zap
EOF
Error: template execute: execute template: template: cli:1:2: executing "cli" at <getv "/foo">: error calling getv: /foo: key does not exist
getvs
Returns all values, []string, where key matches its argument, string-sorted. Wildcard required.
$ clconf --pipe getv / --output go-template --template '{{getvs "/foo/*"}}' <<EOF
foo:
buzz: 1
zap: 10
hop: 2
EOF
[1 10 2]
$ clconf --pipe getv / --output go-template --template '{{getvs "/bar/*"}}' <<EOF
foo:
buzz: 1
zap: 10
hop: 2
EOF
[]
clconf --pipe getv / --output go-template --template '{{getvs "/foo/buzz"}}' <<EOF
foo:
buzz: 1
zap: 10
hop: 2
EOF
[]
# Multiple wildcards supported
$ clconf --pipe getv / --output go-template --template '{{getvs "/*/*"}}' <<EOF
foo:
buzz: 1
zap: 10
hop: 2
bar:
hip: hop
eleven: 11
EOF
[1 10 11 2 hop]
join
Alias for the strings.Join function.
$ clconf --pipe getv / --output go-template --template '{{join (getvs "/things/*") ","}}' <<EOF
things:
- thing1
- thing2
EOF
thing1,thing2
json
Returns an map[string]interface{} of the json value.
$ clconf --pipe getv / --output go-template --template '{{(json (getv "/json_obj_string")).foo}}' <<EOF
json_obj_string: |
{"foo": "bar"}
EOF
bar
jsonArray
Returns a []interface{} from a json array such as ["a", "b", "c"].
$ clconf --pipe getv / --output go-template --template '{{range (jsonArray (getv "/json_array_string"))}}{{ . }}{{"\n"}}{{end}}' <<EOF
json_array_string: |
["foo", "bar"]
EOF
foo
bar
lookupIP
Wrapper for net.LookupIP function. The wrapper also sorts (alphabeticaly) the IP addresses. This is crucial since in dynamic environments DNS servers typically shuffle the addresses linked to domain name. And that would cause unnecessary config reloads.
$ clconf getv / --output go-template --template '{{lookupIP "localhost"}}'
[127.0.0.1]
lookupIPV4
Same as lookupIP but filters down to IPV4 adresses.
lookupIPV6
Same as lookupIP but filters down to IPV6 adresses.
lookupSRV
Wrapper for net.LookupSRV. The wrapper also sorts the SRV records alphabetically by combining all the fields of the net.SRV struct to reduce unnecessary config reloads.
{{range lookupSRV "mail" "tcp" "example.com"}}
target: {{.Target}}
port: {{.Port}}
priority: {{.Priority}}
weight: {{.Weight}}
{{end}}
ls
If the search string exactly matches a key, returns the equivalent of base.
If the search string exists and has subkeys, returns a list of the subkeys.
Returns an empty list if search string is not found.
Wildcards are not supported.
$ clconf --pipe getv / --output go-template --template '[{{join (ls "/foo") ","}}]' <<EOF
foo:
bar: bip
hip:
hop: hoop
zip: zap
EOF
[bar,hip,zip]
$ clconf --pipe getv / --output go-template --template '[{{join (ls "/foo/hip") ","}}]' <<EOF
foo:
bar: bip
hip:
hop: hoop
zip: zap
EOF
[hop]
$ clconf --pipe getv / --output go-template --template '[{{join (ls "/foo/hip/hop") ","}}]' <<EOF
foo:
bar: bip
hip:
hop: hoop
zip: zap
EOF
[hop]
$ clconf --pipe getv / --output go-template --template '[{{join (ls "/") ","}}]' <<EOF
foo:
bar: bip
hip:
hop: hoop
zip: zap
EOF
[foo]
$ clconf --pipe getv / --output go-template --template '[{{join (ls "/bar") ","}}]' <<EOF
foo:
bar: bip
hip:
hop: hoop
zip: zap
EOF
[]
lsdir
Returns all subkeys that are not full keys when appended to the search string.
# Note 'bar' is missing from the returned value because /foo/bar is a full key
$ clconf --pipe getv / --output go-template --template '[{{join (lsdir "/foo") ","}}]' <<EOF
foo:
bar: baz
hip:
- hop: hap
- hup: hep
zip:
zap: bop
EOF
[hip,zip]
$ clconf --pipe getv / --output go-template --template '[{{join (lsdir "/foo/hip") ","}}]' <<EOF
foo:
bar: baz
hip:
- hop: hap
- hup: hep
zip:
zap: bop
EOF
[0,1]
# No results here because /foo/zip/zap would be a full key
$ clconf --pipe getv / --output go-template --template '[{{join (lsdir "/foo/zip") ","}}]' <<EOF
foo:
bar: baz
hip:
- hop: hap
- hup: hep
zip:
zap: bop
EOF
[]
map
Creates a key-value map of string -> interface{}
{{$endpoint := map "name" "elasticsearch" "private_port" 9200 "public_port" 443}}
name: {{index $endpoint "name"}}
private-port: {{index $endpoint "private_port"}}
public-port: {{index $endpoint "public_port"}}
specifically useful if you use a sub-template and you want to pass multiple values to it.
mod
Modulus of two int values.
$ clconf getv / --output go-template --template '{{mod 10 3}}'
1
parseBool
An alias to strconv.ParseBool
$ clconf getv / --output go-template --template '{{parseBool "true"}}'
true
$ clconf getv / --output go-template --template '{{parseBool "T"}}'
true
$ clconf getv / --output go-template --template '{{parseBool "F"}}'
false
$ clconf getv / --output go-template --template '{{parseBool "1"}}'
true
$ clconf getv / --output go-template --template '{{parseBool "R"}}'
Error: template execute: execute template: template: cli:1:2: executing "cli" at <parseBool "R">: error calling parseBool: strconv.ParseBool: parsing "R": invalid syntax
$ clconf getv / --output go-template --template '{{parseBool "-1"}}'
Error: template execute: execute template: template: cli:1:2: executing "cli" at <parseBool "-1">: error calling parseBool: strconv.ParseBool: parsing "-1": invalid syntax
$ clconf getv / --output go-template --template '{{parseBool "0"}}'
false
regexReplace
Given a regex, an original string and a replacement string run regexp.ReplaceAllString and return the result. Returns an error if the regex fails to compile.
$ clconf --pipe getv --output go-template --template '{{regexReplace "o+" "foo" "e"}}' < /dev/null
fe
replace
Alias for the strings.Replace function.
$ clconf getv / --output go-template --template '{{replace "foo" "o" "e" -1}}'
fee
$ clconf getv / --output go-template --template '{{replace "foo" "o" "e" 1}}'
feo
reverse
Reverses a list. If the list is KVPair, will compare keys, not values.
seq
Alias for the template.Seq function.
$ clconf getv / --output go-template --template '{{range (seq 1 10) }}{{.}}{{"\n"}}{{end}}'
1
2
3
4
5
6
7
8
9
10
sort
Sorts the input ([]interface{}) by translating it to the specified type (one of int, string, default: string)
clconf --pipe getv --output go-template --template '{{sort (getvs "/foo/*") }}' <<EOF
foo:
- dog
- bird
- cat
EOF
[bird cat dog]
clconf --pipe getv --output go-template --template '{{ range $i := (sort (ls "/foo") "int")}}{{ getv (printf "/foo/%s" $i) }},{{ end }}' <<EOF
foo:
- dog
- bird
- cat
EOF
dog,bird,cat,
sortByLength
Sorts a list of string values by their length.
sortKvByLength
Sorts a list of KVPair values by the length of their key.
split
Alias for strings.Split. Splits the input string on the separating string and returns a slice of substrings.
{{ $url := split (getv "/deis/service") ":" }}
host: {{index $url 0}}
port: {{index $url 1}}
toLower
Alias for strings.ToLower. Returns lowercased string.
key: {{toLower "Value"}}
toUpper
Alias for strings.ToUpper. Returns uppercased string.
key: {{toUpper "value"}}
trimSuffix
Alias for strings.TrimSuffix.
Example Usage
Given the yaml input:
---
nginx:
domain: example.com
root: /var/www/example_dotcom
worker_processes: 2
app:
upstream:
app1: 10.0.1.100:80
app2: 10.0.1.101:80
And the template:
worker_processes {{getv "/nginx/worker_processes"}};
upstream app {
{{range getvs "/app/upstream/*"}}
server {{.}};
{{end}}
}
server {
listen 80;
server_name www.{{getv "/nginx/domain"}};
access_log /var/log/nginx/{{getv "/nginx/domain"}}.access.log;
error_log /var/log/nginx/{{getv "/nginx/domain"}}.log;
location / {
root {{getv "/nginx/root"}};
index index.html index.htm;
proxy_pass http://app;
proxy_redirect off;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
}
}
Output:
worker_processes 2;
upstream app {
server 10.0.1.100:80;
server 10.0.1.101:80;
}
server {
listen 80;
server_name www.example.com;
access_log /var/log/nginx/example.com.access.log;
error_log /var/log/nginx/example.com.error.log;
location / {
root /var/www/example_dotcom;
index index.html index.htm;
proxy_pass http://app;
proxy_redirect off;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
}
}
Complex example
This examples show how to use a combination of the templates functions to do nested iteration.
{{range $dir := lsdir "/services/web"}}
upstream {{base $dir}} {
{{$custdir := printf "/services/web/%s/*" $dir}}{{range gets $custdir}}
server {{$data := json .Value}}{{$data.IP}}:80;
{{end}}
}
server {
server_name {{base $dir}}.example.com;
location / {
proxy_pass {{base $dir}};
}
}
{{end}}
Output:
upstream cust1 {
server 10.0.0.1:80;
server 10.0.0.2:80;
}
server {
server_name cust1.example.com;
location / {
proxy_pass cust1;
}
}
upstream cust2 {
server 10.0.0.3:80;
server 10.0.0.4:80;
}
server {
server_name cust2.example.com;
location / {
proxy_pass cust2;
}
}
Go's text/template package is very powerful. For more details on it's capabilities see its documentation.