August Feng

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.sh

Though, 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/prometheus

The 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)
}