Skip to content

Kubernetes library

The last section has shown that using a library for creating Kubernetes objects can drastically simplify the code you need to write. However, there is a huge amount of different kinds of objects and the Kubernetes API is evolving (and thus changing) quite rapidly.

Writing and maintaining such a library could be a full-time job on it’s own. Luckily, it is possible to generate such a library from the Kubernetes OpenAPI specification! Even better, it has already been done for you.

k8s-libsonnet

The library is called k8s-libsonnet (replacing the discontinued ksonnet-lib), currently available at https://github.com/jsonnet-libs/k8s-libsonnet.

As k8s-libsonnet has broken compatibility in a few places with ksonnet-lib (for good reason), we have instrumented the widely used ksonnet-util library with a compatibility layer to improve the developer and user experience: https://github.com/grafana/jsonnet-libs/tree/master/ksonnet-util

If you do not have any strong reasons against it, just adopt the wrapper as well, it will ease your work. Many of the original ksonnet-util enhancements have already made their way into k8s-libsonnet.

The docs for k8s-libsonnet library can be found here: https://jsonnet-libs.github.io/k8s-libsonnet/

Installation

Like every other external library, k8s-libsonnet can be installed using jsonnet-bundler. However, Tanka already did this for you during project creation (tk init):

Terminal window
tk init
jb install github.com/jsonnet-libs/k8s-libsonnet/1.21@main github.com/grafana/jsonnet-libs/ksonnet-util

This created the following structure in /vendor:

  • Directoryvendor
    • Directorygithub.com
      • Directorygrafana
        • Directoryjsonnet-libs
          • Directoryksonnet-util
            • kausal.libsonnet # Grafana’s wrapper
      • Directoryjsonnet-libs
        • Directoryk8s-libsonnet
          • Directory1.21
            • main.libsonnet # k8s-libsonnet entrypoint
    • Directory1.21/ -> github.com/jsonnet-libs/k8s-libsonnet/1.21
    • Directoryksonnet-util/ -> github.com/grafana/jsonnet-libs/ksonnet-util

Aliasing

Because of how jb works, the library can be imported as github.com/jsonnet-libs/k8s-libsonnet/1.21/main.libsonnet. Most external libraries (including our wrapper) expect it as a simple k.libsonnet (without the package prefix).

To support both, Tanka automatically created an alias file for you: /lib/k.libsonnet that just imports the actual library, exposing it under this alternative name as well.

Using it

First we need to import it in main.jsonnet:

local k = import "kubernetes.libsonnet";
local k = import "github.com/grafana/jsonnet-libs/ksonnet-util/kausal.libsonnet";
local grafana = import "grafana.libsonnet";
local prometheus = import "prometheus.libsonnet";
{ /* ... */ }

Now that we have installed the correct version, let’s use it in /environments/default/grafana.libsonnet instead of our own helper:

/environments/default/grafana.libsonnet
local k = import "github.com/grafana/jsonnet-libs/ksonnet-util/kausal.libsonnet";
{
// use locals to extract the parts we need
local deploy = k.apps.v1.deployment,
local container = k.core.v1.container,
local port = k.core.v1.containerPort,
local service = k.core.v1.service,
// defining the objects:
new(name, port):: {
// deployment constructor: name, replicas, containers
deployment: deploy.new(name, replicas=1, containers=[
// container constructor
container.new(, "grafana/grafana")
+ container.withPorts( // add ports to the container
[port.new("ui", 3000)] // port constructor
),
]),
// instead of using a service constructor, our wrapper provides
// a handy helper to automatically generate a service for a Deployment
service: k.util.serv.util.serviceFor(self.deployment)
+ service.mixin.spec.withType("NodePort"),
}
}

Full example

Now that creating the individual objects does not take more than 5 lines, we can merge it all back into a single file (main.jsonnet) and take a look at the whole picture:

local k = import "github.com/grafana/jsonnet-libs/ksonnet-util/kausal.libsonnet";
{
local deployment = k.apps.v1.deployment,
local container = k.core.v1.container,
local port = k.core.v1.containerPort,
local service = k.core.v1.service,
prometheus: {
deployment: deployment.new(
name="prometheus", replicas=1,
containers=[
container.new("prometheus", "prom/prometheus")
+ container.withPorts([port.new("api", 9090)]),
],
),
service: k.util.serviceFor(self.deployment),
},
grafana: {
deployment: deployment.new(
name="grafana", replicas=1,
containers=[
container.new("grafana", "grafana/grafana")
+ container.withPorts([port.new("ui", 3000)]),
],
),
service:
k.util.serviceFor(self.deployment)
+ service.mixin.spec.withType("NodePort"),
},
}

That’s a pretty big improvement, considering how verbose and error-prone it was before!

Bonus: Config object

While this is already a huge improvement, we can do a bit more. There is still some repetition in the main.jsonnet file. The most straightforward way to address this is by creating a hidden object that holds all actual values in a single place to be consumed by the actual resources.

Luckily, Jsonnet has the key:: "value" stanza for private fields. Such are only available during compiling and will be removed from the actual output.

Such an object could look like this:

{
_config:: {
grafana: {
port: 3000,
name: "grafana",
},
prometheus: {
port: 9090,
name: "prometheus"
}
}
}

We can then replace hardcoded values with a reference to this object:

local k = import "github.com/grafana/jsonnet-libs/ksonnet-util/kausal.libsonnet";
{ // <- This is $
_config:: {
grafana: {
port: 3000,
name: "grafana",
},
prometheus: {
port: 9090,
name: "prometheus"
}
}
local deployment = k.apps.v1.deployment,
local container = k.core.v1.container,
local port = k.core.v1.containerPort,
local service = k.core.v1.service,
prometheus: {
deployment: deployment.new(
name="prometheus", replicas=1,
// $ refers to the outermost object
name=$._config.prometheus.name, replicas=1,
containers=[
container.new("prometheus", "prom/prometheus")
+ container.withPorts([port.new("api", 9090)]),
container.new($._config.prometheus.name, "prom/prometheus")
+ container.withPorts([port.new("api", $._config.prometheus.port)]),
],
),
service: k.util.serviceFor(self.deployment),
},
grafana: {
deployment: deployment.new(
name="grafana", replicas=1,
name=$._config.grafana.name, replicas=1,
containers=[
container.new("grafana", "grafana/grafana")
+ container.withPorts([port.new("ui", 3000)]),
container.new($._config.grafana.name, "grafana/grafana")
+ container.withPorts([port.new("ui", $._config.grafana.port)]),
],
),
service:
k.util.serviceFor(self.deployment)
+ service.mixin.spec.withType("NodePort"),
},
}