BKL
bkl (short for Baklava because it has layers) is a templating configuration language without the templates. It's designed to be simple to read and write with obvious behavior.
Write your configuration in your favorite format: JSON, YAML, or TOML. Layer configurations on top of each other, even from different file formats. Use filenames to define the inheritance. Have as many layers as you like. bkl merges your layers together with sane default behavior that you can override. Export your results in any supported format for human or machine consumption. Use the CLI directly or in scripts or automate with the library.
Example
addr : 127.0.0.1
name : myService
port : 8080
port = 8081
$ bkl service.test.toml
addr = '127.0.0.1 '
name = 'myService '
port = 8081
bkl knows that
Install
# Install go from go.dev
$ go install github.com/gopatchy/bkl/...@latest
# Verify that ~/go/bin is in your $PATH
# Install brew from brew.sh
$ brew install gopatchy/bkl/bkl
You can also download binaries directly here.
Formats
Output defaults to the same format as the specified input file. Use
$ bkl -f yaml service.test.toml
addr : 127.0.0.1
name : myService
port : 8081
$ bkl -f toml service.test.toml
addr = '127.0.0.1 '
name = 'myService '
port = 8081
$ bkl -f json service.test.toml
{"addr ":"127.0.0.1 ","name ":"myService ","port ":8081 }
$ bkl -f json-pretty service.test.toml
{
"addr ": "127.0.0.1 ",
"name ": "myService ",
"port ": 8081
}
You can also set the output format implicitly by using a different file extension.
$ bkl service.test.yaml # real file is service.test.toml
addr : 127.0.0.1
name : myService
port : 8081
Write
Output goes to stdout by default. Errors always go to stderr.
$ bkl -o out.yaml service.test.toml
Output format is autodetected from output filename (unless specified with
Inputs
$ bkl a.b.yaml c.d.yaml # a.yaml + a.b.yaml + c.yaml + c.d.yaml
Specifying multiple input files merges all layers in order.
$ bkl -- -.yaml <<'EOF'
a : 1
EOF
Specifying an input file with the base name
Symlinks
$ ln -s a.b.yaml c.yaml
$ bkl c.d.yaml # a.yaml + a.b.yaml (c.yaml) + c.d.yaml
bkl follows symbolic links and evaluates the inherited layers on the other side of the symlink.
Inheritance
Inheritance is determined using filenames by default. After stripping the extension, the remaining filename is split on
$parent : a.b # inherits from a.b.<ext>, from a.<ext>
bkl will check for all supported endings of a manually-specified parent file and will still evaluate layers under the parent in the normal order.
$parent :
- a
- b
$parent : a.*
Setting
$ bkl a.b.yaml c.d.yaml # a.yaml + a.b.yaml + c.yaml + c.d.yaml
$ bkl -P a.b.yaml c.d.yaml # a.b.yaml + c.d.yaml
Layer order can be specified with commandline argument order. The
$parent : false # no further inheritance
Setting
Streams
Streams package multiple documents into a single file. YAML/TOML streams are delimited with
To layer streams, bkl has to match documents between layers. By default, child documents are applied to all parent documents.
a : 1
---
b : 2
c : 3
a : 1
c : 3
---
b : 2
c : 3
a : 1
---
b : 2
$match :
b : 2
c : 3
a : 1
---
b : 2
c : 3
The supplied pattern can match multiple documents and the updates will be applied to all of them.
a : 1
---
b : 2
---
a : 1
$match :
a : 1
c : 3
a : 1
c : 3
---
b : 2
---
a : 1
c : 3
a : 1
---
b : 2
$match :
a : 1
$invert : true
c : 3
a : 1
---
b : 2
c : 3
a : 1
$match : null
b : 2
a : 1
---
b : 2
Maps
Maps are merged by default. To change that, use
a : 1
b : 2
a : 1
b : 2
a : 1
b : 2
$replace : true
b : 2
a : 1
b : 2
c : 3
b : $delete
a : 1
c : 3
bkl returns an error if you use
Lists
Lists are merged by default. To change that, use
- 1
- 2
- 1
- 2
- 1
- 2
- $replace : true
- 2
- x : 1
- x : 2
- x : 3
- $delete :
x : 2
- x : 1
- x : 3
bkl returns an error if you use
To update a specific list item from a parent layer, use
- a : 1
- b : 2
- $match :
b : 2
b : 10
- a : 1
- b : 10
- 1
- 2
- $match : 2
$value : 10
- 1
- 10
$""
Use
a : 1
b :
c : foo
d : $"{b.c} bar {a} 2"
a : 1
b :
c : foo
d : foo bar 1 2
$decode
$value : |
{"a":1}
$decode : json
a : 1
$env
Use
# export FOO=test
"$env:FOO" : 1
test : 1
# export FOO=test
a : $env:FOO
a : test
Note that all
$encode
$value : a
$encode : base64
YQ==
a : 1
b : 2
$encode : flags
# [tolist:=, prefix:--]
- --a=1
- --b=2
- - a
- b
- - c
- 4
- e
- $encode : flatten
- a
- b
- c
- 4
- e
$value : [a , b ]
$encode : join:/
a/b
a : 1
b : 2
$encode : json
|
{"a":1,"b":2}
- a : 1
- b : 2
- $encode : json
|
[{"a":1},{"b":2}]
- a
- 2
- $encode : prefix:X
- Xa
- X2
a : 1
b : 2
$encode : tolist:=
- a=1
- b=2
a : 1
b : 2
$encode : [tolist:= , "join:, "]
a=1,b=2
$required
Use
a : 1
b : $required
c : 3
a : 1
b : $required
b : 2
c : 3
a : 1
b : 2
c : 3
a : 1
b :
- $required
c : 3
a : 1
b :
- $required
b :
- 2
c : 3
a : 1
b :
- 2
c : 3
$merge
Use
foo :
bar :
a : 1
zig :
b : 2
$merge : foo.bar
foo :
bar :
a : 1
zig :
a : 1
b : 2
foo :
bar :
- a : 1
zig :
- b : 2
- $merge : foo.bar
foo :
bar :
- a : 1
zig :
- b : 2
- a : 1
foo :
bar :
a : 1
zig :
b : 2
c : $merge:foo.bar.a
foo :
bar :
a : 1
zig :
b : 2
c : 1
You can also merge across documents using
a : 1
b : 2
---
c : 3
$merge :
$match :
a : 1
a : 1
b : 2
---
a : 1
b : 2
c : 3
a : 1
b :
c : 3
---
d : 4
$merge :
$match :
a : 1
$path : b
a : 1
b :
c : 3
---
c : 3
d : 4
a : 1
b : 2
---
c : 3
$merge : [{a: 1}]
a : 1
b : 2
---
a : 1
b : 2
c : 3
a : 1
b :
c : 3
---
d : 4
$merge : [{a: 1}, b]
a : 1
b :
c : 3
---
c : 3
d : 4
$replace
Use
foo :
bar :
a : 1
zig :
b : 2
$replace : foo.bar
foo :
bar :
a : 1
zig :
a : 1
foo :
bar :
- a : 1
zig :
- b : 2
- $replace : foo.bar
foo :
bar :
- a : 1
zig :
- a : 1
foo :
bar :
a : 1
zig :
b : 2
c : $replace:foo.bar.a
foo :
bar :
a : 1
zig :
b : 2
c : 1
Note that
You can also replace across documents using
a : 1
b : 2
---
c : 3
$replace :
$match :
a : 1
a : 1
b : 2
---
a : 1
b : 2
a : 1
b :
c : 3
---
d : 4
$replace :
$match :
a : 1
$path : b
a : 1
b :
c : 3
---
c : 3
a : 1
b : 2
---
c : 3
$replace : [{a: 1}]
a : 1
b : 2
---
a : 1
b : 2
a : 1
b :
c : 3
---
d : 4
$replace : [{a: 1}, b]
a : 1
b :
c : 3
---
c : 3
$output
Use
foo :
bar :
$output : true
a : 1
b : 2
a : 1
b : 1
foo :
bar :
- $output : true
- a : 1
- b : 2
- a : 1
- b : 1
Multiple instances of
Use
a :
b : 1
$output : false
c :
d : 2
c :
d : 2
a :
- b : 1
- $output : false
c :
- d : 2
c :
- d : 2
a : 1
$output : false
---
b : 2
b : 2
Combine
$repeat
a : foo
b : $repeat
$repeat : 2
a : foo
b : 0
---
a : foo
b : 1
$repeat :
a : 2
b : 3
$value : $"{$repeat:a },{$repeat:b }"
0,0
---
0,1
---
0,2
---
1,0
---
1,1
---
1,2
$$
a : $$env:foo
a : $env:foo
TOML
TOML doesn't allow unquoted
"$parent " = false
bklb
$ ln -s ~/go/bin/bklb ~/go/bin/catb # catb could be, e.g. kubectlb
$ cat service.yaml
addr : 127.0.0.1
name : myService
port : 8080
$ cat service.test.toml
port = 8081
$ catb service.test.toml
addr = "127.0.0.1 "
name = "myService "
port = 8081
$ cat service.test.yaml service.test.json
cat: service.test.yaml: No such file or directory
cat: service.test.json: No such file or directory
$ catb service.test.yaml
addr : 127.0.0.1
name : myService
port : 8081
$ catb service.test.json
{"addr ":"127.0.0.1 ","name ":"myService ","port ":8081 }
Note that the files mentioned don't have to exist;
bkld
$ bkld <base_layer_path> <target_output_path>
a : 1
b : $required
c : 3
a : 1
b : 2
d : 4
b : 2
c : null
d : 4
- a : 1
- b : 2
- a : 1
- c : 3
- c : 3
- $delete :
b : 2
- 1
- 2
- 1
- 3
- 1
- 3
- $replace : true
bkli
$ bkli <target_output_path> <target_output_path> ...
Any fields that exist in all upper layers but have different values will be marked
a : 1
b : 2
c : 3
a : 1
b : 10
d : 4
a : 1
b : $required
- a : 1
- b : 2
- c : 3
- a : 1
- b : 10
- d : 4
- a : 1
bklr
$ bklr <lower_layer_path>
a :
b : $required
c : 3
a :
b : $required
a :
- $required
b :
- 2
a :
- $required
kubectl bkl
$ kubectl bkl <kubectl_commands>
# deploy.dev.yaml and deploy.yaml are bkl layer files
$ kubectl bkl apply -f deploy.dev.yaml
deployment.apps/deploy-dev unchanged
Docker
bkl docker images are available at
FROM ghcr.io/gopatchy/bkl:latest AS bkl
FROM debian:latest AS build
COPY --from=bkl /bin /bin
diff
It's easy to see differences between processed bkl files using shell syntax.
$ diff -ud --color <(bkl a.yaml) <(bkl b.yaml)
--- /dev/fd/63 2023-07-12 21:00:55.412475845 -0700
+++ /dev/fd/62 2023-07-12 21:00:55.412475845 -0700
@@ -1,3 +1,2 @@
a: 1
-b: 2
c: 3
$ diff -y --color <(bkl a.yaml) <(bkl b.yaml)
a: 1 a: 1
b: 2 <
c: 3 c: 3
Migrate
Below is an example process for migrating an existing set of configurations to
We assume you start with two K8s deployments called
First, install tools:
It's possible to export configuration directly from existing templating systems (e.g.
# "kubectl neat" removes status and default value fields
$ kubectl neat get -- deploy deploy-dev > deploy-dev-orig.yaml
$ kubectl neat get -- deploy deploy-prod > deploy-prod-orig.yaml
# See bkli details here
$ bkli -o deploy.yaml deploy-dev-orig.yaml deploy-prod-orig.yaml
# See bkld details here
$ bkld -o deploy.dev.yaml deploy.yaml deploy-dev-orig.yaml
$ bkld -o deploy.prod.yaml deploy.yaml deploy-prod-orig.yaml
# Should show no diff
$ kubectl bkl diff -f deploy.dev.yaml
$ kubectl bkl diff -f deploy.prod.yaml
$ rm deploy-dev-orig.yaml deploy-prod-orig.yaml
You now have 3 bkl files:
deploy.yaml is the base layer containing values common to both deploymentsdeploy.dev.yaml is the upper layer containing values specific todeploy-dev deploy.prod.yaml is the upper layer containing values specific todeploy-prod
The migration process isn't deterministic; there are design and aesthetic considerations. Here are some general tips:
- Iterate by diffing your evaluated layers against production (e.g.
kubectl bkl diff -f ), making small changes, then diffing again. - Migrate as much as possible to the lowest layer to reduce duplication and complexity.
- Consider making your base layer match your production configuration then overriding values for dev/test configurations. This makes it very clear where you're drifting away from production.
- Remove duplication with
$merge: but avoid chained merge paths. - Avoid using hidden
$output: false trees as template variables; prefer overriding values in-place. bkld doesn't know how to match entries within lists, so it may remove and replace large entries (e.g. K8scontainers ) that could be trivially patched. Use$match: to select the container and override values within it.
Comparison
bkl has some overlap with other configuration templating tools (e.g. Helm, Kustomize, Hiera). We believe that bkl has a combination of project goals that aren't fully served by any of the alternatives:
- Configuration and templating/layering should be generic, not service-specific.
- Configuration and templating/layering should be separate from deployment tooling.
- Configuration files should be written in standard formats (JSON, YAML, TOML) and parseable by their standard parsers.
- Basic functionality should be available without learning custom syntax.
- Non-templatized configuration (e.g. StackOverflow answers) should be usable without modification.
- File composition should be accomplished without meta configuration (e.g. manifest files).
- Templating/layering behavior should be intuitive and produce expected results.
- Documentation and examples should be excellent.