From 47af158ddb60ca39436465635291b62b0dcd5e0a Mon Sep 17 00:00:00 2001 From: Dimitris Sotirakis Date: Thu, 6 May 2021 19:21:08 +0300 Subject: [PATCH] Scuemata: Add grafana-cli command to validate basic scuemata (#33523) * Add grafana-cli command to validate basic scuemata * Fix c/p outdated message * Fix linting - naming * Add basic testing * Add cue schema validation * Add tests * Fix linting errors * Remove code - refactored tests * Remove unnecessary files - leftovers * Fix linting * Try adding public folder in testdata --- pkg/cmd/grafana-cli/commands/commands.go | 13 ++ .../commands/scuemata_validation_command.go | 43 ++++ .../scuemata_validation_command_test.go | 61 +++++ .../cue/data/gen.cue | 83 +++++++ .../cue/scuemata/panel-plugin.cue | 21 ++ .../cue/scuemata/scuemata.cue | 60 +++++ .../cue/data/gen.cue | 83 +++++++ .../cue/scuemata/panel-plugin.cue | 21 ++ .../cue/scuemata/scuemata.cue | 60 +++++ .../testdata/public/dashlist/models.cue | 22 ++ .../testdata/public/dashlist/plugin.json | 17 ++ .../testdata/valid_scuemata/cue/data/gen.cue | 214 ++++++++++++++++++ .../cue/scuemata/panel-plugin.cue | 21 ++ .../valid_scuemata/cue/scuemata/scuemata.cue | 60 +++++ pkg/schema/load/common.go | 8 + 15 files changed, 787 insertions(+) create mode 100644 pkg/cmd/grafana-cli/commands/scuemata_validation_command.go create mode 100644 pkg/cmd/grafana-cli/commands/scuemata_validation_command_test.go create mode 100644 pkg/cmd/grafana-cli/commands/testdata/invalid_scuemata_missing_family/cue/data/gen.cue create mode 100644 pkg/cmd/grafana-cli/commands/testdata/invalid_scuemata_missing_family/cue/scuemata/panel-plugin.cue create mode 100644 pkg/cmd/grafana-cli/commands/testdata/invalid_scuemata_missing_family/cue/scuemata/scuemata.cue create mode 100644 pkg/cmd/grafana-cli/commands/testdata/invalid_scuemata_missing_panel/cue/data/gen.cue create mode 100644 pkg/cmd/grafana-cli/commands/testdata/invalid_scuemata_missing_panel/cue/scuemata/panel-plugin.cue create mode 100644 pkg/cmd/grafana-cli/commands/testdata/invalid_scuemata_missing_panel/cue/scuemata/scuemata.cue create mode 100644 pkg/cmd/grafana-cli/commands/testdata/public/dashlist/models.cue create mode 100644 pkg/cmd/grafana-cli/commands/testdata/public/dashlist/plugin.json create mode 100644 pkg/cmd/grafana-cli/commands/testdata/valid_scuemata/cue/data/gen.cue create mode 100644 pkg/cmd/grafana-cli/commands/testdata/valid_scuemata/cue/scuemata/panel-plugin.cue create mode 100644 pkg/cmd/grafana-cli/commands/testdata/valid_scuemata/cue/scuemata/scuemata.cue diff --git a/pkg/cmd/grafana-cli/commands/commands.go b/pkg/cmd/grafana-cli/commands/commands.go index 99f0d026c35..bac7dbefc43 100644 --- a/pkg/cmd/grafana-cli/commands/commands.go +++ b/pkg/cmd/grafana-cli/commands/commands.go @@ -134,6 +134,14 @@ var adminCommands = []*cli.Command{ }, } +var cueCommands = []*cli.Command{ + { + Name: "validate-schema", + Usage: "validate *.cue files in the project", + Action: runPluginCommand(cmd.validateScuemataBasics), + }, +} + var Commands = []*cli.Command{ { Name: "plugins", @@ -145,4 +153,9 @@ var Commands = []*cli.Command{ Usage: "Grafana admin commands", Subcommands: adminCommands, }, + { + Name: "cue", + Usage: "Cue validation commands", + Subcommands: cueCommands, + }, } diff --git a/pkg/cmd/grafana-cli/commands/scuemata_validation_command.go b/pkg/cmd/grafana-cli/commands/scuemata_validation_command.go new file mode 100644 index 00000000000..4cb7c2e9e72 --- /dev/null +++ b/pkg/cmd/grafana-cli/commands/scuemata_validation_command.go @@ -0,0 +1,43 @@ +package commands + +import ( + "fmt" + + "github.com/grafana/grafana/pkg/cmd/grafana-cli/utils" + "github.com/grafana/grafana/pkg/schema" + "github.com/grafana/grafana/pkg/schema/load" +) + +var paths = load.GetDefaultLoadPaths() + +func (cmd Command) validateScuemataBasics(c utils.CommandLine) error { + if err := validate(paths, load.BaseDashboardFamily); err != nil { + return err + } + + if err := validate(paths, load.DistDashboardFamily); err != nil { + return err + } + + return nil +} + +func validate(p load.BaseLoadPaths, loader func(p load.BaseLoadPaths) (schema.VersionedCueSchema, error)) error { + dash, err := loader(p) + if err != nil { + return fmt.Errorf("error while loading dashboard scuemata, err: %w", err) + } + + // Check that a CUE value exists. + cueValue := dash.CUE() + if !cueValue.Exists() { + return fmt.Errorf("cue value for schema does not exist") + } + + // Check CUE validity. + if err := cueValue.Validate(); err != nil { + return fmt.Errorf("all schema should be valid with respect to basic CUE rules, %w", err) + } + + return nil +} diff --git a/pkg/cmd/grafana-cli/commands/scuemata_validation_command_test.go b/pkg/cmd/grafana-cli/commands/scuemata_validation_command_test.go new file mode 100644 index 00000000000..408d46b0381 --- /dev/null +++ b/pkg/cmd/grafana-cli/commands/scuemata_validation_command_test.go @@ -0,0 +1,61 @@ +package commands + +import ( + "embed" + "io/fs" + "os" + "path/filepath" + "testing" + + "github.com/grafana/grafana/pkg/schema/load" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +//go:embed testdata/public/*/*.cue testdata/public/*/plugin.json +var base embed.FS +var pluginSchema, _ = fs.Sub(base, "testdata/public") + +func TestValidateScuemataBasics(t *testing.T) { + t.Run("Testing scuemata validity with valid cue schemas", func(t *testing.T) { + tempDir := os.DirFS(filepath.Join("testdata", "valid_scuemata")) + + var baseLoadPaths = load.BaseLoadPaths{ + BaseCueFS: tempDir, + DistPluginCueFS: pluginSchema, + } + + err := validate(baseLoadPaths, load.BaseDashboardFamily) + require.NoError(t, err, "error while loading base dashboard scuemata") + + err = validate(baseLoadPaths, load.DistDashboardFamily) + require.NoError(t, err, "error while loading dist dashboard scuemata") + }) + + t.Run("Testing scuemata validity with invalid cue schemas - family missing", func(t *testing.T) { + tempDir := os.DirFS(filepath.Join("testdata", "invalid_scuemata_missing_family")) + + var baseLoadPaths = load.BaseLoadPaths{ + BaseCueFS: tempDir, + DistPluginCueFS: pluginSchema, + } + + err := validate(baseLoadPaths, load.BaseDashboardFamily) + assert.EqualError(t, err, "error while loading dashboard scuemata, err: dashboard schema family did not exist at expected path in expected file") + }) + + t.Run("Testing scuemata validity with invalid cue schemas - panel missing", func(t *testing.T) { + tempDir := os.DirFS(filepath.Join("testdata", "invalid_scuemata_missing_panel")) + + var baseLoadPaths = load.BaseLoadPaths{ + BaseCueFS: tempDir, + DistPluginCueFS: pluginSchema, + } + + err := validate(baseLoadPaths, load.BaseDashboardFamily) + require.NoError(t, err, "error while loading base dashboard scuemata") + + err = validate(baseLoadPaths, load.DistDashboardFamily) + assert.EqualError(t, err, "all schema should be valid with respect to basic CUE rules, Family.lineages.0.0: field #Panel not allowed") + }) +} diff --git a/pkg/cmd/grafana-cli/commands/testdata/invalid_scuemata_missing_family/cue/data/gen.cue b/pkg/cmd/grafana-cli/commands/testdata/invalid_scuemata_missing_family/cue/data/gen.cue new file mode 100644 index 00000000000..7d008ab7507 --- /dev/null +++ b/pkg/cmd/grafana-cli/commands/testdata/invalid_scuemata_missing_family/cue/data/gen.cue @@ -0,0 +1,83 @@ +package grafanaschema + +import "github.com/grafana/grafana/cue/scuemata" + +Dummy: scuemata.#Family & { + lineages: [ + [ + { // 0.0 + // Unique numeric identifier for the dashboard. + // TODO must isolate or remove identifiers local to a Grafana instance...? + id?: number + // Unique dashboard identifier that can be generated by anyone. string (8-40) + uid: string + // Title of dashboard. + title?: string + // Description of dashboard. + description?: string + + gnetId?: string + // Tags associated with dashboard. + tags?: [...string] + // Theme of dashboard. + style: *"light" | "dark" + // Timezone of dashboard, + timezone?: *"browser" | "utc" + // Whether a dashboard is editable or not. + editable: bool | *true + // 0 for no shared crosshair or tooltip (default). + // 1 for shared crosshair. + // 2 for shared crosshair AND shared tooltip. + graphTooltip: >=0 & <=2 | *0 + // Time range for dashboard, e.g. last 6 hours, last 7 days, etc + time?: { + from: string | *"now-6h" + to: string | *"now" + } + // Timepicker metadata. + timepicker?: { + // Whether timepicker is collapsed or not. + collapse: bool | *false + // Whether timepicker is enabled or not. + enable: bool | *true + // Whether timepicker is visible or not. + hidden: bool | *false + // Selectable intervals for auto-refresh. + refresh_intervals: [...string] | *["5s", "10s", "30s", "1m", "5m", "15m", "30m", "1h", "2h", "1d"] + } + // Templating. + templating?: list: [...{...}] + // Annotations. + annotations?: list: [...{ + builtIn: number | *0 + // Datasource to use for annotation. + datasource: string + // Whether annotation is enabled. + enable?: bool | *true + // Whether to hide annotation. + hide?: bool | *false + // Annotation icon color. + iconColor?: string + // Name of annotation. + name?: string + type: string | *"dashboard" + // Query for annotation data. + rawQuery?: string + showIn: number | *0 + }] + // Auto-refresh interval. + refresh?: string + // Version of the JSON schema, incremented each time a Grafana update brings + // changes to said schema. + schemaVersion: number | *25 + // Version of the dashboard, incremented each time the dashboard is updated. + version?: number + } + ] + ] +} + +#Latest: { + #Dashboard: Dummy.latest + #Panel: Dummy.latest._Panel +} diff --git a/pkg/cmd/grafana-cli/commands/testdata/invalid_scuemata_missing_family/cue/scuemata/panel-plugin.cue b/pkg/cmd/grafana-cli/commands/testdata/invalid_scuemata_missing_family/cue/scuemata/panel-plugin.cue new file mode 100644 index 00000000000..98d010b44dd --- /dev/null +++ b/pkg/cmd/grafana-cli/commands/testdata/invalid_scuemata_missing_family/cue/scuemata/panel-plugin.cue @@ -0,0 +1,21 @@ +package scuemata + +// Definition of the shape of a panel plugin's schema declarations in its +// schema.cue file. +// +// Note that these keys do not appear directly in any real JSON artifact; +// rather, they are composed into panel structures as they are defined within +// the larger Dashboard schema. +#PanelSchema: { + PanelOptions: {...} + PanelFieldConfig: {...} +} + +// A lineage of panel schema +#PanelLineage: [#PanelSchema, ...#PanelSchema] + +// Panel plugin-specific Family +#PanelFamily: { + lineages: [#PanelLineage, ...#PanelLineage] + migrations: [...#Migration] +} diff --git a/pkg/cmd/grafana-cli/commands/testdata/invalid_scuemata_missing_family/cue/scuemata/scuemata.cue b/pkg/cmd/grafana-cli/commands/testdata/invalid_scuemata_missing_family/cue/scuemata/scuemata.cue new file mode 100644 index 00000000000..6e76548f638 --- /dev/null +++ b/pkg/cmd/grafana-cli/commands/testdata/invalid_scuemata_missing_family/cue/scuemata/scuemata.cue @@ -0,0 +1,60 @@ +package scuemata + +// A family is a collection of schemas that specify a single kind of object, +// allowing evolution of the canonical schema for that kind of object over time. +// +// The schemas are organized into a list of Lineages, which are themselves ordered +// lists of schemas where each schema with its predecessor in the lineage. +// +// If it is desired to define a schema with a breaking schema relative to its +// predecessors, a new Lineage must be created, as well as a Migration that defines +// a mapping to the new schema from the latest schema in prior Lineage. +// +// The version number of a schema is not controlled by the schema itself, but by +// its position in the list of lineages - e.g., 0.0 corresponds to the first +// schema in the first lineage. +#Family: { + lineages: [#Lineage, ...#Lineage] + migrations: [...#Migration] + let lseq = lineages[len(lineages)-1] + latest: #LastSchema & {_p: lseq} +} + +// A Lineage is a non-empty list containing an ordered series of schemas that +// all describe a single kind of object, where each schema is backwards +// compatible with its predecessor. +#Lineage: [{...}, ...{...}] + +#LastSchema: { + _p: #Lineage + _p[len(_p)-1] +} + +// A Migration defines a relation between two schemas, "_from" and "_to". The +// relation expresses any complex mappings that must be performed to +// transform an input artifact valid with respect to the _from schema, into +// an artifact valid with respect to the _to schema. This is accomplished +// in two stages: +// 1. A Migration is initially defined by passing in schemas for _from and _to, +// and mappings that translate _from to _to are defined in _rel. +// 2. A concrete object may then be unified with _to, resulting in its values +// being mapped onto "result" by way of _rel. +// +// This is the absolute simplest possible definition of a Migration. It's +// incumbent on the implementor to manually ensure the correctness and +// completeness of the mapping. The primary value in defining such a generic +// structure is to allow comparably generic logic for migrating concrete +// artifacts through schema changes. +// +// If _to isn't backwards compatible (accretion-only) with _from, then _rel must +// explicitly enumerate every field in _from and map it to a field in _to, even +// if they're identical. This is laborious for anything outside trivially tiny +// schema. We'll want to eventually add helpers for whitelisting or blacklisting +// of paths in _from, so that migrations of larger schema can focus narrowly on +// the points of actual change. +#Migration: { + from: {...} + to: {...} + rel: {...} + result: to & rel +} diff --git a/pkg/cmd/grafana-cli/commands/testdata/invalid_scuemata_missing_panel/cue/data/gen.cue b/pkg/cmd/grafana-cli/commands/testdata/invalid_scuemata_missing_panel/cue/data/gen.cue new file mode 100644 index 00000000000..3d3cbe42ba9 --- /dev/null +++ b/pkg/cmd/grafana-cli/commands/testdata/invalid_scuemata_missing_panel/cue/data/gen.cue @@ -0,0 +1,83 @@ +package grafanaschema + +import "github.com/grafana/grafana/cue/scuemata" + +Family: scuemata.#Family & { + lineages: [ + [ + { // 0.0 + // Unique numeric identifier for the dashboard. + // TODO must isolate or remove identifiers local to a Grafana instance...? + id?: number + // Unique dashboard identifier that can be generated by anyone. string (8-40) + uid: string + // Title of dashboard. + title?: string + // Description of dashboard. + description?: string + + gnetId?: string + // Tags associated with dashboard. + tags?: [...string] + // Theme of dashboard. + style: *"light" | "dark" + // Timezone of dashboard, + timezone?: *"browser" | "utc" + // Whether a dashboard is editable or not. + editable: bool | *true + // 0 for no shared crosshair or tooltip (default). + // 1 for shared crosshair. + // 2 for shared crosshair AND shared tooltip. + graphTooltip: >=0 & <=2 | *0 + // Time range for dashboard, e.g. last 6 hours, last 7 days, etc + time?: { + from: string | *"now-6h" + to: string | *"now" + } + // Timepicker metadata. + timepicker?: { + // Whether timepicker is collapsed or not. + collapse: bool | *false + // Whether timepicker is enabled or not. + enable: bool | *true + // Whether timepicker is visible or not. + hidden: bool | *false + // Selectable intervals for auto-refresh. + refresh_intervals: [...string] | *["5s", "10s", "30s", "1m", "5m", "15m", "30m", "1h", "2h", "1d"] + } + // Templating. + templating?: list: [...{...}] + // Annotations. + annotations?: list: [...{ + builtIn: number | *0 + // Datasource to use for annotation. + datasource: string + // Whether annotation is enabled. + enable?: bool | *true + // Whether to hide annotation. + hide?: bool | *false + // Annotation icon color. + iconColor?: string + // Name of annotation. + name?: string + type: string | *"dashboard" + // Query for annotation data. + rawQuery?: string + showIn: number | *0 + }] + // Auto-refresh interval. + refresh?: string + // Version of the JSON schema, incremented each time a Grafana update brings + // changes to said schema. + schemaVersion: number | *25 + // Version of the dashboard, incremented each time the dashboard is updated. + version?: number + } + ] + ] +} + +#Latest: { + #Dashboard: Family.latest + #Panel: Family.latest._Panel +} diff --git a/pkg/cmd/grafana-cli/commands/testdata/invalid_scuemata_missing_panel/cue/scuemata/panel-plugin.cue b/pkg/cmd/grafana-cli/commands/testdata/invalid_scuemata_missing_panel/cue/scuemata/panel-plugin.cue new file mode 100644 index 00000000000..98d010b44dd --- /dev/null +++ b/pkg/cmd/grafana-cli/commands/testdata/invalid_scuemata_missing_panel/cue/scuemata/panel-plugin.cue @@ -0,0 +1,21 @@ +package scuemata + +// Definition of the shape of a panel plugin's schema declarations in its +// schema.cue file. +// +// Note that these keys do not appear directly in any real JSON artifact; +// rather, they are composed into panel structures as they are defined within +// the larger Dashboard schema. +#PanelSchema: { + PanelOptions: {...} + PanelFieldConfig: {...} +} + +// A lineage of panel schema +#PanelLineage: [#PanelSchema, ...#PanelSchema] + +// Panel plugin-specific Family +#PanelFamily: { + lineages: [#PanelLineage, ...#PanelLineage] + migrations: [...#Migration] +} diff --git a/pkg/cmd/grafana-cli/commands/testdata/invalid_scuemata_missing_panel/cue/scuemata/scuemata.cue b/pkg/cmd/grafana-cli/commands/testdata/invalid_scuemata_missing_panel/cue/scuemata/scuemata.cue new file mode 100644 index 00000000000..6e76548f638 --- /dev/null +++ b/pkg/cmd/grafana-cli/commands/testdata/invalid_scuemata_missing_panel/cue/scuemata/scuemata.cue @@ -0,0 +1,60 @@ +package scuemata + +// A family is a collection of schemas that specify a single kind of object, +// allowing evolution of the canonical schema for that kind of object over time. +// +// The schemas are organized into a list of Lineages, which are themselves ordered +// lists of schemas where each schema with its predecessor in the lineage. +// +// If it is desired to define a schema with a breaking schema relative to its +// predecessors, a new Lineage must be created, as well as a Migration that defines +// a mapping to the new schema from the latest schema in prior Lineage. +// +// The version number of a schema is not controlled by the schema itself, but by +// its position in the list of lineages - e.g., 0.0 corresponds to the first +// schema in the first lineage. +#Family: { + lineages: [#Lineage, ...#Lineage] + migrations: [...#Migration] + let lseq = lineages[len(lineages)-1] + latest: #LastSchema & {_p: lseq} +} + +// A Lineage is a non-empty list containing an ordered series of schemas that +// all describe a single kind of object, where each schema is backwards +// compatible with its predecessor. +#Lineage: [{...}, ...{...}] + +#LastSchema: { + _p: #Lineage + _p[len(_p)-1] +} + +// A Migration defines a relation between two schemas, "_from" and "_to". The +// relation expresses any complex mappings that must be performed to +// transform an input artifact valid with respect to the _from schema, into +// an artifact valid with respect to the _to schema. This is accomplished +// in two stages: +// 1. A Migration is initially defined by passing in schemas for _from and _to, +// and mappings that translate _from to _to are defined in _rel. +// 2. A concrete object may then be unified with _to, resulting in its values +// being mapped onto "result" by way of _rel. +// +// This is the absolute simplest possible definition of a Migration. It's +// incumbent on the implementor to manually ensure the correctness and +// completeness of the mapping. The primary value in defining such a generic +// structure is to allow comparably generic logic for migrating concrete +// artifacts through schema changes. +// +// If _to isn't backwards compatible (accretion-only) with _from, then _rel must +// explicitly enumerate every field in _from and map it to a field in _to, even +// if they're identical. This is laborious for anything outside trivially tiny +// schema. We'll want to eventually add helpers for whitelisting or blacklisting +// of paths in _from, so that migrations of larger schema can focus narrowly on +// the points of actual change. +#Migration: { + from: {...} + to: {...} + rel: {...} + result: to & rel +} diff --git a/pkg/cmd/grafana-cli/commands/testdata/public/dashlist/models.cue b/pkg/cmd/grafana-cli/commands/testdata/public/dashlist/models.cue new file mode 100644 index 00000000000..377cc998d20 --- /dev/null +++ b/pkg/cmd/grafana-cli/commands/testdata/public/dashlist/models.cue @@ -0,0 +1,22 @@ +package grafanaschema + +Family: { + lineages: [ + [ + { + PanelOptions: { + showStarred: bool | *true + showRecentlyViewed: bool | *false + showSearch: bool | *false + showHeadings: bool | *true + maxItems: int | *10 + query: string | *"" + folderId?: int + tags: [...string] | *[] + }, + PanelFieldConfig: {} + } + ] + ] + migrations: [] +} diff --git a/pkg/cmd/grafana-cli/commands/testdata/public/dashlist/plugin.json b/pkg/cmd/grafana-cli/commands/testdata/public/dashlist/plugin.json new file mode 100644 index 00000000000..de8f6fd15bb --- /dev/null +++ b/pkg/cmd/grafana-cli/commands/testdata/public/dashlist/plugin.json @@ -0,0 +1,17 @@ +{ + "type": "panel", + "name": "Dashboard list", + "id": "dashlist", + "skipDataQuery": true, + "info": { + "description": "List of dynamic links to other dashboards", + "author": { + "name": "Grafana Labs", + "url": "https://grafana.com" + }, + "logos": { + "small": "img/icn-dashlist-panel.svg", + "large": "img/icn-dashlist-panel.svg" + } + } +} diff --git a/pkg/cmd/grafana-cli/commands/testdata/valid_scuemata/cue/data/gen.cue b/pkg/cmd/grafana-cli/commands/testdata/valid_scuemata/cue/data/gen.cue new file mode 100644 index 00000000000..1a0440c9764 --- /dev/null +++ b/pkg/cmd/grafana-cli/commands/testdata/valid_scuemata/cue/data/gen.cue @@ -0,0 +1,214 @@ +package grafanaschema + +import "github.com/grafana/grafana/cue/scuemata" + +Family: scuemata.#Family & { + lineages: [ + [ + { // 0.0 + // Unique numeric identifier for the dashboard. + // TODO must isolate or remove identifiers local to a Grafana instance...? + id?: number + // Unique dashboard identifier that can be generated by anyone. string (8-40) + uid?: string + // Title of dashboard. + title?: string + // Description of dashboard. + description?: string + + gnetId?: string + // Tags associated with dashboard. + tags?: [...string] + // Theme of dashboard. + style: *"light" | "dark" + // Timezone of dashboard, + timezone?: *"browser" | "utc" + // Whether a dashboard is editable or not. + editable: bool | *true + // 0 for no shared crosshair or tooltip (default). + // 1 for shared crosshair. + // 2 for shared crosshair AND shared tooltip. + graphTooltip: >=0 & <=2 | *0 + // Time range for dashboard, e.g. last 6 hours, last 7 days, etc + time?: { + from: string | *"now-6h" + to: string | *"now" + } + // Timepicker metadata. + timepicker?: { + // Whether timepicker is collapsed or not. + collapse: bool | *false + // Whether timepicker is enabled or not. + enable: bool | *true + // Whether timepicker is visible or not. + hidden: bool | *false + // Selectable intervals for auto-refresh. + refresh_intervals: [...string] | *["5s", "10s", "30s", "1m", "5m", "15m", "30m", "1h", "2h", "1d"] + } + // Templating. + templating?: list: [...{...}] + // Annotations. + annotations?: list: [...{ + builtIn: number | *0 + // Datasource to use for annotation. + datasource: string + // Whether annotation is enabled. + enable?: bool | *true + // Whether to hide annotation. + hide?: bool | *false + // Annotation icon color. + iconColor?: string + // Name of annotation. + name?: string + type: string | *"dashboard" + // Query for annotation data. + rawQuery?: string + showIn: number | *0 + }] + // Auto-refresh interval. + refresh?: string + // Version of the JSON schema, incremented each time a Grafana update brings + // changes to said schema. + schemaVersion: number | *25 + // Version of the dashboard, incremented each time the dashboard is updated. + version?: number + panels?: [...#Panel] + + // Dashboard panels. Panels are canonically defined inline + // because they share a version timeline with the dashboard + // schema; they do not vary independently. We create a separate, + // synthetic Family to represent them in Go, for ease of generating + // e.g. JSON Schema. + #Panel: { + ... + // The panel plugin type id. + type: !="" + + // Internal - the exact major and minor versions of the panel plugin + // schema. Hidden and therefore not a part of the data model, but + // expected to be filled with panel plugin schema versions so that it's + // possible to figure out which schema version matched on a successful + // unification. + // _pv: { maj: int, min: int } + // The major and minor versions of the panel plugin for this schema. + // TODO 2-tuple list instead of struct? + panelSchema?: { maj: number, min: number } + + // Panel title. + title?: string + // Description. + description?: string + // Whether to display the panel without a background. + transparent: bool | *false + // Name of default datasource. + datasource?: string + // Grid position. + gridPos?: { + // Panel + h: number & >0 | *9 + // Panel + w: number & >0 & <=24 | *12 + // Panel x + x: number & >=0 & <24 | *0 + // Panel y + y: number & >=0 | *0 + // true if fixed + static?: bool + } + // Panel links. + // links?: [..._panelLink] + // 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" + // 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. + targets?: [...{...}] + + // The values depend on panel type + options: {...} + + fieldConfig: { + defaults: { + // 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 explict 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 + + // Numeric Options + unit?: string + + // Significant digits (for display) + decimals?: number + + min?: number + max?: number + + // // Convert input values into a display string + // mappings?: ValueMapping[]; + + // // Map numeric values to states + // thresholds?: ThresholdsConfig; + + // // Map values to a display color + // color?: FieldColor; + + // // Used when reducing field values + // nullValueMode?: NullValueMode; + + // // The behavior when clicking on a result + // links?: DataLink[]; + + // Alternative to empty string + noValue?: string + + // Can always exist. Valid fields within this are + // defined by the panel plugin - that's the + // PanelFieldConfig that comes from the plugin. + custom?: {...} + } + overrides: [...{ + matcher: { + id: string | *"" + options?: _ + } + properties: [...{ + id: string | *"" + value?: _ + }] + }] + } + } + } + ] + ] +} + +#Latest: { + #Dashboard: Family.latest + #Panel: Family.latest._Panel +} diff --git a/pkg/cmd/grafana-cli/commands/testdata/valid_scuemata/cue/scuemata/panel-plugin.cue b/pkg/cmd/grafana-cli/commands/testdata/valid_scuemata/cue/scuemata/panel-plugin.cue new file mode 100644 index 00000000000..98d010b44dd --- /dev/null +++ b/pkg/cmd/grafana-cli/commands/testdata/valid_scuemata/cue/scuemata/panel-plugin.cue @@ -0,0 +1,21 @@ +package scuemata + +// Definition of the shape of a panel plugin's schema declarations in its +// schema.cue file. +// +// Note that these keys do not appear directly in any real JSON artifact; +// rather, they are composed into panel structures as they are defined within +// the larger Dashboard schema. +#PanelSchema: { + PanelOptions: {...} + PanelFieldConfig: {...} +} + +// A lineage of panel schema +#PanelLineage: [#PanelSchema, ...#PanelSchema] + +// Panel plugin-specific Family +#PanelFamily: { + lineages: [#PanelLineage, ...#PanelLineage] + migrations: [...#Migration] +} diff --git a/pkg/cmd/grafana-cli/commands/testdata/valid_scuemata/cue/scuemata/scuemata.cue b/pkg/cmd/grafana-cli/commands/testdata/valid_scuemata/cue/scuemata/scuemata.cue new file mode 100644 index 00000000000..6e76548f638 --- /dev/null +++ b/pkg/cmd/grafana-cli/commands/testdata/valid_scuemata/cue/scuemata/scuemata.cue @@ -0,0 +1,60 @@ +package scuemata + +// A family is a collection of schemas that specify a single kind of object, +// allowing evolution of the canonical schema for that kind of object over time. +// +// The schemas are organized into a list of Lineages, which are themselves ordered +// lists of schemas where each schema with its predecessor in the lineage. +// +// If it is desired to define a schema with a breaking schema relative to its +// predecessors, a new Lineage must be created, as well as a Migration that defines +// a mapping to the new schema from the latest schema in prior Lineage. +// +// The version number of a schema is not controlled by the schema itself, but by +// its position in the list of lineages - e.g., 0.0 corresponds to the first +// schema in the first lineage. +#Family: { + lineages: [#Lineage, ...#Lineage] + migrations: [...#Migration] + let lseq = lineages[len(lineages)-1] + latest: #LastSchema & {_p: lseq} +} + +// A Lineage is a non-empty list containing an ordered series of schemas that +// all describe a single kind of object, where each schema is backwards +// compatible with its predecessor. +#Lineage: [{...}, ...{...}] + +#LastSchema: { + _p: #Lineage + _p[len(_p)-1] +} + +// A Migration defines a relation between two schemas, "_from" and "_to". The +// relation expresses any complex mappings that must be performed to +// transform an input artifact valid with respect to the _from schema, into +// an artifact valid with respect to the _to schema. This is accomplished +// in two stages: +// 1. A Migration is initially defined by passing in schemas for _from and _to, +// and mappings that translate _from to _to are defined in _rel. +// 2. A concrete object may then be unified with _to, resulting in its values +// being mapped onto "result" by way of _rel. +// +// This is the absolute simplest possible definition of a Migration. It's +// incumbent on the implementor to manually ensure the correctness and +// completeness of the mapping. The primary value in defining such a generic +// structure is to allow comparably generic logic for migrating concrete +// artifacts through schema changes. +// +// If _to isn't backwards compatible (accretion-only) with _from, then _rel must +// explicitly enumerate every field in _from and map it to a field in _to, even +// if they're identical. This is laborious for anything outside trivially tiny +// schema. We'll want to eventually add helpers for whitelisting or blacklisting +// of paths in _from, so that migrations of larger schema can focus narrowly on +// the points of actual change. +#Migration: { + from: {...} + to: {...} + rel: {...} + result: to & rel +} diff --git a/pkg/schema/load/common.go b/pkg/schema/load/common.go index 3ce07d0e109..90545b88d42 100644 --- a/pkg/schema/load/common.go +++ b/pkg/schema/load/common.go @@ -8,6 +8,7 @@ import ( "cuelang.org/go/cue" "cuelang.org/go/cue/load" + "github.com/grafana/grafana" ) var rt = &cue.Runtime{} @@ -39,6 +40,13 @@ type BaseLoadPaths struct { InstanceCueFS fs.FS } +func GetDefaultLoadPaths() BaseLoadPaths { + return BaseLoadPaths{ + BaseCueFS: grafana.CoreSchema, + DistPluginCueFS: grafana.PluginSchema, + } +} + // toOverlay converts all .cue files in the fs.FS into Source entries in an // overlay map, as expected by load.Config. //