Vendoring helm charts for jsonnet usage
This post goes over the detail of a Makefile target which converts helm charts
into a yaml file, so that it can be used as input for jsonnet.
Why ?
To manipulate the kubernetes manifests file using jsonnet instead of
helm's values.yml file.
Jsonnet gives us the ability to manipulate the data using its very powerful functional DSL.
How ?
PROMETHEUS_VERSION=14.4.0
.PHONY: yq
yq:
@(yq --version | grep "version 4" ) >/dev/null 2>&1 || (echo "yq version 4 is not installed."; exit 1)
HELM_VERSION = 3.6.2
HELM_C := docker run \
--rm \
--entrypoint "" \
alpine/helm:${HELM_VERSION} \
/bin/sh -c
upstream/prometheus.yaml: yq
${HELM_C} "( helm repo add prometheus-community https://prometheus-community.github.io/helm-charts; \
helm repo update; \
) 2>&1 >/dev/null; \
helm template \
--release-name prometheus \
--version ${PROMETHEUS_VERSION} \
prometheus-community/prometheus" > upstream/prometheus.yaml
What ?
Helm provides a docker image for the ease of running helm commands in a CI/CD Pipeline.
For us, using this image is a convenient way to capture a helm binary
without building an image of our own from scratch. However, there are
a few obstacles to overcome before being able to manipulate the helm
binary from this image into scripting context.
docker entrypoint
For those who don't know, docker images execute commands based on a
combination of two fields: ENTRYPOINT and CMD.
We will first need to override the configured ENTRYPOINT of the helm
image. Otherwise, running docker run alpine/helm helm repo add {...}
would translate to:
helm helm repo add {...}Not ideal. helm doesn't accept itself as an argument 🤣.
By specifying --entrypoint = "" and the docker's run command as
/bin/sh -c, we can achieve something more reasonable:
/bin/sh -c "helm repo add {...}"the script content
So the actions that we want to perform within the container are the following:
- add a helm repo, if required.
- update the added repo, if required.
- perform a
helm template - recover the output from the container
This obviously spans more than what a single command can offer. We'll definitely need a script for this, but how do we package it ?
A sure fire way to do this would be simply write and mount a script.sh
and run it like so:
docker run --rm -v ${PWD}/script.sh:/script.sh /script.shThough, personally I like to travel light, and self-contained is my motto. If I can conveniently package both the data and the executable together, I will.
So instead let's turn this into a my favorite form of scripts, elaborate oneliners. 😊
As a file, this is what script.sh would almost look like:
helm repo add prometheus-community https://prometheus-community.github.io/helm-charts
helm repo update
helm template --release-name prometheus prometheus-community/prometheusThe reason I say almost is because we need to recover data from the container. Again, we could simply mount a local directory and write the output to a file in there.
But, I prefer not to involve mounts. Instead I'm choosing to recover
the data through stdout. The issue with this is that certain commands
stream verbose messages to stdout. For example, running
helm repo update after adding the repo prometheus-community will
output:
Hang tight while we grab the latest from your chart repositories…
…Successfully got an update from the "prometheus-community" chart repository
Update Complete. ⎈Happy Helming!⎈
Thanks Helm, but not today.
Let's stuff these commands into a compound command and get rid of the output altogether.
(helm repo add prometheus-community https://prometheus-community.github.io/helm-charts
helm repo update
helm template --release-name prometheus prometheus-community/prometheus) >/dev/null 2>&1
Now that we've configured the environment in container, we can run a
helm template {...} and produce a pure yaml stream from the container.
Add some good 'ol newline escaping to beautify that massive oneliner,
and we've got ourselves the argument for /bin/sh -c.
Profit
With the vendored helm chart as a json file, we use jsonnet to customize the
helm chart without limitations.
For example:
local upstream = std.parseYaml(importstr 'upstream/prometheus.yaml');
{
manifests:
// add label {"foo": "bar"} to all objects!
std.map(function(m)
m {
metadata+: {
labels+: { foo: "bar" }
}
}, upstream)
}