Kindsys: Unify plugins, pfs with kind framework (#61192)
* New pfs impl * Reached codegen parity with old system * Update all models.cue inputs * Rename all models.cue files * Remove unused prefixfs * Changes Queries->DataQuery schema interface * Recodegen * All tests passing, nearly good now * Add SchemaInterface to kindsys props * Add pascal name deriver * Relocate plugin cue files again * Clarify use of injected fields * Remove unnecessary aliasing * Move DataQuery into mudball * Allow forcing ExpandReferences on go type generation * Move DataQuery def into kindsys, add generator to copy it to common * Fix copy generator to replace package name correctly * Fix duplicate type, test failure * Fix linting issues
This commit is contained in:
@@ -14,7 +14,7 @@ import (
|
||||
const prefix = "github.com/grafana/grafana/public/app/plugins"
|
||||
|
||||
// PluginTreeListJenny creates a [codejen.ManyToOne] that produces Go code
|
||||
// for loading a [pfs.TreeList] given [*kindsys.PluginDecl] as inputs.
|
||||
// for loading a [pfs.PluginList] given [*kindsys.PluginDecl] as inputs.
|
||||
func PluginTreeListJenny() codejen.ManyToOne[*pfs.PluginDecl] {
|
||||
outputFile := filepath.Join("pkg", "plugins", "pfs", "corelist", "corelist_load_gen.go")
|
||||
|
||||
|
||||
@@ -9,22 +9,22 @@ import (
|
||||
"github.com/grafana/thema"
|
||||
)
|
||||
|
||||
func makeTreeOrPanic(path string, pkgname string, rt *thema.Runtime) *pfs.Tree {
|
||||
func parsePluginOrPanic(path string, pkgname string, rt *thema.Runtime) pfs.ParsedPlugin {
|
||||
sub, err := fs.Sub(grafana.CueSchemaFS, path)
|
||||
if err != nil {
|
||||
panic("could not create fs sub to " + path)
|
||||
}
|
||||
tree, err := pfs.ParsePluginFS(sub, rt)
|
||||
pp, err := pfs.ParsePluginFS(sub, rt)
|
||||
if err != nil {
|
||||
panic(fmt.Sprintf("error parsing plugin metadata for %s: %s", pkgname, err))
|
||||
}
|
||||
return tree
|
||||
return pp
|
||||
}
|
||||
|
||||
func coreTreeList(rt *thema.Runtime) pfs.TreeList{
|
||||
return pfs.TreeList{
|
||||
func corePlugins(rt *thema.Runtime) []pfs.ParsedPlugin{
|
||||
return []pfs.ParsedPlugin{
|
||||
{{- range .Plugins }}
|
||||
makeTreeOrPanic("{{ .Path }}", "{{ .PkgName }}", rt),
|
||||
parsePluginOrPanic("{{ .Path }}", "{{ .PkgName }}", rt),
|
||||
{{- end }}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -14,7 +14,10 @@ import (
|
||||
// indicates the import path should be dropped in the conversion to TS. Imports
|
||||
// not present in the list are not not allowed, and code generation will fail.
|
||||
var importMap = map[string]string{
|
||||
"github.com/grafana/thema": "",
|
||||
"github.com/grafana/thema": "",
|
||||
|
||||
"github.com/grafana/grafana/pkg/kindsys": "",
|
||||
"github.com/grafana/grafana/pkg/plugins/pfs": "",
|
||||
"github.com/grafana/grafana/packages/grafana-schema/src/common": "@grafana/schema",
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,25 @@
|
||||
package grafanaplugin
|
||||
|
||||
import (
|
||||
"github.com/grafana/thema"
|
||||
"github.com/grafana/grafana/kinds/dashboard:kind"
|
||||
)
|
||||
|
||||
_dummy: coremodel.slots
|
||||
|
||||
composableKinds: PanelCfg: {
|
||||
lineage: {
|
||||
name: "disallowed_cue_import"
|
||||
seqs: [
|
||||
{
|
||||
schemas: [
|
||||
{
|
||||
PanelOptions: {
|
||||
foo: string
|
||||
} @cuetsy(kind="interface")
|
||||
},
|
||||
]
|
||||
},
|
||||
]
|
||||
}
|
||||
}
|
||||
@@ -1,23 +0,0 @@
|
||||
package grafanaplugin
|
||||
|
||||
import (
|
||||
"github.com/grafana/thema"
|
||||
"github.com/grafana/grafana/kinds/dashboard:kind"
|
||||
)
|
||||
|
||||
_dummy: coremodel.slots
|
||||
|
||||
Panel: thema.#Lineage & {
|
||||
name: "disallowed_cue_import"
|
||||
seqs: [
|
||||
{
|
||||
schemas: [
|
||||
{
|
||||
PanelOptions: {
|
||||
foo: string
|
||||
} @cuetsy(kind="interface")
|
||||
},
|
||||
]
|
||||
},
|
||||
]
|
||||
}
|
||||
@@ -1,14 +0,0 @@
|
||||
{
|
||||
"type": "panel",
|
||||
"name": "Slot impl testing",
|
||||
"id": "mismatch-panel",
|
||||
"backend": true,
|
||||
"state": "alpha",
|
||||
"info": {
|
||||
"description": "Test",
|
||||
"author": {
|
||||
"name": "Grafana Labs",
|
||||
"url": "https://grafana.com"
|
||||
}
|
||||
}
|
||||
}
|
||||
+1
-4
@@ -1,9 +1,6 @@
|
||||
package grafanaplugin
|
||||
|
||||
import "github.com/grafana/thema"
|
||||
|
||||
Query: thema.#Lineage & {
|
||||
name: "missing_kind_datasource"
|
||||
composableKinds: DataQuery: lineage: {
|
||||
seqs: [
|
||||
{
|
||||
schemas: [
|
||||
+1
-3
@@ -1,8 +1,6 @@
|
||||
package grafanaplugin
|
||||
|
||||
import "github.com/grafana/thema"
|
||||
|
||||
Panel: thema.#Lineage & {
|
||||
composableKinds: PanelCfg: lineage: {
|
||||
name: "doesnamatch"
|
||||
seqs: [
|
||||
{
|
||||
@@ -0,0 +1,25 @@
|
||||
package grafanaplugin
|
||||
|
||||
import "github.com/grafana/thema"
|
||||
|
||||
composableKinds: PanelCfg: {
|
||||
lineage: {
|
||||
joinSchema: {
|
||||
PanelOptions: {...}
|
||||
PanelFieldConfig: string
|
||||
}
|
||||
name: "panel_conflicting_joinschema"
|
||||
seqs: [
|
||||
{
|
||||
schemas: [
|
||||
{
|
||||
PanelOptions: {
|
||||
foo: string
|
||||
} @cuetsy(kind="interface")
|
||||
PanelFieldConfig: string
|
||||
},
|
||||
]
|
||||
},
|
||||
]
|
||||
}
|
||||
}
|
||||
@@ -1,23 +0,0 @@
|
||||
package grafanaplugin
|
||||
|
||||
import "github.com/grafana/thema"
|
||||
|
||||
Panel: thema.#Lineage & {
|
||||
joinSchema: {
|
||||
PanelOptions: {...}
|
||||
PanelFieldConfig: string
|
||||
}
|
||||
name: "panel_conflicting_joinschema"
|
||||
seqs: [
|
||||
{
|
||||
schemas: [
|
||||
{
|
||||
PanelOptions: {
|
||||
foo: string
|
||||
} @cuetsy(kind="interface")
|
||||
PanelFieldConfig: string
|
||||
},
|
||||
]
|
||||
},
|
||||
]
|
||||
}
|
||||
+21
@@ -0,0 +1,21 @@
|
||||
package grafanaplugin
|
||||
|
||||
import "github.com/grafana/thema"
|
||||
|
||||
composableKinds: PanelCfg: {
|
||||
lineage: {
|
||||
name: "panel_does_not_follow_slot_joinschema"
|
||||
seqs: [
|
||||
{
|
||||
schemas: [
|
||||
{
|
||||
PanelOptions: {
|
||||
foo: string
|
||||
} @cuetsy(kind="interface")
|
||||
PanelFieldConfig: string
|
||||
},
|
||||
]
|
||||
},
|
||||
]
|
||||
}
|
||||
}
|
||||
-19
@@ -1,19 +0,0 @@
|
||||
package grafanaplugin
|
||||
|
||||
import "github.com/grafana/thema"
|
||||
|
||||
Panel: thema.#Lineage & {
|
||||
name: "panel_does_not_follow_slot_joinschema"
|
||||
seqs: [
|
||||
{
|
||||
schemas: [
|
||||
{
|
||||
PanelOptions: {
|
||||
foo: string
|
||||
} @cuetsy(kind="interface")
|
||||
PanelFieldConfig: string
|
||||
},
|
||||
]
|
||||
},
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,13 @@
|
||||
package grafanaplugin
|
||||
|
||||
composableKinds: DataQuery: lineage: {
|
||||
seqs: [
|
||||
{
|
||||
schemas: [
|
||||
{
|
||||
foo: string
|
||||
},
|
||||
]
|
||||
},
|
||||
]
|
||||
}
|
||||
+18
@@ -0,0 +1,18 @@
|
||||
package grafanaplugin
|
||||
|
||||
composableKinds: DataSourceCfg: lineage: {
|
||||
seqs: [
|
||||
{
|
||||
schemas: [
|
||||
{
|
||||
Options: {
|
||||
foo: string
|
||||
}
|
||||
SecureOptions: {
|
||||
bar: string
|
||||
}
|
||||
},
|
||||
]
|
||||
},
|
||||
]
|
||||
}
|
||||
@@ -1,34 +0,0 @@
|
||||
package grafanaplugin
|
||||
|
||||
import "github.com/grafana/thema"
|
||||
|
||||
Query: thema.#Lineage & {
|
||||
name: "valid_model_datasource"
|
||||
seqs: [
|
||||
{
|
||||
schemas: [
|
||||
{
|
||||
foo: string
|
||||
},
|
||||
]
|
||||
},
|
||||
]
|
||||
}
|
||||
|
||||
DSOptions: thema.#Lineage & {
|
||||
name: "valid_model_datasource"
|
||||
seqs: [
|
||||
{
|
||||
schemas: [
|
||||
{
|
||||
Options: {
|
||||
foo: string
|
||||
}
|
||||
SecureOptions: {
|
||||
bar: string
|
||||
}
|
||||
},
|
||||
]
|
||||
},
|
||||
]
|
||||
}
|
||||
+1
-4
@@ -1,9 +1,6 @@
|
||||
package grafanaplugin
|
||||
|
||||
import "github.com/grafana/thema"
|
||||
|
||||
Panel: thema.#Lineage & {
|
||||
name: "mismatch"
|
||||
composableKinds: PanelCfg: lineage: {
|
||||
seqs: [
|
||||
{
|
||||
schemas: [
|
||||
@@ -1,18 +0,0 @@
|
||||
package grafanaplugin
|
||||
|
||||
import "github.com/grafana/thema"
|
||||
|
||||
Panel: thema.#Lineage & {
|
||||
name: "valid_model_panel"
|
||||
seqs: [
|
||||
{
|
||||
schemas: [
|
||||
{
|
||||
PanelOptions: {
|
||||
foo: string
|
||||
} @cuetsy(kind="interface")
|
||||
},
|
||||
]
|
||||
},
|
||||
]
|
||||
}
|
||||
@@ -1,31 +0,0 @@
|
||||
package grafanaplugin
|
||||
|
||||
import "github.com/grafana/thema"
|
||||
|
||||
Query: thema.#Lineage & {
|
||||
name: "wrong_slot_panel"
|
||||
seqs: [
|
||||
{
|
||||
schemas: [
|
||||
{
|
||||
foo: string
|
||||
},
|
||||
]
|
||||
},
|
||||
]
|
||||
}
|
||||
|
||||
Panel: thema.#Lineage & {
|
||||
name: "wrong_slot_panel"
|
||||
seqs: [
|
||||
{
|
||||
schemas: [
|
||||
{
|
||||
PanelOptions: {
|
||||
foo: string
|
||||
} @cuetsy(kind="interface")
|
||||
},
|
||||
]
|
||||
},
|
||||
]
|
||||
}
|
||||
@@ -1,14 +0,0 @@
|
||||
{
|
||||
"type": "panel",
|
||||
"name": "Wrong slot for type",
|
||||
"id": "wrong-slot-panel",
|
||||
"backend": true,
|
||||
"state": "alpha",
|
||||
"info": {
|
||||
"description": "Test",
|
||||
"author": {
|
||||
"name": "Grafana Labs",
|
||||
"url": "https://grafana.com"
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -8,23 +8,23 @@ import (
|
||||
"github.com/grafana/thema"
|
||||
)
|
||||
|
||||
var coreTrees pfs.TreeList
|
||||
var coreTrees []pfs.ParsedPlugin
|
||||
var coreOnce sync.Once
|
||||
|
||||
// New returns a pfs.TreeList containing the plugin trees for all core plugins
|
||||
// New returns a pfs.PluginList containing the plugin trees for all core plugins
|
||||
// in the current version of Grafana.
|
||||
//
|
||||
// Go code within the grafana codebase should only ever call this with nil.
|
||||
func New(rt *thema.Runtime) pfs.TreeList {
|
||||
var tl pfs.TreeList
|
||||
func New(rt *thema.Runtime) []pfs.ParsedPlugin {
|
||||
var pl []pfs.ParsedPlugin
|
||||
if rt == nil {
|
||||
coreOnce.Do(func() {
|
||||
coreTrees = coreTreeList(cuectx.GrafanaThemaRuntime())
|
||||
coreTrees = corePlugins(cuectx.GrafanaThemaRuntime())
|
||||
})
|
||||
tl = make(pfs.TreeList, len(coreTrees))
|
||||
copy(tl, coreTrees)
|
||||
pl = make([]pfs.ParsedPlugin, len(coreTrees))
|
||||
copy(pl, coreTrees)
|
||||
} else {
|
||||
return coreTreeList(rt)
|
||||
return corePlugins(rt)
|
||||
}
|
||||
return tl
|
||||
return pl
|
||||
}
|
||||
|
||||
@@ -18,63 +18,63 @@ import (
|
||||
"github.com/grafana/thema"
|
||||
)
|
||||
|
||||
func makeTreeOrPanic(path string, pkgname string, rt *thema.Runtime) *pfs.Tree {
|
||||
func parsePluginOrPanic(path string, pkgname string, rt *thema.Runtime) pfs.ParsedPlugin {
|
||||
sub, err := fs.Sub(grafana.CueSchemaFS, path)
|
||||
if err != nil {
|
||||
panic("could not create fs sub to " + path)
|
||||
}
|
||||
tree, err := pfs.ParsePluginFS(sub, rt)
|
||||
pp, err := pfs.ParsePluginFS(sub, rt)
|
||||
if err != nil {
|
||||
panic(fmt.Sprintf("error parsing plugin metadata for %s: %s", pkgname, err))
|
||||
}
|
||||
return tree
|
||||
return pp
|
||||
}
|
||||
|
||||
func coreTreeList(rt *thema.Runtime) pfs.TreeList {
|
||||
return pfs.TreeList{
|
||||
makeTreeOrPanic("public/app/plugins/datasource/alertmanager", "alertmanager", rt),
|
||||
makeTreeOrPanic("public/app/plugins/datasource/cloud-monitoring", "stackdriver", rt),
|
||||
makeTreeOrPanic("public/app/plugins/datasource/cloudwatch", "cloudwatch", rt),
|
||||
makeTreeOrPanic("public/app/plugins/datasource/dashboard", "dashboard", rt),
|
||||
makeTreeOrPanic("public/app/plugins/datasource/elasticsearch", "elasticsearch", rt),
|
||||
makeTreeOrPanic("public/app/plugins/datasource/grafana", "grafana", rt),
|
||||
makeTreeOrPanic("public/app/plugins/datasource/grafana-azure-monitor-datasource", "grafana_azure_monitor_datasource", rt),
|
||||
makeTreeOrPanic("public/app/plugins/datasource/graphite", "graphite", rt),
|
||||
makeTreeOrPanic("public/app/plugins/datasource/jaeger", "jaeger", rt),
|
||||
makeTreeOrPanic("public/app/plugins/datasource/loki", "loki", rt),
|
||||
makeTreeOrPanic("public/app/plugins/datasource/mssql", "mssql", rt),
|
||||
makeTreeOrPanic("public/app/plugins/datasource/mysql", "mysql", rt),
|
||||
makeTreeOrPanic("public/app/plugins/datasource/parca", "parca", rt),
|
||||
makeTreeOrPanic("public/app/plugins/datasource/phlare", "phlare", rt),
|
||||
makeTreeOrPanic("public/app/plugins/datasource/postgres", "postgres", rt),
|
||||
makeTreeOrPanic("public/app/plugins/datasource/prometheus", "prometheus", rt),
|
||||
makeTreeOrPanic("public/app/plugins/datasource/tempo", "tempo", rt),
|
||||
makeTreeOrPanic("public/app/plugins/datasource/testdata", "testdata", rt),
|
||||
makeTreeOrPanic("public/app/plugins/datasource/zipkin", "zipkin", rt),
|
||||
makeTreeOrPanic("public/app/plugins/panel/alertGroups", "alertGroups", rt),
|
||||
makeTreeOrPanic("public/app/plugins/panel/alertlist", "alertlist", rt),
|
||||
makeTreeOrPanic("public/app/plugins/panel/annolist", "annolist", rt),
|
||||
makeTreeOrPanic("public/app/plugins/panel/barchart", "barchart", rt),
|
||||
makeTreeOrPanic("public/app/plugins/panel/bargauge", "bargauge", rt),
|
||||
makeTreeOrPanic("public/app/plugins/panel/dashlist", "dashlist", rt),
|
||||
makeTreeOrPanic("public/app/plugins/panel/debug", "debug", rt),
|
||||
makeTreeOrPanic("public/app/plugins/panel/flamegraph", "flamegraph", rt),
|
||||
makeTreeOrPanic("public/app/plugins/panel/gauge", "gauge", rt),
|
||||
makeTreeOrPanic("public/app/plugins/panel/geomap", "geomap", rt),
|
||||
makeTreeOrPanic("public/app/plugins/panel/gettingstarted", "gettingstarted", rt),
|
||||
makeTreeOrPanic("public/app/plugins/panel/graph", "graph", rt),
|
||||
makeTreeOrPanic("public/app/plugins/panel/histogram", "histogram", rt),
|
||||
makeTreeOrPanic("public/app/plugins/panel/icon", "icon", rt),
|
||||
makeTreeOrPanic("public/app/plugins/panel/live", "live", rt),
|
||||
makeTreeOrPanic("public/app/plugins/panel/logs", "logs", rt),
|
||||
makeTreeOrPanic("public/app/plugins/panel/news", "news", rt),
|
||||
makeTreeOrPanic("public/app/plugins/panel/nodeGraph", "nodeGraph", rt),
|
||||
makeTreeOrPanic("public/app/plugins/panel/piechart", "piechart", rt),
|
||||
makeTreeOrPanic("public/app/plugins/panel/stat", "stat", rt),
|
||||
makeTreeOrPanic("public/app/plugins/panel/table-old", "table_old", rt),
|
||||
makeTreeOrPanic("public/app/plugins/panel/text", "text", rt),
|
||||
makeTreeOrPanic("public/app/plugins/panel/traces", "traces", rt),
|
||||
makeTreeOrPanic("public/app/plugins/panel/welcome", "welcome", rt),
|
||||
makeTreeOrPanic("public/app/plugins/panel/xychart", "xychart", rt),
|
||||
func corePlugins(rt *thema.Runtime) []pfs.ParsedPlugin {
|
||||
return []pfs.ParsedPlugin{
|
||||
parsePluginOrPanic("public/app/plugins/datasource/alertmanager", "alertmanager", rt),
|
||||
parsePluginOrPanic("public/app/plugins/datasource/cloud-monitoring", "stackdriver", rt),
|
||||
parsePluginOrPanic("public/app/plugins/datasource/cloudwatch", "cloudwatch", rt),
|
||||
parsePluginOrPanic("public/app/plugins/datasource/dashboard", "dashboard", rt),
|
||||
parsePluginOrPanic("public/app/plugins/datasource/elasticsearch", "elasticsearch", rt),
|
||||
parsePluginOrPanic("public/app/plugins/datasource/grafana", "grafana", rt),
|
||||
parsePluginOrPanic("public/app/plugins/datasource/grafana-azure-monitor-datasource", "grafana_azure_monitor_datasource", rt),
|
||||
parsePluginOrPanic("public/app/plugins/datasource/graphite", "graphite", rt),
|
||||
parsePluginOrPanic("public/app/plugins/datasource/jaeger", "jaeger", rt),
|
||||
parsePluginOrPanic("public/app/plugins/datasource/loki", "loki", rt),
|
||||
parsePluginOrPanic("public/app/plugins/datasource/mssql", "mssql", rt),
|
||||
parsePluginOrPanic("public/app/plugins/datasource/mysql", "mysql", rt),
|
||||
parsePluginOrPanic("public/app/plugins/datasource/parca", "parca", rt),
|
||||
parsePluginOrPanic("public/app/plugins/datasource/phlare", "phlare", rt),
|
||||
parsePluginOrPanic("public/app/plugins/datasource/postgres", "postgres", rt),
|
||||
parsePluginOrPanic("public/app/plugins/datasource/prometheus", "prometheus", rt),
|
||||
parsePluginOrPanic("public/app/plugins/datasource/tempo", "tempo", rt),
|
||||
parsePluginOrPanic("public/app/plugins/datasource/testdata", "testdata", rt),
|
||||
parsePluginOrPanic("public/app/plugins/datasource/zipkin", "zipkin", rt),
|
||||
parsePluginOrPanic("public/app/plugins/panel/alertGroups", "alertGroups", rt),
|
||||
parsePluginOrPanic("public/app/plugins/panel/alertlist", "alertlist", rt),
|
||||
parsePluginOrPanic("public/app/plugins/panel/annolist", "annolist", rt),
|
||||
parsePluginOrPanic("public/app/plugins/panel/barchart", "barchart", rt),
|
||||
parsePluginOrPanic("public/app/plugins/panel/bargauge", "bargauge", rt),
|
||||
parsePluginOrPanic("public/app/plugins/panel/dashlist", "dashlist", rt),
|
||||
parsePluginOrPanic("public/app/plugins/panel/debug", "debug", rt),
|
||||
parsePluginOrPanic("public/app/plugins/panel/flamegraph", "flamegraph", rt),
|
||||
parsePluginOrPanic("public/app/plugins/panel/gauge", "gauge", rt),
|
||||
parsePluginOrPanic("public/app/plugins/panel/geomap", "geomap", rt),
|
||||
parsePluginOrPanic("public/app/plugins/panel/gettingstarted", "gettingstarted", rt),
|
||||
parsePluginOrPanic("public/app/plugins/panel/graph", "graph", rt),
|
||||
parsePluginOrPanic("public/app/plugins/panel/histogram", "histogram", rt),
|
||||
parsePluginOrPanic("public/app/plugins/panel/icon", "icon", rt),
|
||||
parsePluginOrPanic("public/app/plugins/panel/live", "live", rt),
|
||||
parsePluginOrPanic("public/app/plugins/panel/logs", "logs", rt),
|
||||
parsePluginOrPanic("public/app/plugins/panel/news", "news", rt),
|
||||
parsePluginOrPanic("public/app/plugins/panel/nodeGraph", "nodeGraph", rt),
|
||||
parsePluginOrPanic("public/app/plugins/panel/piechart", "piechart", rt),
|
||||
parsePluginOrPanic("public/app/plugins/panel/stat", "stat", rt),
|
||||
parsePluginOrPanic("public/app/plugins/panel/table-old", "table_old", rt),
|
||||
parsePluginOrPanic("public/app/plugins/panel/text", "text", rt),
|
||||
parsePluginOrPanic("public/app/plugins/panel/traces", "traces", rt),
|
||||
parsePluginOrPanic("public/app/plugins/panel/welcome", "welcome", rt),
|
||||
parsePluginOrPanic("public/app/plugins/panel/xychart", "xychart", rt),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3,7 +3,6 @@ package pfs
|
||||
import (
|
||||
"fmt"
|
||||
"io/fs"
|
||||
"log"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"sort"
|
||||
@@ -24,7 +23,9 @@ func NewDeclParser(rt *thema.Runtime, skip map[string]bool) *declParser {
|
||||
}
|
||||
}
|
||||
|
||||
// TODO convert this to be the new parser for Tree
|
||||
func (psr *declParser) Parse(root fs.FS) ([]*PluginDecl, error) {
|
||||
// TODO remove hardcoded tree structure assumption, work from root of provided fs
|
||||
plugins, err := fs.Glob(root, "**/**/plugin.json")
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("error finding plugin dirs: %w", err)
|
||||
@@ -39,31 +40,26 @@ func (psr *declParser) Parse(root fs.FS) ([]*PluginDecl, error) {
|
||||
}
|
||||
|
||||
dir := os.DirFS(path)
|
||||
ptree, err := ParsePluginFS(dir, psr.rt)
|
||||
pp, err := ParsePluginFS(dir, psr.rt)
|
||||
if err != nil {
|
||||
log.Println(fmt.Errorf("parsing plugin failed for %s: %s", dir, err))
|
||||
return nil, fmt.Errorf("parsing plugin failed for %s: %s", dir, err)
|
||||
}
|
||||
|
||||
if len(pp.ComposableKinds) == 0 {
|
||||
decls = append(decls, EmptyPluginDecl(path, pp.Properties))
|
||||
continue
|
||||
}
|
||||
|
||||
p := ptree.RootPlugin()
|
||||
slots := p.SlotImplementations()
|
||||
|
||||
if len(slots) == 0 {
|
||||
decls = append(decls, EmptyPluginDecl(path, p.Meta()))
|
||||
continue
|
||||
}
|
||||
|
||||
for slotName, lin := range slots {
|
||||
for slotName, kind := range pp.ComposableKinds {
|
||||
slot, err := kindsys.FindSchemaInterface(slotName)
|
||||
if err != nil {
|
||||
log.Println(fmt.Errorf("parsing plugin failed for %s: %s", dir, err))
|
||||
continue
|
||||
return nil, fmt.Errorf("parsing plugin failed for %s: %s", dir, err)
|
||||
}
|
||||
decls = append(decls, &PluginDecl{
|
||||
SchemaInterface: &slot,
|
||||
Lineage: lin,
|
||||
Imports: p.CUEImports(),
|
||||
PluginMeta: p.Meta(),
|
||||
Lineage: kind.Lineage(),
|
||||
Imports: pp.CUEImports,
|
||||
PluginMeta: pp.Properties,
|
||||
PluginPath: path,
|
||||
})
|
||||
}
|
||||
|
||||
@@ -1,3 +1,3 @@
|
||||
// Package pfs ("Plugin FS") defines a virtual filesystem representation of Grafana plugins.
|
||||
// Package pfs ("ParsedPlugin FS") defines a virtual filesystem representation of Grafana plugins.
|
||||
|
||||
package pfs
|
||||
|
||||
+15
-12
@@ -11,23 +11,26 @@ var ErrNoRootFile = errors.New("no plugin.json at root of fs.fS")
|
||||
// ErrInvalidRootFile indicates that the root plugin.json file is invalid.
|
||||
var ErrInvalidRootFile = errors.New("plugin.json is invalid")
|
||||
|
||||
// ErrImplementedSlots indicates that a plugin has implemented the wrong set of
|
||||
// slots for its type in models.cue. Either:
|
||||
// - A slot is implemented that is not allowed for its type (e.g. datasource plugin implements Panel)
|
||||
// - A required slot for its type is not implemented (e.g. panel plugin does not implemented Panel)
|
||||
var ErrImplementedSlots = errors.New("slot implementation not allowed for this plugin type")
|
||||
// ErrComposableNotExpected indicates that a plugin has a composable kind for a
|
||||
// schema interface that is not expected, given the type of the plugin. (For
|
||||
// example, a datasource plugin has a panelcfg composable kind)
|
||||
var ErrComposableNotExpected = errors.New("plugin type should not produce composable kind for schema interface")
|
||||
|
||||
// ErrInvalidCUE indicates that a plugin's model.cue file contained invalid CUE.
|
||||
var ErrInvalidCUE = errors.New("CUE syntax error")
|
||||
// ErrExpectedComposable indicates that a plugin lacks a composable kind
|
||||
// implementation for a schema interface that is expected for that plugin's
|
||||
// type. (For example, a datasource plugin lacks a queries composable kind)
|
||||
var ErrExpectedComposable = errors.New("plugin type should produce composable kind for schema interface")
|
||||
|
||||
// ErrInvalidGrafanaPluginInstance indicates a plugin's set of .cue
|
||||
// grafanaplugin package files are invalid with respect to the GrafanaPlugin
|
||||
// spec.
|
||||
var ErrInvalidGrafanaPluginInstance = errors.New("grafanaplugin cue instance is invalid")
|
||||
|
||||
// ErrInvalidLineage indicates that the plugin contains an invalid lineage
|
||||
// declaration, according to Thema's validation rules in
|
||||
// ["github.com/grafana/thema".BindLineage].
|
||||
var ErrInvalidLineage = errors.New("invalid lineage")
|
||||
|
||||
// ErrLineageNameMismatch indicates a plugin slot lineage name did not match the id of the plugin.
|
||||
var ErrLineageNameMismatch = errors.New("lineage name not the same as plugin id")
|
||||
|
||||
// ErrDisallowedCUEImport indicates that a plugin's models.cue file imports a
|
||||
// CUE package that is not on the whitelist for safe imports.
|
||||
// ErrDisallowedCUEImport indicates that a plugin's grafanaplugin cue package
|
||||
// contains that are not on the allowlist.
|
||||
var ErrDisallowedCUEImport = errors.New("CUE import is not allowed")
|
||||
|
||||
@@ -0,0 +1,31 @@
|
||||
package pfs
|
||||
|
||||
import (
|
||||
"github.com/grafana/grafana/pkg/kindsys"
|
||||
)
|
||||
|
||||
// GrafanaPlugin specifies what plugins may declare in .cue files in a
|
||||
// `grafanaplugin` CUE package in the plugin root directory (adjacent to plugin.json).
|
||||
GrafanaPlugin: {
|
||||
// id and pascalName are injected from plugin.json. Plugin authors can write
|
||||
// values for them in .cue files, but the only valid values will be the ones
|
||||
// given in plugin.json.
|
||||
id: string
|
||||
pascalName: string
|
||||
|
||||
// A plugin defines its Composable kinds under this key.
|
||||
//
|
||||
// This struct is open for forwards compatibility - older versions of Grafana (or
|
||||
// dependent tooling) should not break if new versions introduce additional schema interfaces.
|
||||
composableKinds?: [Iface=string]: kindsys.Composable & {
|
||||
name: pascalName + Iface
|
||||
schemaInterface: Iface
|
||||
lineage: name: pascalName + Iface
|
||||
}
|
||||
|
||||
// A plugin defines its Custom kinds under this key.
|
||||
customKinds?: [Name=string]: kindsys.Custom & {
|
||||
name: Name
|
||||
}
|
||||
...
|
||||
}
|
||||
+154
-226
@@ -3,14 +3,19 @@ package pfs
|
||||
import (
|
||||
"fmt"
|
||||
"io/fs"
|
||||
"path/filepath"
|
||||
"sort"
|
||||
"strings"
|
||||
"sync"
|
||||
"testing/fstest"
|
||||
|
||||
"cuelang.org/go/cue"
|
||||
"cuelang.org/go/cue/ast"
|
||||
"cuelang.org/go/cue/build"
|
||||
"cuelang.org/go/cue/cuecontext"
|
||||
"cuelang.org/go/cue/errors"
|
||||
"cuelang.org/go/cue/parser"
|
||||
"github.com/grafana/grafana"
|
||||
"cuelang.org/go/cue/token"
|
||||
"github.com/grafana/grafana/pkg/cuectx"
|
||||
"github.com/grafana/grafana/pkg/kindsys"
|
||||
"github.com/grafana/grafana/pkg/plugins/plugindef"
|
||||
"github.com/grafana/thema"
|
||||
@@ -19,13 +24,41 @@ import (
|
||||
"github.com/yalue/merged_fs"
|
||||
)
|
||||
|
||||
// PermittedCUEImports returns the list of packages that may be imported in a
|
||||
// plugin models.cue file.
|
||||
// PackageName is the name of the CUE package that Grafana will load when
|
||||
// looking for a Grafana plugin's kind declarations.
|
||||
const PackageName = "grafanaplugin"
|
||||
|
||||
var onceGP sync.Once
|
||||
var defaultGP cue.Value
|
||||
|
||||
func doLoadGP(ctx *cue.Context) cue.Value {
|
||||
v, err := cuectx.BuildGrafanaInstance(ctx, filepath.Join("pkg", "plugins", "pfs"), "pfs", nil)
|
||||
if err != nil {
|
||||
// should be unreachable
|
||||
panic(err)
|
||||
}
|
||||
return v.LookupPath(cue.MakePath(cue.Str("GrafanaPlugin")))
|
||||
}
|
||||
|
||||
func loadGP(ctx *cue.Context) cue.Value {
|
||||
if ctx == nil || ctx == cuectx.GrafanaCUEContext() {
|
||||
onceGP.Do(func() {
|
||||
defaultGP = doLoadGP(ctx)
|
||||
})
|
||||
return defaultGP
|
||||
}
|
||||
return doLoadGP(ctx)
|
||||
}
|
||||
|
||||
// PermittedCUEImports returns the list of import paths that may be used in a
|
||||
// plugin's grafanaplugin cue package.
|
||||
//
|
||||
// TODO probably move this into kindsys
|
||||
func PermittedCUEImports() []string {
|
||||
return []string{
|
||||
"github.com/grafana/thema",
|
||||
"github.com/grafana/grafana/pkg/kindsys",
|
||||
"github.com/grafana/grafana/pkg/plugins/pfs",
|
||||
"github.com/grafana/grafana/packages/grafana-schema/src/common",
|
||||
}
|
||||
}
|
||||
@@ -41,12 +74,7 @@ func importAllowed(path string) bool {
|
||||
|
||||
var allowedImportsStr string
|
||||
|
||||
type slotandname struct {
|
||||
name string
|
||||
slot kindsys.SchemaInterface
|
||||
}
|
||||
|
||||
var allslots []slotandname
|
||||
var allsi []kindsys.SchemaInterface
|
||||
|
||||
func init() {
|
||||
all := make([]string, 0, len(PermittedCUEImports()))
|
||||
@@ -55,268 +83,168 @@ func init() {
|
||||
}
|
||||
allowedImportsStr = strings.Join(all, "\n")
|
||||
|
||||
for n, s := range kindsys.SchemaInterfaces(nil) {
|
||||
allslots = append(allslots, slotandname{
|
||||
name: n,
|
||||
slot: s,
|
||||
})
|
||||
for _, s := range kindsys.SchemaInterfaces(nil) {
|
||||
allsi = append(allsi, s)
|
||||
}
|
||||
|
||||
sort.Slice(allslots, func(i, j int) bool {
|
||||
return allslots[i].name < allslots[j].name
|
||||
sort.Slice(allsi, func(i, j int) bool {
|
||||
return allsi[i].Name() < allsi[j].Name()
|
||||
})
|
||||
}
|
||||
|
||||
// Tree represents the contents of a plugin filesystem tree.
|
||||
type Tree struct {
|
||||
raw fs.FS
|
||||
rootinfo PluginInfo
|
||||
}
|
||||
|
||||
func (t *Tree) FS() fs.FS {
|
||||
return t.raw
|
||||
}
|
||||
|
||||
func (t *Tree) RootPlugin() PluginInfo {
|
||||
return t.rootinfo
|
||||
}
|
||||
|
||||
// SubPlugins returned a map of the PluginInfos for subplugins
|
||||
// within the tree, if any, keyed by subpath.
|
||||
func (t *Tree) SubPlugins() map[string]PluginInfo {
|
||||
// TODO implement these once ParsePluginFS descends
|
||||
return nil
|
||||
}
|
||||
|
||||
// TreeList is a slice of validated plugin fs Trees with helper methods
|
||||
// for filtering to particular subsets of its members.
|
||||
type TreeList []*Tree
|
||||
|
||||
// LineagesForSlot returns the set of plugin-defined lineages that implement a
|
||||
// particular named Grafana slot (See ["github.com/grafana/grafana/pkg/framework/coremodel".SchemaInterface]).
|
||||
func (tl TreeList) LineagesForSlot(slotname string) map[string]thema.Lineage {
|
||||
m := make(map[string]thema.Lineage)
|
||||
for _, tree := range tl {
|
||||
rootp := tree.RootPlugin()
|
||||
rid := rootp.Meta().Id
|
||||
|
||||
if lin, has := rootp.SlotImplementations()[slotname]; has {
|
||||
m[rid] = lin
|
||||
}
|
||||
// ParsePluginFS takes a virtual filesystem and checks that it contains a valid
|
||||
// set of files that statically define a Grafana plugin.
|
||||
//
|
||||
// The fsys must contain a plugin.json at the root, which must be valid
|
||||
// according to the [plugindef] schema. If any .cue files exist in the
|
||||
// grafanaplugin package, these will also be loaded and validated according to
|
||||
// the [GrafanaPlugin] specification. This includes the validation of any custom
|
||||
// or composable kinds and their contained lineages, via [thema.BindLineage].
|
||||
//
|
||||
// This function parses exactly one plugin. It does not descend into
|
||||
// subdirectories to search for additional plugin.json or .cue files.
|
||||
//
|
||||
// Calling this with a nil [thema.Runtime] (the singleton returned from
|
||||
// [cuectx.GrafanaThemaRuntime] is used) will memoize certain CUE operations.
|
||||
// Prefer passing nil unless a different thema.Runtime is specifically required.
|
||||
//
|
||||
// [GrafanaPlugin]: https://github.com/grafana/grafana/blob/main/pkg/plugins/pfs/grafanaplugin.cue
|
||||
func ParsePluginFS(fsys fs.FS, rt *thema.Runtime) (ParsedPlugin, error) {
|
||||
if fsys == nil {
|
||||
return ParsedPlugin{}, ErrEmptyFS
|
||||
}
|
||||
if rt == nil {
|
||||
rt = cuectx.GrafanaThemaRuntime()
|
||||
}
|
||||
|
||||
return m
|
||||
}
|
||||
|
||||
// PluginInfo represents everything knowable about a single plugin from static
|
||||
// analysis of its filesystem tree contents.
|
||||
type PluginInfo struct {
|
||||
meta plugindef.PluginDef
|
||||
slotimpls map[string]thema.Lineage
|
||||
imports []*ast.ImportSpec
|
||||
}
|
||||
|
||||
// CUEImports lists the CUE import statements in the plugin's models.cue file,
|
||||
// if any.
|
||||
func (pi PluginInfo) CUEImports() []*ast.ImportSpec {
|
||||
return pi.imports
|
||||
}
|
||||
|
||||
// SlotImplementations returns a map of the plugin's Thema lineages that
|
||||
// implement particular slots, keyed by the name of the slot.
|
||||
//
|
||||
// Returns an empty map if the plugin has not implemented any slots.
|
||||
func (pi PluginInfo) SlotImplementations() map[string]thema.Lineage {
|
||||
return pi.slotimpls
|
||||
}
|
||||
|
||||
// Meta returns the metadata declared in the plugin's plugin.json file.
|
||||
func (pi PluginInfo) Meta() plugindef.PluginDef {
|
||||
return pi.meta
|
||||
}
|
||||
|
||||
// ParsePluginFS takes an fs.FS and checks that it represents exactly one valid
|
||||
// plugin fs tree, with the fs.FS root as the root of the tree.
|
||||
//
|
||||
// It does not descend into subdirectories to search for additional plugin.json
|
||||
// files.
|
||||
//
|
||||
// Calling this with a nil thema.Runtime will take advantage of memoization.
|
||||
// Prefer this approach unless a different thema.Runtime is specifically
|
||||
// required.
|
||||
//
|
||||
// TODO no descent is ok for core plugins, but won't cut it in general
|
||||
func ParsePluginFS(f fs.FS, rt *thema.Runtime) (*Tree, error) {
|
||||
if f == nil {
|
||||
return nil, ErrEmptyFS
|
||||
}
|
||||
lin, err := plugindef.Lineage(rt)
|
||||
if err != nil {
|
||||
panic(fmt.Sprintf("plugindef lineage is invalid or broken, needs dev attention: %s", err))
|
||||
}
|
||||
mux := vmux.NewValueMux(lin.TypedSchema(), vmux.NewJSONCodec("plugin.json"))
|
||||
ctx := rt.Context()
|
||||
|
||||
b, err := fs.ReadFile(f, "plugin.json")
|
||||
b, err := fs.ReadFile(fsys, "plugin.json")
|
||||
if err != nil {
|
||||
if errors.Is(err, fs.ErrNotExist) {
|
||||
return nil, ErrNoRootFile
|
||||
return ParsedPlugin{}, ErrNoRootFile
|
||||
}
|
||||
return nil, fmt.Errorf("error reading plugin.json: %w", err)
|
||||
return ParsedPlugin{}, fmt.Errorf("error reading plugin.json: %w", err)
|
||||
}
|
||||
|
||||
tree := &Tree{
|
||||
raw: f,
|
||||
rootinfo: PluginInfo{
|
||||
slotimpls: make(map[string]thema.Lineage),
|
||||
},
|
||||
pp := ParsedPlugin{
|
||||
ComposableKinds: make(map[string]kindsys.Composable),
|
||||
// CustomKinds: make(map[string]kindsys.Custom),
|
||||
}
|
||||
r := &tree.rootinfo
|
||||
|
||||
// Pass the raw bytes into the muxer, get the populated PluginDef type out that we want.
|
||||
// TODO stop ignoring second return. (for now, lacunas are a WIP and can't occur until there's >1 schema in the plugindef lineage)
|
||||
pmeta, _, err := mux(b)
|
||||
pinst, _, err := vmux.NewTypedMux(lin.TypedSchema(), vmux.NewJSONCodec("plugin.json"))(b)
|
||||
if err != nil {
|
||||
// TODO more nuanced error handling by class of Thema failure
|
||||
return nil, ewrap(err, ErrInvalidRootFile)
|
||||
return ParsedPlugin{}, errors.Wrap(errors.Promote(err, ""), ErrInvalidRootFile)
|
||||
}
|
||||
pp.Properties = *(pinst.ValueP())
|
||||
// FIXME remove this once it's being correctly populated coming out of lineage
|
||||
if pp.Properties.PascalName == "" {
|
||||
pp.Properties.PascalName = plugindef.DerivePascalName(pp.Properties)
|
||||
}
|
||||
r.meta = *pmeta
|
||||
|
||||
if modbyt, err := fs.ReadFile(f, "models.cue"); err == nil {
|
||||
// TODO introduce layered CUE dependency-injecting loader
|
||||
//
|
||||
// Until CUE has proper dependency management (and possibly even after), loading
|
||||
// CUE files with non-stdlib imports requires injecting the imported packages
|
||||
// into cue.mod/pkg/<import path>, unless the imports are within the same CUE
|
||||
// module. Thema introduced a system for this for its dependers, which we use
|
||||
// here, but we'll need to layer the same on top for importable Grafana packages.
|
||||
// Needing to do this twice strongly suggests it needs a generic, standalone
|
||||
// library.
|
||||
if cuefiles, err := fs.Glob(fsys, "*.cue"); err != nil {
|
||||
return ParsedPlugin{}, fmt.Errorf("error globbing for cue files in fsys: %w", err)
|
||||
} else if len(cuefiles) == 0 {
|
||||
return pp, nil
|
||||
}
|
||||
|
||||
mfs := merged_fs.NewMergedFS(f, grafana.CueSchemaFS)
|
||||
gpv := loadGP(rt.Context())
|
||||
|
||||
// Note that this actually will load any .cue files in the fs.FS root dir in the plugindef.PkgName.
|
||||
// That's...maybe good? But not what it says on the tin
|
||||
bi, err := load.InstanceWithThema(mfs, "", load.Package(plugindef.PkgName))
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("loading models.cue failed: %w", err)
|
||||
fsys, err = ensureCueMod(fsys, pp.Properties)
|
||||
if err != nil {
|
||||
return ParsedPlugin{}, fmt.Errorf("%s has invalid cue.mod: %w", pp.Properties.Id, err)
|
||||
}
|
||||
|
||||
bi, err := cuectx.LoadInstanceWithGrafana(fsys, "", load.Package(PackageName))
|
||||
if err != nil || bi.Err != nil {
|
||||
if err == nil {
|
||||
err = bi.Err
|
||||
}
|
||||
return ParsedPlugin{}, errors.Wrap(errors.Newf(token.NoPos, "%s did not load", pp.Properties.Id), err)
|
||||
}
|
||||
|
||||
pf, _ := parser.ParseFile("models.cue", modbyt, parser.ParseComments)
|
||||
f, _ := parser.ParseFile("plugin.json", fmt.Sprintf(`{
|
||||
"id": %q,
|
||||
"pascalName": %q
|
||||
}`, pp.Properties.Id, pp.Properties.PascalName))
|
||||
|
||||
for _, im := range pf.Imports {
|
||||
for _, f := range bi.Files {
|
||||
for _, im := range f.Imports {
|
||||
ip := strings.Trim(im.Path.Value, "\"")
|
||||
if !importAllowed(ip) {
|
||||
return nil, ewrap(errors.Newf(im.Pos(), "import %q in models.cue not allowed, plugins may only import from:\n%s\n", ip, allowedImportsStr), ErrDisallowedCUEImport)
|
||||
}
|
||||
r.imports = append(r.imports, im)
|
||||
}
|
||||
|
||||
val := ctx.BuildInstance(bi)
|
||||
if val.Err() != nil {
|
||||
return nil, ewrap(fmt.Errorf("models.cue is invalid CUE: %w", val.Err()), ErrInvalidCUE)
|
||||
}
|
||||
for _, s := range allslots {
|
||||
iv := val.LookupPath(cue.ParsePath(s.slot.Name()))
|
||||
if iv.Exists() {
|
||||
lin, err := bindSlotLineage(iv, s.slot, r.meta, rt)
|
||||
if lin != nil {
|
||||
r.slotimpls[s.slot.Name()] = lin
|
||||
}
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return ParsedPlugin{}, errors.Wrap(errors.Newf(im.Pos(),
|
||||
"import of %q in grafanaplugin cue package not allowed, plugins may only import from:\n%s\n", ip, allowedImportsStr),
|
||||
ErrDisallowedCUEImport)
|
||||
}
|
||||
pp.CUEImports = append(pp.CUEImports, im)
|
||||
}
|
||||
}
|
||||
|
||||
return tree, nil
|
||||
}
|
||||
// build.Instance.Files has a comment indicating the CUE authors want to change
|
||||
// its behavior. This is a tripwire to tell us if/when they do that - otherwise, if
|
||||
// the change they make ends up making bi.Files empty, the above loop will silently
|
||||
// become a no-op, and we'd lose enforcement of import restrictions in plugins without
|
||||
// realizing it.
|
||||
if len(bi.Files) != len(bi.BuildFiles) {
|
||||
panic("Refactor required - upstream CUE implementation changed, bi.Files is no longer populated")
|
||||
}
|
||||
|
||||
func bindSlotLineage(v cue.Value, s kindsys.SchemaInterface, meta plugindef.PluginDef, rt *thema.Runtime, opts ...thema.BindOption) (thema.Lineage, error) {
|
||||
// temporarily keep this around, there are IMMEDIATE plans to refactor
|
||||
var required bool
|
||||
accept := s.Should(string(meta.Type))
|
||||
exists := v.Exists()
|
||||
// Inject the JSON directly into the build so it gets loaded together
|
||||
bi.BuildFiles = append(bi.BuildFiles, &build.File{
|
||||
Filename: "plugin.json",
|
||||
Encoding: build.JSON,
|
||||
Form: build.Data,
|
||||
Source: b,
|
||||
})
|
||||
bi.Files = append(bi.Files, f)
|
||||
|
||||
if !accept {
|
||||
if exists {
|
||||
// If it's not accepted for the type, but is declared, error out. This keeps a
|
||||
// precise boundary on what's actually expected for plugins to do, which makes
|
||||
// for clearer docs and guarantees for users.
|
||||
return nil, ewrap(fmt.Errorf("%s: %s plugins may not provide a %s slot implementation in models.cue", meta.Id, meta.Type, s.Name()), ErrImplementedSlots)
|
||||
gpi := ctx.BuildInstance(bi).Unify(gpv)
|
||||
if gpi.Err() != nil {
|
||||
return ParsedPlugin{}, errors.Wrap(errors.Promote(ErrInvalidGrafanaPluginInstance, pp.Properties.Id), gpi.Err())
|
||||
}
|
||||
|
||||
for _, si := range allsi {
|
||||
iv := gpi.LookupPath(cue.MakePath(cue.Str("composableKinds"), cue.Str(si.Name())))
|
||||
if !iv.Exists() {
|
||||
continue
|
||||
}
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
if !exists && required {
|
||||
return nil, ewrap(fmt.Errorf("%s: %s plugins must provide a %s slot implementation in models.cue", meta.Id, meta.Type, s.Name()), ErrImplementedSlots)
|
||||
}
|
||||
|
||||
// TODO make this opt real in thema, then uncomment to enforce joinSchema
|
||||
// lin, err := thema.BindLineage(iv, rt, thema.SatisfiesJoinSchema(s.MetaSchema()))
|
||||
lin, err := thema.BindLineage(v, rt, opts...)
|
||||
if err != nil {
|
||||
return nil, ewrap(fmt.Errorf("%s: invalid thema lineage for slot %s: %w", meta.Id, s.Name(), err), ErrInvalidLineage)
|
||||
}
|
||||
|
||||
sanid := sanitizePluginId(meta.Id)
|
||||
if lin.Name() != sanid {
|
||||
errf := func(format string, args ...interface{}) error {
|
||||
var errin error
|
||||
if n := v.LookupPath(cue.ParsePath("name")).Source(); n != nil {
|
||||
errin = errors.Newf(n.Pos(), format, args...)
|
||||
} else {
|
||||
errin = fmt.Errorf(format, args...)
|
||||
}
|
||||
return ewrap(errin, ErrLineageNameMismatch)
|
||||
props, err := kindsys.ToKindProps[kindsys.ComposableProperties](iv)
|
||||
if err != nil {
|
||||
return ParsedPlugin{}, err
|
||||
}
|
||||
if sanid != meta.Id {
|
||||
return nil, errf("%s: %q slot lineage name must be the sanitized plugin id (%q), got %q", meta.Id, s.Name(), sanid, lin.Name())
|
||||
} else {
|
||||
return nil, errf("%s: %q slot lineage name must be the plugin id, got %q", meta.Id, s.Name(), lin.Name())
|
||||
|
||||
compo, err := kindsys.BindComposable(rt, kindsys.Decl[kindsys.ComposableProperties]{
|
||||
Properties: props,
|
||||
V: iv,
|
||||
})
|
||||
if err != nil {
|
||||
return ParsedPlugin{}, err
|
||||
}
|
||||
pp.ComposableKinds[si.Name()] = compo
|
||||
}
|
||||
return lin, nil
|
||||
|
||||
// TODO custom kinds
|
||||
return pp, nil
|
||||
}
|
||||
|
||||
// Plugin IDs are allowed to contain characters that aren't allowed in thema
|
||||
// Lineage names, CUE package names, Go package names, TS or Go type names, etc.
|
||||
func sanitizePluginId(s string) string {
|
||||
return strings.Map(func(r rune) rune {
|
||||
switch {
|
||||
case r >= 'a' && r <= 'z':
|
||||
fallthrough
|
||||
case r >= 'A' && r <= 'Z':
|
||||
fallthrough
|
||||
case r >= '0' && r <= '9':
|
||||
fallthrough
|
||||
case r == '_':
|
||||
return r
|
||||
case r == '-':
|
||||
return '_'
|
||||
default:
|
||||
return -1
|
||||
func ensureCueMod(fsys fs.FS, pdef plugindef.PluginDef) (fs.FS, error) {
|
||||
if modf, err := fs.ReadFile(fsys, filepath.Join("cue.mod", "module.cue")); err != nil {
|
||||
if !errors.Is(err, fs.ErrNotExist) {
|
||||
return nil, err
|
||||
}
|
||||
}, s)
|
||||
}
|
||||
|
||||
func ewrap(actual, is error) error {
|
||||
return &errPassthrough{
|
||||
actual: actual,
|
||||
is: is,
|
||||
return merged_fs.NewMergedFS(fsys, fstest.MapFS{
|
||||
"cue.mod/module.cue": &fstest.MapFile{Data: []byte(fmt.Sprintf(`module: "grafana.com/grafana/plugins/%s"`, pdef.Id))},
|
||||
}), nil
|
||||
} else if _, err := cuecontext.New().CompileBytes(modf).LookupPath(cue.MakePath(cue.Str("module"))).String(); err != nil {
|
||||
return nil, fmt.Errorf("error reading cue module name: %w", err)
|
||||
}
|
||||
}
|
||||
|
||||
type errPassthrough struct {
|
||||
actual error
|
||||
is error
|
||||
}
|
||||
|
||||
func (e *errPassthrough) Is(err error) bool {
|
||||
return errors.Is(err, e.actual) || errors.Is(err, e.is)
|
||||
}
|
||||
|
||||
func (e *errPassthrough) Error() string {
|
||||
return e.actual.Error()
|
||||
return fsys, nil
|
||||
}
|
||||
|
||||
+12
-19
@@ -13,7 +13,7 @@ import (
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
func TestParseTreeTestdata(t *testing.T) {
|
||||
func TestParsePluginTestdata(t *testing.T) {
|
||||
type tt struct {
|
||||
tfs fs.FS
|
||||
// TODO could remove this by getting rid of inconsistent subdirs
|
||||
@@ -110,11 +110,8 @@ func TestParseTreeTestdata(t *testing.T) {
|
||||
"no-rootfile": {
|
||||
err: ErrNoRootFile,
|
||||
},
|
||||
"valid-model-panel": {},
|
||||
"valid-model-datasource": {},
|
||||
"wrong-slot-panel": {
|
||||
err: ErrImplementedSlots,
|
||||
},
|
||||
"valid-model-panel": {},
|
||||
"valid-model-datasource": {},
|
||||
"missing-kind-datasource": {},
|
||||
"panel-conflicting-joinschema": {
|
||||
err: ErrInvalidLineage,
|
||||
@@ -124,11 +121,8 @@ func TestParseTreeTestdata(t *testing.T) {
|
||||
err: ErrInvalidLineage,
|
||||
skip: "TODO implement BindOption in thema, SatisfiesJoinSchema, then use it here",
|
||||
},
|
||||
"name-id-mismatch": {
|
||||
err: ErrLineageNameMismatch,
|
||||
},
|
||||
"mismatch": {
|
||||
err: ErrLineageNameMismatch,
|
||||
"name-mismatch-panel": {
|
||||
err: ErrInvalidGrafanaPluginInstance,
|
||||
},
|
||||
"disallowed-cue-import": {
|
||||
err: ErrDisallowedCUEImport,
|
||||
@@ -170,11 +164,12 @@ func TestParseTreeTestdata(t *testing.T) {
|
||||
t.Skip(tst.skip)
|
||||
}
|
||||
|
||||
tree, err := ParsePluginFS(tst.tfs, lib)
|
||||
pp, err := ParsePluginFS(tst.tfs, lib)
|
||||
if tst.err == nil {
|
||||
require.NoError(t, err, "unexpected error while parsing plugin tree")
|
||||
} else {
|
||||
require.Error(t, err)
|
||||
t.Logf("%T %s", err, err)
|
||||
require.ErrorIs(t, err, tst.err, "unexpected error type while parsing plugin tree")
|
||||
return
|
||||
}
|
||||
@@ -183,8 +178,7 @@ func TestParseTreeTestdata(t *testing.T) {
|
||||
tst.rootid = name
|
||||
}
|
||||
|
||||
rootp := tree.RootPlugin()
|
||||
require.Equal(t, tst.rootid, rootp.Meta().Id, "expected root plugin id and actual root plugin id differ")
|
||||
require.Equal(t, tst.rootid, pp.Properties.Id, "expected plugin id and actual plugin id differ")
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -270,11 +264,11 @@ func TestParseTreeZips(t *testing.T) {
|
||||
t.Skip(tst.skip)
|
||||
}
|
||||
|
||||
tree, err := ParsePluginFS(tst.tfs, lib)
|
||||
pp, err := ParsePluginFS(tst.tfs, lib)
|
||||
if tst.err == nil {
|
||||
require.NoError(t, err, "unexpected error while parsing plugin tree")
|
||||
require.NoError(t, err, "unexpected error while parsing plugin fs")
|
||||
} else {
|
||||
require.ErrorIs(t, err, tst.err, "unexpected error type while parsing plugin tree")
|
||||
require.ErrorIs(t, err, tst.err, "unexpected error type while parsing plugin fs")
|
||||
return
|
||||
}
|
||||
|
||||
@@ -282,8 +276,7 @@ func TestParseTreeZips(t *testing.T) {
|
||||
tst.rootid = name
|
||||
}
|
||||
|
||||
rootp := tree.RootPlugin()
|
||||
require.Equal(t, tst.rootid, rootp.Meta().Id, "expected root plugin id and actual root plugin id differ")
|
||||
require.Equal(t, tst.rootid, pp.Properties.Id, "expected plugin id and actual plugin id differ")
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,48 @@
|
||||
package pfs
|
||||
|
||||
import (
|
||||
"cuelang.org/go/cue/ast"
|
||||
"github.com/grafana/grafana/pkg/kindsys"
|
||||
"github.com/grafana/grafana/pkg/plugins/plugindef"
|
||||
)
|
||||
|
||||
// ParsedPlugin represents everything knowable about a single plugin from static
|
||||
// analysis of its filesystem tree contents, as performed by [ParsePluginFS].
|
||||
//
|
||||
// Guarantees described in the below comments only exist for instances of this
|
||||
// struct returned from [ParsePluginFS].
|
||||
type ParsedPlugin struct {
|
||||
// Properties contains the plugin's definition, as declared in plugin.json.
|
||||
Properties plugindef.PluginDef
|
||||
|
||||
// ComposableKinds is a map of all the composable kinds declared in this plugin.
|
||||
// Keys are the name of the [kindsys.SchemaInterface] implemented by the value.
|
||||
//
|
||||
// Composable kind defs are only populated in this map by [ParsePluginFS] if
|
||||
// they are implementations of a known schema interface, or are for
|
||||
// an unknown schema interface.
|
||||
ComposableKinds map[string]kindsys.Composable
|
||||
|
||||
// CustomKinds is a map of all the custom kinds declared in this plugin.
|
||||
// Keys are the machineName of the custom kind.
|
||||
// CustomKinds map[string]kindsys.Custom
|
||||
|
||||
// CUEImports lists the CUE import statements in the plugin's grafanaplugin CUE
|
||||
// package, if any.
|
||||
CUEImports []*ast.ImportSpec
|
||||
}
|
||||
|
||||
// TODO is this static approach worth using, akin to core generated registries? instead of the ParsedPlugins.ComposableKinds map? in addition to it?
|
||||
// ComposableKinds represents all the possible composable kinds that may be
|
||||
// defined in a Grafana plugin.
|
||||
//
|
||||
// The value of each field, if non-nil, is a standard [kindsys.Decl]
|
||||
// representing a CUE definition of a composable kind that implements the
|
||||
// schema interface corresponding to the field's name. (This invariant is
|
||||
// only enforced in [ComposableKinds] returned from [ParsePluginFS].)
|
||||
//
|
||||
// type ComposableKinds struct {
|
||||
// PanelCfg kindsys.Decl[kindsys.ComposableProperties]
|
||||
// Queries kindsys.Decl[kindsys.ComposableProperties]
|
||||
// DSCfg kindsys.Decl[kindsys.ComposableProperties]
|
||||
// }
|
||||
@@ -0,0 +1,31 @@
|
||||
package pfs
|
||||
|
||||
import (
|
||||
"sort"
|
||||
"testing"
|
||||
|
||||
"github.com/google/go-cmp/cmp"
|
||||
"github.com/grafana/grafana/pkg/kindsys"
|
||||
)
|
||||
|
||||
// This is a brick-dumb test that just ensures known schema interfaces are being
|
||||
// loaded correctly from their declarations in .cue files.
|
||||
//
|
||||
// If this test fails, it's either because:
|
||||
// - They're not being loaded correctly - there's a bug in kindsys or pfs somewhere, fix it
|
||||
// - The set of schema interfaces has been modified - update the static list here
|
||||
func TestSchemaInterfacesAreLoaded(t *testing.T) {
|
||||
knownSI := []string{"PanelCfg", "DataQuery", "DataSourceCfg"}
|
||||
all := kindsys.SchemaInterfaces(nil)
|
||||
var loadedSI []string
|
||||
for k := range all {
|
||||
loadedSI = append(loadedSI, k)
|
||||
}
|
||||
|
||||
sort.Strings(knownSI)
|
||||
sort.Strings(loadedSI)
|
||||
|
||||
if diff := cmp.Diff(knownSI, loadedSI); diff != "" {
|
||||
t.Fatalf("kindsys cue-declared schema interfaces differ from ComposableKinds go struct:\n%s", diff)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,43 @@
|
||||
package plugindef
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
func TestDerivePascal(t *testing.T) {
|
||||
table := []struct {
|
||||
id, name, out string
|
||||
}{
|
||||
{
|
||||
name: "-- Grafana --",
|
||||
out: "Grafana",
|
||||
},
|
||||
{
|
||||
name: "A weird/Thing",
|
||||
out: "AWeirdThing",
|
||||
},
|
||||
{
|
||||
name: "/",
|
||||
out: "Empty",
|
||||
},
|
||||
{
|
||||
name: "some really Long thing WHY would38883 anyone do this i don't know but hey It seems like it this is just going on and",
|
||||
out: "SomeReallyLongThingWHYWouldAnyoneDoThisIDonTKnowButHeyItSeemsLi",
|
||||
},
|
||||
}
|
||||
|
||||
for _, row := range table {
|
||||
if row.id == "" {
|
||||
row.id = "default-empty-panel"
|
||||
}
|
||||
|
||||
pd := PluginDef{
|
||||
Id: row.id,
|
||||
Name: row.name,
|
||||
}
|
||||
|
||||
require.Equal(t, row.out, DerivePascalName(pd))
|
||||
}
|
||||
}
|
||||
@@ -2,6 +2,7 @@ package plugindef
|
||||
|
||||
import (
|
||||
"strings"
|
||||
"regexp"
|
||||
|
||||
"github.com/grafana/thema"
|
||||
)
|
||||
@@ -34,6 +35,18 @@ seqs: [
|
||||
// the UI.
|
||||
name: string
|
||||
|
||||
// FIXME there appears to be a bug in thema that prevents this from working. Maybe it'd
|
||||
// help to refer to it with an alias, but thema can't support using current list syntax.
|
||||
// syntax (fixed by grafana/thema#82). Either way, for now, pascalName gets populated in Go.
|
||||
let sani = (strings.ToTitle(regexp.ReplaceAllLiteral("[^a-zA-Z]+", name, "")))
|
||||
|
||||
// The PascalCase name for the plugin. Used for creating machine-friendly
|
||||
// identifiers, typically in code generation.
|
||||
//
|
||||
// If not provided, defaults to name, but title-cased and sanitized (only
|
||||
// alphabetical characters allowed).
|
||||
pascalName: string & =~"^([A-Z][a-zA-Z]{1,62})$" | *sani
|
||||
|
||||
// Plugin category used on the Add data source page.
|
||||
category?: "tsdb" | "logging" | "cloud" | "tracing" | "sql" | "enterprise" | "profiling" | "other"
|
||||
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
package plugindef
|
||||
|
||||
import (
|
||||
"strings"
|
||||
"sync"
|
||||
|
||||
"cuelang.org/go/cue/build"
|
||||
@@ -10,10 +11,6 @@ import (
|
||||
|
||||
//go:generate go run gen.go
|
||||
|
||||
// PkgName is the name of the CUE package that Grafana will load when looking
|
||||
// for kind declarations by a Grafana plugin.
|
||||
const PkgName = "grafanaplugin"
|
||||
|
||||
func loadInstanceForplugindef() (*build.Instance, error) {
|
||||
return cuectx.LoadGrafanaInstance("pkg/plugins/plugindef", "", nil)
|
||||
}
|
||||
@@ -36,3 +33,41 @@ func Lineage(rt *thema.Runtime, opts ...thema.BindOption) (thema.ConvergentLinea
|
||||
}
|
||||
return doLineage(rt, opts...)
|
||||
}
|
||||
|
||||
// DerivePascalName derives a PascalCase name from a PluginDef.
|
||||
//
|
||||
// This function does not mutate the input PluginDef; as such, it ignores
|
||||
// whether there exists any value for PluginDef.PascalName.
|
||||
//
|
||||
// FIXME this should be removable once CUE logic for it works/unmarshals correctly.
|
||||
func DerivePascalName(pd PluginDef) string {
|
||||
sani := func(s string) string {
|
||||
ret := strings.Title(strings.Map(func(r rune) rune {
|
||||
switch {
|
||||
case r >= 'a' && r <= 'z':
|
||||
return r
|
||||
case r >= 'A' && r <= 'Z':
|
||||
return r
|
||||
default:
|
||||
return -1
|
||||
}
|
||||
}, strings.Title(strings.Map(func(r rune) rune {
|
||||
switch r {
|
||||
case '-', '_':
|
||||
return ' '
|
||||
default:
|
||||
return r
|
||||
}
|
||||
}, s))))
|
||||
if len(ret) > 63 {
|
||||
return ret[:63]
|
||||
}
|
||||
return ret
|
||||
}
|
||||
|
||||
fromname := sani(pd.Name)
|
||||
if len(fromname) != 0 {
|
||||
return fromname
|
||||
}
|
||||
return sani(strings.Split(pd.Id, "-")[1])
|
||||
}
|
||||
|
||||
@@ -432,6 +432,13 @@ type PluginDef struct {
|
||||
// the UI.
|
||||
Name string `json:"name"`
|
||||
|
||||
// The PascalCase name for the plugin. Used for creating machine-friendly
|
||||
// identifiers, typically in code generation.
|
||||
//
|
||||
// If not provided, defaults to name, but title-cased and sanitized (only
|
||||
// alphabetical characters allowed).
|
||||
PascalName string `json:"pascalName"`
|
||||
|
||||
// Initialize plugin on startup. By default, the plugin
|
||||
// initializes on first use.
|
||||
Preload *bool `json:"preload,omitempty"`
|
||||
|
||||
Reference in New Issue
Block a user