2021-04-08 03:11:11 -05:00
package load
import (
"errors"
"fmt"
2021-06-07 05:58:47 -05:00
"path/filepath"
2021-04-08 03:11:11 -05:00
"cuelang.org/go/cue"
"cuelang.org/go/cue/load"
"github.com/grafana/grafana/pkg/schema"
)
2021-05-14 02:02:41 -05:00
var panelSubpath = cue . MakePath ( cue . Def ( "#Panel" ) )
2021-04-08 03:11:11 -05:00
2021-08-25 05:55:04 -05:00
var dashboardDir = filepath . Join ( "packages" , "grafana-schema" , "src" , "scuemata" , "dashboard" )
2021-04-08 03:11:11 -05:00
func defaultOverlay ( p BaseLoadPaths ) ( map [ string ] load . Source , error ) {
overlay := make ( map [ string ] load . Source )
2021-06-07 05:58:47 -05:00
if err := toOverlay ( prefix , p . BaseCueFS , overlay ) ; err != nil {
2021-04-08 03:11:11 -05:00
return nil , err
}
2021-06-07 05:58:47 -05:00
if err := toOverlay ( prefix , p . DistPluginCueFS , overlay ) ; err != nil {
2021-04-08 03:11:11 -05:00
return nil , err
}
return overlay , nil
}
// BaseDashboardFamily loads the family of schema representing the "Base" variant of
// a Grafana dashboard: the core-defined dashboard schema that applies universally to
// all dashboards, independent of any plugins.
//
// The returned VersionedCueSchema will always be the oldest schema in the
// family: the 0.0 schema. schema.Find() provides easy traversal to newer schema
// versions.
func BaseDashboardFamily ( p BaseLoadPaths ) ( schema . VersionedCueSchema , error ) {
2021-09-06 17:53:42 -05:00
v , err := baseDashboardFamily ( p )
2021-04-08 03:11:11 -05:00
if err != nil {
return nil , err
}
2021-09-06 17:53:42 -05:00
return buildGenericScuemata ( v )
}
// Helper that gets the entire scuemata family, for reuse by Dist/Instance callers.
func baseDashboardFamily ( p BaseLoadPaths ) ( cue . Value , error ) {
overlay , err := defaultOverlay ( p )
if err != nil {
return cue . Value { } , err
}
2021-08-25 05:55:04 -05:00
cfg := & load . Config {
Overlay : overlay ,
ModuleRoot : prefix ,
Module : "github.com/grafana/grafana" ,
Dir : filepath . Join ( prefix , dashboardDir ) ,
}
2021-09-28 10:09:09 -05:00
inst := ctx . BuildInstance ( load . Instances ( nil , cfg ) [ 0 ] )
if inst . Err ( ) != nil {
cueError := schema . WrapCUEError ( inst . Err ( ) )
if inst . Err ( ) != nil {
2021-09-06 17:53:42 -05:00
return cue . Value { } , cueError
2021-05-14 02:02:41 -05:00
}
2021-04-08 03:11:11 -05:00
}
2021-09-28 10:09:09 -05:00
famval := inst . LookupPath ( cue . MakePath ( cue . Str ( "Family" ) ) )
2021-04-08 03:11:11 -05:00
if ! famval . Exists ( ) {
2021-09-06 17:53:42 -05:00
return cue . Value { } , errors . New ( "dashboard schema family did not exist at expected path in expected file" )
2021-04-08 03:11:11 -05:00
}
2021-09-06 17:53:42 -05:00
return famval , nil
2021-04-08 03:11:11 -05:00
}
// DistDashboardFamily loads the family of schema representing the "Dist"
// variant of a Grafana dashboard: the "Base" variant (see
// BaseDashboardFamily()), but constrained such that all substructures (e.g.
// panels) must be valid with respect to the schemas provided by the core
// plugins that ship with Grafana.
//
// The returned VersionedCueSchema will always be the oldest schema in the
// family: the 0.0 schema. schema.Find() provides easy traversal to newer schema
// versions.
func DistDashboardFamily ( p BaseLoadPaths ) ( schema . VersionedCueSchema , error ) {
2021-09-06 17:53:42 -05:00
famval , err := baseDashboardFamily ( p )
2021-04-08 03:11:11 -05:00
if err != nil {
return nil , err
}
2021-09-06 17:53:42 -05:00
scuemap , err := loadPanelScuemata ( p )
2021-04-08 03:11:11 -05:00
if err != nil {
return nil , err
}
2021-09-06 17:53:42 -05:00
// TODO see if unifying into the expected form in a loop, then unifying that
// consolidated form improves performance
for typ , fam := range scuemap {
famval = famval . FillPath ( cue . MakePath ( cue . Str ( "compose" ) , cue . Str ( "Panel" ) , cue . Str ( typ ) ) , fam )
}
head , err := buildGenericScuemata ( famval )
2021-04-08 03:11:11 -05:00
if err != nil {
return nil , err
}
2021-04-28 06:38:33 -05:00
2021-09-06 17:53:42 -05:00
// TODO sloppy duplicate logic of what's in readPanelModels(), for now
all := make ( map [ string ] schema . VersionedCueSchema )
for id , val := range scuemap {
fam , err := buildGenericScuemata ( val )
if err != nil {
return nil , err
}
all [ id ] = fam
}
2021-04-08 03:11:11 -05:00
var first , prev * compositeDashboardSchema
for head != nil {
cds := & compositeDashboardSchema {
base : head ,
2021-09-06 17:53:42 -05:00
actual : head . CUE ( ) ,
panelFams : all ,
2021-04-08 03:11:11 -05:00
// TODO migrations
migration : terminalMigrationFunc ,
}
if prev == nil {
first = cds
} else {
prev . next = cds
}
prev = cds
head = head . Successor ( )
}
return first , nil
}
type compositeDashboardSchema struct {
// The base/root dashboard schema
base schema . VersionedCueSchema
actual cue . Value
next * compositeDashboardSchema
migration migrationFunc
panelFams map [ string ] schema . VersionedCueSchema
}
// Validate checks that the resource is correct with respect to the schema.
func ( cds * compositeDashboardSchema ) Validate ( r schema . Resource ) error {
2021-07-15 19:08:03 -05:00
name := r . Name
if name == "" {
name = "resource"
}
2021-09-28 10:09:09 -05:00
rv := ctx . CompileString ( r . Value . ( string ) , cue . Filename ( name ) )
if rv . Err ( ) != nil {
return rv . Err ( )
2021-04-08 03:11:11 -05:00
}
2021-09-28 10:09:09 -05:00
return cds . actual . Unify ( rv ) . Validate ( cue . Concrete ( true ) )
2021-04-08 03:11:11 -05:00
}
// CUE returns the cue.Value representing the actual schema.
func ( cds * compositeDashboardSchema ) CUE ( ) cue . Value {
return cds . actual
}
// Version reports the major and minor versions of the schema.
func ( cds * compositeDashboardSchema ) Version ( ) ( major int , minor int ) {
return cds . base . Version ( )
}
// Returns the next VersionedCueSchema
func ( cds * compositeDashboardSchema ) Successor ( ) schema . VersionedCueSchema {
if cds . next == nil {
// Untyped nil, allows `<sch> == nil` checks to work as people expect
return nil
}
return cds . next
}
func ( cds * compositeDashboardSchema ) Migrate ( x schema . Resource ) ( schema . Resource , schema . VersionedCueSchema , error ) { // TODO restrict input/return type to concrete
r , sch , err := cds . migration ( x . Value )
if err != nil || sch == nil {
// TODO fix sloppy types
r = x . Value . ( cue . Value )
}
return schema . Resource { Value : r } , sch , nil
}
func ( cds * compositeDashboardSchema ) LatestPanelSchemaFor ( id string ) ( schema . VersionedCueSchema , error ) {
// So much slop rn, but it's OK because i FINALLY know where this is going!
psch , has := cds . panelFams [ id ]
if ! has {
// TODO typed errors
return nil , fmt . Errorf ( "unknown panel plugin type %q" , id )
}
latest := schema . Find ( psch , schema . Latest ( ) )
2021-09-06 17:53:42 -05:00
// FIXME this relies on old sloppiness
2021-04-08 03:11:11 -05:00
sch := & genericVersionedSchema {
actual : cds . base . CUE ( ) . LookupPath ( panelSubpath ) . Unify ( mapPanelModel ( id , latest ) ) ,
}
sch . major , sch . minor = latest . Version ( )
return sch , nil
}
// One-off special interface for dashboard composite schema, until the composite
// dashboard schema pattern is fully generalized.
//
// NOTE: THIS IS A TEMPORARY TYPE. IT WILL BE REPLACED WITH A GENERIC INTERFACE
// TO REPRESENT COMPOSITIONAL SCHEMA FAMILY PRIOR TO GRAFANA 8. UPDATING WILL
// SHOULD BE TRIVIAL, BUT IT WILL CAUSE BREAKAGES.
type CompositeDashboardSchema interface {
schema . VersionedCueSchema
LatestPanelSchemaFor ( id string ) ( schema . VersionedCueSchema , error )
}