Skip to content

Instantly share code, notes, and snippets.

@ryuheechul
Last active August 19, 2021 08:54
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save ryuheechul/7ea8964b7146a8807dc599b276386628 to your computer and use it in GitHub Desktop.
Save ryuheechul/7ea8964b7146a8807dc599b276386628 to your computer and use it in GitHub Desktop.
Upload and download Grafana dashboard without pain

GF_JSON

What the hell is this?

It helps you synchronizing your code, your-dashboard.json and quickly iterated result from the GUI, without pain.

Why I made this?

  • Grafana dashboards must live inside of code repository if it is to be deployed in multiple places.
  • Currently I don't buy the story of using jsonnet or grafonnet to maintain the dashboard as a code because it's one directional.
  • Unfortunately editing graphical object is best done by GUI
  • Usually you don't need some data like id, uid, version, iteration to be syncronized between your code and dashboard data in Grafana
  • But that will constantly bother (pulling will dirty your code, pushing will be rejected) you when you sync them frequently.
  • Thus, this small script to get rid of the hustle.

(TBC for the rest of the documentation)

How to use it

#!/usr/bin/env bash
set -e
# Set your grafana specific usage data to be able to continue
function usage_envvar {
echo "
## Set your grafana specific usage data to be able to continue
## Below are examples
export GF_JSON_GF_HOST='https://your.accessible.grafana.host'
# Read https://grafana.com/docs/grafana/latest/http_api/auth/#create-api-token
export GF_JSON_GF_API_KEY='yoUrGraFAnaAPIKEyyoUrGraFAnaAPIKEyyoUrGraFAnaAPIKEy='
# Read https://grafana.com/docs/grafana/latest/http_api/dashboard/#get-dashboard-by-uid
export GF_JSON_DASHBOARD_UID='daSHBoARdUiD'
## These are optional
# Where you would want to sink the dashboard json file
export GF_JSON_SINK_PATH='./my-sinked-dashboard.json'
# Keys to ignore
export GF_JSON_IGNORE_LIST='id uid version iteration'
# to ease the first pull
export GF_JSON_DEFAULT_TEMPLATE='{"id": null, "uid": null, "iteration": 0, "version": 1}'
"
}
### end of setting bespoke data
# fail fast in case conditions are not good
if test -z "${GF_JSON_GF_HOST}"; then
echo '$GF_JSON_GF_HOST not found, printing hint below.'
usage_envvar
exit 1
fi
if test -z "${GF_JSON_GF_API_KEY}"; then
echo '$GF_JSON_GF_API_KEY not found, printing hint below.'
usage_envvar
exit 1
fi
if test -z "${GF_JSON_DASHBOARD_UID}"; then
echo '$GF_JSON_DASHBOARD_UID not found, printing hint below.'
usage_envvar
exit 1
fi
if test -z "$(command -v jq)"; then
echo 'you will need `jq` to continue'
exit 1
fi
# prepare constants/variables
api_pull="${GF_JSON_GF_HOST}/api/dashboards/uid/${GF_JSON_DASHBOARD_UID}"
api_push="${GF_JSON_GF_HOST}/api/dashboards/db"
sink_path="./my-sinked-dashboard.json"
sink_path="${GF_JSON_SINK_PATH:-${sink_path}}"
download="curl -s -H \"Authorization: Bearer ${GF_JSON_GF_API_KEY}\" ${api_pull} | jq -c '.dashboard'"
upload="curl -H \"Authorization: Bearer ${GF_JSON_GF_API_KEY}\" \
-H \"Content-Type: application/json\" \
-H \"Accept: application/json\" \
--request POST --data-binary \"@/dev/stdin\" \
${api_push}"
# set sensible default list of keys to ignore
# currently this works for both pull and push
ignore_list='id uid version iteration'
ignore_list="${GF_JSON_IGNORE_LIST:-${ignore_list}}"
# set sensible default value per key in case the initial pull didn't happen yet
default_template='{"id": null, "uid": null, "iteration": 0, "version": 1}'
default_template="${GF_JSON_DEFAULT_TEMPLATE:-${default_template}}"
function sanitize {
json_source="${1}"
json_target="${2}"
## skip error checking until I figure out how to handle errors from subshell properly
## and just focus on making this function hard to error on the usage level
## since its just an internal function anyway
# if test -z "$(command -v jq)"; then
# echo 'you will need `jq` to continue'
# return 1
# fi
# if test -z "${json_source}"; then
# echo 'you need to provide $GF_JSON_SOURCE'
# return 1
# fi
# if test -z "${json_target}"; then
# echo 'you need to provide $GF_JSON_TARGET'
# exit 1
# fi
result="${json_source}"
# keep values listed in $ignore_list while updating the rest
for ignore in $ignore_list; do
val="$(echo -n "${json_target}" | jq ".${ignore}")"
result="$(echo -n "${result}" | jq ".${ignore} = $val")"
done
echo -n "${result}"
}
# `pull` respects its own data listed in $GF_JSON_IGNORE_LIST
# from a file at $sink_path unless the file didn't exist yet
function pull {
json_source="$(eval ${download})"
json_target="$(cat ${sink_path} || echo "${default_template}" | jq -c)"
sanitize "${json_source}" "${json_target}" > ${sink_path}
echo "sinked to ${sink_path}"
}
# `push` does oppoiste of what pull does
# it doesn't support the initial push without $GF_JSON_DASHBOARD_UID on purpose
# for initial push, follow https://grafana.com/docs/grafana/latest/dashboards/export-import/#import-dashboard
# and `export GF_JSON_DASHBOARD_UID='daSHBoARdUiD'` from just created dashboard
function push {
json_source="$(cat ${sink_path} | jq -c)"
json_target="$(eval ${download})"
sanitize "${json_source}" "${json_target}" | jq -c '{"dashboard": .}' | eval "${upload}"
}
exec_name="$(basename ${0})"
if test "${exec_name}" == "gfdjr"; then
pull
elif test "${exec_name}" == "gfdjs"; then
push
else
echo 'binary(simlink) name should be either `gfdjr` (to pull) or `gfdjs` (to push)'
fi

Initially it was written as one script file and one Makefile. Since it wasn't very portable, later it was ported to one single script file, gfdj.sh

#!/usr/bin/env bash

json_source="${GF_JSON_SOURCE}"
json_target="${GF_JSON_TARGET}"

if test -z "$(command -v jq)"; then
	echo 'you will need `jq` to continue'
	exit 1
fi

if test -z "${json_source}"; then
	echo 'you need to provide $GF_JSON_SOURCE'
	exit 1
fi

if test -z "${json_target}"; then
	echo 'you need to provide $GF_JSON_TARGET'
	exit 1
fi

result="${json_source}"

# set sensible default list
ignore_list="${GF_JSON_IGNORE_LIST:-id uid version iteration}"

# keep values listed in $ignore_list while updating the rest
for ignore in $ignore_list; do
	val="$(echo -n "${json_target}" | jq ".${ignore}")"
	result="$(echo -n "${result}" | jq ".${ignore} = $val")"
done

echo -n "${result}"
### Set your grafana specific usage data to be able to continue 

# GF_JSON_GF_HOST ?= https://your.accessible.grafana.host

## Read https://grafana.com/docs/grafana/latest/http_api/auth/#create-api-token
# GF_JSON_GF_API_KEY ?= yoUrGraFAnaAPIKEyyoUrGraFAnaAPIKEyyoUrGraFAnaAPIKEy= 

## Read https://grafana.com/docs/grafana/latest/http_api/dashboard/#get-dashboard-by-uid
# GF_JSON_DASHBOARD_UID ?= daSHBoARdUiD

## Where you would want to sink the dashboard json file 
# GF_JSON_SINK_PATH ?= ./my-dashboard.json

### end of setting bespoke data

api_pull := $(GF_JSON_GF_HOST)/api/dashboards/uid/$(GF_JSON_DASHBOARD_UID)
api_push := $(GF_JSON_GF_HOST)/api/dashboards/db

sink_path := $(GF_JSON_SINK_PATH)

download := curl -s -H "Authorization: Bearer $(GF_JSON_GF_API_KEY)" $(api_pull) | jq -c '.dashboard'

upload := curl -H "Authorization: Bearer $(GF_JSON_GF_API_KEY)" \
	-H "Content-Type: application/json" \
	-H "Accept: application/json" \
	--request POST --data-binary "@/dev/stdin" \
	$(api_push)

# this is optional
# currently this works for both pull and push
GF_JSON_IGNORE_LIST ?= id uid version iteration

# to ease the first pull
GF_JSON_DEFAULT_TEMPLATE ?= {"id": null, "uid": null, "iteration": 0, "version": 1}

.PHONY: pull
# pull respects its own data listed in $GF_JSON_IGNORE_LIST
# from a file at $sink_path unless the file didn't exist yet
pull:
	@GF_JSON_IGNORE_LIST='$(GF_JSON_IGNORE_LIST)' \
		GF_JSON_SOURCE='$(shell $(download))' \
		GF_JSON_TARGET='$(shell cat $(sink_path) || echo '$(GF_JSON_DEFAULT_TEMPLATE)' | jq -c)' \
		./sanitize.sh > $(sink_path) && echo "sinked to $(sink_path)"

.PHONY: push
# push does oppoiste of what pull does
push:
	@GF_JSON_IGNORE_LIST='$(GF_JSON_IGNORE_LIST)' \
		GF_JSON_SOURCE='$(shell cat $(sink_path) | jq -c)' \
		GF_JSON_TARGET='$(shell $(download))' \
		./sanitize.sh | jq -c '{"dashboard": .}' | $(upload)
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment