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:
sam boyer
2023-01-20 09:41:35 +00:00
committed by GitHub
parent 6a7cbeae6c
commit 3b3059c9ce
91 changed files with 2192 additions and 1870 deletions
+1 -1
View File
@@ -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 }}
}
}
+4 -1
View File
@@ -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")
},
]
},
]
}
-14
View File
@@ -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,9 +1,6 @@
package grafanaplugin
import "github.com/grafana/thema"
Query: thema.#Lineage & {
name: "missing_kind_datasource"
composableKinds: DataQuery: lineage: {
seqs: [
{
schemas: [
@@ -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
},
]
},
]
}
@@ -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
},
]
},
]
}
}
@@ -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
},
]
},
]
}
@@ -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,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"
}
}
}
+9 -9
View File
@@ -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
}
+49 -49
View File
@@ -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),
}
}
+13 -17
View File
@@ -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 -1
View File
@@ -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
View File
@@ -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")
+31
View File
@@ -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
View File
@@ -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
View File
@@ -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")
})
}
}
+48
View File
@@ -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]
// }
+31
View File
@@ -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)
}
}
+43
View File
@@ -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))
}
}
+13
View File
@@ -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"
+39 -4
View File
@@ -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"`