Core: Remove thema and kindsys dependencies (#84499)

* Move some thema code inside grafana

* Use new codegen instead of thema for core kinds

* Replace TS generator

* Use new generator for go types

* Remove thema from oapi generator

* Remove thema from generators

* Don't use kindsys/thema for core kinds

* Remove kindsys/thema from plugins

* Remove last thema related

* Remove most of cuectx and move utils_ts into codegen. It also deletes wire dependency

* Merge plugins generators

* Delete thema dependency 🎉

* Fix CODEOWNERS

* Fix package name

* Fix TS output names

* More path fixes

* Fix mod codeowners

* Use original plugin's name

* Remove kindsys dependency 🎉

* Modify oapi schema and create an apply function to fix elasticsearch errors

* cue.mod was deleted by mistake

* Fix TS panels

* sort imports

* Fixing elasticsearch output

* Downgrade oapi-codegen library

* Update output ts files

* More fixes

* Restore old elasticsearch generated file and skip its generation. Remove core imports into plugins

* More lint fixes

* Add codeowners

* restore embed.go file

* Fix embed.go
This commit is contained in:
Selene
2024-03-21 11:11:29 +01:00
committed by GitHub
parent 856e410480
commit 473898e47c
56 changed files with 1433 additions and 1631 deletions

View File

@@ -8,6 +8,7 @@ import (
"strings"
"github.com/grafana/codejen"
"github.com/grafana/grafana/pkg/plugins/pfs"
)
var registryPath = filepath.Join("pkg", "registry", "schemas")
@@ -27,21 +28,22 @@ func (jenny *PluginRegistryJenny) JennyName() string {
return "PluginRegistryJenny"
}
func (jenny *PluginRegistryJenny) Generate(files []string) (*codejen.File, error) {
if len(files) == 0 {
func (jenny *PluginRegistryJenny) Generate(decls ...*pfs.PluginDecl) (*codejen.File, error) {
if len(decls) == 0 {
return nil, nil
}
schemas := make([]Schema, len(files))
for i, file := range files {
name, err := getSchemaName(file)
schemas := make([]Schema, len(decls))
for i, decl := range decls {
variant := fmt.Sprintf("%s.cue", strings.ToLower(decl.SchemaInterface.Name))
name, err := getSchemaName(decl.PluginPath)
if err != nil {
return nil, fmt.Errorf("unable to find schema name: %s", err)
}
schemas[i] = Schema{
Name: name,
Filename: filepath.Base(file),
FilePath: file,
Filename: variant,
FilePath: "./" + filepath.Join("public", "app", "plugins", decl.PluginPath, variant),
}
}
@@ -65,7 +67,7 @@ func getSchemaName(path string) (string, error) {
if len(parts) < 2 {
return "", fmt.Errorf("path should contain more than 2 elements")
}
folderName := parts[len(parts)-2]
folderName := parts[len(parts)-1]
if renamed, ok := renamedPlugins[folderName]; ok {
folderName = renamed
}

View File

@@ -6,12 +6,9 @@ import (
"strings"
copenapi "cuelang.org/go/encoding/openapi"
"github.com/dave/dst/dstutil"
"github.com/grafana/codejen"
corecodegen "github.com/grafana/grafana/pkg/codegen"
"github.com/grafana/grafana/pkg/codegen/generators"
"github.com/grafana/grafana/pkg/plugins/pfs"
"github.com/grafana/thema/encoding/gocode"
"github.com/grafana/thema/encoding/openapi"
)
// TODO this is duplicative of other Go type jennies. Remove it in favor of a better-abstracted version in thema itself
@@ -30,22 +27,22 @@ func (j *pgoJenny) JennyName() string {
}
func (j *pgoJenny) Generate(decl *pfs.PluginDecl) (*codejen.File, error) {
b := decl.PluginMeta.Backend
if b == nil || !*b || !decl.HasSchema() {
hasBackend := decl.PluginMeta.Backend
// We skip elasticsearch since we have problems with the generated file.
// This is temporal until we migrate to the new system.
if hasBackend == nil || !*hasBackend || decl.PluginMeta.Id == "elasticsearch" {
return nil, nil
}
slotname := strings.ToLower(decl.SchemaInterface.Name)
byt, err := gocode.GenerateTypesOpenAPI(decl.Lineage.Latest(), &gocode.TypeConfigOpenAPI{
Config: &openapi.Config{
Group: decl.SchemaInterface.IsGroup,
byt, err := generators.GenerateTypesGo(decl.CueFile, &generators.GoConfig{
Config: &generators.OpenApiConfig{
Config: &copenapi.Config{
MaxCycleDepth: 10,
},
SplitSchema: true,
IsGroup: decl.SchemaInterface.IsGroup,
},
PackageName: slotname,
ApplyFuncs: []dstutil.ApplyFunc{corecodegen.PrefixDropper(decl.Lineage.Name())},
})
if err != nil {
return nil, err

View File

@@ -11,7 +11,6 @@ import (
tsast "github.com/grafana/cuetsy/ts/ast"
"github.com/grafana/grafana/pkg/build"
"github.com/grafana/grafana/pkg/codegen"
"github.com/grafana/grafana/pkg/cuectx"
"github.com/grafana/grafana/pkg/plugins/pfs"
)
@@ -34,15 +33,11 @@ func (j *ptsJenny) JennyName() string {
}
func (j *ptsJenny) Generate(decl *pfs.PluginDecl) (codejen.Files, error) {
if !decl.HasSchema() {
return nil, nil
}
genFile := &tsast.File{}
versionedFile := &tsast.File{}
for _, im := range decl.Imports {
if tsim, err := cuectx.ConvertImport(im); err != nil {
if tsim, err := codegen.ConvertImport(im); err != nil {
return nil, err
} else if tsim.From.Value != "" {
genFile.Imports = append(genFile.Imports, tsim)
@@ -94,18 +89,46 @@ func getPluginVersion(pluginVersion *string) string {
func adaptToPipeline(j codejen.OneToOne[codegen.SchemaForGen]) codejen.OneToOne[*pfs.PluginDecl] {
return codejen.AdaptOneToOne(j, func(pd *pfs.PluginDecl) codegen.SchemaForGen {
name := strings.ReplaceAll(pd.PluginMeta.Name, " ", "")
if pd.SchemaInterface.Name == "DataQuery" {
name = name + "DataQuery"
}
return codegen.SchemaForGen{
Name: name,
Schema: pd.Lineage.Latest(),
Name: derivePascalName(pd.PluginMeta.Id, pd.PluginMeta.Name) + pd.SchemaInterface.Name,
CueFile: pd.CueFile,
IsGroup: pd.SchemaInterface.IsGroup,
}
})
}
func derivePascalName(id string, name string) 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(name)
if len(fromname) != 0 {
return fromname
}
return sani(strings.Split(id, "-")[1])
}
func getGrafanaVersion() string {
dir, err := os.Getwd()
if err != nil {

View File

@@ -1,30 +0,0 @@
package corelist
import (
"sync"
"github.com/grafana/grafana/pkg/cuectx"
"github.com/grafana/grafana/pkg/plugins/pfs"
"github.com/grafana/thema"
)
var coreTrees []pfs.ParsedPlugin
var coreOnce sync.Once
// 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.ParsedPlugin {
var pl []pfs.ParsedPlugin
if rt == nil {
coreOnce.Do(func() {
coreTrees = corePlugins(cuectx.GrafanaThemaRuntime())
})
pl = make([]pfs.ParsedPlugin, len(coreTrees))
copy(pl, coreTrees)
} else {
return corePlugins(rt)
}
return pl
}

View File

@@ -1,87 +0,0 @@
// Code generated - EDITING IS FUTILE. DO NOT EDIT.
//
// Generated by:
// public/app/plugins/gen.go
// Using jennies:
// PluginTreeListJenny
//
// Run 'make gen-cue' from repository root to regenerate.
package corelist
import (
"fmt"
"io/fs"
"github.com/grafana/grafana"
"github.com/grafana/grafana/pkg/plugins/pfs"
"github.com/grafana/thema"
)
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)
}
pp, err := pfs.ParsePluginFS(sub, rt)
if err != nil {
panic(fmt.Sprintf("error parsing plugin metadata for %s: %s", pkgname, err))
}
return pp
}
func corePlugins(rt *thema.Runtime) []pfs.ParsedPlugin {
return []pfs.ParsedPlugin{
parsePluginOrPanic("public/app/plugins/datasource/alertmanager", "alertmanager", rt),
parsePluginOrPanic("public/app/plugins/datasource/azuremonitor", "grafana_azure_monitor_datasource", 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-postgresql-datasource", "grafana_postgresql_datasource", rt),
parsePluginOrPanic("public/app/plugins/datasource/grafana-pyroscope-datasource", "grafana_pyroscope_datasource", rt),
parsePluginOrPanic("public/app/plugins/datasource/grafana-testdata-datasource", "grafana_testdata_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/prometheus", "prometheus", rt),
parsePluginOrPanic("public/app/plugins/datasource/tempo", "tempo", rt),
parsePluginOrPanic("public/app/plugins/datasource/zipkin", "zipkin", 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/candlestick", "candlestick", rt),
parsePluginOrPanic("public/app/plugins/panel/canvas", "canvas", rt),
parsePluginOrPanic("public/app/plugins/panel/dashlist", "dashlist", rt),
parsePluginOrPanic("public/app/plugins/panel/datagrid", "datagrid", 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/heatmap", "heatmap", rt),
parsePluginOrPanic("public/app/plugins/panel/histogram", "histogram", 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/state-timeline", "state_timeline", rt),
parsePluginOrPanic("public/app/plugins/panel/status-history", "status_history", rt),
parsePluginOrPanic("public/app/plugins/panel/table", "table", rt),
parsePluginOrPanic("public/app/plugins/panel/table-old", "table_old", rt),
parsePluginOrPanic("public/app/plugins/panel/text", "text", rt),
parsePluginOrPanic("public/app/plugins/panel/timeseries", "timeseries", rt),
parsePluginOrPanic("public/app/plugins/panel/traces", "traces", rt),
parsePluginOrPanic("public/app/plugins/panel/trend", "trend", rt),
parsePluginOrPanic("public/app/plugins/panel/welcome", "welcome", rt),
parsePluginOrPanic("public/app/plugins/panel/xychart", "xychart", rt),
}
}

View File

@@ -1,18 +1,16 @@
package pfs
import (
"cuelang.org/go/cue"
"cuelang.org/go/cue/ast"
"github.com/grafana/kindsys"
"github.com/grafana/thema"
)
type PluginDecl struct {
SchemaInterface *SchemaInterface
Lineage thema.Lineage
SchemaInterface SchemaInterface
CueFile cue.Value
Imports []*ast.ImportSpec
PluginPath string
PluginMeta Metadata
KindDecl kindsys.Def[kindsys.ComposableProperties]
}
type SchemaInterface struct {
@@ -26,15 +24,3 @@ type Metadata struct {
Backend *bool
Version *string
}
func EmptyPluginDecl(path string, meta Metadata) *PluginDecl {
return &PluginDecl{
PluginPath: path,
PluginMeta: meta,
Imports: make([]*ast.ImportSpec, 0),
}
}
func (decl *PluginDecl) HasSchema() bool {
return decl.Lineage != nil && decl.SchemaInterface != nil
}

View File

@@ -6,35 +6,22 @@ import (
"path/filepath"
"sort"
"github.com/grafana/thema"
"cuelang.org/go/cue/cuecontext"
)
type declParser struct {
rt *thema.Runtime
type DeclParser struct {
skip map[string]bool
}
// Extracted from kindsys repository
var schemaInterfaces = map[string]*SchemaInterface{
"PanelCfg": {
Name: "PanelCfg",
IsGroup: true,
},
"DataQuery": {
Name: "DataQuery",
IsGroup: false,
},
}
func NewDeclParser(rt *thema.Runtime, skip map[string]bool) *declParser {
return &declParser{
rt: rt,
func NewDeclParser(skip map[string]bool) *DeclParser {
return &DeclParser{
skip: skip,
}
}
// TODO convert this to be the new parser for Tree
func (psr *declParser) Parse(root fs.FS) ([]*PluginDecl, error) {
func (psr *DeclParser) Parse(root fs.FS) ([]*PluginDecl, error) {
ctx := cuecontext.New()
// TODO remove hardcoded tree structure assumption, work from root of provided fs
plugins, err := fs.Glob(root, "**/**/plugin.json")
if err != nil {
@@ -50,29 +37,22 @@ func (psr *declParser) Parse(root fs.FS) ([]*PluginDecl, error) {
}
dir, _ := fs.Sub(root, path)
pp, err := ParsePluginFS(dir, psr.rt)
pp, err := ParsePluginFS(ctx, dir, path)
if err != nil {
return nil, fmt.Errorf("parsing plugin failed for %s: %s", dir, err)
}
if len(pp.ComposableKinds) == 0 {
decls = append(decls, EmptyPluginDecl(path, pp.Properties))
if !pp.CueFile.Exists() {
continue
}
for slotName, kind := range pp.ComposableKinds {
if err != nil {
return nil, fmt.Errorf("parsing plugin failed for %s: %s", dir, err)
}
decls = append(decls, &PluginDecl{
SchemaInterface: schemaInterfaces[slotName],
Lineage: kind.Lineage(),
Imports: pp.CUEImports,
PluginMeta: pp.Properties,
PluginPath: path,
KindDecl: kind.Def(),
})
}
decls = append(decls, &PluginDecl{
SchemaInterface: pp.Variant,
CueFile: pp.CueFile,
Imports: pp.CUEImports,
PluginMeta: pp.Properties,
PluginPath: path,
})
}
sort.Slice(decls, func(i, j int) bool {

View File

@@ -4,29 +4,32 @@ import (
"encoding/json"
"fmt"
"io/fs"
"sort"
"strings"
"testing/fstest"
"cuelang.org/go/cue"
"cuelang.org/go/cue/cuecontext"
"cuelang.org/go/cue/errors"
"cuelang.org/go/cue/token"
"github.com/grafana/kindsys"
"github.com/grafana/thema"
"github.com/grafana/thema/load"
"github.com/yalue/merged_fs"
"github.com/grafana/grafana/pkg/cuectx"
"cuelang.org/go/cue/load"
"github.com/grafana/grafana/pkg/codegen"
)
// 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 schemaInterface = map[string]SchemaInterface{
"DataQuery": {
Name: "DataQuery",
IsGroup: false,
},
"PanelCfg": {
Name: "PanelCfg",
IsGroup: true,
},
}
// PermittedCUEImports returns the list of import paths that may be used in a
// plugin's grafanaplugin cue package.
var PermittedCUEImports = cuectx.PermittedCUEImports
var PermittedCUEImports = codegen.PermittedCUEImports
func importAllowed(path string) bool {
for _, p := range PermittedCUEImports() {
@@ -39,22 +42,12 @@ func importAllowed(path string) bool {
var allowedImportsStr string
var allsi []kindsys.SchemaInterface
func init() {
all := make([]string, 0, len(PermittedCUEImports()))
for _, im := range PermittedCUEImports() {
all = append(all, fmt.Sprintf("\t%s", im))
}
allowedImportsStr = strings.Join(all, "\n")
for _, s := range kindsys.SchemaInterfaces(nil) {
allsi = append(allsi, s)
}
sort.Slice(allsi, func(i, j int) bool {
return allsi[i].Name() < allsi[j].Name()
})
}
// ParsePluginFS takes a virtual filesystem and checks that it contains a valid
@@ -69,17 +62,17 @@ func init() {
// 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) {
func ParsePluginFS(ctx *cue.Context, fsys fs.FS, dir string) (ParsedPlugin, error) {
if fsys == nil {
return ParsedPlugin{}, ErrEmptyFS
}
if rt == nil {
rt = cuectx.GrafanaThemaRuntime()
cuefiles, err := fs.Glob(fsys, "*.cue")
if err != nil {
return ParsedPlugin{}, fmt.Errorf("error globbing for cue files in fsys: %w", err)
} else if len(cuefiles) == 0 {
return ParsedPlugin{}, nil
}
metadata, err := getPluginMetadata(fsys)
@@ -88,27 +81,19 @@ func ParsePluginFS(fsys fs.FS, rt *thema.Runtime) (ParsedPlugin, error) {
}
pp := ParsedPlugin{
ComposableKinds: make(map[string]kindsys.Composable),
Properties: metadata,
Properties: metadata,
}
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
}
fsys, err = ensureCueMod(fsys, pp.Properties)
if err != nil {
return ParsedPlugin{}, fmt.Errorf("%s has invalid cue.mod: %w", pp.Properties.Id, err)
return ParsedPlugin{}, 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)
bi := load.Instances(cuefiles, &load.Config{
Package: PackageName,
Dir: dir,
})[0]
if bi.Err != nil {
return ParsedPlugin{}, bi.Err
}
for _, f := range bi.Files {
@@ -132,105 +117,35 @@ func ParsePluginFS(fsys fs.FS, rt *thema.Runtime) (ParsedPlugin, error) {
panic("Refactor required - upstream CUE implementation changed, bi.Files is no longer populated")
}
gpi := rt.Context().BuildInstance(bi)
gpi := ctx.BuildInstance(bi)
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())))
for name, si := range schemaInterface {
iv := gpi.LookupPath(cue.MakePath(cue.Str("composableKinds"), cue.Str(name)))
if !iv.Exists() {
continue
}
iv = iv.FillPath(cue.MakePath(cue.Str("schemaInterface")), si.Name())
iv = iv.FillPath(cue.MakePath(cue.Str("name")), derivePascalName(pp.Properties.Id, pp.Properties.Name)+si.Name())
iv = iv.FillPath(cue.MakePath(cue.Str("schemaInterface")), name)
iv = iv.FillPath(cue.MakePath(cue.Str("name")), derivePascalName(pp.Properties.Id, pp.Properties.Name)+name)
lineageNamePath := iv.LookupPath(cue.MakePath(cue.Str("lineage"), cue.Str("name")))
if !lineageNamePath.Exists() {
iv = iv.FillPath(cue.MakePath(cue.Str("lineage"), cue.Str("name")), derivePascalName(pp.Properties.Id, pp.Properties.Name)+si.Name())
iv = iv.FillPath(cue.MakePath(cue.Str("lineage"), cue.Str("name")), derivePascalName(pp.Properties.Id, pp.Properties.Name)+name)
}
validSchema := iv.LookupPath(cue.ParsePath("lineage.schemas[0].schema"))
if !validSchema.Exists() {
return ParsedPlugin{}, errors.Wrap(errors.Promote(ErrInvalidGrafanaPluginInstance, pp.Properties.Id), validSchema.Err())
}
props, err := kindsys.ToKindProps[kindsys.ComposableProperties](iv)
if err != nil {
return ParsedPlugin{}, err
}
compo, err := kindsys.BindComposable(rt, kindsys.Def[kindsys.ComposableProperties]{
Properties: props,
V: iv,
})
if err != nil {
return ParsedPlugin{}, err
}
pp.ComposableKinds[si.Name()] = compo
pp.Variant = si
pp.CueFile = iv
}
return pp, nil
}
// LoadComposableKindDef loads and validates a composable kind definition.
// On success, it returns a [Def] which contains the entire contents of the kind definition.
//
// defpath is the path to the directory containing the composable kind definition,
// relative to the root of the caller's repository.
//
// NOTE This function will be deprecated in favor of a more generic loader when kind
// providers will be implemented.
func LoadComposableKindDef(fsys fs.FS, rt *thema.Runtime, defpath string) (kindsys.Def[kindsys.ComposableProperties], error) {
pp := ParsedPlugin{
ComposableKinds: make(map[string]kindsys.Composable),
Properties: Metadata{
Id: defpath,
},
}
fsys, err := ensureCueMod(fsys, pp.Properties)
if err != nil {
return kindsys.Def[kindsys.ComposableProperties]{}, fmt.Errorf("%s has invalid cue.mod: %w", pp.Properties.Id, err)
}
bi, err := cuectx.LoadInstanceWithGrafana(fsys, "", load.Package(PackageName))
if err != nil {
return kindsys.Def[kindsys.ComposableProperties]{}, err
}
ctx := rt.Context()
v := ctx.BuildInstance(bi)
if v.Err() != nil {
return kindsys.Def[kindsys.ComposableProperties]{}, fmt.Errorf("%s not a valid CUE instance: %w", defpath, v.Err())
}
props, err := kindsys.ToKindProps[kindsys.ComposableProperties](v)
if err != nil {
return kindsys.Def[kindsys.ComposableProperties]{}, err
}
return kindsys.Def[kindsys.ComposableProperties]{
V: v,
Properties: props,
}, nil
}
func ensureCueMod(fsys fs.FS, metadata Metadata) (fs.FS, error) {
if modf, err := fs.ReadFile(fsys, "cue.mod/module.cue"); err != nil {
if !errors.Is(err, fs.ErrNotExist) {
return nil, err
}
return merged_fs.NewMergedFS(fsys, fstest.MapFS{
"cue.mod/module.cue": &fstest.MapFile{Data: []byte(fmt.Sprintf(`module: "grafana.com/grafana/plugins/%s"`, metadata.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)
}
return fsys, nil
}
func getPluginMetadata(fsys fs.FS) (Metadata, error) {
b, err := fs.ReadFile(fsys, "plugin.json")
if err != nil {

View File

@@ -1,297 +0,0 @@
package pfs
import (
"archive/zip"
"io/fs"
"os"
"path/filepath"
"sort"
"testing"
"github.com/google/go-cmp/cmp"
"github.com/grafana/grafana/pkg/cuectx"
"github.com/stretchr/testify/require"
)
func TestParsePluginTestdata(t *testing.T) {
type tt struct {
tfs fs.FS
// TODO could remove this by getting rid of inconsistent subdirs
subpath string
skip string
err error
// TODO could remove this by expecting that dirname == id
rootid string
}
tab := map[string]tt{
"app-with-child": {
rootid: "myorgid-simple-app",
subpath: "dist",
skip: "schema violation, weirdness in info.version field",
},
"duplicate-plugins": {
rootid: "test-app",
subpath: "nested",
skip: "schema violation, dependencies don't follow naming constraints",
},
"includes-symlinks": {
skip: "schema violation, dependencies don't follow naming constraints",
},
"installer": {
rootid: "test-datasource",
subpath: "plugin",
},
"invalid-plugin-json": {
rootid: "test-app",
err: ErrInvalidRootFile,
},
"invalid-v1-signature": {
rootid: "test-datasource",
subpath: "plugin",
},
"invalid-v2-extra-file": {
rootid: "test-datasource",
subpath: "plugin",
},
"invalid-v2-missing-file": {
rootid: "test-datasource",
subpath: "plugin",
},
"lacking-files": {
rootid: "test-datasource",
subpath: "plugin",
},
"nested-plugins": {
rootid: "test-datasource",
subpath: "parent",
},
"non-pvt-with-root-url": {
rootid: "test-datasource",
subpath: "plugin",
},
"renderer-added-file": {
rootid: "test-renderer",
subpath: "plugin",
},
"symbolic-plugin-dirs": {
skip: "io/fs-based scanner will not traverse symlinks; caller of ParsePluginFS() must do it",
},
"test-app": {
skip: "schema violation, dependencies don't follow naming constraints",
rootid: "test-app",
},
"test-app-with-includes": {
rootid: "test-app",
skip: "has a 'page'-type include which isn't a known part of spec",
},
"test-app-with-roles": {
rootid: "test-app",
},
"unsigned-datasource": {
rootid: "test-datasource",
subpath: "plugin",
},
"unsigned-panel": {
rootid: "test-panel",
subpath: "plugin",
},
"valid-v2-pvt-signature": {
rootid: "test-datasource",
subpath: "plugin",
},
"valid-v2-pvt-signature-root-url-uri": {
rootid: "test-datasource",
subpath: "plugin",
},
"valid-v2-signature": {
rootid: "test-datasource",
subpath: "plugin",
},
"plugin-with-dist": {
rootid: "test-datasource",
subpath: "plugin",
},
"no-rootfile": {
err: ErrNoRootFile,
},
"valid-model-panel": {},
"valid-model-datasource": {},
"missing-kind-datasource": {},
"panel-conflicting-joinschema": {
err: ErrInvalidLineage,
skip: "TODO implement BindOption in thema, SatisfiesJoinSchema, then use it here",
},
"panel-does-not-follow-slot-joinschema": {
err: ErrInvalidLineage,
skip: "TODO implement BindOption in thema, SatisfiesJoinSchema, then use it here",
},
"pluginRootWithDist": {
err: ErrNoRootFile,
skip: "This folder is used to test multiple plugins in the same folder",
},
"name-mismatch-panel": {
err: ErrInvalidGrafanaPluginInstance,
},
"disallowed-cue-import": {
err: ErrDisallowedCUEImport,
},
"cdn": {
rootid: "grafana-worldmap-panel",
subpath: "plugin",
},
"external-registration": {
rootid: "grafana-test-datasource",
},
}
staticRootPath, err := filepath.Abs(filepath.Join("..", "manager", "testdata"))
require.NoError(t, err)
dfs := os.DirFS(staticRootPath)
ents, err := fs.ReadDir(dfs, ".")
require.NoError(t, err)
// Ensure table test and dir list are ==
var dirs, tts []string
for k := range tab {
tts = append(tts, k)
}
for _, ent := range ents {
dirs = append(dirs, ent.Name())
}
sort.Strings(tts)
sort.Strings(dirs)
if !cmp.Equal(tts, dirs) {
t.Fatalf("table test map (-) and pkg/plugins/manager/testdata dirs (+) differ: %s", cmp.Diff(tts, dirs))
}
for _, ent := range ents {
tst := tab[ent.Name()]
tst.tfs, err = fs.Sub(dfs, filepath.Join(ent.Name(), tst.subpath))
require.NoError(t, err)
tab[ent.Name()] = tst
}
lib := cuectx.GrafanaThemaRuntime()
for name, otst := range tab {
tst := otst // otherwise var is shadowed within func by looping
t.Run(name, func(t *testing.T) {
if tst.skip != "" {
t.Skip(tst.skip)
}
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
}
if tst.rootid == "" {
tst.rootid = name
}
require.Equal(t, tst.rootid, pp.Properties.Id, "expected plugin id and actual plugin id differ")
})
}
}
func TestParseTreeZips(t *testing.T) {
type tt struct {
tfs fs.FS
// TODO could remove this by getting rid of inconsistent subdirs
subpath string
skip string
err error
// TODO could remove this by expecting that dirname == id
rootid string
}
tab := map[string]tt{
"grafana-simple-json-datasource-ec18fa4da8096a952608a7e4c7782b4260b41bcf.zip": {
skip: "binary plugin",
},
"plugin-with-absolute-member.zip": {
skip: "not actually a plugin, no plugin.json?",
},
"plugin-with-absolute-symlink-dir.zip": {
skip: "not actually a plugin, no plugin.json?",
},
"plugin-with-absolute-symlink.zip": {
skip: "not actually a plugin, no plugin.json?",
},
"plugin-with-parent-member.zip": {
skip: "not actually a plugin, no plugin.json?",
},
"plugin-with-symlink-dir.zip": {
skip: "not actually a plugin, no plugin.json?",
},
"plugin-with-symlink.zip": {
skip: "not actually a plugin, no plugin.json?",
},
"plugin-with-symlinks.zip": {
subpath: "test-app",
rootid: "test-app",
},
}
staticRootPath, err := filepath.Abs(filepath.Join("..", "storage", "testdata"))
require.NoError(t, err)
ents, err := os.ReadDir(staticRootPath)
require.NoError(t, err)
// Ensure table test and dir list are ==
var dirs, tts []string
for k := range tab {
tts = append(tts, k)
}
for _, ent := range ents {
dirs = append(dirs, ent.Name())
}
sort.Strings(tts)
sort.Strings(dirs)
if !cmp.Equal(tts, dirs) {
t.Fatalf("table test map (-) and pkg/plugins/installer/testdata dirs (+) differ: %s", cmp.Diff(tts, dirs))
}
for _, ent := range ents {
tst := tab[ent.Name()]
r, err := zip.OpenReader(filepath.Join(staticRootPath, ent.Name()))
require.NoError(t, err)
defer r.Close() //nolint:errcheck
if tst.subpath != "" {
tst.tfs, err = fs.Sub(r, tst.subpath)
require.NoError(t, err)
} else {
tst.tfs = r
}
tab[ent.Name()] = tst
}
lib := cuectx.GrafanaThemaRuntime()
for name, otst := range tab {
tst := otst // otherwise var is shadowed within func by looping
t.Run(name, func(t *testing.T) {
if tst.skip != "" {
t.Skip(tst.skip)
}
pp, err := ParsePluginFS(tst.tfs, lib)
if tst.err == nil {
require.NoError(t, err, "unexpected error while parsing plugin fs")
} else {
require.ErrorIs(t, err, tst.err, "unexpected error type while parsing plugin fs")
return
}
if tst.rootid == "" {
tst.rootid = name
}
require.Equal(t, tst.rootid, pp.Properties.Id, "expected plugin id and actual plugin id differ")
})
}
}

View File

@@ -1,8 +1,8 @@
package pfs
import (
"cuelang.org/go/cue"
"cuelang.org/go/cue/ast"
"github.com/grafana/kindsys"
)
// ParsedPlugin represents everything knowable about a single plugin from static
@@ -13,35 +13,10 @@ import (
type ParsedPlugin struct {
// Properties contains the plugin's definition, as declared in plugin.json.
Properties Metadata
// 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
CueFile cue.Value
Variant SchemaInterface
// 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.Def]
// 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.Def[kindsys.ComposableProperties]
// Queries kindsys.Def[kindsys.ComposableProperties]
// DSCfg kindsys.Def[kindsys.ComposableProperties]
// }

View File

@@ -1,31 +0,0 @@
package pfs
import (
"sort"
"testing"
"github.com/google/go-cmp/cmp"
"github.com/grafana/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)
}
}