Skip to content

Apply over the API

File provisioning (see Provisioning) needs the build’s files mounted into Grafana’s container. When you cannot mount files, or you want real folders and library panels, apply the build over Grafana’s HTTP API instead. The applier creates and updates folders, library panels and dashboards, labels what it writes as the project’s, and with prune deletes the project’s dashboards and folders that the build no longer declares.

It writes to Grafana 12.4 and 13.x through the dashboard.grafana.app and folder.grafana.app APIs, and to Grafana 11 through /api/dashboards/db and /api/folders.

The applier uses the same grafana.profiles.<env> entry as Drift and Live Export, or GRAFANA_URL with GRAFANA_TOKEN when the environment has no profile. The service account needs the Editor role, or a custom role with the actions in grafanaActionsFor("Apply", "Prune"):

import { grafanaActionsFor } from "@intentius/chant-lexicon-grafana";
grafanaActionsFor("Apply", "Prune");
// ["dashboards:read", "dashboards:create", "dashboards:write", "folders:read", ...]

Set the project’s ownership stack in chant.config.ts. The applier labels every dashboard and folder with it, and prune deletes only what carries it:

export default {
lexicons: ["grafana"],
ownership: { stack: "shop", env: "prod" },
grafana: { profiles: { prod: { url: "https://grafana.example.com", token: { env: "GRAFANA_PROD_TOKEN" } } } },
} satisfies ChantConfig;

Build, then apply the build’s index, the file chant build -o wrote. The applier reads the dashboard files from the same directory:

// ops/grafana.op.ts, with "build:grafana": "chant build src --lexicon grafana -o dist/grafana.json" in package.json
import { Op, phase, build } from "@intentius/chant/op";
import { grafanaApply } from "@intentius/chant-lexicon-grafana/op/builders";
export default Op({
name: "grafana-prod",
phases: [
phase("Build", [build(".", { script: "build:grafana" })]),
phase("Apply", [grafanaApply("dist/grafana.json", { environment: "prod", prune: true })]),
],
});

The step reports each resource once, in core’s apply envelope:

  • applied, as created, updated or unchanged. A dashboard whose live content already covers what the build would send, in the same folder and with the same labels, is unchanged and is not written. Grafana’s own additions (the built-in annotation, its defaults, a migrated schemaVersion) do not count as changes; a panel title edited in the UI does, and the apply puts it back.
  • pruned, with prune: the project’s dashboards and folders the build no longer has.
  • not attempted, with the reason: no-binding or no-credentials for every resource when the environment names no Grafana or the token is refused, and not-prunable for what prune could not consider (below).

A failed write stops the step with Grafana’s status and message.

ApplyOp takes target: "grafana" and runs the same applier after its Build and Plan phases, with an approval gate when delete is "gated". env picks the grafana.profiles.<env> entry, and any delete other than "never" turns on prune. The Build phase runs the project’s build script, and output (default dist/grafana.json) has to name the index that script writes:

// ops/grafana.op.ts, with "build": "chant build src --lexicon grafana -o dist/grafana.json" in package.json
import { ApplyOp } from "@intentius/chant/op";
export const { op } = ApplyOp({
name: "grafana-prod",
env: "prod",
target: "grafana",
delete: "gated",
});

The ownership stack and env still come from chant.config.ts. ApplyOp reports the counts (applied, pruned, notAttempted), and the per-resource lines go to the step’s log. Use the grafanaApply step above when you need stack, ownershipEnv or cwd, which ApplyOp does not pass.

  • Folders. A dashboard’s folder is a path or a Folder (see Provisioning). The applier makes one folder per level, parents first, each under its parent, with the uid its Folder pins or one made from its path ("Team A" becomes team-a, "Platform/Kubernetes" a platform-kubernetes inside platform), so the next apply finds it again. It reads them from the build’s index, which also lists a Folder no dashboard is in yet. A folder that already has that uid is used, moved under its declared parent and labelled.
  • Library panels. Every library panel a dashboard carries in __elements is written to the library before the dashboard: the ones the build writes for a LibraryPanel (see Library panels), and those of exported dashboard JSON. Each goes in the folder its LibraryPanel names, else in the folder of the first dashboard that carries it. One whose name, folder and model already match is unchanged, and an update sends the version Grafana has. The dashboard is sent without __elements, __inputs and __requires.
  • Dashboards. Sent with the project’s labels (app.kubernetes.io/managed-by: chant, chant.intentius.io/stack, chant.intentius.io/env) and their folder. A dashboard with the same uid that somebody else made is updated and labelled; labels already on it are kept.

On Grafana 11 there is nowhere to put labels, so nothing is labelled.

Prune deletes a dashboard or folder only when its labels carry managed-by: chant with the project’s stack and env. A dashboard saved in the UI, one another chant project applied, and one loaded from provisioning files are never deleted.

Dashboards go first, then folders, a nested folder before its parent. A folder that still holds something, such as a dashboard somebody saved into it, is left and reported not-prunable with what it holds; the next prune deletes it once it is empty.

Three things are never pruned, and are reported as not-prunable:

  • library panels, because their API has no labels;
  • anything on Grafana 11, which has no labels to read;
  • anything, when no ownership stack is set, since the applier could not tell the project’s dashboards from another project’s.