Following up on my first post, where we looked at a simple example of using a Jsonnet template to determine what kind of icon path to use for a Chart.yaml file, I’d like to take a look at a less simple, but possibly still contrived, example of utilizing Jsonnet to dynamically reconstruct YAML.

Consider the following my-application.yaml:

---
apiVersion: v1
kind: Namespace
metadata:
  labels:
    app.kubernetes.io/name: my-application
  name: my-application-namespace
---
apiVersion: v1
kind: ServiceAccount
metadata:
  labels:
    app.kubernetes.io/name: my-application
  name: my-application
  namespace: my-application-namespace

I’d like to be able to identify which release of my-application is being run by adding an additional label to each resource type:

  labels:
    app.kubernetes.io/name: my-application
    app.kubernetes.io/version: 1.0.11

Since the version number changes between releases, it is unrealistic for me to have to manually edit this file everytime it changes. So let’s take a look at how we can use a Jsonnet template to help us re-generate this YAML file and dynamically add the version label.

my-application.jsonnet

local resources = std.parseJson(std.extVar('resources'));
local version = std.extVar('version');

local addVersionToMetadataLabels(resource) = resource + {
	metadata+: { labels+:
	 super.labels + { "app.kubernetes.io/version": version }
	}
};
local resourcesWithVersion = std.map(addVersionToMetadataLabels, resources);

{
  ["namespace.v" + version + ".json"]:
    std.filter(function(res) res.kind == "Namespace", resourcesWithVersion)[0],
  ["serviceaccount.v" + version + ".json"]:
    std.filter(function(res) res.kind == "ServiceAccount", resourcesWithVersion)[0]
}

Let’s break it down.

local resources = std.parseJson(std.extVar('resources'));
local version = std.extVar('version');

This template accepts 2 external parameters resources and version. Here, resources is the contents of the my-application.yaml file, which has been converted to JSON format, and version is whatever version number you want to use.

local addVersionToMetadataLabels(resource) = resource + {
	metadata+: { labels+:
	 super.labels + { "app.kubernetes.io/version": version }
	}
};
local resourcesWithVersion = std.map(addVersionToMetadataLabels, resources);

Next, we have a function addVersionToMetadataLabels that takes a resource (in JSON format) and subsequently overrides metadata.labels to add an additional label with key app.kubernetes.io/version and the provided version value.

Then we use the Jsonnet standard library map function to apply that to all the JSON resources in resources.

Now that we have modified the original contents to include a new additional app.kubernetes.io/version label, we can now output the new contents and reconstruct our JSON.

{
  ["namespace.v" + version + ".json"]:
    std.filter(function(res) res.kind == "Namespace", resourcesWithVersion)[0],
  ["serviceaccount.v" + version + ".json"]:
    std.filter(function(res) res.kind == "ServiceAccount", resourcesWithVersion)[0]
}

This generates 2 separate JSON files, one for Namespace and the other for ServiceAccount, and then I have written some bash scripting to convert the JSON back to YAML and shove them back into a single YAML file. The reason I’ve done this is because Jsonnet outputs JSON in alphabetically order by key, and I needed to preserve the original order of the Kubernetes resources specified in my-application.yaml.

Hopefully these few examples illustrates some of the many ways Jsonnet can help manage YAML files and consequently your Kubernetes configuration in a slightly saner way.

I recently gave a talk on this subject at a Women Who Code Austin virtual meetup. I’ve published the code I’ve used and the slides on this repo. Feel free to copy and paste whatever makes sense.

If anyone else has other experiences with managing Kubernetes configuration using other tools and would like to share, please do in the comments section below. Would love to learn from you.