From 2b1e6c136fe5aaa94ef127731b5ebdccc5c93298 Mon Sep 17 00:00:00 2001 From: Dimitris Sotirakis Date: Fri, 14 May 2021 10:02:41 +0300 Subject: [PATCH] Scuemata: Add error wrapper for CUE sanity errors (#33934) * Add error wrapper for CUE sanity errors * Fix error * Fix linting * Update test * Update TestCueErrorWrapper assertions --- pkg/schema/load/dashboard.go | 27 ++++++++- pkg/schema/load/load_test.go | 21 +++++++ .../testdata/malformed_cue/cue/data/gen.cue | 1 + .../cue/scuemata/panel-plugin.cue | 22 +++++++ .../malformed_cue/cue/scuemata/scuemata.cue | 60 +++++++++++++++++++ 5 files changed, 129 insertions(+), 2 deletions(-) create mode 100644 pkg/schema/load/testdata/malformed_cue/cue/data/gen.cue create mode 100644 pkg/schema/load/testdata/malformed_cue/cue/scuemata/panel-plugin.cue create mode 100644 pkg/schema/load/testdata/malformed_cue/cue/scuemata/scuemata.cue diff --git a/pkg/schema/load/dashboard.go b/pkg/schema/load/dashboard.go index 5bc98e425e5..1f5c09f251e 100644 --- a/pkg/schema/load/dashboard.go +++ b/pkg/schema/load/dashboard.go @@ -5,11 +5,19 @@ import ( "fmt" "cuelang.org/go/cue" + errs "cuelang.org/go/cue/errors" "cuelang.org/go/cue/load" "github.com/grafana/grafana/pkg/schema" ) -var panelSubpath cue.Path = cue.MakePath(cue.Def("#Panel")) +// cueError wraps errors caused by malformed cue files. +type cueError struct { + errors []errs.Error + filename string + line int +} + +var panelSubpath = cue.MakePath(cue.Def("#Panel")) func defaultOverlay(p BaseLoadPaths) (map[string]load.Source, error) { overlay := make(map[string]load.Source) @@ -39,7 +47,10 @@ func BaseDashboardFamily(p BaseLoadPaths) (schema.VersionedCueSchema, error) { cfg := &load.Config{Overlay: overlay} inst, err := rt.Build(load.Instances([]string{"/cue/data/gen.cue"}, cfg)[0]) if err != nil { - return nil, err + cueErrors := wrapCUEError(err) + if err != nil { + return nil, fmt.Errorf("errors: %q, in file: %s, on line: %d", cueErrors.errors, cueErrors.filename, cueErrors.line) + } } famval := inst.Value().LookupPath(cue.MakePath(cue.Str("Family"))) @@ -185,3 +196,15 @@ type CompositeDashboardSchema interface { schema.VersionedCueSchema LatestPanelSchemaFor(id string) (schema.VersionedCueSchema, error) } + +func wrapCUEError(err error) cueError { + var cErr errs.Error + if ok := errors.As(err, &cErr); ok { + return cueError{ + errors: errs.Errors(err), + filename: errs.Errors(err)[0].Position().File().Name(), + line: errs.Errors(err)[0].Position().Line(), + } + } + return cueError{} +} diff --git a/pkg/schema/load/load_test.go b/pkg/schema/load/load_test.go index 07d6dff602f..b2efa2ce9c9 100644 --- a/pkg/schema/load/load_test.go +++ b/pkg/schema/load/load_test.go @@ -123,3 +123,24 @@ func TestPanelValidity(t *testing.T) { return nil })) } + +func TestCueErrorWrapper(t *testing.T) { + t.Run("Testing scuemata validity with valid cue schemas", func(t *testing.T) { + tempDir := os.DirFS(filepath.Join("testdata", "malformed_cue")) + + var baseLoadPaths = BaseLoadPaths{ + BaseCueFS: tempDir, + DistPluginCueFS: GetDefaultLoadPaths().DistPluginCueFS, + } + + _, err := BaseDashboardFamily(baseLoadPaths) + require.Error(t, err) + require.Contains(t, err.Error(), "in file") + require.Contains(t, err.Error(), "on line") + + _, err = DistDashboardFamily(baseLoadPaths) + require.Error(t, err) + require.Contains(t, err.Error(), "in file") + require.Contains(t, err.Error(), "on line") + }) +} diff --git a/pkg/schema/load/testdata/malformed_cue/cue/data/gen.cue b/pkg/schema/load/testdata/malformed_cue/cue/data/gen.cue new file mode 100644 index 00000000000..ae01d92a9d2 --- /dev/null +++ b/pkg/schema/load/testdata/malformed_cue/cue/data/gen.cue @@ -0,0 +1 @@ +;;;;;;; diff --git a/pkg/schema/load/testdata/malformed_cue/cue/scuemata/panel-plugin.cue b/pkg/schema/load/testdata/malformed_cue/cue/scuemata/panel-plugin.cue new file mode 100644 index 00000000000..f4953838f48 --- /dev/null +++ b/pkg/schema/load/testdata/malformed_cue/cue/scuemata/panel-plugin.cue @@ -0,0 +1,22 @@ +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/schema/load/testdata/malformed_cue/cue/scuemata/scuemata.cue b/pkg/schema/load/testdata/malformed_cue/cue/scuemata/scuemata.cue new file mode 100644 index 00000000000..6e76548f638 --- /dev/null +++ b/pkg/schema/load/testdata/malformed_cue/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 +}