Dashboards: Add Dashboard Schema validation (1) (#103662)

pull/103935/head
Marco de Abreu 1 month ago committed by GitHub
parent 920c7b1de5
commit 95f04c79cd
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
  1. 7
      Makefile
  2. 7
      apps/dashboard/Makefile
  3. 4
      apps/dashboard/go.mod
  4. 22
      apps/dashboard/go.sum
  5. 759
      apps/dashboard/pkg/apis/dashboard/v0alpha1/dashboard_kind.cue
  6. 90
      apps/dashboard/pkg/apis/dashboard/v0alpha1/validation.go
  7. 759
      apps/dashboard/pkg/apis/dashboard/v1alpha1/dashboard_kind.cue
  8. 90
      apps/dashboard/pkg/apis/dashboard/v1alpha1/validation.go
  9. 965
      apps/dashboard/pkg/apis/dashboard/v2alpha1/dashboard_spec.cue
  10. 84
      apps/dashboard/pkg/apis/dashboard/v2alpha1/validation.go
  11. 2
      devenv/dev-dashboards/all-panels.json
  12. 3
      docs/sources/setup-grafana/configure-grafana/feature-toggles/index.md
  13. 12
      packages/grafana-data/src/types/featureToggles.gen.ts
  14. 2
      pkg/registry/apis/dashboard/large_test.go
  15. 56
      pkg/registry/apis/dashboard/mutate.go
  16. 12
      pkg/registry/apis/dashboard/mutation_test.go
  17. 78
      pkg/registry/apis/dashboard/schema_validation.go
  18. 12
      pkg/services/apiserver/client/client.go
  19. 8
      pkg/services/apiserver/client/client_mock.go
  20. 16
      pkg/services/dashboards/service/dashboard_service.go
  21. 20
      pkg/services/dashboards/service/dashboard_service_test.go
  22. 18
      pkg/services/featuremgmt/registry.go
  23. 3
      pkg/services/featuremgmt/toggles_gen.csv
  24. 12
      pkg/services/featuremgmt/toggles_gen.go
  25. 62
      pkg/services/featuremgmt/toggles_gen.json
  26. 8
      pkg/services/folder/folderimpl/unifiedstore.go
  27. 2
      pkg/services/store/kind/dashboard/testdata/devdash-all-panels-info.json
  28. 3
      pkg/tests/apis/dashboard/dashboards_test.go
  29. 3
      pkg/tests/apis/dashboard/integration/api_validation_test.go
  30. 1
      pkg/tests/apis/dashboard/testdata/dashboard-test-v1.yaml

@ -148,6 +148,13 @@ gen-cue: ## Do all CUE/Thema code generation
@echo "generate code from .cue files"
go generate ./kinds/gen.go
go generate ./public/app/plugins/gen.go
@echo "// This file is managed by Grafana - DO NOT EDIT MANUALLY" > apps/dashboard/pkg/apis/dashboard/v1alpha1/dashboard_kind.cue
@echo "// Source: kinds/dashboard/dashboard_kind.cue" >> apps/dashboard/pkg/apis/dashboard/v1alpha1/dashboard_kind.cue
@echo "// To sync changes, run: make gen-cue" >> apps/dashboard/pkg/apis/dashboard/v1alpha1/dashboard_kind.cue
@echo "" >> apps/dashboard/pkg/apis/dashboard/v1alpha1/dashboard_kind.cue
@cat kinds/dashboard/dashboard_kind.cue >> apps/dashboard/pkg/apis/dashboard/v1alpha1/dashboard_kind.cue
@cp apps/dashboard/pkg/apis/dashboard/v1alpha1/dashboard_kind.cue apps/dashboard/pkg/apis/dashboard/v0alpha1/dashboard_kind.cue
.PHONY: gen-cuev2
gen-cuev2: ## Do all CUE code generation

@ -53,3 +53,10 @@ post-generate-cleanup: ## Clean up the generated code
@sed -e '/\/\/ DeepCopyInto deep copies Spec into another Spec object/,+3d' ./pkg/apis/dashboard/v1alpha1/dashboard_object_gen.go.tmp > ./pkg/apis/dashboard/v1alpha1/dashboard_object_gen.go.tmp2
@rm ./pkg/apis/dashboard/v1alpha1/dashboard_object_gen.go.tmp
@mv ./pkg/apis/dashboard/v1alpha1/dashboard_object_gen.go.tmp2 ./pkg/apis/dashboard/v1alpha1/dashboard_object_gen.go
# Copy dashboard/v2alpha1 spec so we can use it for schema validation
@echo "// This file is managed by grafana-app-sdk - DO NOT EDIT MANUALLY" > ./pkg/apis/dashboard/v2alpha1/dashboard_spec.cue
@echo "// Source: apps/dashboard/kinds/v2alpha1/dashboard_spec.cue" >> ./pkg/apis/dashboard/v2alpha1/dashboard_spec.cue
@echo "// To sync changes, run: make generate in apps/dashboard" >> ./pkg/apis/dashboard/v2alpha1/dashboard_spec.cue
@echo "" >> ./pkg/apis/dashboard/v2alpha1/dashboard_spec.cue
@cat ./kinds/v2alpha1/dashboard_spec.cue >> ./pkg/apis/dashboard/v2alpha1/dashboard_spec.cue

@ -3,6 +3,7 @@ module github.com/grafana/grafana/apps/dashboard
go 1.24.2
require (
cuelang.org/go v0.11.1
github.com/grafana/grafana-app-sdk v0.35.1
github.com/grafana/grafana-plugin-sdk-go v0.274.1-0.20250318081012-21a7f15619b0
github.com/grafana/grafana/pkg/apimachinery v0.0.0-20250312121619-f64be062c432
@ -19,6 +20,7 @@ require (
github.com/cespare/xxhash/v2 v2.3.0 // indirect
github.com/cheekybits/genny v1.0.0 // indirect
github.com/chromedp/cdproto v0.0.0-20240810084448-b931b754e476 // indirect
github.com/cockroachdb/apd/v3 v3.2.1 // indirect
github.com/cpuguy83/go-md2man/v2 v2.0.6 // indirect
github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc // indirect
github.com/elazarl/goproxy v1.7.2 // indirect
@ -55,6 +57,7 @@ require (
github.com/jszwedko/go-datemath v0.1.1-0.20230526204004-640a500621d6 // indirect
github.com/klauspost/compress v1.18.0 // indirect
github.com/klauspost/cpuid/v2 v2.2.10 // indirect
github.com/lib/pq v1.10.9 // indirect
github.com/magefile/mage v1.15.0 // indirect
github.com/mailru/easyjson v0.7.7 // indirect
github.com/mattetti/filebuffer v1.0.1 // indirect
@ -69,6 +72,7 @@ require (
github.com/oasdiff/yaml3 v0.0.0-20250309153720-d2182401db90 // indirect
github.com/oklog/run v1.1.0 // indirect
github.com/olekukonko/tablewriter v0.0.5 // indirect
github.com/pelletier/go-toml/v2 v2.2.3 // indirect
github.com/perimeterx/marshmallow v1.1.5 // indirect
github.com/pierrec/lz4/v4 v4.1.22 // indirect
github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 // indirect

@ -1,3 +1,7 @@
cuelabs.dev/go/oci/ociregistry v0.0.0-20240906074133-82eb438dd565 h1:R5wwEcbEZSBmeyg91MJZTxfd7WpBo2jPof3AYjRbxwY=
cuelabs.dev/go/oci/ociregistry v0.0.0-20240906074133-82eb438dd565/go.mod h1:5A4xfTzHTXfeVJBU6RAUf+QrlfTCW+017q/QiW+sMLg=
cuelang.org/go v0.11.1 h1:pV+49MX1mmvDm8Qh3Za3M786cty8VKPWzQ1Ho4gZRP0=
cuelang.org/go v0.11.1/go.mod h1:PBY6XvPUswPPJ2inpvUozP9mebDVTXaeehQikhZPBz0=
github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU=
github.com/BurntSushi/toml v1.4.0/go.mod h1:ukJfTF/6rtPPRCnwkur4qwRxa8vTRFBF0uk2lLoLwho=
github.com/BurntSushi/toml v1.5.0 h1:W5quZX/G/csjUnuI8SUYlsHs9M38FC7znL0lIO+DvMg=
@ -20,6 +24,8 @@ github.com/cheekybits/genny v1.0.0 h1:uGGa4nei+j20rOSeDeP5Of12XVm7TGUd4dJA9RDitf
github.com/cheekybits/genny v1.0.0/go.mod h1:+tQajlRqAUrPI7DOSpB0XAqZYtQakVtB7wXkRAgjxjQ=
github.com/chromedp/cdproto v0.0.0-20240810084448-b931b754e476 h1:VnjHsRXCRti7Av7E+j4DCha3kf68echfDzQ+wD11SBU=
github.com/chromedp/cdproto v0.0.0-20240810084448-b931b754e476/go.mod h1:GKljq0VrfU4D5yc+2qA6OVr8pmO/MBbPEWqWQ/oqGEs=
github.com/cockroachdb/apd/v3 v3.2.1 h1:U+8j7t0axsIgvQUqthuNm82HIrYXodOV2iWLWtEaIwg=
github.com/cockroachdb/apd/v3 v3.2.1/go.mod h1:klXJcjp+FffLTHlhIG69tezTDvdP065naDsHzKhYSqc=
github.com/cpuguy83/go-md2man/v2 v2.0.0-20190314233015-f79a8a8ca69d/go.mod h1:maD7wRr/U5Z6m/iR4s+kqSMx2CaBsrgA7czyZG/E6dU=
github.com/cpuguy83/go-md2man/v2 v2.0.5/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o=
github.com/cpuguy83/go-md2man/v2 v2.0.6 h1:XJtiaUW6dEEqVuZiMTn1ldk455QWwEIsMIJlo5vtkx0=
@ -32,6 +38,8 @@ github.com/elazarl/goproxy v1.7.2 h1:Y2o6urb7Eule09PjlhQRGNsqRfPmYI3KKQLFpCAV3+o
github.com/elazarl/goproxy v1.7.2/go.mod h1:82vkLNir0ALaW14Rc399OTTjyNREgmdL2cVoIbS6XaE=
github.com/emicklei/go-restful/v3 v3.11.0 h1:rAQeMHw1c7zTmncogyy8VvRZwtkmkZ4FxERmMY4rD+g=
github.com/emicklei/go-restful/v3 v3.11.0/go.mod h1:6n3XBCmQQb25CM2LCACGz8ukIrRry+4bhvbpWn3mrbc=
github.com/emicklei/proto v1.13.2 h1:z/etSFO3uyXeuEsVPzfl56WNgzcvIr42aQazXaQmFZY=
github.com/emicklei/proto v1.13.2/go.mod h1:rn1FgRS/FANiZdD2djyH7TMA9jdRDcYQ9IEN9yvjX0A=
github.com/fatih/color v1.13.0/go.mod h1:kLAiJbzzSOZDVNGyDpeOxJ47H46qBXwg5ILebYFFOfk=
github.com/fatih/color v1.18.0 h1:S8gINlzdQ840/4pfAwic/ZE0djQEH3wM94VfqLTZcOM=
github.com/fatih/color v1.18.0/go.mod h1:4FelSpRwEGDpQ12mAdzqdOukCy4u8WUtOY6lkT/6HfU=
@ -56,6 +64,8 @@ github.com/go-openapi/jsonreference v0.21.0 h1:Rs+Y7hSXT83Jacb7kFyjn4ijOuVGSvOdF
github.com/go-openapi/jsonreference v0.21.0/go.mod h1:LmZmgsrTkVg9LG4EaHeY8cBDslNPMo06cago5JNLkm4=
github.com/go-openapi/swag v0.23.0 h1:vsEVJDUo2hPJ2tu0/Xc+4noaxyEffXNIs3cOULZ+GrE=
github.com/go-openapi/swag v0.23.0/go.mod h1:esZ8ITTYEsH1V2trKHjAN8Ai7xHb8RV+YSZ577vPjgQ=
github.com/go-quicktest/qt v1.101.0 h1:O1K29Txy5P2OK0dGo59b7b0LR6wKfIhttaAhHUyn7eI=
github.com/go-quicktest/qt v1.101.0/go.mod h1:14Bz/f7NwaXPtdYEgzsx46kqSxVwTbzVZsDC26tQJow=
github.com/go-test/deep v1.0.8 h1:TDsG77qcSprGbC6vTN8OuXp5g+J+b5Pcguhf7Zt61VM=
github.com/go-test/deep v1.0.8/go.mod h1:5C2ZWiW0ErCdrYzpqxLbTX7MG14M9iiw8DgHncVwcsE=
github.com/goccy/go-json v0.10.5 h1:Fq85nIqj+gXn/S5ahsiTlK3TmC85qgirsdTP/+DeaC4=
@ -133,6 +143,8 @@ github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
github.com/kylelemons/godebug v1.1.0 h1:RPNrshWIDI6G2gRW9EHilWtl7Z6Sb1BR0xunSBf0SNc=
github.com/kylelemons/godebug v1.1.0/go.mod h1:9/0rRGxNHcop5bhtWyNeEfOS8JIWk580+fNqagV/RAw=
github.com/lib/pq v1.10.9 h1:YXG7RB+JIjhP29X+OtkiDnYaXQwpS4JEWq7dtCCRUEw=
github.com/lib/pq v1.10.9/go.mod h1:AlVN5x4E4T544tWzH6hKfbfQvm3HdbOxrmggDNAPY9o=
github.com/magefile/mage v1.15.0 h1:BvGheCMAsG3bWUDbZ8AyXXpCNwU9u5CB6sM+HNb9HYg=
github.com/magefile/mage v1.15.0/go.mod h1:z5UZb/iS3GoOSn0JgWuiw7dxlurVYTu+/jHXqQg881A=
github.com/mailru/easyjson v0.7.7 h1:UGYAvKxe3sBsEDzO8ZeWOSlIQfWFlxbzLZe7hwFURr0=
@ -154,6 +166,8 @@ github.com/minio/asm2plan9s v0.0.0-20200509001527-cdd76441f9d8 h1:AMFGa4R4MiIpsp
github.com/minio/asm2plan9s v0.0.0-20200509001527-cdd76441f9d8/go.mod h1:mC1jAcsrzbxHt8iiaC+zU4b1ylILSosueou12R++wfY=
github.com/minio/c2goasm v0.0.0-20190812172519-36a3d3bbc4f3 h1:+n/aFZefKZp7spd8DFdX7uMikMLXX4oubIzJF4kv/wI=
github.com/minio/c2goasm v0.0.0-20190812172519-36a3d3bbc4f3/go.mod h1:RagcQ7I8IeTMnF8JTXieKnO4Z6JCsikNEzj0DwauVzE=
github.com/mitchellh/go-wordwrap v1.0.1 h1:TLuKupo69TCn6TQSyGxwI1EblZZEsQ0vMlAFQflz0v0=
github.com/mitchellh/go-wordwrap v1.0.1/go.mod h1:R62XHJLzvMFRBbcrT7m7WgmE1eOyTSsCt+hzestvNj0=
github.com/mitchellh/reflectwalk v1.0.2 h1:G2LzWKi524PWgd3mLHV8Y5k7s6XUvT0Gef6zxSIeXaQ=
github.com/mitchellh/reflectwalk v1.0.2/go.mod h1:mSTlrgnPZtwu0c4WaC2kGObEpuNDbx0jmZXqmk4esnw=
github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
@ -173,6 +187,12 @@ github.com/oklog/run v1.1.0 h1:GEenZ1cK0+q0+wsJew9qUg/DyD8k3JzYsZAi5gYi2mA=
github.com/oklog/run v1.1.0/go.mod h1:sVPdnTZT1zYwAJeCMu2Th4T21pA3FPOQRfWjQlk7DVU=
github.com/olekukonko/tablewriter v0.0.5 h1:P2Ga83D34wi1o9J6Wh1mRuqd4mF/x/lgBS7N7AbDhec=
github.com/olekukonko/tablewriter v0.0.5/go.mod h1:hPp6KlRPjbx+hW8ykQs1w3UBbZlj6HuIJcUGPhkA7kY=
github.com/opencontainers/go-digest v1.0.0 h1:apOUWs51W5PlhuyGyz9FCeeBIOUDA/6nW8Oi/yOhh5U=
github.com/opencontainers/go-digest v1.0.0/go.mod h1:0JzlMkj0TRzQZfJkVvzbP0HBR3IKzErnv2BNG4W4MAM=
github.com/opencontainers/image-spec v1.1.0 h1:8SG7/vwALn54lVB/0yZ/MMwhFrPYtpEHQb2IpWsCzug=
github.com/opencontainers/image-spec v1.1.0/go.mod h1:W4s4sFTMaBeK1BQLXbG4AdM2szdn85PY75RI83NrTrM=
github.com/pelletier/go-toml/v2 v2.2.3 h1:YmeHyLY8mFWbdkNWwpr+qIL2bEqT0o95WSdkNHvL12M=
github.com/pelletier/go-toml/v2 v2.2.3/go.mod h1:MfCQTFTvCcUyyvvwm1+G6H/jORL20Xlb6rzQu9GuUkc=
github.com/perimeterx/marshmallow v1.1.5 h1:a2LALqQ1BlHM8PZblsDdidgv1mWi1DgC2UmX50IvK2s=
github.com/perimeterx/marshmallow v1.1.5/go.mod h1:dsXbUu8CRzfYP5a87xpp0xq9S3u0Vchtcl8we9tYaXw=
github.com/pierrec/lz4/v4 v4.1.22 h1:cKFw6uJDK+/gfw5BcDL0JL5aBsAFdsIT18eRtLj7VIU=
@ -190,6 +210,8 @@ github.com/prometheus/common v0.62.0 h1:xasJaQlnWAeyHdUBeGjXmutelfJHWMRr+Fg4QszZ
github.com/prometheus/common v0.62.0/go.mod h1:vyBcEuLSvWos9B1+CyL7JZ2up+uFzXhkqml0W5zIY1I=
github.com/prometheus/procfs v0.15.1 h1:YagwOFzUgYfKKHX6Dr+sHT7km/hxC76UB0learggepc=
github.com/prometheus/procfs v0.15.1/go.mod h1:fB45yRUv8NstnjriLhBQLuOUt+WW4BsoGhij/e3PBqk=
github.com/protocolbuffers/txtpbfmt v0.0.0-20241112170944-20d2c9ebc01d h1:HWfigq7lB31IeJL8iy7jkUmU/PG1Sr8jVGhS749dbUA=
github.com/protocolbuffers/txtpbfmt v0.0.0-20241112170944-20d2c9ebc01d/go.mod h1:jgxiZysxFPM+iWKwQwPR+y+Jvo54ARd4EisXxKYpB5c=
github.com/rivo/uniseg v0.2.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc=
github.com/rivo/uniseg v0.4.7 h1:WUdvkW8uEhrYfLC4ZzdpI2ztxP1I582+49Oc5Mq64VQ=
github.com/rivo/uniseg v0.4.7/go.mod h1:FN3SvrM+Zdj16jyLfmOkMNblXMcoc8DfTHruCPUcx88=

@ -0,0 +1,759 @@
// This file is managed by Grafana - DO NOT EDIT MANUALLY
// Source: kinds/dashboard/dashboard_kind.cue
// To sync changes, run: make gen-cue
package kind
import (
"strings"
t "time"
)
name: "Dashboard"
maturity: "experimental"
description: "A Grafana dashboard."
crd: dummySchema: true
lineage: schemas: [{
version: [0, 0]
schema: {
spec: {
// Unique numeric identifier for the dashboard.
// `id` is internal to a specific Grafana instance. `uid` should be used to identify a dashboard across Grafana instances.
id?: int64 | null // TODO eliminate this null option
// Unique dashboard identifier that can be generated by anyone. string (8-40)
uid?: string
// Title of dashboard.
title?: string
// Description of dashboard.
description?: string
// This property should only be used in dashboards defined by plugins. It is a quick check
// to see if the version has changed since the last time.
revision?: int64
// ID of a dashboard imported from the https://grafana.com/grafana/dashboards/ portal
gnetId?: string
// Tags associated with dashboard.
tags?: [...string]
// Timezone of dashboard. Accepted values are IANA TZDB zone ID or "browser" or "utc".
timezone?: string | *"browser"
// Whether a dashboard is editable or not.
editable?: bool | *true
// Configuration of dashboard cursor sync behavior.
// Accepted values are 0 (sync turned off), 1 (shared crosshair), 2 (shared crosshair and tooltip).
graphTooltip?: #DashboardCursorSync
// Time range for dashboard.
// Accepted values are relative time strings like {from: 'now-6h', to: 'now'} or absolute time strings like {from: '2020-07-10T08:00:00.000Z', to: '2020-07-10T14:00:00.000Z'}.
time?: {
from: string | *"now-6h"
to: string | *"now"
}
// Configuration of the time picker shown at the top of a dashboard.
timepicker?: #TimePickerConfig
// The month that the fiscal year starts on. 0 = January, 11 = December
fiscalYearStartMonth?: uint8 & <12 | *0
// When set to true, the dashboard will redraw panels at an interval matching the pixel width.
// This will keep data "moving left" regardless of the query refresh rate. This setting helps
// avoid dashboards presenting stale live data
liveNow?: bool
// Day when the week starts. Expressed by the name of the day in lowercase, e.g. "monday".
weekStart?: string
// Refresh rate of dashboard. Represented via interval string, e.g. "5s", "1m", "1h", "1d".
refresh?: string
// Version of the JSON schema, incremented each time a Grafana update brings
// changes to said schema.
schemaVersion: uint16 | *41
// Version of the dashboard, incremented each time the dashboard is updated.
version?: uint32
// List of dashboard panels
panels?: [...(#Panel | #RowPanel)]
// Configured template variables
templating?: {
// List of configured template variables with their saved values along with some other metadata
list?: [...#VariableModel]
}
// Contains the list of annotations that are associated with the dashboard.
// Annotations are used to overlay event markers and overlay event tags on graphs.
// Grafana comes with a native annotation store and the ability to add annotation events directly from the graph panel or via the HTTP API.
// See https://grafana.com/docs/grafana/latest/dashboards/build-dashboards/annotate-visualizations/
annotations?: #AnnotationContainer
// Links with references to other dashboards or external websites.
links?: [...#DashboardLink]
// Snapshot options. They are present only if the dashboard is a snapshot.
snapshot?: #Snapshot @grafanamaturity(NeedsExpertReview)
// When set to true, the dashboard will load all panels in the dashboard when it's loaded.
preload?: bool
} @cuetsy(kind="interface") @grafana(TSVeneer="type")
///////////////////////////////////////
// Definitions (referenced above) are declared below
// TODO: this should be a regular DataQuery that depends on the selected dashboard
// these match the properties of the "grafana" datasouce that is default in most dashboards
#AnnotationTarget: {
// Only required/valid for the grafana datasource...
// but code+tests is already depending on it so hard to change
limit: int64
// Only required/valid for the grafana datasource...
// but code+tests is already depending on it so hard to change
matchAny: bool
// Only required/valid for the grafana datasource...
// but code+tests is already depending on it so hard to change
tags: [...string]
// Only required/valid for the grafana datasource...
// but code+tests is already depending on it so hard to change
type: string
... // datasource will stick their raw DataQuery here
} @cuetsy(kind="interface") @grafanamaturity(NeedsExpertReview)
#AnnotationPanelFilter: {
// Should the specified panels be included or excluded
exclude?: bool | *false
// Panel IDs that should be included or excluded
ids: [...uint8]
} @cuetsy(kind="interface")
// Contains the list of annotations that are associated with the dashboard.
// Annotations are used to overlay event markers and overlay event tags on graphs.
// Grafana comes with a native annotation store and the ability to add annotation events directly from the graph panel or via the HTTP API.
// See https://grafana.com/docs/grafana/latest/dashboards/build-dashboards/annotate-visualizations/
#AnnotationContainer: {
// List of annotations
list?: [...#AnnotationQuery]
} @cuetsy(kind="interface") @grafana(TSVeneer="type")
// TODO docs
// FROM: AnnotationQuery in grafana-data/src/types/annotations.ts
#AnnotationQuery: {
// Name of annotation.
name: string
// Datasource where the annotations data is
datasource: #DataSourceRef
// When enabled the annotation query is issued with every dashboard refresh
enable: bool | *true
// Annotation queries can be toggled on or off at the top of the dashboard.
// When hide is true, the toggle is not shown in the dashboard.
hide?: bool | *false
// Color to use for the annotation event markers
iconColor: string
// Filters to apply when fetching annotations
filter?: #AnnotationPanelFilter
// TODO.. this should just be a normal query target
target?: #AnnotationTarget
// TODO -- this should not exist here, it is based on the --grafana-- datasource
type?: string @grafanamaturity(NeedsExpertReview)
// Set to 1 for the standard annotation query all dashboards have by default.
builtIn?: number | *0
// unless datasources have migrated to the target+mapping,
// they just spread their query into the base object :(
...
} @cuetsy(kind="interface") @grafana(TSVeneer="type") @grafanamaturity(NeedsExpertReview)
// A variable is a placeholder for a value. You can use variables in metric queries and in panel titles.
#VariableModel: {
// Type of variable
type: #VariableType
// Name of variable
name: string
// Optional display name
label?: string
// Visibility configuration for the variable
hide?: #VariableHide
// Whether the variable value should be managed by URL query params or not
skipUrlSync?: bool | *false
// Description of variable. It can be defined but `null`.
description?: string
// Query used to fetch values for a variable
query?: string | {...}
// Data source used to fetch values for a variable. It can be defined but `null`.
datasource?: #DataSourceRef
// Shows current selected variable text/value on the dashboard
current?: #VariableOption
// Whether multiple values can be selected or not from variable value list
multi?: bool | *false
// Allow custom values to be entered in the variable
allowCustomValue?: bool | *true
// Options that can be selected for a variable.
options?: [...#VariableOption]
// Options to config when to refresh a variable
refresh?: #VariableRefresh
// Options sort order
sort?: #VariableSort
// Whether all value option is available or not
includeAll?: bool | *false
// Custom all value
allValue?: string
// Optional field, if you want to extract part of a series name or metric node segment.
// Named capture groups can be used to separate the display text and value.
regex?: string
...
} @cuetsy(kind="interface") @grafana(TSVeneer="type") @grafanamaturity(NeedsExpertReview)
// Option to be selected in a variable.
#VariableOption: {
// Whether the option is selected or not
selected?: bool
// Text to be displayed for the option
text: string | [...string]
// Value of the option
value: string | [...string]
} @cuetsy(kind="interface")
// Options to config when to refresh a variable
// `0`: Never refresh the variable
// `1`: Queries the data source every time the dashboard loads.
// `2`: Queries the data source when the dashboard time range changes.
#VariableRefresh: 0 | 1 | 2 @cuetsy(kind="enum",memberNames="never|onDashboardLoad|onTimeRangeChanged")
// Determine if the variable shows on dashboard
// Accepted values are 0 (show label and value), 1 (show value only), 2 (show nothing).
#VariableHide: 0 | 1 | 2 @cuetsy(kind="enum",memberNames="dontHide|hideLabel|hideVariable") @grafana(TSVeneer="type")
// Sort variable options
// Accepted values are:
// `0`: No sorting
// `1`: Alphabetical ASC
// `2`: Alphabetical DESC
// `3`: Numerical ASC
// `4`: Numerical DESC
// `5`: Alphabetical Case Insensitive ASC
// `6`: Alphabetical Case Insensitive DESC
// `7`: Natural ASC
// `8`: Natural DESC
#VariableSort: 0 | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 @cuetsy(kind="enum",memberNames="disabled|alphabeticalAsc|alphabeticalDesc|numericalAsc|numericalDesc|alphabeticalCaseInsensitiveAsc|alphabeticalCaseInsensitiveDesc|naturalAsc|naturalDesc")
// Ref to a DataSource instance
#DataSourceRef: {
// The plugin type-id
type?: string
// Specific datasource instance
uid?: string
} @cuetsy(kind="interface") @grafana(TSVeneer="type")
// Links with references to other dashboards or external resources
#DashboardLink: {
// Title to display with the link
title: string
// Link type. Accepted values are dashboards (to refer to another dashboard) and link (to refer to an external resource)
type: #DashboardLinkType
// Icon name to be displayed with the link
icon: string
// Tooltip to display when the user hovers their mouse over it
tooltip: string
// Link URL. Only required/valid if the type is link
url?: string
// List of tags to limit the linked dashboards. If empty, all dashboards will be displayed. Only valid if the type is dashboards
tags: [...string]
// If true, all dashboards links will be displayed in a dropdown. If false, all dashboards links will be displayed side by side. Only valid if the type is dashboards
asDropdown: bool | *false
// If true, the link will be opened in a new tab
targetBlank: bool | *false
// If true, includes current template variables values in the link as query params
includeVars: bool | *false
// If true, includes current time range in the link as query params
keepTime: bool | *false
} @cuetsy(kind="interface")
// Dashboard Link type. Accepted values are dashboards (to refer to another dashboard) and link (to refer to an external resource)
#DashboardLinkType: "link" | "dashboards" @cuetsy(kind="type")
// Dashboard variable type
// `query`: Query-generated list of values such as metric names, server names, sensor IDs, data centers, and so on.
// `adhoc`: Key/value filters that are automatically added to all metric queries for a data source (Prometheus, Loki, InfluxDB, and Elasticsearch only).
// `constant`: Define a hidden constant.
// `datasource`: Quickly change the data source for an entire dashboard.
// `interval`: Interval variables represent time spans.
// `textbox`: Display a free text input field with an optional default value.
// `custom`: Define the variable options manually using a comma-separated list.
// `system`: Variables defined by Grafana. See: https://grafana.com/docs/grafana/latest/dashboards/variables/add-template-variables/#global-variables
#VariableType: "query" | "adhoc" | "groupby" | "constant" | "datasource" | "interval" | "textbox" | "custom" |
"system" | "snapshot" @cuetsy(kind="type") @grafanamaturity(NeedsExpertReview)
// Color mode for a field. You can specify a single color, or select a continuous (gradient) color schemes, based on a value.
// Continuous color interpolates a color using the percentage of a value relative to min and max.
// Accepted values are:
// `thresholds`: From thresholds. Informs Grafana to take the color from the matching threshold
// `palette-classic`: Classic palette. Grafana will assign color by looking up a color in a palette by series index. Useful for Graphs and pie charts and other categorical data visualizations
// `palette-classic-by-name`: Classic palette (by name). Grafana will assign color by looking up a color in a palette by series name. Useful for Graphs and pie charts and other categorical data visualizations
// `continuous-GrYlRd`: ontinuous Green-Yellow-Red palette mode
// `continuous-RdYlGr`: Continuous Red-Yellow-Green palette mode
// `continuous-BlYlRd`: Continuous Blue-Yellow-Red palette mode
// `continuous-YlRd`: Continuous Yellow-Red palette mode
// `continuous-BlPu`: Continuous Blue-Purple palette mode
// `continuous-YlBl`: Continuous Yellow-Blue palette mode
// `continuous-blues`: Continuous Blue palette mode
// `continuous-reds`: Continuous Red palette mode
// `continuous-greens`: Continuous Green palette mode
// `continuous-purples`: Continuous Purple palette mode
// `shades`: Shades of a single color. Specify a single color, useful in an override rule.
// `fixed`: Fixed color mode. Specify a single color, useful in an override rule.
#FieldColorModeId: "thresholds" | "palette-classic" | "palette-classic-by-name" | "continuous-GrYlRd" | "continuous-RdYlGr" | "continuous-BlYlRd" | "continuous-YlRd" | "continuous-BlPu" | "continuous-YlBl" | "continuous-blues" | "continuous-reds" | "continuous-greens" | "continuous-purples" | "fixed" | "shades" @cuetsy(kind="enum",memberNames="Thresholds|PaletteClassic|PaletteClassicByName|ContinuousGrYlRd|ContinuousRdYlGr|ContinuousBlYlRd|ContinuousYlRd|ContinuousBlPu|ContinuousYlBl|ContinuousBlues|ContinuousReds|ContinuousGreens|ContinuousPurples|Fixed|Shades") @grafanamaturity(NeedsExpertReview)
// Defines how to assign a series color from "by value" color schemes. For example for an aggregated data points like a timeseries, the color can be assigned by the min, max or last value.
#FieldColorSeriesByMode: "min" | "max" | "last" @cuetsy(kind="type")
// Map a field to a color.
#FieldColor: {
// The main color scheme mode.
mode: #FieldColorModeId
// The fixed color value for fixed or shades color modes.
fixedColor?: string
// Some visualizations need to know how to assign a series color from by value color schemes.
seriesBy?: #FieldColorSeriesByMode
} @cuetsy(kind="interface")
// Position and dimensions of a panel in the grid
#GridPos: {
// Panel height. The height is the number of rows from the top edge of the panel.
h: uint32 & >0 | *9
// Panel width. The width is the number of columns from the left edge of the panel.
w: uint32 & >0 & <=24 | *12
// Panel x. The x coordinate is the number of columns from the left edge of the grid
x: uint32 & >=0 & <24 | *0
// Panel y. The y coordinate is the number of rows from the top edge of the grid
y: uint32 & >=0 | *0
// Whether the panel is fixed within the grid. If true, the panel will not be affected by other panels' interactions
static?: bool
} @cuetsy(kind="interface")
// User-defined value for a metric that triggers visual changes in a panel when this value is met or exceeded
// They are used to conditionally style and color visualizations based on query results , and can be applied to most visualizations.
#Threshold: {
// Value represents a specified metric for the threshold, which triggers a visual change in the dashboard when this value is met or exceeded.
// Nulls currently appear here when serializing -Infinity to JSON.
value: number | null @grafanamaturity(NeedsExpertReview)
// Color represents the color of the visual change that will occur in the dashboard when the threshold value is met or exceeded.
color: string @grafanamaturity(NeedsExpertReview)
} @cuetsy(kind="interface") @grafanamaturity(NeedsExpertReview)
// Thresholds can either be `absolute` (specific number) or `percentage` (relative to min or max, it will be values between 0 and 1).
#ThresholdsMode: "absolute" | "percentage" @cuetsy(kind="enum",memberNames="Absolute|Percentage")
// Thresholds configuration for the panel
#ThresholdsConfig: {
// Thresholds mode.
mode: #ThresholdsMode
// Must be sorted by 'value', first value is always -Infinity
steps: [...#Threshold] @grafanamaturity(NeedsExpertReview)
} @cuetsy(kind="interface") @grafanamaturity(NeedsExpertReview)
// Allow to transform the visual representation of specific data values in a visualization, irrespective of their original units
#ValueMapping: #ValueMap | #RangeMap | #RegexMap | #SpecialValueMap @cuetsy(kind="type") @grafanamaturity(NeedsExpertReview)
// Supported value mapping types
// `value`: Maps text values to a color or different display text and color. For example, you can configure a value mapping so that all instances of the value 10 appear as Perfection! rather than the number.
// `range`: Maps numerical ranges to a display text and color. For example, if a value is within a certain range, you can configure a range value mapping to display Low or High rather than the number.
// `regex`: Maps regular expressions to replacement text and a color. For example, if a value is www.example.com, you can configure a regex value mapping so that Grafana displays www and truncates the domain.
// `special`: Maps special values like Null, NaN (not a number), and boolean values like true and false to a display text and color. See SpecialValueMatch to see the list of special values. For example, you can configure a special value mapping so that null values appear as N/A.
#MappingType: "value" | "range" | "regex" | "special" @cuetsy(kind="enum",memberNames="ValueToText|RangeToText|RegexToText|SpecialValue") @grafanamaturity(NeedsExpertReview)
// Maps text values to a color or different display text and color.
// For example, you can configure a value mapping so that all instances of the value 10 appear as Perfection! rather than the number.
#ValueMap: {
type: #MappingType & "value"
// Map with <value_to_match>: ValueMappingResult. For example: { "10": { text: "Perfection!", color: "green" } }
options: [string]: #ValueMappingResult
} @cuetsy(kind="interface")
// Maps numerical ranges to a display text and color.
// For example, if a value is within a certain range, you can configure a range value mapping to display Low or High rather than the number.
#RangeMap: {
type: #MappingType & "range"
// Range to match against and the result to apply when the value is within the range
options: {
// Min value of the range. It can be null which means -Infinity
from: float64 | null
// Max value of the range. It can be null which means +Infinity
to: float64 | null
// Config to apply when the value is within the range
result: #ValueMappingResult
}
} @cuetsy(kind="interface") @grafanamaturity(NeedsExpertReview)
// Maps regular expressions to replacement text and a color.
// For example, if a value is www.example.com, you can configure a regex value mapping so that Grafana displays www and truncates the domain.
#RegexMap: {
type: #MappingType & "regex"
// Regular expression to match against and the result to apply when the value matches the regex
options: {
// Regular expression to match against
pattern: string
// Config to apply when the value matches the regex
result: #ValueMappingResult
}
} @cuetsy(kind="interface") @grafanamaturity(NeedsExpertReview)
// Maps special values like Null, NaN (not a number), and boolean values like true and false to a display text and color.
// See SpecialValueMatch to see the list of special values.
// For example, you can configure a special value mapping so that null values appear as N/A.
#SpecialValueMap: {
type: #MappingType & "special"
options: {
// Special value to match against
match: #SpecialValueMatch
// Config to apply when the value matches the special value
result: #ValueMappingResult
}
} @cuetsy(kind="interface") @grafanamaturity(NeedsExpertReview)
// Special value types supported by the `SpecialValueMap`
#SpecialValueMatch: "true" | "false" | "null" | "nan" | "null+nan" | "empty" @cuetsy(kind="enum",memberNames="True|False|Null|NaN|NullAndNan|Empty")
// Result used as replacement with text and color when the value matches
#ValueMappingResult: {
// Text to display when the value matches
text?: string
// Text to use when the value matches
color?: string
// Icon to display when the value matches. Only specific visualizations.
icon?: string
// Position in the mapping array. Only used internally.
index?: int32
} @cuetsy(kind="interface")
// Transformations allow to manipulate data returned by a query before the system applies a visualization.
// Using transformations you can: rename fields, join time series data, perform mathematical operations across queries,
// use the output of one transformation as the input to another transformation, etc.
#DataTransformerConfig: {
// Unique identifier of transformer
id: string
// Disabled transformations are skipped
disabled?: bool
// Optional frame matcher. When missing it will be applied to all results
filter?: #MatcherConfig
// Where to pull DataFrames from as input to transformation
topic?: "series" | "annotations" | "alertStates" // replaced with common.DataTopic
// Options to be passed to the transformer
// Valid options depend on the transformer id
options: _
} @cuetsy(kind="interface") @grafana(TSVeneer="type")
// Counterpart for TypeScript's TimeOption type.
#TimeOption: {
display: string
from: string
to: string
} @cuetsy(kind="interface") @grafana(TSVeneer="type")
// Time picker configuration
// It defines the default config for the time picker and the refresh picker for the specific dashboard.
#TimePickerConfig: {
// Whether timepicker is visible or not.
hidden?: bool | *false
// Interval options available in the refresh picker dropdown.
refresh_intervals?: [...string] | *["5s", "10s", "30s", "1m", "5m", "15m", "30m", "1h", "2h", "1d"]
// Quick ranges for time picker.
quick_ranges?: [...#TimeOption]
// Override the now time by entering a time delay. Use this option to accommodate known delays in data aggregation to avoid null values.
nowDelay?: string
} @cuetsy(kind="interface") @grafana(TSVeneer="type")
// 0 for no shared crosshair or tooltip (default).
// 1 for shared crosshair.
// 2 for shared crosshair AND shared tooltip.
#DashboardCursorSync: *0 | 1 | 2 @cuetsy(kind="enum",memberNames="Off|Crosshair|Tooltip")
// Schema for panel targets is specified by datasource
// plugins. We use a placeholder definition, which the Go
// schema loader either left open/as-is with the Base
// variant of the Dashboard and Panel families, or filled
// with types derived from plugins in the Instance variant.
// When working directly from CUE, importers can extend this
// type directly to achieve the same effect.
#Target: {...}
// A dashboard snapshot shares an interactive dashboard publicly.
// It is a read-only version of a dashboard, and is not editable.
// It is possible to create a snapshot of a snapshot.
// Grafana strips away all sensitive information from the dashboard.
// Sensitive information stripped: queries (metric, template,annotation) and panel links.
#Snapshot: {
// Time when the snapshot was created
created: string & t.Time
// Time when the snapshot expires, default is never to expire
expires: string @grafanamaturity(NeedsExpertReview)
// Is the snapshot saved in an external grafana instance
external: bool @grafanamaturity(NeedsExpertReview)
// external url, if snapshot was shared in external grafana instance
externalUrl: string @grafanamaturity(NeedsExpertReview)
// original url, url of the dashboard that was snapshotted
originalUrl: string @grafanamaturity(NeedsExpertReview)
// Unique identifier of the snapshot
id: uint32 @grafanamaturity(NeedsExpertReview)
// Optional, defined the unique key of the snapshot, required if external is true
key: string @grafanamaturity(NeedsExpertReview)
// Optional, name of the snapshot
name: string @grafanamaturity(NeedsExpertReview)
// org id of the snapshot
orgId: uint32 @grafanamaturity(NeedsExpertReview)
// last time when the snapshot was updated
updated: string & t.Time
// url of the snapshot, if snapshot was shared internally
url?: string @grafanamaturity(NeedsExpertReview)
// user id of the snapshot creator
userId: uint32 @grafanamaturity(NeedsExpertReview)
} @grafanamaturity(NeedsExpertReview)
// Dashboard panels are the basic visualization building blocks.
#Panel: {
// The panel plugin type id. This is used to find the plugin to display the panel.
type: string & strings.MinRunes(1)
// Unique identifier of the panel. Generated by Grafana when creating a new panel. It must be unique within a dashboard, but not globally.
id?: uint32
// The version of the plugin that is used for this panel. This is used to find the plugin to display the panel and to migrate old panel configs.
pluginVersion?: string
// Depends on the panel plugin. See the plugin documentation for details.
targets?: [...#Target]
// Panel title.
title?: string
// Panel description.
description?: string
// Whether to display the panel without a background.
transparent?: bool | *false
// The datasource used in all targets.
datasource?: #DataSourceRef
// Grid position.
gridPos?: #GridPos
// Panel links.
links?: [...#DashboardLink]
// Name of template variable to repeat for.
repeat?: string
// Direction to repeat in if 'repeat' is set.
// `h` for horizontal, `v` for vertical.
repeatDirection?: *"h" | "v"
// Option for repeated panels that controls max items per row
// Only relevant for horizontally repeated panels
maxPerRow?: number
// The maximum number of data points that the panel queries are retrieving.
maxDataPoints?: number
// List of transformations that are applied to the panel data before rendering.
// When there are multiple transformations, Grafana applies them in the order they are listed.
// Each transformation creates a result set that then passes on to the next transformation in the processing pipeline.
transformations?: [...#DataTransformerConfig]
// The min time interval setting defines a lower limit for the $__interval and $__interval_ms variables.
// This value must be formatted as a number followed by a valid time
// identifier like: "40s", "3d", etc.
// See: https://grafana.com/docs/grafana/latest/panels-visualizations/query-transform-data/#query-options
interval?: string
// Overrides the relative time range for individual panels,
// which causes them to be different than what is selected in
// the dashboard time picker in the top-right corner of the dashboard. You can use this to show metrics from different
// time periods or days on the same dashboard.
// The value is formatted as time operation like: `now-5m` (Last 5 minutes), `now/d` (the day so far),
// `now-5d/d`(Last 5 days), `now/w` (This week so far), `now-2y/y` (Last 2 years).
// Note: Panel time overrides have no effect when the dashboard’s time range is absolute.
// See: https://grafana.com/docs/grafana/latest/panels-visualizations/query-transform-data/#query-options
timeFrom?: string
// Overrides the time range for individual panels by shifting its start and end relative to the time picker.
// For example, you can shift the time range for the panel to be two hours earlier than the dashboard time picker setting `2h`.
// Note: Panel time overrides have no effect when the dashboard’s time range is absolute.
// See: https://grafana.com/docs/grafana/latest/panels-visualizations/query-transform-data/#query-options
timeShift?: string
// Controls if the timeFrom or timeShift overrides are shown in the panel header
hideTimeOverride?: bool
// Dynamically load the panel
libraryPanel?: #LibraryPanelRef
// Sets panel queries cache timeout.
cacheTimeout?: string
// Overrides the data source configured time-to-live for a query cache item in milliseconds
queryCachingTTL?: number
// It depends on the panel plugin. They are specified by the Options field in panel plugin schemas.
options?: {...} @grafanamaturity(NeedsExpertReview)
// Field options allow you to change how the data is displayed in your visualizations.
fieldConfig?: #FieldConfigSource
} @cuetsy(kind="interface") @grafana(TSVeneer="type") @grafanamaturity(NeedsExpertReview)
// The data model used in Grafana, namely the data frame, is a columnar-oriented table structure that unifies both time series and table query results.
// Each column within this structure is called a field. A field can represent a single time series or table column.
// Field options allow you to change how the data is displayed in your visualizations.
#FieldConfigSource: {
// Defaults are the options applied to all fields.
defaults: #FieldConfig
// Overrides are the options applied to specific fields overriding the defaults.
overrides: [...{
matcher: #MatcherConfig
properties: [...#DynamicConfigValue]
}] @grafanamaturity(NeedsExpertReview)
} @cuetsy(kind="interface") @grafana(TSVeneer="type") @grafanamaturity(NeedsExpertReview)
// A library panel is a reusable panel that you can use in any dashboard.
// When you make a change to a library panel, that change propagates to all instances of where the panel is used.
// Library panels streamline reuse of panels across multiple dashboards.
#LibraryPanelRef: {
// Library panel name
name: string
// Library panel uid
uid: string
} @cuetsy(kind="interface")
// Matcher is a predicate configuration. Based on the config a set of field(s) or values is filtered in order to apply override / transformation.
// It comes with in id ( to resolve implementation from registry) and a configuration that’s specific to a particular matcher type.
#MatcherConfig: {
// The matcher id. This is used to find the matcher implementation from registry.
id: string | *"" @grafanamaturity(NeedsExpertReview)
// The matcher options. This is specific to the matcher implementation.
options?: _ @grafanamaturity(NeedsExpertReview)
} @cuetsy(kind="interface") @grafana(TSVeneer="type")
#DynamicConfigValue: {
id: string | *"" @grafanamaturity(NeedsExpertReview)
value?: _ @grafanamaturity(NeedsExpertReview)
}
// The data model used in Grafana, namely the data frame, is a columnar-oriented table structure that unifies both time series and table query results.
// Each column within this structure is called a field. A field can represent a single time series or table column.
// Field options allow you to change how the data is displayed in your visualizations.
#FieldConfig: {
// The display value for this field. This supports template variables blank is auto
displayName?: string @grafanamaturity(NeedsExpertReview)
// This can be used by data sources that return and explicit naming structure for values and labels
// When this property is configured, this value is used rather than the default naming strategy.
displayNameFromDS?: string @grafanamaturity(NeedsExpertReview)
// Human readable field metadata
description?: string @grafanamaturity(NeedsExpertReview)
// An explicit path to the field in the datasource. When the frame meta includes a path,
// This will default to `${frame.meta.path}/${field.name}
//
// When defined, this value can be used as an identifier within the datasource scope, and
// may be used to update the results
path?: string @grafanamaturity(NeedsExpertReview)
// True if data source can write a value to the path. Auth/authz are supported separately
writeable?: bool @grafanamaturity(NeedsExpertReview)
// True if data source field supports ad-hoc filters
filterable?: bool @grafanamaturity(NeedsExpertReview)
// Unit a field should use. The unit you select is applied to all fields except time.
// You can use the units ID availables in Grafana or a custom unit.
// Available units in Grafana: https://github.com/grafana/grafana/blob/main/packages/grafana-data/src/valueFormats/categories.ts
// As custom unit, you can use the following formats:
// `suffix:<suffix>` for custom unit that should go after value.
// `prefix:<prefix>` for custom unit that should go before value.
// `time:<format>` For custom date time formats type for example `time:YYYY-MM-DD`.
// `si:<base scale><unit characters>` for custom SI units. For example: `si: mF`. This one is a bit more advanced as you can specify both a unit and the source data scale. So if your source data is represented as milli (thousands of) something prefix the unit with that SI scale character.
// `count:<unit>` for a custom count unit.
// `currency:<unit>` for custom a currency unit.
unit?: string @grafanamaturity(NeedsExpertReview)
// Specify the number of decimals Grafana includes in the rendered value.
// If you leave this field blank, Grafana automatically truncates the number of decimals based on the value.
// For example 1.1234 will display as 1.12 and 100.456 will display as 100.
// To display all decimals, set the unit to `String`.
decimals?: number @grafanamaturity(NeedsExpertReview)
// The minimum value used in percentage threshold calculations. Leave blank for auto calculation based on all series and fields.
min?: number @grafanamaturity(NeedsExpertReview)
// The maximum value used in percentage threshold calculations. Leave blank for auto calculation based on all series and fields.
max?: number @grafanamaturity(NeedsExpertReview)
// Convert input values into a display string
mappings?: [...#ValueMapping] @grafanamaturity(NeedsExpertReview)
// Map numeric values to states
thresholds?: #ThresholdsConfig @grafanamaturity(NeedsExpertReview)
// Panel color configuration
color?: #FieldColor
// The behavior when clicking on a result
links?: [...] @grafanamaturity(NeedsExpertReview)
// Alternative to empty string
noValue?: string @grafanamaturity(NeedsExpertReview)
// custom is specified by the FieldConfig field
// in panel plugin schemas.
custom?: {...} @grafanamaturity(NeedsExpertReview)
} @cuetsy(kind="interface") @grafana(TSVeneer="type") @grafanamaturity(NeedsExpertReview)
// Row panel
#RowPanel: {
// The panel type
type: "row"
// Whether this row should be collapsed or not.
collapsed: bool | *false
// Row title
title?: string
// Name of default datasource for the row
datasource?: #DataSourceRef
// Row grid position
gridPos?: #GridPos
// Unique identifier of the panel. Generated by Grafana when creating a new panel. It must be unique within a dashboard, but not globally.
id: uint32
// List of panels in the row
panels: [...#Panel]
// Name of template variable to repeat for.
repeat?: string
} @cuetsy(kind="interface") @grafana(TSVeneer="type")
}
},
]

@ -0,0 +1,90 @@
package v0alpha1
import (
_ "embed"
json "encoding/json"
fmt "fmt"
"strings"
"sync"
"github.com/grafana/grafana/apps/dashboard/pkg/migration/schemaversion"
"k8s.io/apimachinery/pkg/util/validation/field"
"cuelang.org/go/cue"
"cuelang.org/go/cue/cuecontext"
"cuelang.org/go/cue/errors"
cuejson "cuelang.org/go/encoding/json"
)
func ValidateDashboardSpec(obj *Dashboard, forceValidation bool) (field.ErrorList, field.ErrorList) {
var schemaVersionError field.ErrorList
schemaVersion := schemaversion.GetSchemaVersion(obj.Spec.Object)
if schemaVersion != schemaversion.LATEST_VERSION {
schemaVersionError = field.ErrorList{field.Invalid(field.NewPath("spec", "schemaVersion"), field.OmitValueType{}, fmt.Sprintf("Schema version %d is not supported - please upgrade to %d", schemaVersion, schemaversion.LATEST_VERSION))}
if !forceValidation {
return nil, schemaVersionError
}
}
data, err := json.Marshal(obj.Spec.Object)
if err != nil {
return field.ErrorList{
field.Invalid(field.NewPath("spec"), field.OmitValueType{}, err.Error()),
}, schemaVersionError
}
if err := cuejson.Validate(data, getCueSchema()); err != nil {
errs := field.ErrorList{}
for _, e := range errors.Errors(err) {
if
// We don't want to return confusing "empty disjunction" errors,
// because the users don't necessarily understand what to do with them.
// For empty disjunctions, CUE will also return more specific errors,
// so we can safely ignore the generic ones.
strings.Contains(e.Error(), "disjunction") ||
// We don't want to return errors about unknown fields either.
strings.Contains(e.Error(), "field not allowed") {
continue
}
// We want to manually format the error message,
// because e.Error() contains the full CUE path.
format, args := e.Msg()
errs = append(errs, field.Invalid(
field.NewPath(formatErrorPath(e.Path())),
field.OmitValueType{},
fmt.Sprintf(format, args...),
))
}
return errs, schemaVersionError
}
return nil, schemaVersionError
}
func formatErrorPath(path []string) string {
// omitting the "lineage.schemas[0].schema.spec" prefix here.
return strings.Join(path[4:], ".")
}
var (
compiledSchema cue.Value
getSchemaOnce sync.Once
)
//go:embed dashboard_kind.cue
var schemaSource string
func getCueSchema() cue.Value {
getSchemaOnce.Do(func() {
cueCtx := cuecontext.New()
compiledSchema = cueCtx.CompileString(schemaSource).LookupPath(
cue.ParsePath("lineage.schemas[0].schema.spec"),
)
})
return compiledSchema
}

@ -0,0 +1,759 @@
// This file is managed by Grafana - DO NOT EDIT MANUALLY
// Source: kinds/dashboard/dashboard_kind.cue
// To sync changes, run: make gen-cue
package kind
import (
"strings"
t "time"
)
name: "Dashboard"
maturity: "experimental"
description: "A Grafana dashboard."
crd: dummySchema: true
lineage: schemas: [{
version: [0, 0]
schema: {
spec: {
// Unique numeric identifier for the dashboard.
// `id` is internal to a specific Grafana instance. `uid` should be used to identify a dashboard across Grafana instances.
id?: int64 | null // TODO eliminate this null option
// Unique dashboard identifier that can be generated by anyone. string (8-40)
uid?: string
// Title of dashboard.
title?: string
// Description of dashboard.
description?: string
// This property should only be used in dashboards defined by plugins. It is a quick check
// to see if the version has changed since the last time.
revision?: int64
// ID of a dashboard imported from the https://grafana.com/grafana/dashboards/ portal
gnetId?: string
// Tags associated with dashboard.
tags?: [...string]
// Timezone of dashboard. Accepted values are IANA TZDB zone ID or "browser" or "utc".
timezone?: string | *"browser"
// Whether a dashboard is editable or not.
editable?: bool | *true
// Configuration of dashboard cursor sync behavior.
// Accepted values are 0 (sync turned off), 1 (shared crosshair), 2 (shared crosshair and tooltip).
graphTooltip?: #DashboardCursorSync
// Time range for dashboard.
// Accepted values are relative time strings like {from: 'now-6h', to: 'now'} or absolute time strings like {from: '2020-07-10T08:00:00.000Z', to: '2020-07-10T14:00:00.000Z'}.
time?: {
from: string | *"now-6h"
to: string | *"now"
}
// Configuration of the time picker shown at the top of a dashboard.
timepicker?: #TimePickerConfig
// The month that the fiscal year starts on. 0 = January, 11 = December
fiscalYearStartMonth?: uint8 & <12 | *0
// When set to true, the dashboard will redraw panels at an interval matching the pixel width.
// This will keep data "moving left" regardless of the query refresh rate. This setting helps
// avoid dashboards presenting stale live data
liveNow?: bool
// Day when the week starts. Expressed by the name of the day in lowercase, e.g. "monday".
weekStart?: string
// Refresh rate of dashboard. Represented via interval string, e.g. "5s", "1m", "1h", "1d".
refresh?: string
// Version of the JSON schema, incremented each time a Grafana update brings
// changes to said schema.
schemaVersion: uint16 | *41
// Version of the dashboard, incremented each time the dashboard is updated.
version?: uint32
// List of dashboard panels
panels?: [...(#Panel | #RowPanel)]
// Configured template variables
templating?: {
// List of configured template variables with their saved values along with some other metadata
list?: [...#VariableModel]
}
// Contains the list of annotations that are associated with the dashboard.
// Annotations are used to overlay event markers and overlay event tags on graphs.
// Grafana comes with a native annotation store and the ability to add annotation events directly from the graph panel or via the HTTP API.
// See https://grafana.com/docs/grafana/latest/dashboards/build-dashboards/annotate-visualizations/
annotations?: #AnnotationContainer
// Links with references to other dashboards or external websites.
links?: [...#DashboardLink]
// Snapshot options. They are present only if the dashboard is a snapshot.
snapshot?: #Snapshot @grafanamaturity(NeedsExpertReview)
// When set to true, the dashboard will load all panels in the dashboard when it's loaded.
preload?: bool
} @cuetsy(kind="interface") @grafana(TSVeneer="type")
///////////////////////////////////////
// Definitions (referenced above) are declared below
// TODO: this should be a regular DataQuery that depends on the selected dashboard
// these match the properties of the "grafana" datasouce that is default in most dashboards
#AnnotationTarget: {
// Only required/valid for the grafana datasource...
// but code+tests is already depending on it so hard to change
limit: int64
// Only required/valid for the grafana datasource...
// but code+tests is already depending on it so hard to change
matchAny: bool
// Only required/valid for the grafana datasource...
// but code+tests is already depending on it so hard to change
tags: [...string]
// Only required/valid for the grafana datasource...
// but code+tests is already depending on it so hard to change
type: string
... // datasource will stick their raw DataQuery here
} @cuetsy(kind="interface") @grafanamaturity(NeedsExpertReview)
#AnnotationPanelFilter: {
// Should the specified panels be included or excluded
exclude?: bool | *false
// Panel IDs that should be included or excluded
ids: [...uint8]
} @cuetsy(kind="interface")
// Contains the list of annotations that are associated with the dashboard.
// Annotations are used to overlay event markers and overlay event tags on graphs.
// Grafana comes with a native annotation store and the ability to add annotation events directly from the graph panel or via the HTTP API.
// See https://grafana.com/docs/grafana/latest/dashboards/build-dashboards/annotate-visualizations/
#AnnotationContainer: {
// List of annotations
list?: [...#AnnotationQuery]
} @cuetsy(kind="interface") @grafana(TSVeneer="type")
// TODO docs
// FROM: AnnotationQuery in grafana-data/src/types/annotations.ts
#AnnotationQuery: {
// Name of annotation.
name: string
// Datasource where the annotations data is
datasource: #DataSourceRef
// When enabled the annotation query is issued with every dashboard refresh
enable: bool | *true
// Annotation queries can be toggled on or off at the top of the dashboard.
// When hide is true, the toggle is not shown in the dashboard.
hide?: bool | *false
// Color to use for the annotation event markers
iconColor: string
// Filters to apply when fetching annotations
filter?: #AnnotationPanelFilter
// TODO.. this should just be a normal query target
target?: #AnnotationTarget
// TODO -- this should not exist here, it is based on the --grafana-- datasource
type?: string @grafanamaturity(NeedsExpertReview)
// Set to 1 for the standard annotation query all dashboards have by default.
builtIn?: number | *0
// unless datasources have migrated to the target+mapping,
// they just spread their query into the base object :(
...
} @cuetsy(kind="interface") @grafana(TSVeneer="type") @grafanamaturity(NeedsExpertReview)
// A variable is a placeholder for a value. You can use variables in metric queries and in panel titles.
#VariableModel: {
// Type of variable
type: #VariableType
// Name of variable
name: string
// Optional display name
label?: string
// Visibility configuration for the variable
hide?: #VariableHide
// Whether the variable value should be managed by URL query params or not
skipUrlSync?: bool | *false
// Description of variable. It can be defined but `null`.
description?: string
// Query used to fetch values for a variable
query?: string | {...}
// Data source used to fetch values for a variable. It can be defined but `null`.
datasource?: #DataSourceRef
// Shows current selected variable text/value on the dashboard
current?: #VariableOption
// Whether multiple values can be selected or not from variable value list
multi?: bool | *false
// Allow custom values to be entered in the variable
allowCustomValue?: bool | *true
// Options that can be selected for a variable.
options?: [...#VariableOption]
// Options to config when to refresh a variable
refresh?: #VariableRefresh
// Options sort order
sort?: #VariableSort
// Whether all value option is available or not
includeAll?: bool | *false
// Custom all value
allValue?: string
// Optional field, if you want to extract part of a series name or metric node segment.
// Named capture groups can be used to separate the display text and value.
regex?: string
...
} @cuetsy(kind="interface") @grafana(TSVeneer="type") @grafanamaturity(NeedsExpertReview)
// Option to be selected in a variable.
#VariableOption: {
// Whether the option is selected or not
selected?: bool
// Text to be displayed for the option
text: string | [...string]
// Value of the option
value: string | [...string]
} @cuetsy(kind="interface")
// Options to config when to refresh a variable
// `0`: Never refresh the variable
// `1`: Queries the data source every time the dashboard loads.
// `2`: Queries the data source when the dashboard time range changes.
#VariableRefresh: 0 | 1 | 2 @cuetsy(kind="enum",memberNames="never|onDashboardLoad|onTimeRangeChanged")
// Determine if the variable shows on dashboard
// Accepted values are 0 (show label and value), 1 (show value only), 2 (show nothing).
#VariableHide: 0 | 1 | 2 @cuetsy(kind="enum",memberNames="dontHide|hideLabel|hideVariable") @grafana(TSVeneer="type")
// Sort variable options
// Accepted values are:
// `0`: No sorting
// `1`: Alphabetical ASC
// `2`: Alphabetical DESC
// `3`: Numerical ASC
// `4`: Numerical DESC
// `5`: Alphabetical Case Insensitive ASC
// `6`: Alphabetical Case Insensitive DESC
// `7`: Natural ASC
// `8`: Natural DESC
#VariableSort: 0 | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 @cuetsy(kind="enum",memberNames="disabled|alphabeticalAsc|alphabeticalDesc|numericalAsc|numericalDesc|alphabeticalCaseInsensitiveAsc|alphabeticalCaseInsensitiveDesc|naturalAsc|naturalDesc")
// Ref to a DataSource instance
#DataSourceRef: {
// The plugin type-id
type?: string
// Specific datasource instance
uid?: string
} @cuetsy(kind="interface") @grafana(TSVeneer="type")
// Links with references to other dashboards or external resources
#DashboardLink: {
// Title to display with the link
title: string
// Link type. Accepted values are dashboards (to refer to another dashboard) and link (to refer to an external resource)
type: #DashboardLinkType
// Icon name to be displayed with the link
icon: string
// Tooltip to display when the user hovers their mouse over it
tooltip: string
// Link URL. Only required/valid if the type is link
url?: string
// List of tags to limit the linked dashboards. If empty, all dashboards will be displayed. Only valid if the type is dashboards
tags: [...string]
// If true, all dashboards links will be displayed in a dropdown. If false, all dashboards links will be displayed side by side. Only valid if the type is dashboards
asDropdown: bool | *false
// If true, the link will be opened in a new tab
targetBlank: bool | *false
// If true, includes current template variables values in the link as query params
includeVars: bool | *false
// If true, includes current time range in the link as query params
keepTime: bool | *false
} @cuetsy(kind="interface")
// Dashboard Link type. Accepted values are dashboards (to refer to another dashboard) and link (to refer to an external resource)
#DashboardLinkType: "link" | "dashboards" @cuetsy(kind="type")
// Dashboard variable type
// `query`: Query-generated list of values such as metric names, server names, sensor IDs, data centers, and so on.
// `adhoc`: Key/value filters that are automatically added to all metric queries for a data source (Prometheus, Loki, InfluxDB, and Elasticsearch only).
// `constant`: Define a hidden constant.
// `datasource`: Quickly change the data source for an entire dashboard.
// `interval`: Interval variables represent time spans.
// `textbox`: Display a free text input field with an optional default value.
// `custom`: Define the variable options manually using a comma-separated list.
// `system`: Variables defined by Grafana. See: https://grafana.com/docs/grafana/latest/dashboards/variables/add-template-variables/#global-variables
#VariableType: "query" | "adhoc" | "groupby" | "constant" | "datasource" | "interval" | "textbox" | "custom" |
"system" | "snapshot" @cuetsy(kind="type") @grafanamaturity(NeedsExpertReview)
// Color mode for a field. You can specify a single color, or select a continuous (gradient) color schemes, based on a value.
// Continuous color interpolates a color using the percentage of a value relative to min and max.
// Accepted values are:
// `thresholds`: From thresholds. Informs Grafana to take the color from the matching threshold
// `palette-classic`: Classic palette. Grafana will assign color by looking up a color in a palette by series index. Useful for Graphs and pie charts and other categorical data visualizations
// `palette-classic-by-name`: Classic palette (by name). Grafana will assign color by looking up a color in a palette by series name. Useful for Graphs and pie charts and other categorical data visualizations
// `continuous-GrYlRd`: ontinuous Green-Yellow-Red palette mode
// `continuous-RdYlGr`: Continuous Red-Yellow-Green palette mode
// `continuous-BlYlRd`: Continuous Blue-Yellow-Red palette mode
// `continuous-YlRd`: Continuous Yellow-Red palette mode
// `continuous-BlPu`: Continuous Blue-Purple palette mode
// `continuous-YlBl`: Continuous Yellow-Blue palette mode
// `continuous-blues`: Continuous Blue palette mode
// `continuous-reds`: Continuous Red palette mode
// `continuous-greens`: Continuous Green palette mode
// `continuous-purples`: Continuous Purple palette mode
// `shades`: Shades of a single color. Specify a single color, useful in an override rule.
// `fixed`: Fixed color mode. Specify a single color, useful in an override rule.
#FieldColorModeId: "thresholds" | "palette-classic" | "palette-classic-by-name" | "continuous-GrYlRd" | "continuous-RdYlGr" | "continuous-BlYlRd" | "continuous-YlRd" | "continuous-BlPu" | "continuous-YlBl" | "continuous-blues" | "continuous-reds" | "continuous-greens" | "continuous-purples" | "fixed" | "shades" @cuetsy(kind="enum",memberNames="Thresholds|PaletteClassic|PaletteClassicByName|ContinuousGrYlRd|ContinuousRdYlGr|ContinuousBlYlRd|ContinuousYlRd|ContinuousBlPu|ContinuousYlBl|ContinuousBlues|ContinuousReds|ContinuousGreens|ContinuousPurples|Fixed|Shades") @grafanamaturity(NeedsExpertReview)
// Defines how to assign a series color from "by value" color schemes. For example for an aggregated data points like a timeseries, the color can be assigned by the min, max or last value.
#FieldColorSeriesByMode: "min" | "max" | "last" @cuetsy(kind="type")
// Map a field to a color.
#FieldColor: {
// The main color scheme mode.
mode: #FieldColorModeId
// The fixed color value for fixed or shades color modes.
fixedColor?: string
// Some visualizations need to know how to assign a series color from by value color schemes.
seriesBy?: #FieldColorSeriesByMode
} @cuetsy(kind="interface")
// Position and dimensions of a panel in the grid
#GridPos: {
// Panel height. The height is the number of rows from the top edge of the panel.
h: uint32 & >0 | *9
// Panel width. The width is the number of columns from the left edge of the panel.
w: uint32 & >0 & <=24 | *12
// Panel x. The x coordinate is the number of columns from the left edge of the grid
x: uint32 & >=0 & <24 | *0
// Panel y. The y coordinate is the number of rows from the top edge of the grid
y: uint32 & >=0 | *0
// Whether the panel is fixed within the grid. If true, the panel will not be affected by other panels' interactions
static?: bool
} @cuetsy(kind="interface")
// User-defined value for a metric that triggers visual changes in a panel when this value is met or exceeded
// They are used to conditionally style and color visualizations based on query results , and can be applied to most visualizations.
#Threshold: {
// Value represents a specified metric for the threshold, which triggers a visual change in the dashboard when this value is met or exceeded.
// Nulls currently appear here when serializing -Infinity to JSON.
value: number | null @grafanamaturity(NeedsExpertReview)
// Color represents the color of the visual change that will occur in the dashboard when the threshold value is met or exceeded.
color: string @grafanamaturity(NeedsExpertReview)
} @cuetsy(kind="interface") @grafanamaturity(NeedsExpertReview)
// Thresholds can either be `absolute` (specific number) or `percentage` (relative to min or max, it will be values between 0 and 1).
#ThresholdsMode: "absolute" | "percentage" @cuetsy(kind="enum",memberNames="Absolute|Percentage")
// Thresholds configuration for the panel
#ThresholdsConfig: {
// Thresholds mode.
mode: #ThresholdsMode
// Must be sorted by 'value', first value is always -Infinity
steps: [...#Threshold] @grafanamaturity(NeedsExpertReview)
} @cuetsy(kind="interface") @grafanamaturity(NeedsExpertReview)
// Allow to transform the visual representation of specific data values in a visualization, irrespective of their original units
#ValueMapping: #ValueMap | #RangeMap | #RegexMap | #SpecialValueMap @cuetsy(kind="type") @grafanamaturity(NeedsExpertReview)
// Supported value mapping types
// `value`: Maps text values to a color or different display text and color. For example, you can configure a value mapping so that all instances of the value 10 appear as Perfection! rather than the number.
// `range`: Maps numerical ranges to a display text and color. For example, if a value is within a certain range, you can configure a range value mapping to display Low or High rather than the number.
// `regex`: Maps regular expressions to replacement text and a color. For example, if a value is www.example.com, you can configure a regex value mapping so that Grafana displays www and truncates the domain.
// `special`: Maps special values like Null, NaN (not a number), and boolean values like true and false to a display text and color. See SpecialValueMatch to see the list of special values. For example, you can configure a special value mapping so that null values appear as N/A.
#MappingType: "value" | "range" | "regex" | "special" @cuetsy(kind="enum",memberNames="ValueToText|RangeToText|RegexToText|SpecialValue") @grafanamaturity(NeedsExpertReview)
// Maps text values to a color or different display text and color.
// For example, you can configure a value mapping so that all instances of the value 10 appear as Perfection! rather than the number.
#ValueMap: {
type: #MappingType & "value"
// Map with <value_to_match>: ValueMappingResult. For example: { "10": { text: "Perfection!", color: "green" } }
options: [string]: #ValueMappingResult
} @cuetsy(kind="interface")
// Maps numerical ranges to a display text and color.
// For example, if a value is within a certain range, you can configure a range value mapping to display Low or High rather than the number.
#RangeMap: {
type: #MappingType & "range"
// Range to match against and the result to apply when the value is within the range
options: {
// Min value of the range. It can be null which means -Infinity
from: float64 | null
// Max value of the range. It can be null which means +Infinity
to: float64 | null
// Config to apply when the value is within the range
result: #ValueMappingResult
}
} @cuetsy(kind="interface") @grafanamaturity(NeedsExpertReview)
// Maps regular expressions to replacement text and a color.
// For example, if a value is www.example.com, you can configure a regex value mapping so that Grafana displays www and truncates the domain.
#RegexMap: {
type: #MappingType & "regex"
// Regular expression to match against and the result to apply when the value matches the regex
options: {
// Regular expression to match against
pattern: string
// Config to apply when the value matches the regex
result: #ValueMappingResult
}
} @cuetsy(kind="interface") @grafanamaturity(NeedsExpertReview)
// Maps special values like Null, NaN (not a number), and boolean values like true and false to a display text and color.
// See SpecialValueMatch to see the list of special values.
// For example, you can configure a special value mapping so that null values appear as N/A.
#SpecialValueMap: {
type: #MappingType & "special"
options: {
// Special value to match against
match: #SpecialValueMatch
// Config to apply when the value matches the special value
result: #ValueMappingResult
}
} @cuetsy(kind="interface") @grafanamaturity(NeedsExpertReview)
// Special value types supported by the `SpecialValueMap`
#SpecialValueMatch: "true" | "false" | "null" | "nan" | "null+nan" | "empty" @cuetsy(kind="enum",memberNames="True|False|Null|NaN|NullAndNan|Empty")
// Result used as replacement with text and color when the value matches
#ValueMappingResult: {
// Text to display when the value matches
text?: string
// Text to use when the value matches
color?: string
// Icon to display when the value matches. Only specific visualizations.
icon?: string
// Position in the mapping array. Only used internally.
index?: int32
} @cuetsy(kind="interface")
// Transformations allow to manipulate data returned by a query before the system applies a visualization.
// Using transformations you can: rename fields, join time series data, perform mathematical operations across queries,
// use the output of one transformation as the input to another transformation, etc.
#DataTransformerConfig: {
// Unique identifier of transformer
id: string
// Disabled transformations are skipped
disabled?: bool
// Optional frame matcher. When missing it will be applied to all results
filter?: #MatcherConfig
// Where to pull DataFrames from as input to transformation
topic?: "series" | "annotations" | "alertStates" // replaced with common.DataTopic
// Options to be passed to the transformer
// Valid options depend on the transformer id
options: _
} @cuetsy(kind="interface") @grafana(TSVeneer="type")
// Counterpart for TypeScript's TimeOption type.
#TimeOption: {
display: string
from: string
to: string
} @cuetsy(kind="interface") @grafana(TSVeneer="type")
// Time picker configuration
// It defines the default config for the time picker and the refresh picker for the specific dashboard.
#TimePickerConfig: {
// Whether timepicker is visible or not.
hidden?: bool | *false
// Interval options available in the refresh picker dropdown.
refresh_intervals?: [...string] | *["5s", "10s", "30s", "1m", "5m", "15m", "30m", "1h", "2h", "1d"]
// Quick ranges for time picker.
quick_ranges?: [...#TimeOption]
// Override the now time by entering a time delay. Use this option to accommodate known delays in data aggregation to avoid null values.
nowDelay?: string
} @cuetsy(kind="interface") @grafana(TSVeneer="type")
// 0 for no shared crosshair or tooltip (default).
// 1 for shared crosshair.
// 2 for shared crosshair AND shared tooltip.
#DashboardCursorSync: *0 | 1 | 2 @cuetsy(kind="enum",memberNames="Off|Crosshair|Tooltip")
// Schema for panel targets is specified by datasource
// plugins. We use a placeholder definition, which the Go
// schema loader either left open/as-is with the Base
// variant of the Dashboard and Panel families, or filled
// with types derived from plugins in the Instance variant.
// When working directly from CUE, importers can extend this
// type directly to achieve the same effect.
#Target: {...}
// A dashboard snapshot shares an interactive dashboard publicly.
// It is a read-only version of a dashboard, and is not editable.
// It is possible to create a snapshot of a snapshot.
// Grafana strips away all sensitive information from the dashboard.
// Sensitive information stripped: queries (metric, template,annotation) and panel links.
#Snapshot: {
// Time when the snapshot was created
created: string & t.Time
// Time when the snapshot expires, default is never to expire
expires: string @grafanamaturity(NeedsExpertReview)
// Is the snapshot saved in an external grafana instance
external: bool @grafanamaturity(NeedsExpertReview)
// external url, if snapshot was shared in external grafana instance
externalUrl: string @grafanamaturity(NeedsExpertReview)
// original url, url of the dashboard that was snapshotted
originalUrl: string @grafanamaturity(NeedsExpertReview)
// Unique identifier of the snapshot
id: uint32 @grafanamaturity(NeedsExpertReview)
// Optional, defined the unique key of the snapshot, required if external is true
key: string @grafanamaturity(NeedsExpertReview)
// Optional, name of the snapshot
name: string @grafanamaturity(NeedsExpertReview)
// org id of the snapshot
orgId: uint32 @grafanamaturity(NeedsExpertReview)
// last time when the snapshot was updated
updated: string & t.Time
// url of the snapshot, if snapshot was shared internally
url?: string @grafanamaturity(NeedsExpertReview)
// user id of the snapshot creator
userId: uint32 @grafanamaturity(NeedsExpertReview)
} @grafanamaturity(NeedsExpertReview)
// Dashboard panels are the basic visualization building blocks.
#Panel: {
// The panel plugin type id. This is used to find the plugin to display the panel.
type: string & strings.MinRunes(1)
// Unique identifier of the panel. Generated by Grafana when creating a new panel. It must be unique within a dashboard, but not globally.
id?: uint32
// The version of the plugin that is used for this panel. This is used to find the plugin to display the panel and to migrate old panel configs.
pluginVersion?: string
// Depends on the panel plugin. See the plugin documentation for details.
targets?: [...#Target]
// Panel title.
title?: string
// Panel description.
description?: string
// Whether to display the panel without a background.
transparent?: bool | *false
// The datasource used in all targets.
datasource?: #DataSourceRef
// Grid position.
gridPos?: #GridPos
// Panel links.
links?: [...#DashboardLink]
// Name of template variable to repeat for.
repeat?: string
// Direction to repeat in if 'repeat' is set.
// `h` for horizontal, `v` for vertical.
repeatDirection?: *"h" | "v"
// Option for repeated panels that controls max items per row
// Only relevant for horizontally repeated panels
maxPerRow?: number
// The maximum number of data points that the panel queries are retrieving.
maxDataPoints?: number
// List of transformations that are applied to the panel data before rendering.
// When there are multiple transformations, Grafana applies them in the order they are listed.
// Each transformation creates a result set that then passes on to the next transformation in the processing pipeline.
transformations?: [...#DataTransformerConfig]
// The min time interval setting defines a lower limit for the $__interval and $__interval_ms variables.
// This value must be formatted as a number followed by a valid time
// identifier like: "40s", "3d", etc.
// See: https://grafana.com/docs/grafana/latest/panels-visualizations/query-transform-data/#query-options
interval?: string
// Overrides the relative time range for individual panels,
// which causes them to be different than what is selected in
// the dashboard time picker in the top-right corner of the dashboard. You can use this to show metrics from different
// time periods or days on the same dashboard.
// The value is formatted as time operation like: `now-5m` (Last 5 minutes), `now/d` (the day so far),
// `now-5d/d`(Last 5 days), `now/w` (This week so far), `now-2y/y` (Last 2 years).
// Note: Panel time overrides have no effect when the dashboard’s time range is absolute.
// See: https://grafana.com/docs/grafana/latest/panels-visualizations/query-transform-data/#query-options
timeFrom?: string
// Overrides the time range for individual panels by shifting its start and end relative to the time picker.
// For example, you can shift the time range for the panel to be two hours earlier than the dashboard time picker setting `2h`.
// Note: Panel time overrides have no effect when the dashboard’s time range is absolute.
// See: https://grafana.com/docs/grafana/latest/panels-visualizations/query-transform-data/#query-options
timeShift?: string
// Controls if the timeFrom or timeShift overrides are shown in the panel header
hideTimeOverride?: bool
// Dynamically load the panel
libraryPanel?: #LibraryPanelRef
// Sets panel queries cache timeout.
cacheTimeout?: string
// Overrides the data source configured time-to-live for a query cache item in milliseconds
queryCachingTTL?: number
// It depends on the panel plugin. They are specified by the Options field in panel plugin schemas.
options?: {...} @grafanamaturity(NeedsExpertReview)
// Field options allow you to change how the data is displayed in your visualizations.
fieldConfig?: #FieldConfigSource
} @cuetsy(kind="interface") @grafana(TSVeneer="type") @grafanamaturity(NeedsExpertReview)
// The data model used in Grafana, namely the data frame, is a columnar-oriented table structure that unifies both time series and table query results.
// Each column within this structure is called a field. A field can represent a single time series or table column.
// Field options allow you to change how the data is displayed in your visualizations.
#FieldConfigSource: {
// Defaults are the options applied to all fields.
defaults: #FieldConfig
// Overrides are the options applied to specific fields overriding the defaults.
overrides: [...{
matcher: #MatcherConfig
properties: [...#DynamicConfigValue]
}] @grafanamaturity(NeedsExpertReview)
} @cuetsy(kind="interface") @grafana(TSVeneer="type") @grafanamaturity(NeedsExpertReview)
// A library panel is a reusable panel that you can use in any dashboard.
// When you make a change to a library panel, that change propagates to all instances of where the panel is used.
// Library panels streamline reuse of panels across multiple dashboards.
#LibraryPanelRef: {
// Library panel name
name: string
// Library panel uid
uid: string
} @cuetsy(kind="interface")
// Matcher is a predicate configuration. Based on the config a set of field(s) or values is filtered in order to apply override / transformation.
// It comes with in id ( to resolve implementation from registry) and a configuration that’s specific to a particular matcher type.
#MatcherConfig: {
// The matcher id. This is used to find the matcher implementation from registry.
id: string | *"" @grafanamaturity(NeedsExpertReview)
// The matcher options. This is specific to the matcher implementation.
options?: _ @grafanamaturity(NeedsExpertReview)
} @cuetsy(kind="interface") @grafana(TSVeneer="type")
#DynamicConfigValue: {
id: string | *"" @grafanamaturity(NeedsExpertReview)
value?: _ @grafanamaturity(NeedsExpertReview)
}
// The data model used in Grafana, namely the data frame, is a columnar-oriented table structure that unifies both time series and table query results.
// Each column within this structure is called a field. A field can represent a single time series or table column.
// Field options allow you to change how the data is displayed in your visualizations.
#FieldConfig: {
// The display value for this field. This supports template variables blank is auto
displayName?: string @grafanamaturity(NeedsExpertReview)
// This can be used by data sources that return and explicit naming structure for values and labels
// When this property is configured, this value is used rather than the default naming strategy.
displayNameFromDS?: string @grafanamaturity(NeedsExpertReview)
// Human readable field metadata
description?: string @grafanamaturity(NeedsExpertReview)
// An explicit path to the field in the datasource. When the frame meta includes a path,
// This will default to `${frame.meta.path}/${field.name}
//
// When defined, this value can be used as an identifier within the datasource scope, and
// may be used to update the results
path?: string @grafanamaturity(NeedsExpertReview)
// True if data source can write a value to the path. Auth/authz are supported separately
writeable?: bool @grafanamaturity(NeedsExpertReview)
// True if data source field supports ad-hoc filters
filterable?: bool @grafanamaturity(NeedsExpertReview)
// Unit a field should use. The unit you select is applied to all fields except time.
// You can use the units ID availables in Grafana or a custom unit.
// Available units in Grafana: https://github.com/grafana/grafana/blob/main/packages/grafana-data/src/valueFormats/categories.ts
// As custom unit, you can use the following formats:
// `suffix:<suffix>` for custom unit that should go after value.
// `prefix:<prefix>` for custom unit that should go before value.
// `time:<format>` For custom date time formats type for example `time:YYYY-MM-DD`.
// `si:<base scale><unit characters>` for custom SI units. For example: `si: mF`. This one is a bit more advanced as you can specify both a unit and the source data scale. So if your source data is represented as milli (thousands of) something prefix the unit with that SI scale character.
// `count:<unit>` for a custom count unit.
// `currency:<unit>` for custom a currency unit.
unit?: string @grafanamaturity(NeedsExpertReview)
// Specify the number of decimals Grafana includes in the rendered value.
// If you leave this field blank, Grafana automatically truncates the number of decimals based on the value.
// For example 1.1234 will display as 1.12 and 100.456 will display as 100.
// To display all decimals, set the unit to `String`.
decimals?: number @grafanamaturity(NeedsExpertReview)
// The minimum value used in percentage threshold calculations. Leave blank for auto calculation based on all series and fields.
min?: number @grafanamaturity(NeedsExpertReview)
// The maximum value used in percentage threshold calculations. Leave blank for auto calculation based on all series and fields.
max?: number @grafanamaturity(NeedsExpertReview)
// Convert input values into a display string
mappings?: [...#ValueMapping] @grafanamaturity(NeedsExpertReview)
// Map numeric values to states
thresholds?: #ThresholdsConfig @grafanamaturity(NeedsExpertReview)
// Panel color configuration
color?: #FieldColor
// The behavior when clicking on a result
links?: [...] @grafanamaturity(NeedsExpertReview)
// Alternative to empty string
noValue?: string @grafanamaturity(NeedsExpertReview)
// custom is specified by the FieldConfig field
// in panel plugin schemas.
custom?: {...} @grafanamaturity(NeedsExpertReview)
} @cuetsy(kind="interface") @grafana(TSVeneer="type") @grafanamaturity(NeedsExpertReview)
// Row panel
#RowPanel: {
// The panel type
type: "row"
// Whether this row should be collapsed or not.
collapsed: bool | *false
// Row title
title?: string
// Name of default datasource for the row
datasource?: #DataSourceRef
// Row grid position
gridPos?: #GridPos
// Unique identifier of the panel. Generated by Grafana when creating a new panel. It must be unique within a dashboard, but not globally.
id: uint32
// List of panels in the row
panels: [...#Panel]
// Name of template variable to repeat for.
repeat?: string
} @cuetsy(kind="interface") @grafana(TSVeneer="type")
}
},
]

@ -0,0 +1,90 @@
package v1alpha1
import (
_ "embed"
json "encoding/json"
fmt "fmt"
"strings"
"sync"
"k8s.io/apimachinery/pkg/util/validation/field"
"cuelang.org/go/cue"
"cuelang.org/go/cue/cuecontext"
"cuelang.org/go/cue/errors"
cuejson "cuelang.org/go/encoding/json"
"github.com/grafana/grafana/apps/dashboard/pkg/migration/schemaversion"
)
func ValidateDashboardSpec(obj *Dashboard, forceValidation bool) (field.ErrorList, field.ErrorList) {
var schemaVersionError field.ErrorList
schemaVersion := schemaversion.GetSchemaVersion(obj.Spec.Object)
if schemaVersion != schemaversion.LATEST_VERSION {
schemaVersionError = field.ErrorList{field.Invalid(field.NewPath("spec", "schemaVersion"), field.OmitValueType{}, fmt.Sprintf("Schema version %d is not supported - please upgrade to %d", schemaVersion, schemaversion.LATEST_VERSION))}
if !forceValidation {
return nil, schemaVersionError
}
}
data, err := json.Marshal(obj.Spec.Object)
if err != nil {
return field.ErrorList{
field.Invalid(field.NewPath("spec"), field.OmitValueType{}, err.Error()),
}, schemaVersionError
}
if err := cuejson.Validate(data, getCueSchema()); err != nil {
errs := field.ErrorList{}
for _, e := range errors.Errors(err) {
if
// We don't want to return confusing "empty disjunction" errors,
// because the users don't necessarily understand what to do with them.
// For empty disjunctions, CUE will also return more specific errors,
// so we can safely ignore the generic ones.
strings.Contains(e.Error(), "disjunction") ||
// We don't want to return errors about unknown fields either.
strings.Contains(e.Error(), "field not allowed") {
continue
}
// We want to manually format the error message,
// because e.Error() contains the full CUE path.
format, args := e.Msg()
errs = append(errs, field.Invalid(
field.NewPath(formatErrorPath(e.Path())),
field.OmitValueType{},
fmt.Sprintf(format, args...),
))
}
return errs, schemaVersionError
}
return nil, schemaVersionError
}
func formatErrorPath(path []string) string {
// omitting the "lineage.schemas[0].schema.spec" prefix here.
return strings.Join(path[4:], ".")
}
var (
compiledSchema cue.Value
getSchemaOnce sync.Once
)
//go:embed dashboard_kind.cue
var schemaSource string
func getCueSchema() cue.Value {
getSchemaOnce.Do(func() {
cueCtx := cuecontext.New()
compiledSchema = cueCtx.CompileString(schemaSource).LookupPath(
cue.ParsePath("lineage.schemas[0].schema.spec"),
)
})
return compiledSchema
}

@ -0,0 +1,965 @@
// This file is managed by grafana-app-sdk - DO NOT EDIT MANUALLY
// Source: apps/dashboard/kinds/v2alpha1/dashboard_spec.cue
// To sync changes, run: make generate in apps/dashboard
package v2alpha1
DashboardSpec: {
// Title of dashboard.
annotations: [...AnnotationQueryKind]
// Configuration of dashboard cursor sync behavior.
// "Off" for no shared crosshair or tooltip (default).
// "Crosshair" for shared crosshair.
// "Tooltip" for shared crosshair AND shared tooltip.
cursorSync: DashboardCursorSync
// Description of dashboard.
description?: string
// Whether a dashboard is editable or not.
editable?: bool | *true
elements: [ElementReference.name]: Element
layout: GridLayoutKind | RowsLayoutKind | AutoGridLayoutKind | TabsLayoutKind
// Links with references to other dashboards or external websites.
links: [...DashboardLink]
// When set to true, the dashboard will redraw panels at an interval matching the pixel width.
// This will keep data "moving left" regardless of the query refresh rate. This setting helps
// avoid dashboards presenting stale live data.
liveNow?: bool
// When set to true, the dashboard will load all panels in the dashboard when it's loaded.
preload: bool
// Plugins only. The version of the dashboard installed together with the plugin.
// This is used to determine if the dashboard should be updated when the plugin is updated.
revision?: uint16
// Tags associated with dashboard.
tags: [...string]
timeSettings: TimeSettingsSpec
// Title of dashboard.
title: string
// Configured template variables.
variables: [...VariableKind]
}
// Supported dashboard elements
Element: PanelKind | LibraryPanelKind // |* more element types in the future
LibraryPanelKind: {
kind: "LibraryPanel"
spec: LibraryPanelKindSpec
}
LibraryPanelKindSpec: {
// Panel ID for the library panel in the dashboard
id: number
// Title for the library panel in the dashboard
title: string
libraryPanel: LibraryPanelRef
}
// A library panel is a reusable panel that you can use in any dashboard.
// When you make a change to a library panel, that change propagates to all instances of where the panel is used.
// Library panels streamline reuse of panels across multiple dashboards.
LibraryPanelRef: {
// Library panel name
name: string
// Library panel uid
uid: string
}
AnnotationPanelFilter: {
// Should the specified panels be included or excluded
exclude?: bool | *false
// Panel IDs that should be included or excluded
ids: [...uint8]
}
// "Off" for no shared crosshair or tooltip (default).
// "Crosshair" for shared crosshair.
// "Tooltip" for shared crosshair AND shared tooltip.
DashboardCursorSync: "Off" | "Crosshair" | "Tooltip"
// Links with references to other dashboards or external resources
DashboardLink: {
// Title to display with the link
title: string
// Link type. Accepted values are dashboards (to refer to another dashboard) and link (to refer to an external resource)
// FIXME: The type is generated as `type: DashboardLinkType | dashboardLinkType.Link;` but it should be `type: DashboardLinkType`
type: DashboardLinkType
// Icon name to be displayed with the link
icon: string
// Tooltip to display when the user hovers their mouse over it
tooltip: string
// Link URL. Only required/valid if the type is link
url?: string
// List of tags to limit the linked dashboards. If empty, all dashboards will be displayed. Only valid if the type is dashboards
tags: [...string]
// If true, all dashboards links will be displayed in a dropdown. If false, all dashboards links will be displayed side by side. Only valid if the type is dashboards
asDropdown: bool | *false
// If true, the link will be opened in a new tab
targetBlank: bool | *false
// If true, includes current template variables values in the link as query params
includeVars: bool | *false
// If true, includes current time range in the link as query params
keepTime: bool | *false
}
DataSourceRef: {
// The plugin type-id
type?: string
// Specific datasource instance
uid?: string
}
// A topic is attached to DataFrame metadata in query results.
// This specifies where the data should be used.
DataTopic: "series" | "annotations" | "alertStates" @cog(kind="enum",memberNames="Series|Annotations|AlertStates")
// Transformations allow to manipulate data returned by a query before the system applies a visualization.
// Using transformations you can: rename fields, join time series data, perform mathematical operations across queries,
// use the output of one transformation as the input to another transformation, etc.
DataTransformerConfig: {
// Unique identifier of transformer
id: string
// Disabled transformations are skipped
disabled?: bool
// Optional frame matcher. When missing it will be applied to all results
filter?: MatcherConfig
// Where to pull DataFrames from as input to transformation
topic?: DataTopic
// Options to be passed to the transformer
// Valid options depend on the transformer id
options: _
}
DataLink: {
title: string
url: string
targetBlank?: bool
}
// The data model used in Grafana, namely the data frame, is a columnar-oriented table structure that unifies both time series and table query results.
// Each column within this structure is called a field. A field can represent a single time series or table column.
// Field options allow you to change how the data is displayed in your visualizations.
FieldConfigSource: {
// Defaults are the options applied to all fields.
defaults: FieldConfig
// Overrides are the options applied to specific fields overriding the defaults.
overrides: [...{
matcher: MatcherConfig
properties: [...DynamicConfigValue]
}]
}
// The data model used in Grafana, namely the data frame, is a columnar-oriented table structure that unifies both time series and table query results.
// Each column within this structure is called a field. A field can represent a single time series or table column.
// Field options allow you to change how the data is displayed in your visualizations.
FieldConfig: {
// The display value for this field. This supports template variables blank is auto
displayName?: string
// This can be used by data sources that return and explicit naming structure for values and labels
// When this property is configured, this value is used rather than the default naming strategy.
displayNameFromDS?: string
// Human readable field metadata
description?: string
// An explicit path to the field in the datasource. When the frame meta includes a path,
// This will default to `${frame.meta.path}/${field.name}
//
// When defined, this value can be used as an identifier within the datasource scope, and
// may be used to update the results
path?: string
// True if data source can write a value to the path. Auth/authz are supported separately
writeable?: bool
// True if data source field supports ad-hoc filters
filterable?: bool
// Unit a field should use. The unit you select is applied to all fields except time.
// You can use the units ID availables in Grafana or a custom unit.
// Available units in Grafana: https://github.com/grafana/grafana/blob/main/packages/grafana-data/src/valueFormats/categories.ts
// As custom unit, you can use the following formats:
// `suffix:<suffix>` for custom unit that should go after value.
// `prefix:<prefix>` for custom unit that should go before value.
// `time:<format>` For custom date time formats type for example `time:YYYY-MM-DD`.
// `si:<base scale><unit characters>` for custom SI units. For example: `si: mF`. This one is a bit more advanced as you can specify both a unit and the source data scale. So if your source data is represented as milli (thousands of) something prefix the unit with that SI scale character.
// `count:<unit>` for a custom count unit.
// `currency:<unit>` for custom a currency unit.
unit?: string
// Specify the number of decimals Grafana includes in the rendered value.
// If you leave this field blank, Grafana automatically truncates the number of decimals based on the value.
// For example 1.1234 will display as 1.12 and 100.456 will display as 100.
// To display all decimals, set the unit to `String`.
decimals?: number
// The minimum value used in percentage threshold calculations. Leave blank for auto calculation based on all series and fields.
min?: number
// The maximum value used in percentage threshold calculations. Leave blank for auto calculation based on all series and fields.
max?: number
// Convert input values into a display string
mappings?: [...ValueMapping]
// Map numeric values to states
thresholds?: ThresholdsConfig
// Panel color configuration
color?: FieldColor
// The behavior when clicking on a result
links?: [...]
// Alternative to empty string
noValue?: string
// custom is specified by the FieldConfig field
// in panel plugin schemas.
custom?: {...}
}
DynamicConfigValue: {
id: string | *""
value?: _
}
// Matcher is a predicate configuration. Based on the config a set of field(s) or values is filtered in order to apply override / transformation.
// It comes with in id ( to resolve implementation from registry) and a configuration that’s specific to a particular matcher type.
MatcherConfig: {
// The matcher id. This is used to find the matcher implementation from registry.
id: string | *""
// The matcher options. This is specific to the matcher implementation.
options?: _
}
Threshold: {
value: number
color: string
}
ThresholdsMode: "absolute" | "percentage"
ThresholdsConfig: {
mode: ThresholdsMode
steps: [...Threshold]
}
ValueMapping: ValueMap | RangeMap | RegexMap | SpecialValueMap
// Supported value mapping types
// `value`: Maps text values to a color or different display text and color. For example, you can configure a value mapping so that all instances of the value 10 appear as Perfection! rather than the number.
// `range`: Maps numerical ranges to a display text and color. For example, if a value is within a certain range, you can configure a range value mapping to display Low or High rather than the number.
// `regex`: Maps regular expressions to replacement text and a color. For example, if a value is www.example.com, you can configure a regex value mapping so that Grafana displays www and truncates the domain.
// `special`: Maps special values like Null, NaN (not a number), and boolean values like true and false to a display text and color. See SpecialValueMatch to see the list of special values. For example, you can configure a special value mapping so that null values appear as N/A.
MappingType: "value" | "range" | "regex" | "special"
// Maps text values to a color or different display text and color.
// For example, you can configure a value mapping so that all instances of the value 10 appear as Perfection! rather than the number.
ValueMap: {
type: MappingType & "value"
// Map with <value_to_match>: ValueMappingResult. For example: { "10": { text: "Perfection!", color: "green" } }
options: [string]: ValueMappingResult
}
// Maps numerical ranges to a display text and color.
// For example, if a value is within a certain range, you can configure a range value mapping to display Low or High rather than the number.
RangeMap: {
type: MappingType & "range"
// Range to match against and the result to apply when the value is within the range
options: {
// Min value of the range. It can be null which means -Infinity
from: float64 | null
// Max value of the range. It can be null which means +Infinity
to: float64 | null
// Config to apply when the value is within the range
result: ValueMappingResult
}
}
// Maps regular expressions to replacement text and a color.
// For example, if a value is www.example.com, you can configure a regex value mapping so that Grafana displays www and truncates the domain.
RegexMap: {
type: MappingType & "regex"
// Regular expression to match against and the result to apply when the value matches the regex
options: {
// Regular expression to match against
pattern: string
// Config to apply when the value matches the regex
result: ValueMappingResult
}
}
// Maps special values like Null, NaN (not a number), and boolean values like true and false to a display text and color.
// See SpecialValueMatch to see the list of special values.
// For example, you can configure a special value mapping so that null values appear as N/A.
SpecialValueMap: {
type: MappingType & "special"
options: {
// Special value to match against
match: SpecialValueMatch
// Config to apply when the value matches the special value
result: ValueMappingResult
}
}
// Special value types supported by the `SpecialValueMap`
SpecialValueMatch: "true" | "false" | "null" | "nan" | "null+nan" | "empty" @cog(kind="enum",memberNames="True|False|Null|NaN|NullAndNaN|Empty")
// Result used as replacement with text and color when the value matches
ValueMappingResult: {
// Text to display when the value matches
text?: string
// Text to use when the value matches
color?: string
// Icon to display when the value matches. Only specific visualizations.
icon?: string
// Position in the mapping array. Only used internally.
index?: int32
}
// Color mode for a field. You can specify a single color, or select a continuous (gradient) color schemes, based on a value.
// Continuous color interpolates a color using the percentage of a value relative to min and max.
// Accepted values are:
// `thresholds`: From thresholds. Informs Grafana to take the color from the matching threshold
// `palette-classic`: Classic palette. Grafana will assign color by looking up a color in a palette by series index. Useful for Graphs and pie charts and other categorical data visualizations
// `palette-classic-by-name`: Classic palette (by name). Grafana will assign color by looking up a color in a palette by series name. Useful for Graphs and pie charts and other categorical data visualizations
// `continuous-GrYlRd`: ontinuous Green-Yellow-Red palette mode
// `continuous-RdYlGr`: Continuous Red-Yellow-Green palette mode
// `continuous-BlYlRd`: Continuous Blue-Yellow-Red palette mode
// `continuous-YlRd`: Continuous Yellow-Red palette mode
// `continuous-BlPu`: Continuous Blue-Purple palette mode
// `continuous-YlBl`: Continuous Yellow-Blue palette mode
// `continuous-blues`: Continuous Blue palette mode
// `continuous-reds`: Continuous Red palette mode
// `continuous-greens`: Continuous Green palette mode
// `continuous-purples`: Continuous Purple palette mode
// `shades`: Shades of a single color. Specify a single color, useful in an override rule.
// `fixed`: Fixed color mode. Specify a single color, useful in an override rule.
FieldColorModeId: "thresholds" | "palette-classic" | "palette-classic-by-name" | "continuous-GrYlRd" | "continuous-RdYlGr" | "continuous-BlYlRd" | "continuous-YlRd" | "continuous-BlPu" | "continuous-YlBl" | "continuous-blues" | "continuous-reds" | "continuous-greens" | "continuous-purples" | "fixed" | "shades"
// Defines how to assign a series color from "by value" color schemes. For example for an aggregated data points like a timeseries, the color can be assigned by the min, max or last value.
FieldColorSeriesByMode: "min" | "max" | "last"
// Map a field to a color.
FieldColor: {
// The main color scheme mode.
mode: FieldColorModeId
// The fixed color value for fixed or shades color modes.
fixedColor?: string
// Some visualizations need to know how to assign a series color from by value color schemes.
seriesBy?: FieldColorSeriesByMode
}
// Dashboard Link type. Accepted values are dashboards (to refer to another dashboard) and link (to refer to an external resource)
DashboardLinkType: "link" | "dashboards"
// --- Common types ---
Kind: {
kind: string
spec: _
metadata?: _
}
// --- Kinds ---
VizConfigSpec: {
pluginVersion: string
options: [string]: _
fieldConfig: FieldConfigSource
}
VizConfigKind: {
// The kind of a VizConfigKind is the plugin ID
kind: string
spec: VizConfigSpec
}
AnnotationQuerySpec: {
datasource?: DataSourceRef
query?: DataQueryKind
enable: bool
hide: bool
iconColor: string
name: string
builtIn?: bool | *false
filter?: AnnotationPanelFilter
options?: [string]: _ //Catch-all field for datasource-specific properties
}
AnnotationQueryKind: {
kind: "AnnotationQuery"
spec: AnnotationQuerySpec
}
QueryOptionsSpec: {
timeFrom?: string
maxDataPoints?: int
timeShift?: string
queryCachingTTL?: int
interval?: string
cacheTimeout?: string
hideTimeOverride?: bool
}
DataQueryKind: {
// The kind of a DataQueryKind is the datasource type
kind: string
spec: [string]: _
}
PanelQuerySpec: {
query: DataQueryKind
datasource?: DataSourceRef
refId: string
hidden: bool
}
PanelQueryKind: {
kind: "PanelQuery"
spec: PanelQuerySpec
}
TransformationKind: {
// The kind of a TransformationKind is the transformation ID
kind: string
spec: DataTransformerConfig
}
QueryGroupSpec: {
queries: [...PanelQueryKind]
transformations: [...TransformationKind]
queryOptions: QueryOptionsSpec
}
QueryGroupKind: {
kind: "QueryGroup"
spec: QueryGroupSpec
}
TimeRangeOption: {
display: string | *"Last 6 hours"
from: string | *"now-6h"
to: string | *"now"
}
// Time configuration
// It defines the default time config for the time picker, the refresh picker for the specific dashboard.
TimeSettingsSpec: {
// Timezone of dashboard. Accepted values are IANA TZDB zone ID or "browser" or "utc".
timezone?: string | *"browser"
// Start time range for dashboard.
// Accepted values are relative time strings like "now-6h" or absolute time strings like "2020-07-10T08:00:00.000Z".
from: string | *"now-6h"
// End time range for dashboard.
// Accepted values are relative time strings like "now-6h" or absolute time strings like "2020-07-10T08:00:00.000Z".
to: string | *"now"
// Refresh rate of dashboard. Represented via interval string, e.g. "5s", "1m", "1h", "1d".
autoRefresh: string // v1: refresh
// Interval options available in the refresh picker dropdown.
autoRefreshIntervals: [...string] | *["5s", "10s", "30s", "1m", "5m", "15m", "30m", "1h", "2h", "1d"] // v1: timepicker.refresh_intervals
// Selectable options available in the time picker dropdown. Has no effect on provisioned dashboard.
quickRanges?: [...TimeRangeOption] // v1: timepicker.quick_ranges , not exposed in the UI
// Whether timepicker is visible or not.
hideTimepicker: bool // v1: timepicker.hidden
// Day when the week starts. Expressed by the name of the day in lowercase, e.g. "monday".
weekStart?: "saturday" | "monday" | "sunday"
// The month that the fiscal year starts on. 0 = January, 11 = December
fiscalYearStartMonth: int
// Override the now time by entering a time delay. Use this option to accommodate known delays in data aggregation to avoid null values.
nowDelay?: string // v1: timepicker.nowDelay
}
RepeatMode: "variable" // other repeat modes will be added in the future: label, frame
RepeatOptions: {
mode: RepeatMode
value: string
direction?: "h" | "v"
maxPerRow?: int
}
RowRepeatOptions: {
mode: RepeatMode
value: string
}
AutoGridRepeatOptions: {
mode: RepeatMode
value: string
}
GridLayoutItemSpec: {
x: int
y: int
width: int
height: int
element: ElementReference // reference to a PanelKind from dashboard.spec.elements Expressed as JSON Schema reference
repeat?: RepeatOptions
}
GridLayoutItemKind: {
kind: "GridLayoutItem"
spec: GridLayoutItemSpec
}
GridLayoutRowKind: {
kind: "GridLayoutRow"
spec: GridLayoutRowSpec
}
GridLayoutRowSpec: {
y: int
collapsed: bool
title: string
elements: [...GridLayoutItemKind] // Grid items in the row will have their Y value be relative to the rows Y value. This means a panel positioned at Y: 0 in a row with Y: 10 will be positioned at Y: 11 (row header has a heigh of 1) in the dashboard.
repeat?: RowRepeatOptions
}
GridLayoutSpec: {
items: [...GridLayoutItemKind | GridLayoutRowKind]
}
GridLayoutKind: {
kind: "GridLayout"
spec: GridLayoutSpec
}
RowsLayoutKind: {
kind: "RowsLayout"
spec: RowsLayoutSpec
}
RowsLayoutSpec: {
rows: [...RowsLayoutRowKind]
}
RowsLayoutRowKind: {
kind: "RowsLayoutRow"
spec: RowsLayoutRowSpec
}
RowsLayoutRowSpec: {
title?: string
collapse?: bool
hideHeader?: bool
fillScreen?: bool
conditionalRendering?: ConditionalRenderingGroupKind
repeat?: RowRepeatOptions
layout: GridLayoutKind | AutoGridLayoutKind | TabsLayoutKind | RowsLayoutKind
}
AutoGridLayoutKind: {
kind: "AutoGridLayout"
spec: AutoGridLayoutSpec
}
AutoGridLayoutSpec: {
maxColumnCount?: number | *3
columnWidthMode: "narrow" | *"standard" | "wide" | "custom"
columnWidth?: number
rowHeightMode: "short" | *"standard" | "tall" | "custom"
rowHeight?: number
fillScreen?: bool | *false
items: [...AutoGridLayoutItemKind]
}
AutoGridLayoutItemKind: {
kind: "AutoGridLayoutItem"
spec: AutoGridLayoutItemSpec
}
AutoGridLayoutItemSpec: {
element: ElementReference
repeat?: AutoGridRepeatOptions
conditionalRendering?: ConditionalRenderingGroupKind
}
TabsLayoutKind: {
kind: "TabsLayout"
spec: TabsLayoutSpec
}
TabsLayoutSpec: {
tabs: [...TabsLayoutTabKind]
}
TabsLayoutTabKind: {
kind: "TabsLayoutTab"
spec: TabsLayoutTabSpec
}
TabsLayoutTabSpec: {
title?: string
layout: GridLayoutKind | RowsLayoutKind | AutoGridLayoutKind | TabsLayoutKind
conditionalRendering?: ConditionalRenderingGroupKind
}
PanelSpec: {
id: number
title: string
description: string
links: [...DataLink]
data: QueryGroupKind
vizConfig: VizConfigKind
transparent?: bool
}
PanelKind: {
kind: "Panel"
spec: PanelSpec
}
ElementReference: {
kind: "ElementReference"
name: string
}
// Start FIXME: variables - in CUE PR - this are things that should be added into the cue schema
// TODO: properties such as `hide`, `skipUrlSync`, `multi` are type boolean, and in the old schema they are conditional,
// should we make them conditional in the new schema as well? or should we make them required but default to false?
// Variable types
VariableValue: VariableValueSingle | [...VariableValueSingle]
VariableValueSingle: string | bool | number | CustomVariableValue
// Custom formatter variable
CustomFormatterVariable: {
name: string
type: VariableType
multi: bool
includeAll: bool
}
// Custom variable value
CustomVariableValue: {
// The format name or function used in the expression
formatter: *null | string | VariableCustomFormatterFn
}
// Custom formatter function
VariableCustomFormatterFn: {
value: _
legacyVariableModel: {
name: string
type: VariableType
multi: bool
includeAll: bool
}
legacyDefaultFormatter?: VariableCustomFormatterFn
}
// Dashboard variable type
// `query`: Query-generated list of values such as metric names, server names, sensor IDs, data centers, and so on.
// `adhoc`: Key/value filters that are automatically added to all metric queries for a data source (Prometheus, Loki, InfluxDB, and Elasticsearch only).
// `constant`: Define a hidden constant.
// `datasource`: Quickly change the data source for an entire dashboard.
// `interval`: Interval variables represent time spans.
// `textbox`: Display a free text input field with an optional default value.
// `custom`: Define the variable options manually using a comma-separated list.
// `system`: Variables defined by Grafana. See: https://grafana.com/docs/grafana/latest/dashboards/variables/add-template-variables/#global-variables
VariableType: "query" | "adhoc" | "groupby" | "constant" | "datasource" | "interval" | "textbox" | "custom" |
"system" | "snapshot"
VariableKind: QueryVariableKind | TextVariableKind | ConstantVariableKind | DatasourceVariableKind | IntervalVariableKind | CustomVariableKind | GroupByVariableKind | AdhocVariableKind
// Sort variable options
// Accepted values are:
// `disabled`: No sorting
// `alphabeticalAsc`: Alphabetical ASC
// `alphabeticalDesc`: Alphabetical DESC
// `numericalAsc`: Numerical ASC
// `numericalDesc`: Numerical DESC
// `alphabeticalCaseInsensitiveAsc`: Alphabetical Case Insensitive ASC
// `alphabeticalCaseInsensitiveDesc`: Alphabetical Case Insensitive DESC
// `naturalAsc`: Natural ASC
// `naturalDesc`: Natural DESC
// VariableSort enum with default value
VariableSort: "disabled" | "alphabeticalAsc" | "alphabeticalDesc" | "numericalAsc" | "numericalDesc" | "alphabeticalCaseInsensitiveAsc" | "alphabeticalCaseInsensitiveDesc" | "naturalAsc" | "naturalDesc"
// Options to config when to refresh a variable
// `never`: Never refresh the variable
// `onDashboardLoad`: Queries the data source every time the dashboard loads.
// `onTimeRangeChanged`: Queries the data source when the dashboard time range changes.
VariableRefresh: *"never" | "onDashboardLoad" | "onTimeRangeChanged"
// Determine if the variable shows on dashboard
// Accepted values are `dontHide` (show label and value), `hideLabel` (show value only), `hideVariable` (show nothing).
VariableHide: *"dontHide" | "hideLabel" | "hideVariable"
// FIXME: should we introduce this? --- Variable value option
VariableValueOption: {
label: string
value: VariableValueSingle
group?: string
}
// Variable option specification
VariableOption: {
// Whether the option is selected or not
selected?: bool
// Text to be displayed for the option
text: string | [...string]
// Value of the option
value: string | [...string]
}
// Query variable specification
QueryVariableSpec: {
name: string | *""
current: VariableOption | *{
text: ""
value: ""
}
label?: string
hide: VariableHide
refresh: VariableRefresh
skipUrlSync: bool | *false
description?: string
datasource?: DataSourceRef
query: DataQueryKind
regex: string | *""
sort: VariableSort
definition?: string
options: [...VariableOption] | *[]
multi: bool | *false
includeAll: bool | *false
allValue?: string
placeholder?: string
}
// Query variable kind
QueryVariableKind: {
kind: "QueryVariable"
spec: QueryVariableSpec
}
// Text variable specification
TextVariableSpec: {
name: string | *""
current: VariableOption | *{
text: ""
value: ""
}
query: string | *""
label?: string
hide: VariableHide
skipUrlSync: bool | *false
description?: string
}
// Text variable kind
TextVariableKind: {
kind: "TextVariable"
spec: TextVariableSpec
}
// Constant variable specification
ConstantVariableSpec: {
name: string | *""
query: string | *""
current: VariableOption | *{
text: ""
value: ""
}
label?: string
hide: VariableHide
skipUrlSync: bool | *false
description?: string
}
// Constant variable kind
ConstantVariableKind: {
kind: "ConstantVariable"
spec: ConstantVariableSpec
}
// Datasource variable specification
DatasourceVariableSpec: {
name: string | *""
pluginId: string | *""
refresh: VariableRefresh
regex: string | *""
current: VariableOption | *{
text: ""
value: ""
}
options: [...VariableOption] | *[]
multi: bool | *false
includeAll: bool | *false
allValue?: string
label?: string
hide: VariableHide
skipUrlSync: bool | *false
description?: string
}
// Datasource variable kind
DatasourceVariableKind: {
kind: "DatasourceVariable"
spec: DatasourceVariableSpec
}
// Interval variable specification
IntervalVariableSpec: {
name: string | *""
query: string | *""
current: VariableOption | *{
text: ""
value: ""
}
options: [...VariableOption] | *[]
auto: bool | *false
auto_min: string | *""
auto_count: int | *0
refresh: VariableRefresh
label?: string
hide: VariableHide
skipUrlSync: bool | *false
description?: string
}
// Interval variable kind
IntervalVariableKind: {
kind: "IntervalVariable"
spec: IntervalVariableSpec
}
// Custom variable specification
CustomVariableSpec: {
name: string | *""
query: string | *""
current: VariableOption
options: [...VariableOption] | *[]
multi: bool | *false
includeAll: bool | *false
allValue?: string
label?: string
hide: VariableHide
skipUrlSync: bool | *false
description?: string
}
// Custom variable kind
CustomVariableKind: {
kind: "CustomVariable"
spec: CustomVariableSpec
}
// GroupBy variable specification
GroupByVariableSpec: {
name: string | *""
datasource?: DataSourceRef
current: VariableOption | *{
text: ""
value: ""
}
options: [...VariableOption] | *[]
multi: bool | *false
label?: string
hide: VariableHide
skipUrlSync: bool | *false
description?: string
}
// Group variable kind
GroupByVariableKind: {
kind: "GroupByVariable"
spec: GroupByVariableSpec
}
// Adhoc variable specification
AdhocVariableSpec: {
name: string | *""
datasource?: DataSourceRef
baseFilters: [...AdHocFilterWithLabels] | *[]
filters: [...AdHocFilterWithLabels] | *[]
defaultKeys: [...MetricFindValue] | *[]
label?: string
hide: VariableHide
skipUrlSync: bool | *false
description?: string
}
// Define the MetricFindValue type
MetricFindValue: {
text: string
value?: string | number
group?: string
expandable?: bool
}
// Define the AdHocFilterWithLabels type
AdHocFilterWithLabels: {
key: string
operator: string
value: string
values?: [...string]
keyLabel?: string
valueLabels?: [...string]
forceEdit?: bool
// @deprecated
condition?: string
}
// Adhoc variable kind
AdhocVariableKind: {
kind: "AdhocVariable"
spec: AdhocVariableSpec
}
ConditionalRenderingGroupKind: {
kind: "ConditionalRenderingGroup"
spec: ConditionalRenderingGroupSpec
}
ConditionalRenderingGroupSpec: {
visibility: "show" | "hide"
condition: "and" | "or"
items: [...ConditionalRenderingVariableKind | ConditionalRenderingDataKind | ConditionalRenderingTimeRangeSizeKind]
}
ConditionalRenderingVariableKind: {
kind: "ConditionalRenderingVariable"
spec: ConditionalRenderingVariableSpec
}
ConditionalRenderingVariableSpec: {
variable: string
operator: "equals" | "notEquals"
value: string
}
ConditionalRenderingDataKind: {
kind: "ConditionalRenderingData"
spec: ConditionalRenderingDataSpec
}
ConditionalRenderingDataSpec: {
value: bool
}
ConditionalRenderingTimeRangeSizeKind: {
kind: "ConditionalRenderingTimeRangeSize"
spec: ConditionalRenderingTimeRangeSizeSpec
}
ConditionalRenderingTimeRangeSizeSpec: {
value: string
}

@ -0,0 +1,84 @@
package v2alpha1
import (
_ "embed"
json "encoding/json"
fmt "fmt"
"strings"
"sync"
"k8s.io/apimachinery/pkg/util/validation/field"
"cuelang.org/go/cue"
"cuelang.org/go/cue/cuecontext"
"cuelang.org/go/cue/errors"
cuejson "cuelang.org/go/encoding/json"
)
func ValidateDashboardSpec(obj *Dashboard) field.ErrorList {
data, err := json.Marshal(obj.Spec)
if err != nil {
return field.ErrorList{
field.Invalid(field.NewPath("spec"), field.OmitValueType{}, err.Error()),
}
}
if err := cuejson.Validate(data, getCueSchema()); err != nil {
errs := field.ErrorList{}
for _, e := range errors.Errors(err) {
if
// We don't want to return confusing "empty disjunction" errors,
// because the users don't necessarily understand what to do with them.
// For empty disjunctions, CUE will also return more specific errors,
// so we can safely ignore the generic ones.
strings.Contains(e.Error(), "disjunction") ||
// We don't want to return errors about unknown fields either.
strings.Contains(e.Error(), "field not allowed") {
continue
}
if strings.Contains(e.Error(), "mismatched types null and list") {
// Go populates empty slices as nil, which the cue validator does not like
continue
}
// We want to manually format the error message,
// because e.Error() contains the full CUE path.
format, args := e.Msg()
errs = append(errs, field.Invalid(
field.NewPath(formatErrorPath(e.Path())),
field.OmitValueType{},
fmt.Sprintf(format, args...),
))
}
return errs
}
return nil
}
func formatErrorPath(path []string) string {
return strings.Join(path, ".")
}
var (
compiledSchema cue.Value
getSchemaOnce sync.Once
)
//go:embed dashboard_spec.cue
var schemaSource string
func getCueSchema() cue.Value {
getSchemaOnce.Do(func() {
cueCtx := cuecontext.New()
compiledSchema = cueCtx.CompileString(schemaSource).LookupPath(
cue.ParsePath("DashboardSpec"),
)
})
return compiledSchema
}

@ -883,7 +883,7 @@
}
],
"refresh": "",
"schemaVersion": 33,
"schemaVersion": 36,
"tags": [
"gdev",
"panel-tests",

@ -157,6 +157,9 @@ Experimental features might be changed or removed without prior notice.
| `disableClassicHTTPHistogram` | Disables classic HTTP Histogram (use with enableNativeHTTPHistogram) |
| `kubernetesSnapshots` | Routes snapshot requests from /api to the /apis endpoint |
| `kubernetesDashboards` | Use the kubernetes API in the frontend for dashboards |
| `dashboardDisableSchemaValidationV1` | Disable schema validation for dashboards/v1 |
| `dashboardDisableSchemaValidationV2` | Disable schema validation for dashboards/v2 |
| `dashboardSchemaValidationLogging` | Log schema validation errors so they can be analyzed later |
| `datasourceQueryTypes` | Show query type endpoints in datasource API servers (currently hardcoded for testdata, expressions, and prometheus) |
| `queryService` | Register /apis/query.grafana.app/ -- will eventually replace /api/ds/query |
| `queryServiceRewrite` | Rewrite requests targeting /ds/query to the query service |

@ -340,6 +340,18 @@ export interface FeatureToggles {
*/
kubernetesClientDashboardsFolders?: boolean;
/**
* Disable schema validation for dashboards/v1
*/
dashboardDisableSchemaValidationV1?: boolean;
/**
* Disable schema validation for dashboards/v2
*/
dashboardDisableSchemaValidationV2?: boolean;
/**
* Log schema validation errors so they can be analyzed later
*/
dashboardSchemaValidationLogging?: boolean;
/**
* Show query type endpoints in datasource API servers (currently hardcoded for testdata, expressions, and prometheus)
*/
datasourceQueryTypes?: boolean;

@ -50,7 +50,7 @@ func TestLargeDashboardSupport(t *testing.T) {
small, err := json.MarshalIndent(&dash.Spec, "", " ")
require.NoError(t, err)
require.JSONEq(t, `{
"schemaVersion": 33,
"schemaVersion": 36,
"title": "Panel tests - All panels",
"tags": ["gdev","panel-tests","all-panels"]
}`, string(small))

@ -12,6 +12,9 @@ import (
"github.com/grafana/grafana/apps/dashboard/pkg/migration"
"github.com/grafana/grafana/apps/dashboard/pkg/migration/schemaversion"
"github.com/grafana/grafana/pkg/apimachinery/utils"
apierrors "k8s.io/apimachinery/pkg/api/errors"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/util/validation/field"
)
func (b *DashboardsAPIBuilder) Mutate(ctx context.Context, a admission.Attributes, o admission.ObjectInterfaces) (err error) {
@ -28,6 +31,8 @@ func (b *DashboardsAPIBuilder) Mutate(ctx context.Context, a admission.Attribute
return err
}
var migrationErr error
var resourceInfo utils.ResourceInfo
switch v := obj.(type) {
case *dashboardV0.Dashboard:
delete(v.Spec.Object, "uid")
@ -36,6 +41,7 @@ func (b *DashboardsAPIBuilder) Mutate(ctx context.Context, a admission.Attribute
delete(v.Spec.Object, "id")
internalID = int64(id)
}
resourceInfo = dashboardV0.DashboardResourceInfo
case *dashboardV1.Dashboard:
delete(v.Spec.Object, "uid")
delete(v.Spec.Object, "version")
@ -43,15 +49,16 @@ func (b *DashboardsAPIBuilder) Mutate(ctx context.Context, a admission.Attribute
delete(v.Spec.Object, "id")
internalID = int64(id)
}
// do not error here if the migrations fail
err = migration.Migrate(v.Spec.Object, schemaversion.LATEST_VERSION)
if err != nil {
resourceInfo = dashboardV1.DashboardResourceInfo
migrationErr = migration.Migrate(v.Spec.Object, schemaversion.LATEST_VERSION)
if migrationErr != nil {
v.Status.Conversion = &dashboardV1.DashboardConversionStatus{
Failed: true,
Error: err.Error(),
Error: migrationErr.Error(),
}
}
case *dashboardV2.Dashboard:
resourceInfo = dashboardV2.DashboardResourceInfo
// Noop for V2
default:
return fmt.Errorf("mutation error: expected to dashboard, got %T", obj)
@ -61,5 +68,46 @@ func (b *DashboardsAPIBuilder) Mutate(ctx context.Context, a admission.Attribute
meta.SetDeprecatedInternalID(internalID) // nolint:staticcheck
}
fieldValidationMode := getFieldValidationMode(a)
var validationErrorList field.ErrorList
var validationProcessingError error
if migrationErr == nil {
// Migration check passed, validate the spec now - this will respect the field validation mode!
validationErrorList, validationProcessingError = b.ValidateDashboardSpec(ctx, obj, fieldValidationMode)
}
// Only fail if the field validation mode is strict
if fieldValidationMode == metav1.FieldValidationStrict {
if migrationErr != nil {
return apierrors.NewInvalid(resourceInfo.GroupVersionKind().GroupKind(), meta.GetName(), field.ErrorList{
field.Invalid(field.NewPath("spec"), meta.GetName(), migrationErr.Error())})
}
if validationProcessingError != nil {
return validationProcessingError
}
if len(validationErrorList) > 0 {
return apierrors.NewInvalid(resourceInfo.GroupVersionKind().GroupKind(), meta.GetName(), validationErrorList)
}
}
return nil
}
func getFieldValidationMode(a admission.Attributes) string {
var validation string
switch opts := a.GetOperationOptions().(type) {
case *metav1.CreateOptions:
validation = opts.FieldValidation
case *metav1.UpdateOptions:
validation = opts.FieldValidation
default:
validation = metav1.FieldValidationStrict
}
if validation == "" {
validation = metav1.FieldValidationStrict
}
return validation
}

@ -22,6 +22,7 @@ func TestDashboardAPIBuilder_Mutate(t *testing.T) {
operation admission.Operation
expectedID int64
migrationExpected bool
expectedError bool
}{
{
name: "should skip non-create/update operations",
@ -62,7 +63,7 @@ func TestDashboardAPIBuilder_Mutate(t *testing.T) {
migrationExpected: true,
},
{
name: "v1 should not error mutation hook if migration fails",
name: "v1 should error mutation hook if migration fails",
inputObj: &dashv1.Dashboard{
Spec: common.Unstructured{
Object: map[string]interface{}{
@ -71,8 +72,8 @@ func TestDashboardAPIBuilder_Mutate(t *testing.T) {
},
},
},
operation: admission.Create,
expectedID: 456,
operation: admission.Create,
expectedError: true,
},
}
@ -92,6 +93,11 @@ func TestDashboardAPIBuilder_Mutate(t *testing.T) {
false,
nil,
), nil)
if tt.expectedError {
require.Error(t, err)
return
}
require.NoError(t, err)
if tt.operation == admission.Create || tt.operation == admission.Update {

@ -0,0 +1,78 @@
package dashboard
import (
"context"
_ "embed"
"errors"
"fmt"
"k8s.io/apimachinery/pkg/runtime"
"k8s.io/apimachinery/pkg/util/validation/field"
"github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v0alpha1"
"github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v1alpha1"
"github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2alpha1"
"github.com/grafana/grafana/pkg/apimachinery/utils"
"github.com/grafana/grafana/pkg/services/featuremgmt"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
)
// ValidateDashboardSpec validates the dashboard spec and throws a detailed error if there are validation errors.
func (b *DashboardsAPIBuilder) ValidateDashboardSpec(ctx context.Context, obj runtime.Object, fieldValidationMode string) (field.ErrorList, error) {
// This will be removed with the other PR
return nil, nil
// Unreachable code is intentional until the code above is removed
//nolint:govet
accessor, err := utils.MetaAccessor(obj)
if err != nil {
return nil, fmt.Errorf("error getting meta accessor: %w", err)
}
errorOnSchemaMismatches := false
mode := fieldValidationMode
if mode != metav1.FieldValidationIgnore {
switch obj.(type) {
case *v0alpha1.Dashboard:
errorOnSchemaMismatches = false // Never error for v0
case *v1alpha1.Dashboard:
errorOnSchemaMismatches = !b.features.IsEnabled(ctx, featuremgmt.FlagDashboardDisableSchemaValidationV1)
case *v2alpha1.Dashboard:
errorOnSchemaMismatches = !b.features.IsEnabled(ctx, featuremgmt.FlagDashboardDisableSchemaValidationV2)
default:
return nil, fmt.Errorf("Invalid dashboard type: %T", obj)
}
}
if mode == metav1.FieldValidationWarn {
return nil, errors.New("FieldValidationWarn is not supported")
}
alwaysLogSchemaValidationErrors := b.features.IsEnabled(ctx, featuremgmt.FlagDashboardSchemaValidationLogging)
var errors field.ErrorList
var schemaVersionError field.ErrorList
if errorOnSchemaMismatches || alwaysLogSchemaValidationErrors {
switch v := obj.(type) {
case *v0alpha1.Dashboard:
errors, schemaVersionError = v0alpha1.ValidateDashboardSpec(v, alwaysLogSchemaValidationErrors)
case *v1alpha1.Dashboard:
errors, schemaVersionError = v1alpha1.ValidateDashboardSpec(v, alwaysLogSchemaValidationErrors)
case *v2alpha1.Dashboard:
errors = v2alpha1.ValidateDashboardSpec(v)
}
}
if alwaysLogSchemaValidationErrors && len(errors) > 0 {
b.log.Info("Schema validation errors during dashboard validation", "group_version", obj.GetObjectKind().GroupVersionKind().GroupVersion().String(), "name", accessor.GetName(), "errors", errors.ToAggregate().Error(), "schema_version_mismatch", schemaVersionError != nil)
}
if errorOnSchemaMismatches {
if schemaVersionError != nil {
return schemaVersionError, nil
}
if len(errors) > 0 {
return errors, nil
}
}
return nil, nil
}

@ -24,8 +24,8 @@ import (
type K8sHandler interface {
GetNamespace(orgID int64) string
Get(ctx context.Context, name string, orgID int64, options v1.GetOptions, subresource ...string) (*unstructured.Unstructured, error)
Create(ctx context.Context, obj *unstructured.Unstructured, orgID int64) (*unstructured.Unstructured, error)
Update(ctx context.Context, obj *unstructured.Unstructured, orgID int64) (*unstructured.Unstructured, error)
Create(ctx context.Context, obj *unstructured.Unstructured, orgID int64, opts v1.CreateOptions) (*unstructured.Unstructured, error)
Update(ctx context.Context, obj *unstructured.Unstructured, orgID int64, opts v1.UpdateOptions) (*unstructured.Unstructured, error)
Delete(ctx context.Context, name string, orgID int64, options v1.DeleteOptions) error
DeleteCollection(ctx context.Context, orgID int64) error
List(ctx context.Context, orgID int64, options v1.ListOptions) (*unstructured.UnstructuredList, error)
@ -71,22 +71,22 @@ func (h *k8sHandler) Get(ctx context.Context, name string, orgID int64, options
return client.Get(ctx, name, options, subresource...)
}
func (h *k8sHandler) Create(ctx context.Context, obj *unstructured.Unstructured, orgID int64) (*unstructured.Unstructured, error) {
func (h *k8sHandler) Create(ctx context.Context, obj *unstructured.Unstructured, orgID int64, opts v1.CreateOptions) (*unstructured.Unstructured, error) {
client, err := h.getClient(ctx, orgID)
if err != nil {
return nil, err
}
return client.Create(ctx, obj, v1.CreateOptions{})
return client.Create(ctx, obj, opts)
}
func (h *k8sHandler) Update(ctx context.Context, obj *unstructured.Unstructured, orgID int64) (*unstructured.Unstructured, error) {
func (h *k8sHandler) Update(ctx context.Context, obj *unstructured.Unstructured, orgID int64, opts v1.UpdateOptions) (*unstructured.Unstructured, error) {
client, err := h.getClient(ctx, orgID)
if err != nil {
return nil, err
}
return client.Update(ctx, obj, v1.UpdateOptions{})
return client.Update(ctx, obj, opts)
}
func (h *k8sHandler) Delete(ctx context.Context, name string, orgID int64, options v1.DeleteOptions) error {

@ -32,16 +32,16 @@ func (m *MockK8sHandler) Get(ctx context.Context, name string, orgID int64, opti
return args.Get(0).(*unstructured.Unstructured), args.Error(1)
}
func (m *MockK8sHandler) Create(ctx context.Context, obj *unstructured.Unstructured, orgID int64) (*unstructured.Unstructured, error) {
args := m.Called(ctx, obj, orgID)
func (m *MockK8sHandler) Create(ctx context.Context, obj *unstructured.Unstructured, orgID int64, opts v1.CreateOptions) (*unstructured.Unstructured, error) {
args := m.Called(ctx, obj, orgID, opts)
if args.Get(0) == nil {
return nil, args.Error(1)
}
return args.Get(0).(*unstructured.Unstructured), args.Error(1)
}
func (m *MockK8sHandler) Update(ctx context.Context, obj *unstructured.Unstructured, orgID int64) (*unstructured.Unstructured, error) {
args := m.Called(ctx, obj, orgID)
func (m *MockK8sHandler) Update(ctx context.Context, obj *unstructured.Unstructured, orgID int64, opts v1.UpdateOptions) (*unstructured.Unstructured, error) {
args := m.Called(ctx, obj, orgID, opts)
if args.Get(0) == nil {
return nil, args.Error(1)
}

@ -1833,10 +1833,14 @@ func (dr *DashboardServiceImpl) saveProvisionedDashboardThroughK8s(ctx context.C
meta.SetManagerProperties(m)
meta.SetSourceProperties(s)
out, err := dr.k8sclient.Update(ctx, obj, cmd.OrgID)
out, err := dr.k8sclient.Update(ctx, obj, cmd.OrgID, v1.UpdateOptions{
FieldValidation: v1.FieldValidationIgnore,
})
if err != nil && apierrors.IsNotFound(err) {
// Create if it doesn't already exist.
out, err = dr.k8sclient.Create(ctx, obj, cmd.OrgID)
out, err = dr.k8sclient.Create(ctx, obj, cmd.OrgID, v1.CreateOptions{
FieldValidation: v1.FieldValidationIgnore,
})
if err != nil {
return nil, err
}
@ -1854,10 +1858,14 @@ func (dr *DashboardServiceImpl) saveDashboardThroughK8s(ctx context.Context, cmd
}
dashboard.SetPluginIDMeta(obj, cmd.PluginID)
out, err := dr.k8sclient.Update(ctx, obj, orgID)
out, err := dr.k8sclient.Update(ctx, obj, orgID, v1.UpdateOptions{
FieldValidation: v1.FieldValidationIgnore,
})
if err != nil && apierrors.IsNotFound(err) {
// Create if it doesn't already exist.
out, err = dr.k8sclient.Create(ctx, obj, orgID)
out, err = dr.k8sclient.Create(ctx, obj, orgID, v1.CreateOptions{
FieldValidation: v1.FieldValidationIgnore,
})
if err != nil {
return nil, err
}

@ -1133,7 +1133,9 @@ func TestUnprovisionDashboard(t *testing.T) {
},
}}
// should update it to be without annotations
k8sCliMock.On("Update", mock.Anything, dashWithoutAnnotations, mock.Anything).Return(dashWithoutAnnotations, nil)
k8sCliMock.On("Update", mock.Anything, dashWithoutAnnotations, mock.Anything, metav1.UpdateOptions{
FieldValidation: metav1.FieldValidationIgnore,
}).Return(dashWithoutAnnotations, nil)
k8sCliMock.On("GetNamespace", mock.Anything).Return("default")
k8sCliMock.On("GetUsersFromMeta", mock.Anything, mock.Anything).Return(map[string]*user.User{}, nil)
k8sCliMock.On("Search", mock.Anything, mock.Anything, mock.Anything).Return(&resource.ResourceSearchResponse{
@ -1357,7 +1359,9 @@ func TestSaveProvisionedDashboard(t *testing.T) {
ctx, k8sCliMock := setupK8sDashboardTests(service)
fakeStore.On("SaveProvisionedDashboard", mock.Anything, mock.Anything, mock.Anything).Return(&dashboards.Dashboard{}, nil)
k8sCliMock.On("GetUsersFromMeta", mock.Anything, mock.Anything).Return(map[string]*user.User{}, nil)
k8sCliMock.On("Update", mock.Anything, mock.Anything, mock.Anything, mock.Anything).Return(&dashboardUnstructured, nil)
k8sCliMock.On("Update", mock.Anything, mock.Anything, mock.Anything, metav1.UpdateOptions{
FieldValidation: metav1.FieldValidationIgnore,
}).Return(&dashboardUnstructured, nil)
k8sCliMock.On("GetNamespace", mock.Anything).Return("default")
dashboard, err := service.SaveProvisionedDashboard(ctx, query, &dashboards.DashboardProvisioning{})
@ -1419,7 +1423,9 @@ func TestSaveDashboard(t *testing.T) {
ctx, k8sCliMock := setupK8sDashboardTests(service)
k8sCliMock.On("GetUsersFromMeta", mock.Anything, mock.Anything).Return(map[string]*user.User{}, nil)
k8sCliMock.On("GetNamespace", mock.Anything).Return("default")
k8sCliMock.On("Update", mock.Anything, mock.Anything, mock.Anything, mock.Anything).Return(&dashboardUnstructured, nil)
k8sCliMock.On("Update", mock.Anything, mock.Anything, mock.Anything, metav1.UpdateOptions{
FieldValidation: metav1.FieldValidationIgnore,
}).Return(&dashboardUnstructured, nil)
dashboard, err := service.SaveDashboard(ctx, query, false)
require.NoError(t, err)
@ -1430,7 +1436,9 @@ func TestSaveDashboard(t *testing.T) {
ctx, k8sCliMock := setupK8sDashboardTests(service)
k8sCliMock.On("GetUsersFromMeta", mock.Anything, mock.Anything).Return(map[string]*user.User{}, nil)
k8sCliMock.On("GetNamespace", mock.Anything).Return("default")
k8sCliMock.On("Update", mock.Anything, mock.Anything, mock.Anything, mock.Anything).Return(&dashboardUnstructured, nil)
k8sCliMock.On("Update", mock.Anything, mock.Anything, mock.Anything, metav1.UpdateOptions{
FieldValidation: metav1.FieldValidationIgnore,
}).Return(&dashboardUnstructured, nil)
dashboard, err := service.SaveDashboard(ctx, query, false)
require.NoError(t, err)
@ -1440,7 +1448,9 @@ func TestSaveDashboard(t *testing.T) {
t.Run("Should return an error if uid is invalid", func(t *testing.T) {
ctx, k8sCliMock := setupK8sDashboardTests(service)
k8sCliMock.On("GetNamespace", mock.Anything).Return("default")
k8sCliMock.On("Update", mock.Anything, mock.Anything, mock.Anything, mock.Anything).Return(&dashboardUnstructured, nil)
k8sCliMock.On("Update", mock.Anything, mock.Anything, mock.Anything, metav1.UpdateOptions{
FieldValidation: metav1.FieldValidationIgnore,
}).Return(&dashboardUnstructured, nil)
query.Dashboard.UID = "invalid/uid"
_, err := service.SaveDashboard(ctx, query, false)

@ -568,6 +568,24 @@ var (
Owner: grafanaAppPlatformSquad,
Expression: "true", // enabled by default
},
{
Name: "dashboardDisableSchemaValidationV1",
Description: "Disable schema validation for dashboards/v1",
Stage: FeatureStageExperimental,
Owner: grafanaAppPlatformSquad,
},
{
Name: "dashboardDisableSchemaValidationV2",
Description: "Disable schema validation for dashboards/v2",
Stage: FeatureStageExperimental,
Owner: grafanaAppPlatformSquad,
},
{
Name: "dashboardSchemaValidationLogging",
Description: "Log schema validation errors so they can be analyzed later",
Stage: FeatureStageExperimental,
Owner: grafanaAppPlatformSquad,
},
{
Name: "datasourceQueryTypes",
Description: "Show query type endpoints in datasource API servers (currently hardcoded for testdata, expressions, and prometheus)",

@ -73,6 +73,9 @@ kubernetesPlaylists,GA,@grafana/grafana-app-platform-squad,false,true,false
kubernetesSnapshots,experimental,@grafana/grafana-app-platform-squad,false,true,false
kubernetesDashboards,experimental,@grafana/grafana-app-platform-squad,false,false,true
kubernetesClientDashboardsFolders,GA,@grafana/grafana-app-platform-squad,false,false,false
dashboardDisableSchemaValidationV1,experimental,@grafana/grafana-app-platform-squad,false,false,false
dashboardDisableSchemaValidationV2,experimental,@grafana/grafana-app-platform-squad,false,false,false
dashboardSchemaValidationLogging,experimental,@grafana/grafana-app-platform-squad,false,false,false
datasourceQueryTypes,experimental,@grafana/grafana-app-platform-squad,false,true,false
queryService,experimental,@grafana/grafana-app-platform-squad,false,true,false
queryServiceRewrite,experimental,@grafana/grafana-app-platform-squad,false,true,false

1 Name Stage Owner requiresDevMode RequiresRestart FrontendOnly
73 kubernetesSnapshots experimental @grafana/grafana-app-platform-squad false true false
74 kubernetesDashboards experimental @grafana/grafana-app-platform-squad false false true
75 kubernetesClientDashboardsFolders GA @grafana/grafana-app-platform-squad false false false
76 dashboardDisableSchemaValidationV1 experimental @grafana/grafana-app-platform-squad false false false
77 dashboardDisableSchemaValidationV2 experimental @grafana/grafana-app-platform-squad false false false
78 dashboardSchemaValidationLogging experimental @grafana/grafana-app-platform-squad false false false
79 datasourceQueryTypes experimental @grafana/grafana-app-platform-squad false true false
80 queryService experimental @grafana/grafana-app-platform-squad false true false
81 queryServiceRewrite experimental @grafana/grafana-app-platform-squad false true false

@ -303,6 +303,18 @@ const (
// Route the folder and dashboard service requests to k8s
FlagKubernetesClientDashboardsFolders = "kubernetesClientDashboardsFolders"
// FlagDashboardDisableSchemaValidationV1
// Disable schema validation for dashboards/v1
FlagDashboardDisableSchemaValidationV1 = "dashboardDisableSchemaValidationV1"
// FlagDashboardDisableSchemaValidationV2
// Disable schema validation for dashboards/v2
FlagDashboardDisableSchemaValidationV2 = "dashboardDisableSchemaValidationV2"
// FlagDashboardSchemaValidationLogging
// Log schema validation errors so they can be analyzed later
FlagDashboardSchemaValidationLogging = "dashboardSchemaValidationLogging"
// FlagDatasourceQueryTypes
// Show query type endpoints in datasource API servers (currently hardcoded for testdata, expressions, and prometheus)
FlagDatasourceQueryTypes = "datasourceQueryTypes"

@ -731,6 +731,30 @@
"frontend": true
}
},
{
"metadata": {
"name": "dashboardDisableSchemaValidationV1",
"resourceVersion": "1744303023863",
"creationTimestamp": "2025-04-10T16:37:03Z"
},
"spec": {
"description": "Disable schema validation for dashboards/v1",
"stage": "experimental",
"codeowner": "@grafana/grafana-app-platform-squad"
}
},
{
"metadata": {
"name": "dashboardDisableSchemaValidationV2",
"resourceVersion": "1744303023863",
"creationTimestamp": "2025-04-10T16:37:03Z"
},
"spec": {
"description": "Disable schema validation for dashboards/v2",
"stage": "experimental",
"codeowner": "@grafana/grafana-app-platform-squad"
}
},
{
"metadata": {
"name": "dashboardNewLayouts",
@ -786,6 +810,18 @@
"expression": "true"
}
},
{
"metadata": {
"name": "dashboardSchemaValidationLogging",
"resourceVersion": "1744303223631",
"creationTimestamp": "2025-04-10T16:40:23Z"
},
"spec": {
"description": "Log schema validation errors so they can be analyzed later",
"stage": "experimental",
"codeowner": "@grafana/grafana-app-platform-squad"
}
},
{
"metadata": {
"name": "dashgpt",
@ -1278,6 +1314,32 @@
"hideFromDocs": true
}
},
{
"metadata": {
"name": "flagDashboardDisableSchemaValidationV1",
"resourceVersion": "1744302136118",
"creationTimestamp": "2025-04-10T16:22:16Z",
"deletionTimestamp": "2025-04-10T16:37:03Z"
},
"spec": {
"description": "Disable schema validation for dashboards/v1",
"stage": "experimental",
"codeowner": "@grafana/grafana-app-platform-squad"
}
},
{
"metadata": {
"name": "flagDashboardDisableSchemaValidationV2",
"resourceVersion": "1744302136118",
"creationTimestamp": "2025-04-10T16:22:16Z",
"deletionTimestamp": "2025-04-10T16:37:03Z"
},
"spec": {
"description": "Disable schema validation for dashboards/v2",
"stage": "experimental",
"codeowner": "@grafana/grafana-app-platform-squad"
}
},
{
"metadata": {
"name": "formatString",

@ -50,8 +50,8 @@ func (ss *FolderUnifiedStoreImpl) Create(ctx context.Context, cmd folder.CreateF
if err != nil {
return nil, err
}
out, err := ss.k8sclient.Create(ctx, obj, cmd.OrgID)
out, err := ss.k8sclient.Create(ctx, obj, cmd.OrgID, v1.CreateOptions{
FieldValidation: v1.FieldValidationIgnore})
if err != nil {
return nil, err
}
@ -106,7 +106,9 @@ func (ss *FolderUnifiedStoreImpl) Update(ctx context.Context, cmd folder.UpdateF
meta.SetFolder(*cmd.NewParentUID)
}
out, err := ss.k8sclient.Update(ctx, updated, cmd.OrgID)
out, err := ss.k8sclient.Update(ctx, updated, cmd.OrgID, v1.UpdateOptions{
FieldValidation: v1.FieldValidationIgnore,
})
if err != nil {
return nil, err
}

@ -228,7 +228,7 @@
]
}
],
"schemaVersion": 33,
"schemaVersion": 36,
"linkCount": 2,
"timeFrom": "now-6h",
"timeTo": "now",

@ -42,7 +42,8 @@ func runDashboardTest(t *testing.T, helper *apis.K8sTestHelper, gvr schema.Group
obj := &unstructured.Unstructured{
Object: map[string]interface{}{
"spec": map[string]any{
"title": "Test empty dashboard",
"title": "Test empty dashboard",
"schemaVersion": 41,
},
},
}

@ -793,7 +793,8 @@ func createDashboardObject(t *testing.T, title string, folderUID string, generat
},
},
"spec": map[string]interface{}{
"title": title,
"title": title,
"schemaVersion": 41,
},
},
}

@ -6,3 +6,4 @@ spec:
title: Test dashboard. Created at v1
uid: test-v1 # will be removed by mutation hook
version: 1234567 # will be removed by mutation hook
schemaVersion: 41

Loading…
Cancel
Save