From 78f03400318d62d4ca39d3047994448aeeadc12f Mon Sep 17 00:00:00 2001 From: sam boyer Date: Tue, 15 Nov 2022 08:48:31 -0500 Subject: [PATCH] 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 --- Makefile | 1 + embed.go | 2 +- go.mod | 4 +- go.sum | 12 +- kinds/gen.go | 3 - pkg/codegen/coremodel.go | 2 +- pkg/codegen/generators.go | 19 +- pkg/codegen/jenny_tsveneerindex.go | 3 +- pkg/codegen/pluggen.go | 8 +- pkg/codegen/tmpl/kind_corestructured.tmpl | 6 +- pkg/codegen/util_go.go | 33 +++- pkg/cuectx/ctx.go | 86 +++++---- pkg/framework/coremodel/helpers.go | 107 ------------ pkg/framework/coremodel/interface.go | 29 ---- pkg/kinds/dashboard/dashboard_kind_gen.go | 6 +- pkg/kinds/playlist/playlist_kind_gen.go | 6 +- pkg/kindsys/kindcats.cue | 1 - pkg/kindsys/load.go | 2 +- pkg/{framework/coremodel => kindsys}/slot.go | 34 ++-- pkg/kindsys/slot_test.go | 29 ++++ .../coremodel => kindsys}/slots.cue | 2 +- .../testdata/disallowed-cue-import/models.cue | 2 +- pkg/plugins/pfs/pfs.go | 82 +++------ pkg/plugins/pfs/pfs_test.go | 1 + pkg/plugins/plugindef/gen.go | 132 ++++++++++++++ .../plugindef/plugindef.cue} | 10 +- pkg/plugins/plugindef/plugindef.go | 38 ++++ .../plugindef/plugindef_bindings_gen.go | 85 +++++++++ .../plugindef/plugindef_types_gen.go} | 164 ++---------------- 29 files changed, 458 insertions(+), 451 deletions(-) delete mode 100644 pkg/framework/coremodel/helpers.go delete mode 100644 pkg/framework/coremodel/interface.go rename pkg/{framework/coremodel => kindsys}/slot.go (72%) create mode 100644 pkg/kindsys/slot_test.go rename pkg/{framework/coremodel => kindsys}/slots.cue (99%) create mode 100644 pkg/plugins/plugindef/gen.go rename pkg/{coremodel/pluginmeta/coremodel.cue => plugins/plugindef/plugindef.cue} (94%) create mode 100644 pkg/plugins/plugindef/plugindef.go create mode 100644 pkg/plugins/plugindef/plugindef_bindings_gen.go rename pkg/{coremodel/pluginmeta/pluginmeta_gen.go => plugins/plugindef/plugindef_types_gen.go} (70%) diff --git a/Makefile b/Makefile index 52700ac46bb..03a2c40b48b 100644 --- a/Makefile +++ b/Makefile @@ -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 diff --git a/embed.go b/embed.go index c1353761781..7051e0af971 100644 --- a/embed.go +++ b/embed.go @@ -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 diff --git a/go.mod b/go.mod index 1630089a699..9edbe559b78 100644 --- a/go.mod +++ b/go.mod @@ -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 ) diff --git a/go.sum b/go.sum index 4bd65ce99be..9abdd47c2e5 100644 --- a/go.sum +++ b/go.sum @@ -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= diff --git a/kinds/gen.go b/kinds/gen.go index 8fd79172c7f..588fa614631 100644 --- a/kinds/gen.go +++ b/kinds/gen.go @@ -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 { diff --git a/pkg/codegen/coremodel.go b/pkg/codegen/coremodel.go index d3b8ae92464..b0c174e4303 100644 --- a/pkg/codegen/coremodel.go +++ b/pkg/codegen/coremodel.go @@ -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, diff --git a/pkg/codegen/generators.go b/pkg/codegen/generators.go index c6090dfe4f5..51d9075557d 100644 --- a/pkg/codegen/generators.go +++ b/pkg/codegen/generators.go @@ -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 } } diff --git a/pkg/codegen/jenny_tsveneerindex.go b/pkg/codegen/jenny_tsveneerindex.go index c213a20ddfd..0bb82482e5f 100644 --- a/pkg/codegen/jenny_tsveneerindex.go +++ b/pkg/codegen/jenny_tsveneerindex.go @@ -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 diff --git a/pkg/codegen/pluggen.go b/pkg/codegen/pluggen.go index 34beeac4b2d..0694faf8445 100644 --- a/pkg/codegen/pluggen.go +++ b/pkg/codegen/pluggen.go @@ -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) } diff --git a/pkg/codegen/tmpl/kind_corestructured.tmpl b/pkg/codegen/tmpl/kind_corestructured.tmpl index d617ccfd2b6..71fbac06074 100644 --- a/pkg/codegen/tmpl/kind_corestructured.tmpl +++ b/pkg/codegen/tmpl/kind_corestructured.tmpl @@ -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 } diff --git a/pkg/codegen/util_go.go b/pkg/codegen/util_go.go index 3c8b05f3a64..b65df3ba85d 100644 --- a/pkg/codegen/util_go.go +++ b/pkg/codegen/util_go.go @@ -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 } } diff --git a/pkg/cuectx/ctx.go b/pkg/cuectx/ctx.go index 7b29041eb3c..c4084fdd6f6 100644 --- a/pkg/cuectx/ctx.go +++ b/pkg/cuectx/ctx.go @@ -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 } diff --git a/pkg/framework/coremodel/helpers.go b/pkg/framework/coremodel/helpers.go deleted file mode 100644 index e7b47a2951c..00000000000 --- a/pkg/framework/coremodel/helpers.go +++ /dev/null @@ -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 -} diff --git a/pkg/framework/coremodel/interface.go b/pkg/framework/coremodel/interface.go deleted file mode 100644 index e0d64ae7763..00000000000 --- a/pkg/framework/coremodel/interface.go +++ /dev/null @@ -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{} -} diff --git a/pkg/kinds/dashboard/dashboard_kind_gen.go b/pkg/kinds/dashboard/dashboard_kind_gen.go index 6bdb47dacf3..ee275dc8426 100644 --- a/pkg/kinds/dashboard/dashboard_kind_gen.go +++ b/pkg/kinds/dashboard/dashboard_kind_gen.go @@ -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 } diff --git a/pkg/kinds/playlist/playlist_kind_gen.go b/pkg/kinds/playlist/playlist_kind_gen.go index 2ae79506035..eb82358c7bb 100644 --- a/pkg/kinds/playlist/playlist_kind_gen.go +++ b/pkg/kinds/playlist/playlist_kind_gen.go @@ -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 } diff --git a/pkg/kindsys/kindcats.cue b/pkg/kindsys/kindcats.cue index 4321b45b78b..30ab638439e 100644 --- a/pkg/kindsys/kindcats.cue +++ b/pkg/kindsys/kindcats.cue @@ -163,4 +163,3 @@ _sharedKind: { // It is required that lineage.name is the same as the [machineName]. lineage: thema.#Lineage & { name: S.machineName } } - diff --git a/pkg/kindsys/load.go b/pkg/kindsys/load.go index d8d4cda1c67..08e4b857435 100644 --- a/pkg/kindsys/load.go +++ b/pkg/kindsys/load.go @@ -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 } diff --git a/pkg/framework/coremodel/slot.go b/pkg/kindsys/slot.go similarity index 72% rename from pkg/framework/coremodel/slot.go rename to pkg/kindsys/slot.go index 69c212b3c06..acfd07d1d79 100644 --- a/pkg/framework/coremodel/slot.go +++ b/pkg/kindsys/slot.go @@ -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 diff --git a/pkg/kindsys/slot_test.go b/pkg/kindsys/slot_test.go new file mode 100644 index 00000000000..a4a17c8ae45 --- /dev/null +++ b/pkg/kindsys/slot_test.go @@ -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") +} diff --git a/pkg/framework/coremodel/slots.cue b/pkg/kindsys/slots.cue similarity index 99% rename from pkg/framework/coremodel/slots.cue rename to pkg/kindsys/slots.cue index 6de83523482..2d4976ff274 100644 --- a/pkg/framework/coremodel/slots.cue +++ b/pkg/kindsys/slots.cue @@ -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 diff --git a/pkg/plugins/manager/testdata/disallowed-cue-import/models.cue b/pkg/plugins/manager/testdata/disallowed-cue-import/models.cue index 7094c2fb31f..b123e68c276 100644 --- a/pkg/plugins/manager/testdata/disallowed-cue-import/models.cue +++ b/pkg/plugins/manager/testdata/disallowed-cue-import/models.cue @@ -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 diff --git a/pkg/plugins/pfs/pfs.go b/pkg/plugins/pfs/pfs.go index d176c7dff3e..04682f07380 100644 --- a/pkg/plugins/pfs/pfs.go +++ b/pkg/plugins/pfs/pfs.go @@ -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() diff --git a/pkg/plugins/pfs/pfs_test.go b/pkg/plugins/pfs/pfs_test.go index 93755700c68..eed41673925 100644 --- a/pkg/plugins/pfs/pfs_test.go +++ b/pkg/plugins/pfs/pfs_test.go @@ -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 } diff --git a/pkg/plugins/plugindef/gen.go b/pkg/plugins/plugindef/gen.go new file mode 100644 index 00000000000..07354ec2c46 --- /dev/null +++ b/pkg/plugins/plugindef/gen.go @@ -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) + } +} diff --git a/pkg/coremodel/pluginmeta/coremodel.cue b/pkg/plugins/plugindef/plugindef.cue similarity index 94% rename from pkg/coremodel/pluginmeta/coremodel.cue rename to pkg/plugins/plugindef/plugindef.cue index 15a51a69197..555f78531c8 100644 --- a/pkg/coremodel/pluginmeta/coremodel.cue +++ b/pkg/plugins/plugindef/plugindef.cue @@ -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 } diff --git a/pkg/plugins/plugindef/plugindef.go b/pkg/plugins/plugindef/plugindef.go new file mode 100644 index 00000000000..ae36dd64244 --- /dev/null +++ b/pkg/plugins/plugindef/plugindef.go @@ -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...) +} diff --git a/pkg/plugins/plugindef/plugindef_bindings_gen.go b/pkg/plugins/plugindef/plugindef_bindings_gen.go new file mode 100644 index 00000000000..8551b65269e --- /dev/null +++ b/pkg/plugins/plugindef/plugindef_bindings_gen.go @@ -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 diff --git a/pkg/coremodel/pluginmeta/pluginmeta_gen.go b/pkg/plugins/plugindef/plugindef_types_gen.go similarity index 70% rename from pkg/coremodel/pluginmeta/pluginmeta_gen.go rename to pkg/plugins/plugindef/plugindef_types_gen.go index 7de04ea12da..b6532b774ea 100644 --- a/pkg/coremodel/pluginmeta/pluginmeta_gen.go +++ b/pkg/plugins/plugindef/plugindef_types_gen.go @@ -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 -}