JaaS

A basic deployment of Grafana with a dashboard using Jsonnet-as-a-Service (JaaS).

---
apiVersion: grafana.integreatly.org/v1beta1
kind: Grafana
metadata:
  name: grafana
  labels:
    dashboards: "grafana"
spec:
  client:
    preferIngress: true
  config:
    log:
      mode: "console"
    auth:
      disable_login_form: "false"
    security:
      admin_user: root
      admin_password: secret
---
apiVersion: grafana.integreatly.org/v1beta1
kind: GrafanaDashboard
metadata:
  name: grafanadashboard-jaas
spec:
  instanceSelector:
    matchLabels:
      dashboards: "grafana"
  url: "http://jaas.jaas.svc.cluster.local:8080/jsonnet/your-dashboard"

JaaS expects that your dashboards are packaged as OCI objects and mounts them using the Kubernetes OCI volume feature. In general, you need to perform the following steps to expose your Grafana dashboards with JaaS:

  1. Write your dashboards
  2. Publish OCI objects
  3. Modify your JaaS instance
  4. Define your GrafanaDashboard resource

Write your dashboards

The JaaS Helm chart includes Grafonnet and other required libraries as OCI volumes to write Grafana dashboards using Jsonnet.

When using Jsonnet to define your dashboards, you will most likely want to use Grafonnet which is exposed like this in JaaS:

// using 'latest'
local grafonnet = import "github.com/grafana/grafonnet/gen/grafonnet-latest/main.libsonnet";

// using a specific version
local grafonnet = import "github.com/grafana/grafonnet/gen/grafonnet-v11.4.0/main.libsonnet";

In general, it is highly recommend to use the latest version and control the actual version of Grafonnet you want to use by modifying your JaaS deployment. If you follow this recommendation, you only have to change the version of Grafonnet in one single place instead of touching all of your dashboards every time Grafonnet releases a new version. Likewise, you can add additional libraries you have written yourself to JaaS and import them just like the built-in Grafonnet library.

An entire dashboard in Jsonnet might look like this:

local grafonnet = import "github.com/grafana/grafonnet/gen/grafonnet-latest/main.libsonnet";
local dashboard = grafonnet.dashboard;
local datasource = dashboard.variable.datasource;
local timeSeries = grafonnet.panel.timeSeries;
local prometheus = grafonnet.query.prometheus;

local prometheusDatasource = datasource.new('my-datasource', 'prometheus')
    + datasource.generalOptions.withLabel('Prometheus')
    + datasource.generalOptions.withDescription('Some prometheus instance you want to use')
    + datasource.generalOptions.showOnDashboard.withNothing();

dashboard.new("Your Dashboard")
    + dashboard.withDescription("My fancy Jsonnet dashboard")
    + dashboard.withEditable(false)
    + dashboard.withTags(["tag1", "tag2", "tag3"])
    + dashboard.withVariables([prometheusDatasource])
    + dashboard.withPanels([
        timeSeries.new("CPU Requested")
        + timeSeries.panelOptions.withDescription("Container CPU resource request")
        + timeSeries.queryOptions.withTargets([
            prometheus.new('$%s' % prometheusDatasource.name, |||
              kube_pod_container_resource_requests{resource="cpu",exported_namespace="some-namespace",exported_pod="some-pod"}
            |||)
            + prometheus.withLegendFormat("Requested by {{ exported_pod }}/{{ exported_container }}"),
        ])
        + timeSeries.standardOptions.withNoValue("N/A")
        + timeSeries.options.tooltip.withMode("multi")
        + timeSeries.options.tooltip.withSort("desc"),
    ])
    + dashboard.withTimezone("browser")
    + dashboard.graphTooltip.withSharedCrosshair()

In case your dashboard definition gets longer and longer, you can extract parts of it into their own files and import them, e.g., the following example moves the panels from the above example in their own file:

local grafonnet = import "github.com/grafana/grafonnet/gen/grafonnet-latest/main.libsonnet";
local datasources = import "datasources.libsonnet";
local panels = import "panels.libsonnet";
local dashboard = grafonnet.dashboard;

dashboard.new("Your Dashboard")
    + dashboard.withDescription("My fancy Jsonnet dashboard")
    + dashboard.withEditable(false)
    + dashboard.withTags(["tag1", "tag2", "tag3"])
    + dashboard.withVariables([datasources.prometheus])
    + dashboard.withPanels([panels.cpuRequested])
    + dashboard.withTimezone("browser")
    + dashboard.graphTooltip.withSharedCrosshair()

If you need to parameterize parts of your dashboard, you can use Jsonnet top-level arguments (TLAs) like this:

local grafonnet = import "github.com/grafana/grafonnet/gen/grafonnet-latest/main.libsonnet";
local datasources = import "datasources.libsonnet";
local panels = import "panels.libsonnet";
local dashboard = grafonnet.dashboard;

function(title="Your Dashboard", timezone="browser")
  dashboard.new(title)
    + dashboard.withDescription("My fancy Jsonnet dashboard")
    + dashboard.withEditable(false)
    + dashboard.withTags(["tag1", "tag2", "tag3"])
    + dashboard.withVariables([datasources.prometheus])
    + dashboard.withPanels([panels.cpuRequested])
    + dashboard.withTimezone(timezone)
    + dashboard.graphTooltip.withSharedCrosshair()

Specify parameters to your dashboard in the GrafanaDashboard resource like this:

apiVersion: grafana.integreatly.org/v1beta1
kind: GrafanaDashboard
metadata:
  name: grafanadashboard-jaas
spec:
  instanceSelector:
    matchLabels:
      dashboards: "grafana"
  url: "http://jaas.jaas.svc.cluster.local:8080/jsonnet/your-dashboard?title=Something&timezone=UTC"

The above example shows that top-level arguments are exposed as URL query parameters in JaaS. Likewise, it is possible to use Jsonnet external variables (std.extVar) like this:

local grafonnet = import "github.com/grafana/grafonnet/gen/grafonnet-latest/main.libsonnet";
local datasources = import "datasources.libsonnet";
local panels = import "panels.libsonnet";
local dashboard = grafonnet.dashboard;

function(title="Your Dashboard", timezone="browser")
  dashboard.new(title)
    + dashboard.withDescription(std.extVar('description'))
    + dashboard.withEditable(std.parseJson(std.extVar('editable')))
    + dashboard.withTags([std.extVar('some-tag')])
    + dashboard.withVariables([datasources.prometheus])
    + dashboard.withPanels([panels.cpuRequested])
    + dashboard.withTimezone(timezone)
    + dashboard.graphTooltip.withSharedCrosshair()

In order to define external variables, modify your JaaS deployment so that it contains JAAS_EXT_VAR_* environment variables, e.g., JAAS_EXT_VAR_description and JAAS_EXT_VAR_editable in the above example. Take a look at the JaaS documentation on how to do this.

Publish OCI objects

JaaS evaluates Jsonnet snippets and expects that each snippet contains a main.jsonnet file as its entrypoint. When writing dashboards, you can use the following directory structure:

<your-repository>
├── main.jsonnet
├── datasources.libsonnet   # local library
└── panels.libsonnet        # local library

An example Dockerfile which packages your dashboard looks like this:

FROM scratch

COPY main.jsonnet /main.jsonnet
COPY datasources.libsonnet /datasources.libsonnet
COPY panels.libsonnet /panels.libsonnet

In case you have split your dashboard definition into multiple files, adjust as necessary so that the /main.jsonnet file can find them in the Dockerfile as well, e.g., by using relative imports and copying everything into the root folder.

When packaging Jsonnet libraries, you just need to make sure that you recreate the same folder structure that is used by jsonnet-bundler in non-legacy mode, e.g., if your library is in a repository at https://git.example.com/my/jsonnet/library and all your Jsonnet files are in a src/something subdirectory of that repository, your Dockerfile for your Jsonnet library should look like this:

FROM scratch

# The fully qualified path is the URL of your repository + the (optional) subfolder of your Jsonnet files
COPY src/something /git.example.com/my/jsonnet/library/src/something

If you follow this structure, you can use jsonnet-bundler locally to develop your dashboards and use them as-is from JaaS as well. In case you do not care about jsonnet-bundler, you are free to choose any structure you want.

There is no restriction on file names for libraries since it’s up to you to import them in your dashboards, e.g., the above example library could be imported like this, assuming that there is a file called main.libsonnet in src/something:

// Import grafonnet
local grafonnet = import "github.com/grafana/grafonnet/gen/grafonnet-latest/main.libsonnet";

// Import your own stuff. The library has to exists within the OCI image, no network requests are made.
local something = import "git.example.com/my/jsonnet/library/src/something/main.libsonnet";

Modify your JaaS instance

Once your dashboards and libraries are packages as OCI objects, add them to your JaaS deployment as explained in their documentation for snippets and libraries .

Note that this does require OCI volume support in your Kubernetes cluster.

Define your GrafanaDashboard resource

Finally, you can define your GrafanaDashboard resource like this:

apiVersion: grafana.integreatly.org/v1beta1
kind: GrafanaDashboard
metadata:
  name: grafanadashboard-jaas
spec:
  instanceSelector:
    matchLabels:
      dashboards: "grafana"
  url: "http://jaas.jaas.svc.cluster.local:8080/jsonnet/your-dashboard"

The name your-dashboard must match one of the snippets you added to JaaS.