plugindef: Move pluginmeta out of coremodels as standalone thema lineage (#56765)

* Get pluginmeta mostly moved over to pkg/plugins/plugindef

* Remove dead func

* Fix up pfs, use sync.Once in plugindef

* Update to latest thema

* Chase Endec->Codec conversion in Thema

* Comments on slash header gen; use ToSlash

* Also generate JSON schema for plugindef

* Generate JSON Schema as well

* Fix slot loading from kindsys cue decls

* Remove unused vars

* skip generating plugin.schema.json for now

Co-authored-by: Marcus Efraimsson <marcus.efraimsson@gmail.com>
This commit is contained in:
sam boyer 2022-11-15 08:48:31 -05:00 committed by GitHub
parent ff1afbb699
commit 78f0340031
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
29 changed files with 458 additions and 451 deletions

View File

@ -66,6 +66,7 @@ openapi3-gen: swagger-api-spec ## Generates OpenApi 3 specs from the Swagger 2 a
##@ Building
gen-cue: ## Do all CUE/Thema code generation
@echo "generate code from .cue files"
go generate ./pkg/plugins/plugindef
go generate ./kinds/gen.go
go generate ./pkg/framework/coremodel
go generate ./public/app/plugins

View File

@ -6,5 +6,5 @@ import (
// CueSchemaFS embeds all schema-related CUE files in the Grafana project.
//
//go:embed cue.mod/module.cue kinds/*/*.cue kinds/*/*/*.cue packages/grafana-schema/src/schema/*.cue public/app/plugins/*/*/*.cue public/app/plugins/*/*/plugin.json pkg/framework/coremodel/*.cue pkg/kindsys/*.cue
//go:embed cue.mod/module.cue kinds/*/*.cue kinds/*/*/*.cue packages/grafana-schema/src/schema/*.cue public/app/plugins/*/*/*.cue public/app/plugins/*/*/plugin.json pkg/kindsys/*.cue pkg/plugins/plugindef/*.cue
var CueSchemaFS embed.FS

4
go.mod
View File

@ -61,7 +61,7 @@ require (
github.com/grafana/grafana-aws-sdk v0.11.0
github.com/grafana/grafana-azure-sdk-go v1.3.1
github.com/grafana/grafana-plugin-sdk-go v0.142.0
github.com/grafana/thema v0.0.0-20221107225215-00ad2949c7bc
github.com/grafana/thema v0.0.0-20221113112305-b441ed85a1fd
github.com/grpc-ecosystem/go-grpc-middleware v1.3.0
github.com/hashicorp/go-hclog v1.0.0
github.com/hashicorp/go-plugin v1.4.3
@ -349,7 +349,7 @@ require (
github.com/yudai/pp v2.0.1+incompatible // indirect
go.opentelemetry.io/otel/exporters/otlp/internal/retry v1.7.0 // indirect
go.opentelemetry.io/proto/otlp v0.16.0 // indirect
golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4 // indirect
golang.org/x/mod v0.7.0 // indirect
gopkg.in/warnings.v0 v0.1.2 // indirect
)

12
go.sum
View File

@ -1242,7 +1242,6 @@ github.com/google/go-replayers/grpcreplay v1.1.0/go.mod h1:qzAvJ8/wi57zq7gWqaE6A
github.com/google/go-replayers/httpreplay v1.1.1/go.mod h1:gN9GeLIs7l6NUoVaSSnv2RiqK1NiwAmD0MrKeC9IIks=
github.com/google/gofuzz v0.0.0-20161122191042-44d81051d367/go.mod h1:HP5RmnzzSNb993RKQDq4+1A4ia9nllfqcQFTQJedwGI=
github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg=
github.com/google/gofuzz v1.1.0 h1:Hsa8mG0dQ46ij8Sl2AYJDUv1oA9/d6Vk+3LG99Oe02g=
github.com/google/gofuzz v1.1.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg=
github.com/google/gofuzz v1.2.0 h1:xRy4A+RhZaiKjJ1bPfwQ8sedCA+YS2YcCHW6ec7JMi0=
github.com/google/martian v2.1.0+incompatible/go.mod h1:9I4somxYTbIHy5NJKHRl3wXiIaQGbYVAs8BPL6v8lEs=
@ -1370,8 +1369,10 @@ github.com/grafana/prometheus-alertmanager v0.24.1-0.20221012142027-823cd9150293
github.com/grafana/prometheus-alertmanager v0.24.1-0.20221012142027-823cd9150293/go.mod h1:HVHqK+BVPa/tmL8EMhLCCrPt2a1GdJpEyxr5hgur2UI=
github.com/grafana/saml v0.4.9-0.20220727151557-61cd9c9353fc h1:1PY8n+rXuBNr3r1JQhoytWDCpc+pq+BibxV0SZv+Cr4=
github.com/grafana/saml v0.4.9-0.20220727151557-61cd9c9353fc/go.mod h1:9Zh6dWPtB3MSzTRt8fIFH60Z351QQ+s7hCU3J/tTlA4=
github.com/grafana/thema v0.0.0-20221107225215-00ad2949c7bc h1:Icv777/PBaqhLmbSBSDaajDl424cbmh5ee77Du2rUFE=
github.com/grafana/thema v0.0.0-20221107225215-00ad2949c7bc/go.mod h1:wnIJykzNiNVANl6g/Z4nkXxoMqaaH1LoG0IPNW++BEk=
github.com/grafana/thema v0.0.0-20221113034006-50fd3c0da5ce h1:N1K0WWaG0B5i/703ri0WSazQYVsCYj1mgODgElCz0o8=
github.com/grafana/thema v0.0.0-20221113034006-50fd3c0da5ce/go.mod h1:ZJHKwNE86ngdQ7edJIFHepCiIg9YP9x+YZPEm3dlkL4=
github.com/grafana/thema v0.0.0-20221113112305-b441ed85a1fd h1:y6H9I5fy4sRKf2FJ7W94YWero4mXH50Ft8NAPZ9DapQ=
github.com/grafana/thema v0.0.0-20221113112305-b441ed85a1fd/go.mod h1:ZJHKwNE86ngdQ7edJIFHepCiIg9YP9x+YZPEm3dlkL4=
github.com/grafana/xorm v0.8.3-0.20220614223926-2fcda7565af6 h1:I9dh1MXGX0wGyxdV/Sl7+ugnki4Dfsy8lv2s5Yf887o=
github.com/grafana/xorm v0.8.3-0.20220614223926-2fcda7565af6/go.mod h1:ZkJLEYLoVyg7amJK/5r779bHyzs2AU8f8VMiP6BM7uY=
github.com/gregjones/httpcache v0.0.0-20180305231024-9cad4c3443a7/go.mod h1:FecbI9+v66THATjSRHfNgh1IVFe/9kFxbXtjV0ctIMA=
@ -1823,8 +1824,6 @@ github.com/mattn/go-shellwords v1.0.3/go.mod h1:3xCvwCdWdlDJUrvuMn7Wuy9eWs4pE8vq
github.com/mattn/go-sqlite3 v1.10.0/go.mod h1:FPy6KqzDD04eiIsT53CuJW3U88zkxoIYsOqkbpncsNc=
github.com/mattn/go-sqlite3 v1.11.0/go.mod h1:FPy6KqzDD04eiIsT53CuJW3U88zkxoIYsOqkbpncsNc=
github.com/mattn/go-sqlite3 v1.14.6/go.mod h1:NyWgC/yNuGj7Q9rpYnZvas74GogHl5/Z4A/KQRfk6bU=
github.com/mattn/go-sqlite3 v1.14.7 h1:fxWBnXkxfM6sRiuH3bqJ4CfzZojMOLVc0UTsTglEghA=
github.com/mattn/go-sqlite3 v1.14.7/go.mod h1:NyWgC/yNuGj7Q9rpYnZvas74GogHl5/Z4A/KQRfk6bU=
github.com/mattn/go-sqlite3 v1.14.16 h1:yOQRA0RpS5PFz/oikGwBEqvAWhWg5ufRz4ETLjwpU1Y=
github.com/mattn/go-sqlite3 v1.14.16/go.mod h1:2eHXhiwb8IkHr+BDWZGa96P6+rkvnG63S2DGjv9HUNg=
github.com/mattn/go-tty v0.0.0-20180907095812-13ff1204f104/go.mod h1:XPvLUNfbS4fJH25nqRHfWLMa1ONC8Amw+mIA639KxkE=
@ -2756,8 +2755,9 @@ golang.org/x/mod v0.4.1/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
golang.org/x/mod v0.4.2/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
golang.org/x/mod v0.5.0/go.mod h1:5OXOZSfqPIIbmVBIIKWRFfZjPR0E5r58TLhUjH0a2Ro=
golang.org/x/mod v0.6.0-dev.0.20220106191415-9b9b3d81d5e3/go.mod h1:3p9vT2HGsQu2K1YbXdKPJLVgG5VJdoTa1poYQBtP1AY=
golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4 h1:6zppjxzCulZykYSLyVDYbneBfbaBIQPYMevg0bEwv2s=
golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4=
golang.org/x/mod v0.7.0 h1:LapD9S96VoQRhi/GrNTqeBJFrUjs5UHCAtTlgwA5oZA=
golang.org/x/mod v0.7.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs=
golang.org/x/net v0.0.0-20170114055629-f2499483f923/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20180530234432-1e491301e022/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=

View File

@ -109,9 +109,6 @@ func main() {
if err != nil {
die(fmt.Errorf("core kinddirs codegen failed: %w", err))
}
// for _, f := range jfs.AsFiles() {
// fmt.Println(filepath.Join(groot, f.RelativePath))
// }
if _, set := os.LookupEnv("CODEGEN_VERIFY"); set {
if err = jfs.Verify(context.Background(), groot); err != nil {

View File

@ -237,7 +237,7 @@ type tplVars struct {
}
func (cd *CoremodelDeclaration) GenerateTypescriptCoremodel() (*tsast.File, error) {
schv := thema.SchemaP(cd.Lineage, thema.LatestVersion(cd.Lineage)).UnwrapCUE()
schv := cd.Lineage.Latest().Underlying()
tf, err := cuetsy.GenerateAST(schv, cuetsy.Config{
Export: true,

View File

@ -3,6 +3,7 @@ package codegen
import (
"bytes"
"fmt"
"path/filepath"
"github.com/grafana/codejen"
"github.com/grafana/grafana/pkg/kindsys"
@ -39,12 +40,22 @@ func (decl *DeclForGen) Lineage() thema.Lineage {
return decl.lin
}
// SlashHeaderMapper produces a FileMapper that injects a comment header onto
// a [codejen.File] indicating the main generator that produced it (via the provided
// maingen, which should be a path) and the jenny or jennies that constructed the
// file.
func SlashHeaderMapper(maingen string) codejen.FileMapper {
return func(f codejen.File) (codejen.File, error) {
b := new(bytes.Buffer)
fmt.Fprintf(b, headerTmpl, maingen, f.FromString())
fmt.Fprint(b, string(f.Data))
f.Data = b.Bytes()
// Never inject on certain filetypes, it's never valid
switch filepath.Ext(f.RelativePath) {
case ".json", ".yml", ".yaml":
return f, nil
default:
b := new(bytes.Buffer)
fmt.Fprintf(b, headerTmpl, filepath.ToSlash(maingen), f.FromString())
fmt.Fprint(b, string(f.Data))
f.Data = b.Bytes()
}
return f, nil
}
}

View File

@ -66,11 +66,10 @@ func (gen *genTSVeneerIndex) Generate(decls []*DeclForGen) (*codejen.File, error
func (gen *genTSVeneerIndex) extractTSIndexVeneerElements(decl *DeclForGen, tf *ast.File) ([]ast.Decl, error) {
lin := decl.Lineage()
sch := thema.SchemaP(lin, thema.LatestVersion(lin))
comm := decl.Meta.Common()
// Check the root, then walk the tree
rootv := sch.UnwrapCUE()
rootv := lin.Latest().Underlying()
var raw, custom, rawD, customD ast.Idents

View File

@ -15,7 +15,7 @@ import (
"github.com/getkin/kin-openapi/openapi3"
"github.com/grafana/cuetsy"
tsast "github.com/grafana/cuetsy/ts/ast"
"github.com/grafana/grafana/pkg/framework/coremodel"
"github.com/grafana/grafana/pkg/kindsys"
"github.com/grafana/grafana/pkg/plugins/pfs"
"github.com/grafana/thema"
"github.com/grafana/thema/encoding/openapi"
@ -146,7 +146,7 @@ func (pt *PluginTree) GenerateTypeScriptAST() (*tsast.File, error) {
// whether the slot is a grouped lineage:
// https://github.com/grafana/thema/issues/62
if isGroupLineage(slotname) {
tsf, err := cuetsy.GenerateAST(sch.UnwrapCUE(), cuetsy.Config{
tsf, err := cuetsy.GenerateAST(sch.Underlying(), cuetsy.Config{
Export: true,
})
if err != nil {
@ -154,7 +154,7 @@ func (pt *PluginTree) GenerateTypeScriptAST() (*tsast.File, error) {
}
f.Nodes = append(f.Nodes, tsf.Nodes...)
} else {
pair, err := cuetsy.GenerateSingleAST(strings.Title(lin.Name()), sch.UnwrapCUE(), cuetsy.TypeInterface)
pair, err := cuetsy.GenerateSingleAST(strings.Title(lin.Name()), sch.Underlying(), cuetsy.TypeInterface)
if err != nil {
return nil, fmt.Errorf("error translating %s lineage to TypeScript: %w", slotname, err)
}
@ -169,7 +169,7 @@ func (pt *PluginTree) GenerateTypeScriptAST() (*tsast.File, error) {
}
func isGroupLineage(slotname string) bool {
sl, has := coremodel.AllSlots()[slotname]
sl, has := kindsys.AllSlots(nil)[slotname]
if !has {
panic("unknown slotname name: " + slotname)
}

View File

@ -15,7 +15,7 @@ const rootrel string = "kinds/structured/{{ .Meta.MachineName }}"
// TODO standard generated docs
type Kind struct {
lin thema.ConvergentLineage[*{{ .Meta.Name }}]
jendec vmux.Endec
jcodec vmux.Codec
valmux vmux.ValueMux[*{{ .Meta.Name }}]
decl kindsys.Decl[kindsys.CoreStructuredMeta]
}
@ -47,9 +47,9 @@ func NewKind(rt *thema.Runtime, opts ...thema.BindOption) (*Kind, error) {
return nil, err
}
k.jendec = vmux.NewJSONEndec("{{ .Meta.MachineName }}.json")
k.jcodec = vmux.NewJSONCodec("{{ .Meta.MachineName }}.json")
k.lin = tsch.ConvergentLineage()
k.valmux = vmux.NewValueMux(k.lin.TypedSchema(), k.jendec)
k.valmux = vmux.NewValueMux(k.lin.TypedSchema(), k.jcodec)
return k, nil
}

View File

@ -69,8 +69,8 @@ func postprocessGoFile(cfg genGoFile) ([]byte, error) {
}
type prefixmod struct {
str string
base string
prefix string
replace string
rxp *regexp.Regexp
rxpsuff *regexp.Regexp
}
@ -80,7 +80,22 @@ type prefixmod struct {
// comments in a generated Go file.
func PrefixDropper(prefix string) astutil.ApplyFunc {
return (&prefixmod{
str: prefix,
prefix: prefix,
rxpsuff: regexp.MustCompile(fmt.Sprintf(`%s([a-zA-Z_]+)`, prefix)),
rxp: regexp.MustCompile(fmt.Sprintf(`%s([\s.,;-])`, prefix)),
}).applyfunc
}
// PrefixReplacer returns an astutil.ApplyFunc that removes the provided prefix
// string when it appears as a leading sequence in type names, var names, and
// comments in a generated Go file.
//
// When an exact match for prefix is found, the provided replace string
// is substituted.
func PrefixReplacer(prefix, replace string) astutil.ApplyFunc {
return (&prefixmod{
prefix: prefix,
replace: replace,
rxpsuff: regexp.MustCompile(fmt.Sprintf(`%s([a-zA-Z_]+)`, prefix)),
rxp: regexp.MustCompile(fmt.Sprintf(`%s([\s.,;-])`, prefix)),
}).applyfunc
@ -113,8 +128,8 @@ func (d prefixmod) applyfunc(c *astutil.Cursor) bool {
case *ast.CommentGroup:
for _, c := range x.List {
c.Text = d.rxpsuff.ReplaceAllString(c.Text, "$1")
if d.base != "" {
c.Text = d.rxp.ReplaceAllString(c.Text, d.base+"$1")
if d.replace != "" {
c.Text = d.rxp.ReplaceAllString(c.Text, d.replace+"$1")
}
}
}
@ -142,9 +157,9 @@ func (d prefixmod) handleExpr(e ast.Expr) {
}
func (d prefixmod) do(n *ast.Ident) {
if n.Name != d.str {
n.Name = strings.TrimPrefix(n.Name, d.str)
} else if d.base != "" {
n.Name = d.base
if n.Name != d.prefix {
n.Name = strings.TrimPrefix(n.Name, d.prefix)
} else if d.replace != "" {
n.Name = d.replace
}
}

View File

@ -50,7 +50,7 @@ func GrafanaThemaRuntime() *thema.Runtime {
// call it repeatedly. Most use cases should probably prefer making
// their own Thema/CUE decoders.
func JSONtoCUE(path string, b []byte) (cue.Value, error) {
return vmux.NewJSONEndec(path).Decode(ctx, b)
return vmux.NewJSONCodec(path).Decode(ctx, b)
}
// LoadGrafanaInstancesWithThema loads CUE files containing a lineage
@ -61,7 +61,7 @@ func JSONtoCUE(path string, b []byte) (cue.Value, error) {
// path from the grafana root to the directory containing the lineage.cue. The
// lineage.cue file must be the sole contents of the provided fs.FS.
//
// More details on underlying behavior can be found in the docs for github.com/grafana/thema/load.InstancesWithThema.
// More details on underlying behavior can be found in the docs for github.com/grafana/thema/load.InstanceWithThema.
//
// TODO this approach is complicated and confusing, refactor to something understandable
func LoadGrafanaInstancesWithThema(path string, cueFS fs.FS, rt *thema.Runtime, opts ...thema.BindOption) (thema.Lineage, error) {
@ -70,7 +70,7 @@ func LoadGrafanaInstancesWithThema(path string, cueFS fs.FS, rt *thema.Runtime,
if err != nil {
return nil, err
}
inst, err := load.InstancesWithThema(fs, prefix)
inst, err := load.InstanceWithThema(fs, prefix)
// Need to trick loading by creating the embedded file and
// making it look like a module in the root dir.
@ -93,7 +93,7 @@ func LoadGrafanaInstancesWithThema(path string, cueFS fs.FS, rt *thema.Runtime,
// The provided prefix should be the relative path from the grafana repository
// root to the directory root of the provided inputfs.
//
// The returned fs.FS is suitable for passing to a CUE loader, such as [load.InstancesWithThema].
// The returned fs.FS is suitable for passing to a CUE loader, such as [load.InstanceWithThema].
func prefixWithGrafanaCUE(prefix string, inputfs fs.FS) (fs.FS, error) {
m := fstest.MapFS{
// fstest can recognize only forward slashes.
@ -124,10 +124,9 @@ func prefixWithGrafanaCUE(prefix string, inputfs fs.FS) (fs.FS, error) {
return merged_fs.NewMergedFS(m, grafana.CueSchemaFS), nil
}
// BuildGrafanaInstance wraps [load.InstancesWithThema] to load a
// LoadGrafanaInstance wraps [load.InstanceWithThema] to load a
// [*build.Instance] corresponding to a particular path within the
// github.com/grafana/grafana CUE module, then builds that into a [cue.Value],
// checks it for errors and returns.
// github.com/grafana/grafana CUE module.
//
// This allows resolution of imports within the grafana or thema CUE modules to
// work correctly and consistently by relying on the embedded FS at
@ -143,7 +142,7 @@ func prefixWithGrafanaCUE(prefix string, inputfs fs.FS) (fs.FS, error) {
// is the same as the parent directory name, it should be omitted.
//
// NOTE this function will be removed in favor of a more generic loader
func BuildGrafanaInstance(relpath string, pkg string, ctx *cue.Context, overlay fs.FS) (cue.Value, error) {
func LoadGrafanaInstance(relpath string, pkg string, overlay fs.FS) (*build.Instance, error) {
// notes about how this crap needs to work
//
// Within grafana/grafana, need:
@ -151,41 +150,6 @@ func BuildGrafanaInstance(relpath string, pkg string, ctx *cue.Context, overlay
// - has no cue.mod
// - gets prefixed with the appropriate path within grafana/grafana
// - and merged with all the other .cue files from grafana/grafana
if ctx == nil {
ctx = GrafanaCUEContext()
}
relpath = filepath.ToSlash(relpath)
var v cue.Value
var f fs.FS = grafana.CueSchemaFS
var err error
if overlay != nil {
f, err = prefixWithGrafanaCUE(relpath, overlay)
if err != nil {
return v, err
}
}
var bi *build.Instance
if pkg != "" {
bi, err = load.InstancesWithThema(f, relpath, load.Package(pkg))
} else {
bi, err = load.InstancesWithThema(f, relpath)
}
if err != nil {
return v, err
}
v = ctx.BuildInstance(bi)
if v.Err() != nil {
return v, fmt.Errorf("%s not a valid CUE instance: %w", relpath, v.Err())
}
return v, nil
}
// TODO docs
// NOTE this function will be removed in favor of a more generic loader
func LoadInstanceWithGrafana(ifs fs.FS, prefix string) (*build.Instance, error) {
// notes about how this crap needs to work
//
// Need a prefixing instance loader that:
@ -193,6 +157,40 @@ func LoadInstanceWithGrafana(ifs fs.FS, prefix string) (*build.Instance, error)
// - reconcile at most one of the provided fs with cwd
// - behavior must differ depending on whether cwd is in a cue module
// - behavior should(?) be controllable depending on
relpath = filepath.ToSlash(relpath)
panic("TODO")
var f fs.FS = grafana.CueSchemaFS
var err error
if overlay != nil {
f, err = prefixWithGrafanaCUE(relpath, overlay)
if err != nil {
return nil, err
}
}
if pkg != "" {
return load.InstanceWithThema(f, relpath, load.Package(pkg))
}
return load.InstanceWithThema(f, relpath)
}
// BuildGrafanaInstance wraps [LoadGrafanaInstance], additionally building
// the returned [*build.Instance], if valid, into a [cue.Value] that is checked
// for errors before returning.
//
// NOTE this function will be removed in favor of a more generic loader
func BuildGrafanaInstance(ctx *cue.Context, relpath string, pkg string, overlay fs.FS) (cue.Value, error) {
bi, err := LoadGrafanaInstance(relpath, pkg, overlay)
if err != nil {
return cue.Value{}, err
}
if ctx == nil {
ctx = GrafanaCUEContext()
}
v := ctx.BuildInstance(bi)
if v.Err() != nil {
return v, fmt.Errorf("%s not a valid CUE instance: %w", relpath, v.Err())
}
return v, nil
}

View File

@ -1,107 +0,0 @@
package coremodel
import (
"embed"
"fmt"
"io/fs"
"path/filepath"
"testing/fstest"
"cuelang.org/go/cue"
"cuelang.org/go/cue/load"
tload "github.com/grafana/thema/load"
"github.com/grafana/grafana/pkg/cuectx"
)
// Embed for all framework-related CUE files in this directory
//
//go:embed *.cue
var cueFS embed.FS
var defaultFramework cue.Value
func init() {
var err error
defaultFramework, err = doLoadFrameworkCUE(cuectx.GrafanaCUEContext())
if err != nil {
panic(err)
}
}
var prefix = filepath.Join("/pkg", "framework", "coremodel")
//nolint:nakedret
func doLoadFrameworkCUE(ctx *cue.Context) (v cue.Value, err error) {
m := make(fstest.MapFS)
err = fs.WalkDir(cueFS, ".", func(path string, d fs.DirEntry, err error) error {
if err != nil {
return err
}
if d.IsDir() {
return nil
}
b, err := fs.ReadFile(cueFS, path)
if err != nil {
return err
}
m[path] = &fstest.MapFile{Data: b}
return nil
})
if err != nil {
return
}
over := make(map[string]load.Source)
absolutePath := prefix
if !filepath.IsAbs(absolutePath) {
absolutePath, err = filepath.Abs(absolutePath)
if err != nil {
return
}
}
err = tload.ToOverlay(absolutePath, m, over)
if err != nil {
return
}
bi := load.Instances(nil, &load.Config{
Dir: absolutePath,
Package: "coremodel",
Overlay: over,
})
v = ctx.BuildInstance(bi[0])
if v.Err() != nil {
return cue.Value{}, fmt.Errorf("coremodel framework loaded cue.Value has err: %w", v.Err())
}
return
}
// CUEFramework returns a cue.Value representing all the coremodel framework
// raw CUE files.
//
// For low-level use in constructing other types and APIs, while still letting
// us declare all the frameworky CUE bits in a single package. Other types and
// subpackages make the constructs in this value easy to use.
//
// The returned cue.Value is built from Grafana's standard central CUE context,
// ["github.com/grafana/grafana/pkg/cuectx".ProvideCueContext].
func CUEFramework() cue.Value {
return defaultFramework
}
// CUEFrameworkWithContext is the same as CUEFramework, but allows control over
// the cue.Context that's used.
//
// Prefer CUEFramework unless you understand cue.Context, and absolutely need
// this control.
func CUEFrameworkWithContext(ctx *cue.Context) cue.Value {
// Error guaranteed to be nil here because erroring would have caused init() to panic
v, _ := doLoadFrameworkCUE(ctx) // nolint:errcheck
return v
}

View File

@ -1,29 +0,0 @@
package coremodel
// Generates all code derived from coremodel Thema lineages that's used directly
// by both the frontend and backend.
//go:generate go run gen.go
import (
"github.com/grafana/thema"
)
// Interface is the primary coremodel interface that must be implemented by all
// Grafana coremodels. A coremodel is the foundational, canonical schema for
// some known-at-compile-time Grafana object.
//
// Currently, all Coremodels are expressed as Thema lineages.
type Interface interface {
// Lineage should return the canonical Thema lineage for the coremodel.
Lineage() thema.Lineage
// CurrentSchema should return the schema of the version that the Grafana backend
// is currently written against. (While Grafana can accept data from all
// older versions of the Thema schema, backend Go code is written against a
// single version for simplicity)
CurrentSchema() thema.Schema
// GoType should return a pointer to the Go struct type that corresponds to
// the Current() schema.
GoType() interface{}
}

View File

@ -24,7 +24,7 @@ const rootrel string = "kinds/structured/dashboard"
// TODO standard generated docs
type Kind struct {
lin thema.ConvergentLineage[*Dashboard]
jendec vmux.Endec
jcodec vmux.Codec
valmux vmux.ValueMux[*Dashboard]
decl kindsys.Decl[kindsys.CoreStructuredMeta]
}
@ -56,9 +56,9 @@ func NewKind(rt *thema.Runtime, opts ...thema.BindOption) (*Kind, error) {
return nil, err
}
k.jendec = vmux.NewJSONEndec("dashboard.json")
k.jcodec = vmux.NewJSONCodec("dashboard.json")
k.lin = tsch.ConvergentLineage()
k.valmux = vmux.NewValueMux(k.lin.TypedSchema(), k.jendec)
k.valmux = vmux.NewValueMux(k.lin.TypedSchema(), k.jcodec)
return k, nil
}

View File

@ -24,7 +24,7 @@ const rootrel string = "kinds/structured/playlist"
// TODO standard generated docs
type Kind struct {
lin thema.ConvergentLineage[*Playlist]
jendec vmux.Endec
jcodec vmux.Codec
valmux vmux.ValueMux[*Playlist]
decl kindsys.Decl[kindsys.CoreStructuredMeta]
}
@ -56,9 +56,9 @@ func NewKind(rt *thema.Runtime, opts ...thema.BindOption) (*Kind, error) {
return nil, err
}
k.jendec = vmux.NewJSONEndec("playlist.json")
k.jcodec = vmux.NewJSONCodec("playlist.json")
k.lin = tsch.ConvergentLineage()
k.valmux = vmux.NewValueMux(k.lin.TypedSchema(), k.jendec)
k.valmux = vmux.NewValueMux(k.lin.TypedSchema(), k.jcodec)
return k, nil
}

View File

@ -163,4 +163,3 @@ _sharedKind: {
// It is required that lineage.name is the same as the [machineName].
lineage: thema.#Lineage & { name: S.machineName }
}

View File

@ -227,7 +227,7 @@ func (decl *Decl[T]) Some() *SomeDecl {
// For representations of core kinds that are useful in Go programs at runtime,
// see ["github.com/grafana/grafana/pkg/registry/corekind"].
func LoadCoreKind[T RawMeta | CoreStructuredMeta](declpath string, ctx *cue.Context, overlay fs.FS) (*Decl[T], error) {
vk, err := cuectx.BuildGrafanaInstance(declpath, "kind", ctx, overlay)
vk, err := cuectx.BuildGrafanaInstance(ctx, declpath, "kind", overlay)
if err != nil {
return nil, err
}

View File

@ -1,24 +1,28 @@
package coremodel
package kindsys
import (
"cuelang.org/go/cue"
)
// Slot represents one of Grafana's named Thema composition slot definitions.
// Slot represents one of Grafana's named slot definitions.
// TODO link to framework docs
type Slot struct {
name string
raw cue.Value
plugins map[string]bool
}
// Name returns the name of the Slot. The name is also used as the path at which
// a Slot lineage is defined in a plugin models.cue file.
// Name returns the name of the Slot.
//
// The name is also used as the path at which a Slot lineage is defined in a
// plugin models.cue file.
func (s Slot) Name() string {
return s.name
}
// MetaSchema returns the meta-schema that is the contract between coremodels
// that compose the Slot, and plugins that implement it.
// MetaSchema returns the meta-schema that is the contract between core or
// custom kinds that compose the meta-schema, and the plugin-declared composable
// kinds that implement the meta-schema.
func (s Slot) MetaSchema() cue.Value {
return s.raw
}
@ -28,15 +32,15 @@ func (s Slot) MetaSchema() cue.Value {
// may, whether they must produce one (second return value).
//
// Expected values here are those in the set of
// ["github.com/grafana/grafana/pkg/coremodel/pluginmeta".Type], though passing
// ["github.com/grafana/grafana/pkg/plugins/plugindef".Type], though passing
// a string not in that set will harmlessly return {false, false}. That type is
// not used here to avoid import cycles.
//
// Note that, at least for now, plugins are not required to provide any slot
// implementations, and do so by simply not containing a models.cue file.
// Consequently, the "must" return value here is best understood as, "IF a
// plugin provides a models.cue file, it MUST contain an implementation of this
// slot."
// implementations, and do so by simply not containing any .cue files in the
// "grafanaplugin" package. Consequently, the "must" return value is best
// understood as, "IF a plugin provides a *.cue files, it MUST contain an
// implementation of this slot."
func (s Slot) ForPluginType(plugintype string) (may, must bool) {
must, may = s.plugins[plugintype]
return
@ -58,8 +62,12 @@ func (s Slot) IsGroup() bool {
}
}
func AllSlots() map[string]*Slot {
fw := CUEFramework()
// AllSlots returns a map of all [Slot]s defined in the Grafana kindsys
// framework.
//
// TODO cache this for core context
func AllSlots(ctx *cue.Context) map[string]*Slot {
fw := CUEFramework(ctx)
slots := make(map[string]*Slot)
// Ignore err, can only happen if we change structure of fw files, and all we'd

29
pkg/kindsys/slot_test.go Normal file
View File

@ -0,0 +1,29 @@
package kindsys
import (
"sort"
"testing"
"cuelang.org/go/cue/cuecontext"
"github.com/stretchr/testify/require"
)
// This is a brick-dumb test that just ensures slots 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 somewhere, fix it
// - The set of slots names has been modified - update the static list here
func TestSlotsAreLoaded(t *testing.T) {
slots := []string{"Panel", "Query", "DSOptions"}
all := AllSlots(cuecontext.New())
var loadedSlots []string
for k := range all {
loadedSlots = append(loadedSlots, k)
}
sort.Strings(slots)
sort.Strings(loadedSlots)
require.Equal(t, slots, loadedSlots, "slots loaded from cue differs from fixture set - either a bug or fixture needs updating")
}

View File

@ -1,4 +1,4 @@
package coremodel
package kindsys
// The slots named and specified in this file are meta-schemas that act as a
// shared contract between Grafana plugins (producers) and coremodel types

View File

@ -2,7 +2,7 @@ package grafanaplugin
import (
"github.com/grafana/thema"
"github.com/grafana/grafana/pkg/framework/coremodel"
"github.com/grafana/grafana/kinds/structured/dashboard:kind"
)
_dummy: coremodel.slots

View File

@ -5,16 +5,14 @@ import (
"io/fs"
"sort"
"strings"
"sync"
"cuelang.org/go/cue"
"cuelang.org/go/cue/ast"
"cuelang.org/go/cue/errors"
"cuelang.org/go/cue/parser"
"github.com/grafana/grafana"
"github.com/grafana/grafana/pkg/coremodel/pluginmeta"
"github.com/grafana/grafana/pkg/cuectx"
"github.com/grafana/grafana/pkg/framework/coremodel"
"github.com/grafana/grafana/pkg/kindsys"
"github.com/grafana/grafana/pkg/plugins/plugindef"
"github.com/grafana/thema"
"github.com/grafana/thema/load"
"github.com/grafana/thema/vmux"
@ -23,6 +21,8 @@ import (
// PermittedCUEImports returns the list of packages that may be imported in a
// plugin models.cue file.
//
// TODO probably move this into kindsys
func PermittedCUEImports() []string {
return []string{
"github.com/grafana/thema",
@ -41,20 +41,13 @@ func importAllowed(path string) bool {
var allowedImportsStr string
// Name expected to be used for all models.cue files in Grafana plugins
const pkgname = "grafanaplugin"
type slotandname struct {
name string
slot *coremodel.Slot
slot *kindsys.Slot
}
var allslots []slotandname
// TODO re-enable after go1.18
var tsch thema.TypedSchema[*pluginmeta.Model]
var plugmux vmux.ValueMux[*pluginmeta.Model]
func init() {
var all []string
for _, im := range PermittedCUEImports() {
@ -62,7 +55,7 @@ func init() {
}
allowedImportsStr = strings.Join(all, "\n")
for n, s := range coremodel.AllSlots() {
for n, s := range kindsys.AllSlots(nil) {
allslots = append(allslots, slotandname{
name: n,
slot: s,
@ -74,42 +67,6 @@ func init() {
})
}
var muxonce sync.Once
// This used to be in init(), but that creates a risk for codegen.
//
// thema.BindType ensures that Go type and Thema schema are aligned. If we were
// to call it during init(), then the code generator that fixes misalignments
// between those two could trigger it if it depends on this package. That would
// mean that schema changes to pluginmeta get caught in a loop where the codegen
// process can't heal itself.
//
// In theory, that dependency shouldn't exist - this package should only be
// imported for plugin codegen, which should all happen after coremodel codegen.
// But in practice, it might exist. And it's really brittle and confusing to
// fix if that does happen.
//
// Better to be resilient to the possibility instead. So, this is a standalone function,
// called as needed to get our muxer, and internally relies on a sync.Once to avoid
// repeated processing of thema.BindType.
// TODO mux loading is easily generalizable in pkg/f/coremodel, shouldn't need one-off
func loadMux() (thema.TypedSchema[*pluginmeta.Model], vmux.ValueMux[*pluginmeta.Model]) {
muxonce.Do(func() {
var err error
t := new(pluginmeta.Model)
pm, err := pluginmeta.New(cuectx.GrafanaThemaRuntime())
if err != nil {
panic(err)
}
tsch, err = thema.BindType[*pluginmeta.Model](pm.CurrentSchema(), t)
if err != nil {
panic(err)
}
plugmux = vmux.NewValueMux(tsch, vmux.NewJSONEndec("plugin.json"))
})
return tsch, plugmux
}
// Tree represents the contents of a plugin filesystem tree.
type Tree struct {
raw fs.FS
@ -154,7 +111,7 @@ func (tl TreeList) LineagesForSlot(slotname string) map[string]thema.Lineage {
// PluginInfo represents everything knowable about a single plugin from static
// analysis of its filesystem tree contents.
type PluginInfo struct {
meta pluginmeta.Model
meta plugindef.PluginDef
slotimpls map[string]thema.Lineage
imports []*ast.ImportSpec
}
@ -174,7 +131,7 @@ func (pi PluginInfo) SlotImplementations() map[string]thema.Lineage {
}
// Meta returns the metadata declared in the plugin's plugin.json file.
func (pi PluginInfo) Meta() pluginmeta.Model {
func (pi PluginInfo) Meta() plugindef.PluginDef {
return pi.meta
}
@ -183,12 +140,21 @@ func (pi PluginInfo) Meta() pluginmeta.Model {
//
// 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
}
_, mux := loadMux()
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")
@ -207,13 +173,11 @@ func ParsePluginFS(f fs.FS, rt *thema.Runtime) (*Tree, error) {
}
r := &tree.rootinfo
// Pass the raw bytes into the muxer, get the populated Model 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 pluginmeta lineage)
// metaany, _, err := mux(b)
// 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)
if err != nil {
// TODO more nuanced error handling by class of Thema failure
// return nil, fmt.Errorf("plugin.json was invalid: %w", err)
return nil, ewrap(err, ErrInvalidRootFile)
}
r.meta = *pmeta
@ -231,9 +195,9 @@ func ParsePluginFS(f fs.FS, rt *thema.Runtime) (*Tree, error) {
mfs := merged_fs.NewMergedFS(f, grafana.CueSchemaFS)
// Note that this actually will load any .cue files in the fs.FS root dir in the pkgname.
// 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.InstancesWithThema(mfs, "", load.Package(pkgname))
bi, err := load.InstanceWithThema(mfs, "", load.Package(plugindef.PkgName))
if err != nil {
return nil, fmt.Errorf("loading models.cue failed: %w", err)
}
@ -267,7 +231,7 @@ func ParsePluginFS(f fs.FS, rt *thema.Runtime) (*Tree, error) {
return tree, nil
}
func bindSlotLineage(v cue.Value, s *coremodel.Slot, meta pluginmeta.Model, rt *thema.Runtime, opts ...thema.BindOption) (thema.Lineage, error) {
func bindSlotLineage(v cue.Value, s *kindsys.Slot, meta plugindef.PluginDef, rt *thema.Runtime, opts ...thema.BindOption) (thema.Lineage, error) {
accept, required := s.ForPluginType(string(meta.Type))
exists := v.Exists()

View File

@ -172,6 +172,7 @@ func TestParseTreeTestdata(t *testing.T) {
if tst.err == nil {
require.NoError(t, err, "unexpected error while parsing plugin tree")
} else {
require.Error(t, err)
require.ErrorIs(t, err, tst.err, "unexpected error type while parsing plugin tree")
return
}

View File

@ -0,0 +1,132 @@
//go:build ignore
// +build ignore
package main
import (
"bytes"
"context"
"encoding/json"
"fmt"
"go/ast"
"os"
"path/filepath"
"strings"
"cuelang.org/go/cue/cuecontext"
"github.com/grafana/codejen"
"github.com/grafana/grafana/pkg/codegen"
"github.com/grafana/grafana/pkg/cuectx"
"github.com/grafana/thema"
"github.com/grafana/thema/encoding/gocode"
"github.com/grafana/thema/encoding/jsonschema"
"golang.org/x/tools/go/ast/astutil"
)
var dirPlugindef = filepath.Join("pkg", "plugins", "plugindef")
// main generator for plugindef. plugindef isn't a kind, so it has its own
// one-off main generator.
func main() {
v := elsedie(cuectx.BuildGrafanaInstance(nil, dirPlugindef, "", nil))("could not load plugindef cue package")
lin := elsedie(thema.BindLineage(v, cuectx.GrafanaThemaRuntime()))("plugindef lineage is invalid")
jl := &codejen.JennyList[thema.Lineage]{}
jl.AppendOneToOne(&jennytypego{}, &jennybindgo{})
jl.AddPostprocessors(codegen.SlashHeaderMapper(filepath.Join(dirPlugindef, "gen.go")))
cwd, err := os.Getwd()
if err != nil {
fmt.Fprintf(os.Stderr, "could not get working directory: %s", err)
os.Exit(1)
}
grootp := strings.Split(cwd, string(os.PathSeparator))
groot := filepath.Join(string(os.PathSeparator), filepath.Join(grootp[:len(grootp)-3]...))
jfs := elsedie(jl.GenerateFS([]thema.Lineage{lin}))("plugindef jenny pipeline failed")
if _, set := os.LookupEnv("CODEGEN_VERIFY"); set {
if err := jfs.Verify(context.Background(), groot); err != nil {
die(fmt.Errorf("generated code is out of sync with inputs:\n%s\nrun `make gen-cue` to regenerate", err))
}
} else if err := jfs.Write(context.Background(), groot); err != nil {
die(fmt.Errorf("error while writing generated code to disk:\n%s", err))
}
}
// one-off jenny for plugindef go types
type jennytypego struct{}
func (j *jennytypego) JennyName() string {
return "PluginGoTypes"
}
func (j *jennytypego) Generate(lin thema.Lineage) (*codejen.File, error) {
b, err := gocode.GenerateTypesOpenAPI(lin.Latest(), &gocode.TypeConfigOpenAPI{
ApplyFuncs: []astutil.ApplyFunc{
codegen.PrefixReplacer("Plugindef", "PluginDef"),
},
})
if err != nil {
return nil, err
}
return codejen.NewFile(filepath.Join(dirPlugindef, "plugindef_types_gen.go"), b, j), nil
}
// one-off jenny for plugindef go bindings
type jennybindgo struct{}
func (j *jennybindgo) JennyName() string {
return "PluginGoBindings"
}
func (j *jennybindgo) Generate(lin thema.Lineage) (*codejen.File, error) {
b, err := gocode.GenerateLineageBinding(lin, &gocode.BindingConfig{
TitleName: "PluginDef",
Assignee: ast.NewIdent("*PluginDef"),
PrivateFactory: true,
})
if err != nil {
return nil, err
}
return codejen.NewFile(filepath.Join(dirPlugindef, "plugindef_bindings_gen.go"), b, j), nil
}
// one-off jenny for plugindef json schema generator
type jennyjschema struct{}
func (j *jennyjschema) JennyName() string {
return "PluginJSONSchema"
}
func (j *jennyjschema) Generate(lin thema.Lineage) (*codejen.File, error) {
f, err := jsonschema.GenerateSchema(lin.Latest())
if err != nil {
return nil, err
}
b, _ := cuecontext.New().BuildFile(f).MarshalJSON()
nb := new(bytes.Buffer)
die(json.Indent(nb, b, "", " "))
return codejen.NewFile(filepath.FromSlash("docs/sources/developers/plugins/plugin.schema.json"), nb.Bytes(), j), nil
}
func elsedie[T any](t T, err error) func(msg string) T {
if err != nil {
return func(msg string) T {
fmt.Fprintf(os.Stderr, "%s: %s\n", msg, err)
os.Exit(1)
return t
}
}
return func(msg string) T {
return t
}
}
func die(err error) {
if err != nil {
fmt.Fprint(os.Stderr, err, "\n")
os.Exit(1)
}
}

View File

@ -1,4 +1,4 @@
package pluginmeta
package plugindef
import (
"strings"
@ -7,7 +7,7 @@ import (
)
thema.#Lineage
name: "pluginmeta"
name: "plugindef"
seqs: [
{
schemas: [
@ -16,7 +16,7 @@ seqs: [
// grafana.com, then the plugin id has to follow the naming
// conventions.
id: string & strings.MinRunes(1)
id: =~"^([0-9a-z]+\\-([0-9a-z]+\\-)?(\(strings.Join([for t in _types {t}], "|"))))|(alertGroups|alertlist|annolist|barchart|bargauge|candlestick|canvas|dashlist|debug|gauge|geomap|gettingstarted|graph|heatmap|heatmap-old|histogram|icon|live|logs|news|nodeGraph|piechart|pluginlist|stat|state-timeline|status-history|table|table-old|text|timeseries|traces|welcome|xychart|alertmanager|cloudwatch|dashboard|elasticsearch|grafana|grafana-azure-monitor-datasource|graphite|influxdb|jaeger|loki|mixed|mssql|mysql|opentsdb|postgres|prometheus|stackdriver|tempo|testdata|zipkin|phlare|parca)$"
id: =~"^([0-9a-z]+\\-([0-9a-z]+\\-)?(\(strings.Join([ for t in _types {t}], "|"))))|(alertGroups|alertlist|annolist|barchart|bargauge|candlestick|canvas|dashlist|debug|gauge|geomap|gettingstarted|graph|heatmap|heatmap-old|histogram|icon|live|logs|news|nodeGraph|piechart|pluginlist|stat|state-timeline|status-history|table|table-old|text|timeseries|traces|welcome|xychart|alertmanager|cloudwatch|dashboard|elasticsearch|grafana|grafana-azure-monitor-datasource|graphite|influxdb|jaeger|loki|mixed|mssql|mysql|opentsdb|postgres|prometheus|stackdriver|tempo|testdata|zipkin|phlare|parca)$"
// The set of all plugin types. This hidden field exists solely
// so that the set can be string-interpolated into other fields.
@ -132,7 +132,7 @@ seqs: [
autoEnabled?: bool
// Optional list of RBAC RoleRegistrations.
// Describes and organizes the default permissions associated with any of the Grafana basic roles,
// Describes and organizes the default permissions associated with any of the Grafana basic roles,
// which characterizes what viewers, editors, admins, or grafana admins can do on the plugin.
// The Admin basic role inherits its default permissions from the Editor basic role which in turn
// inherits them from the Viewer basic role.
@ -167,7 +167,7 @@ seqs: [
// scope.
// Example: action: 'test-app.schedules:read', scope: 'test-app.schedules:*'
#Permission: {
action: string,
action: string
scope?: string
}

View File

@ -0,0 +1,38 @@
package plugindef
import (
"sync"
"cuelang.org/go/cue/build"
"github.com/grafana/grafana/pkg/cuectx"
"github.com/grafana/thema"
)
//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)
}
var linonce sync.Once
var pdlin thema.ConvergentLineage[*PluginDef]
var pdlinerr error
// Lineage returns the [thema.ConvergentLineage] for plugindef, the canonical
// specification for Grafana plugin.json files.
//
// Unless a custom thema.Runtime is specifically needed, prefer calling this with
// nil, as a cached lineage will be returned.
func Lineage(rt *thema.Runtime, opts ...thema.BindOption) (thema.ConvergentLineage[*PluginDef], error) {
if len(opts) == 0 && (rt == nil || rt == cuectx.GrafanaThemaRuntime()) {
linonce.Do(func() {
pdlin, pdlinerr = doLineage(rt)
})
return pdlin, pdlinerr
}
return doLineage(rt, opts...)
}

View File

@ -0,0 +1,85 @@
// THIS FILE IS GENERATED. EDITING IS FUTILE.
//
// Generated by:
// pkg/plugins/plugindef/gen.go
// Using jennies:
// PluginGoBindings
//
// Run 'make gen-cue' from repository root to regenerate.
package plugindef
import (
"cuelang.org/go/cue/build"
"github.com/grafana/thema"
)
// doLineage returns a [thema.ConvergentLineage] for the 'plugindef' Thema lineage.
//
// The lineage is the canonical specification of plugindef. It contains all
// schema versions that have ever existed for plugindef, and the lenses that
// allow valid instances of one schema in the lineage to be translated to
// another schema in the lineage.
//
// As a [thema.ConvergentLineage], the returned lineage has one primary schema, 0.0,
// which is [thema.AssignableTo] [*PluginDef], the lineage's parameterized type.
//
// This function will return an error if the [Thema invariants] are not met by
// the underlying lineage declaration in CUE, or if [*PluginDef] is not
// [thema.AssignableTo] the 0.0 schema.
//
// [Thema's general invariants]: https://github.com/grafana/thema/blob/main/docs/invariants.md
func doLineage(rt *thema.Runtime, opts ...thema.BindOption) (thema.ConvergentLineage[*PluginDef], error) {
lin, err := baseLineage(rt, opts...)
if err != nil {
return nil, err
}
sch := thema.SchemaP(lin, thema.SV(0, 0))
typ := new(PluginDef)
tsch, err := thema.BindType(sch, typ)
if err != nil {
// This will error out if the 0.0 schema isn't assignable to
// *PluginDef. If Thema also generates that type, this should be unreachable,
// barring a critical bug in Thema's Go generator.
return nil, err
}
return tsch.ConvergentLineage(), nil
}
func baseLineage(rt *thema.Runtime, opts ...thema.BindOption) (thema.Lineage, error) {
// First, we must get the bytes of the .cue file(s) in which the "plugindef" lineage
// is declared, and load them into a
// "cuelang.org/go/cue/build".Instance.
//
// For most Thema-based development workflows, these bytes should come from an embed.FS.
// This ensures Go is always compiled with the current state of the .cue files.
var inst *build.Instance
var err error
// loadInstanceForplugindef must be manually implemented in another file in this
// Go package.
inst, err = loadInstanceForplugindef()
if err != nil {
// Errors at this point indicate a problem with basic loading of .cue file bytes,
// which typically means the code generator was misconfigured and a path input
// is incorrect.
return nil, err
}
raw := rt.Context().BuildInstance(inst)
// An error returned from thema.BindLineage indicates one of the following:
// - The parsed path does not exist in the loaded CUE file (["github.com/grafana/thema/errors".ErrValueNotExist])
// - The value at the parsed path exists, but does not appear to be a Thema
// lineage (["github.com/grafana/thema/errors".ErrValueNotALineage])
// - The value at the parsed path exists and is a lineage (["github.com/grafana/thema/errors".ErrInvalidLineage]),
// but is invalid due to the violation of some general Thema invariant -
// for example, declared schemas don't follow backwards compatibility rules,
// lenses are incomplete.
return thema.BindLineage(raw, rt)
}
// type guards
var _ thema.ConvergentLineageFactory[*PluginDef] = doLineage
var _ thema.LineageFactory = baseLineage

View File

@ -1,21 +1,13 @@
// This file is autogenerated. DO NOT EDIT.
// THIS FILE IS GENERATED. EDITING IS FUTILE.
//
// Generated by pkg/framework/coremodel/gen.go
// Generated by:
// pkg/plugins/plugindef/gen.go
// Using jennies:
// PluginGoTypes
//
// Derived from the Thema lineage declared in pkg/coremodel/pluginmeta/coremodel.cue
//
// Run `make gen-cue` from repository root to regenerate.
// Run 'make gen-cue' from repository root to regenerate.
package pluginmeta
import (
"embed"
"path/filepath"
"github.com/grafana/grafana/pkg/cuectx"
"github.com/grafana/grafana/pkg/framework/coremodel"
"github.com/grafana/thema"
)
package plugindef
// Defines values for Category.
const (
@ -117,11 +109,8 @@ const (
RoleRegistrationGrantsViewer RoleRegistrationGrants = "Viewer"
)
// Model is the Go representation of a pluginmeta.
//
// THIS TYPE IS INTENDED FOR INTERNAL USE BY THE GRAFANA BACKEND, AND IS SUBJECT TO BREAKING CHANGES.
// Equivalent Go types at stable import paths are provided in https://github.com/grafana/grok.
type Model struct {
// PluginDef defines model for plugindef.
type PluginDef struct {
// For data source plugins, if the plugin supports alerting.
Alerting *bool `json:"alerting,omitempty"`
@ -308,30 +297,18 @@ type Model struct {
}
// Plugin category used on the Add data source page.
//
// THIS TYPE IS INTENDED FOR INTERNAL USE BY THE GRAFANA BACKEND, AND IS SUBJECT TO BREAKING CHANGES.
// Equivalent Go types at stable import paths are provided in https://github.com/grafana/grok.
type Category string
// type indicates which type of Grafana plugin this is, of the defined
// set of Grafana plugin types.
//
// THIS TYPE IS INTENDED FOR INTERNAL USE BY THE GRAFANA BACKEND, AND IS SUBJECT TO BREAKING CHANGES.
// Equivalent Go types at stable import paths are provided in https://github.com/grafana/grok.
type Type string
// BasicRole is a Grafana basic role, which can be 'Viewer', 'Editor', 'Admin' or 'Grafana Admin'.
// With RBAC, the Admin basic role inherits its default permissions from the Editor basic role which
// in turn inherits them from the Viewer basic role.
//
// THIS TYPE IS INTENDED FOR INTERNAL USE BY THE GRAFANA BACKEND, AND IS SUBJECT TO BREAKING CHANGES.
// Equivalent Go types at stable import paths are provided in https://github.com/grafana/grok.
type BasicRole string
// BuildInfo is the Go representation of a pluginmeta.BuildInfo.
//
// THIS TYPE IS INTENDED FOR INTERNAL USE BY THE GRAFANA BACKEND, AND IS SUBJECT TO BREAKING CHANGES.
// Equivalent Go types at stable import paths are provided in https://github.com/grafana/grok.
// BuildInfo defines model for plugindef.BuildInfo.
type BuildInfo struct {
// Git branch the plugin was built from.
Branch *string `json:"branch,omitempty"`
@ -348,10 +325,7 @@ type BuildInfo struct {
Time *int64 `json:"time,omitempty"`
}
// Dependencies is the Go representation of a pluginmeta.Dependencies.
//
// THIS TYPE IS INTENDED FOR INTERNAL USE BY THE GRAFANA BACKEND, AND IS SUBJECT TO BREAKING CHANGES.
// Equivalent Go types at stable import paths are provided in https://github.com/grafana/grok.
// Dependencies defines model for plugindef.Dependencies.
type Dependencies struct {
// Required Grafana version for this plugin. Validated using
// https://github.com/npm/node-semver.
@ -369,9 +343,6 @@ type Dependencies struct {
// Dependency describes another plugin on which a plugin depends.
// The id refers to the plugin package identifier, as given on
// the grafana.com plugin marketplace.
//
// THIS TYPE IS INTENDED FOR INTERNAL USE BY THE GRAFANA BACKEND, AND IS SUBJECT TO BREAKING CHANGES.
// Equivalent Go types at stable import paths are provided in https://github.com/grafana/grok.
type Dependency struct {
Id string `json:"id"`
Name string `json:"name"`
@ -379,26 +350,17 @@ type Dependency struct {
Version string `json:"version"`
}
// DependencyType is the Go representation of a Dependency.Type.
//
// THIS TYPE IS INTENDED FOR INTERNAL USE BY THE GRAFANA BACKEND, AND IS SUBJECT TO BREAKING CHANGES.
// Equivalent Go types at stable import paths are provided in https://github.com/grafana/grok.
// DependencyType defines model for Dependency.Type.
type DependencyType string
// Header describes an HTTP header that is forwarded with a proxied request for
// a plugin route.
//
// THIS TYPE IS INTENDED FOR INTERNAL USE BY THE GRAFANA BACKEND, AND IS SUBJECT TO BREAKING CHANGES.
// Equivalent Go types at stable import paths are provided in https://github.com/grafana/grok.
type Header struct {
Content string `json:"content"`
Name string `json:"name"`
}
// A resource to be included in a plugin.
//
// THIS TYPE IS INTENDED FOR INTERNAL USE BY THE GRAFANA BACKEND, AND IS SUBJECT TO BREAKING CHANGES.
// Equivalent Go types at stable import paths are provided in https://github.com/grafana/grok.
type Include struct {
// Add the include to the side menu.
AddToNav *bool `json:"addToNav,omitempty"`
@ -424,23 +386,14 @@ type Include struct {
Uid *string `json:"uid,omitempty"`
}
// IncludeRole is the Go representation of a Include.Role.
//
// THIS TYPE IS INTENDED FOR INTERNAL USE BY THE GRAFANA BACKEND, AND IS SUBJECT TO BREAKING CHANGES.
// Equivalent Go types at stable import paths are provided in https://github.com/grafana/grok.
// IncludeRole defines model for Include.Role.
type IncludeRole string
// IncludeType is the Go representation of a Include.Type.
//
// THIS TYPE IS INTENDED FOR INTERNAL USE BY THE GRAFANA BACKEND, AND IS SUBJECT TO BREAKING CHANGES.
// Equivalent Go types at stable import paths are provided in https://github.com/grafana/grok.
// IncludeType defines model for Include.Type.
type IncludeType string
// Metadata about a Grafana plugin. Some fields are used on the plugins
// page in Grafana and others on grafana.com, if the plugin is published.
//
// THIS TYPE IS INTENDED FOR INTERNAL USE BY THE GRAFANA BACKEND, AND IS SUBJECT TO BREAKING CHANGES.
// Equivalent Go types at stable import paths are provided in https://github.com/grafana/grok.
type Info struct {
// Information about the plugin author.
Author *struct {
@ -497,9 +450,6 @@ type Info struct {
// TODO docs
// TODO should this really be separate from TokenAuth?
//
// THIS TYPE IS INTENDED FOR INTERNAL USE BY THE GRAFANA BACKEND, AND IS SUBJECT TO BREAKING CHANGES.
// Equivalent Go types at stable import paths are provided in https://github.com/grafana/grok.
type JWTTokenAuth struct {
// Parameters for the JWT token authentication request.
Params map[string]interface{} `json:"params"`
@ -515,26 +465,17 @@ type JWTTokenAuth struct {
// Permission describes an RBAC permission on the plugin. A permission has an action and an option
// scope.
// Example: action: 'test-app.schedules:read', scope: 'test-app.schedules:*'
//
// THIS TYPE IS INTENDED FOR INTERNAL USE BY THE GRAFANA BACKEND, AND IS SUBJECT TO BREAKING CHANGES.
// Equivalent Go types at stable import paths are provided in https://github.com/grafana/grok.
type Permission struct {
Action string `json:"action"`
Scope *string `json:"scope,omitempty"`
}
// ReleaseState indicates release maturity state of a plugin.
//
// THIS TYPE IS INTENDED FOR INTERNAL USE BY THE GRAFANA BACKEND, AND IS SUBJECT TO BREAKING CHANGES.
// Equivalent Go types at stable import paths are provided in https://github.com/grafana/grok.
type ReleaseState string
// Role describes an RBAC role which allows grouping multiple related permissions on the plugin,
// each of which has an action and an optional scope.
// Example: the role 'Schedules Reader' bundles permissions to view all schedules of the plugin.
//
// THIS TYPE IS INTENDED FOR INTERNAL USE BY THE GRAFANA BACKEND, AND IS SUBJECT TO BREAKING CHANGES.
// Equivalent Go types at stable import paths are provided in https://github.com/grafana/grok.
type Role struct {
Description string `json:"description"`
Name string `json:"name"`
@ -549,9 +490,6 @@ type Role struct {
// will get them by default.
// Example: the role 'Schedules Reader' bundles permissions to view all schedules of the plugin
// which will be granted to Admins by default.
//
// THIS TYPE IS INTENDED FOR INTERNAL USE BY THE GRAFANA BACKEND, AND IS SUBJECT TO BREAKING CHANGES.
// Equivalent Go types at stable import paths are provided in https://github.com/grafana/grok.
type RoleRegistration struct {
// Default assignment of the role to Grafana basic roles (Viewer, Editor, Admin, Grafana Admin)
// The Admin basic role inherits its default permissions from the Editor basic role which in turn
@ -569,19 +507,13 @@ type RoleRegistration struct {
} `json:"role"`
}
// RoleRegistrationGrants is the Go representation of a RoleRegistration.Grants.
//
// THIS TYPE IS INTENDED FOR INTERNAL USE BY THE GRAFANA BACKEND, AND IS SUBJECT TO BREAKING CHANGES.
// Equivalent Go types at stable import paths are provided in https://github.com/grafana/grok.
// RoleRegistrationGrants defines model for RoleRegistration.Grants.
type RoleRegistrationGrants string
// A proxy route used in datasource plugins for plugin authentication
// and adding headers to HTTP requests made by the plugin.
// For more information, refer to [Authentication for data source
// plugins](https://grafana.com/docs/grafana/latest/developers/plugins/authentication/).
//
// THIS TYPE IS INTENDED FOR INTERNAL USE BY THE GRAFANA BACKEND, AND IS SUBJECT TO BREAKING CHANGES.
// Equivalent Go types at stable import paths are provided in https://github.com/grafana/grok.
type Route struct {
// For data source plugins. Route headers set the body content and
// length to the proxied request.
@ -616,9 +548,6 @@ type Route struct {
}
// TODO docs
//
// THIS TYPE IS INTENDED FOR INTERNAL USE BY THE GRAFANA BACKEND, AND IS SUBJECT TO BREAKING CHANGES.
// Equivalent Go types at stable import paths are provided in https://github.com/grafana/grok.
type TokenAuth struct {
// Parameters for the token authentication request.
Params map[string]interface{} `json:"params"`
@ -633,70 +562,7 @@ type TokenAuth struct {
// URLParam describes query string parameters for
// a url in a plugin route
//
// THIS TYPE IS INTENDED FOR INTERNAL USE BY THE GRAFANA BACKEND, AND IS SUBJECT TO BREAKING CHANGES.
// Equivalent Go types at stable import paths are provided in https://github.com/grafana/grok.
type URLParam struct {
Content string `json:"content"`
Name string `json:"name"`
}
//go:embed coremodel.cue
var cueFS embed.FS
// The current version of the coremodel schema, as declared in coremodel.cue.
// This version determines what schema version is returned from [Coremodel.CurrentSchema],
// and which schema version is used for code generation within the grafana/grafana repository.
//
// The code generator ensures that this is always the latest Thema schema version.
var currentVersion = thema.SV(0, 0)
// Lineage returns the Thema lineage representing a Grafana pluginmeta.
//
// The lineage is the canonical specification of the current pluginmeta schema,
// all prior schema versions, and the mappings that allow migration between
// schema versions.
func Lineage(rt *thema.Runtime, opts ...thema.BindOption) (thema.Lineage, error) {
return cuectx.LoadGrafanaInstancesWithThema(filepath.Join("pkg", "coremodel", "pluginmeta"), cueFS, rt, opts...)
}
var _ thema.LineageFactory = Lineage
var _ coremodel.Interface = &Coremodel{}
// Coremodel contains the foundational schema declaration for pluginmetas.
// It implements coremodel.Interface.
type Coremodel struct {
lin thema.Lineage
}
// Lineage returns the canonical pluginmeta Lineage.
func (c *Coremodel) Lineage() thema.Lineage {
return c.lin
}
// CurrentSchema returns the current (latest) pluginmeta Thema schema.
func (c *Coremodel) CurrentSchema() thema.Schema {
return thema.SchemaP(c.lin, currentVersion)
}
// GoType returns a pointer to an empty Go struct that corresponds to
// the current Thema schema.
func (c *Coremodel) GoType() interface{} {
return &Model{}
}
// New returns a new instance of the pluginmeta coremodel.
//
// Note that this function does not cache, and initially loading a Thema lineage
// can be expensive. As such, the Grafana backend should prefer to access this
// coremodel through a registry (pkg/framework/coremodel/registry), which does cache.
func New(rt *thema.Runtime) (*Coremodel, error) {
lin, err := Lineage(rt)
if err != nil {
return nil, err
}
return &Coremodel{
lin: lin,
}, nil
}