From ced53a8dc2f9c6e68fc8fa45c328c5c1a9b60270 Mon Sep 17 00:00:00 2001 From: sam boyer Date: Wed, 14 Sep 2022 10:15:09 -0400 Subject: [PATCH] plugins: Introduce generated, static core plugin registry (#54118) * Refactor towards template/codegen framework * Add templates for plugin gen * Add Go codegen for plugins; overhaul framework, too * Add new codegen output; assorted framework fixes * Regenerate after merge * Remove accidental commit file, update templates * Export the pfs.Tree loader from plugin types * Print details from cuetsy errors * Generate loaders for all plugins and list in registry * Use pfs_gen.go over lineage_gen.go * Un-un-ignore main file * Introduce simple List static registry for plugins * Last tweaks to codegen * remove unused tvars * Ensure loop-local instances for both vars * Generate pfs parsing in-place in registry * Stop generating pfs_gen.go * Move Tree into pfs, rename subdir * Change package name to match dir * Ignore gocyclo on HTTPServer.getNavTree --- .gitignore | 2 + .../dashboard/dashboard_experimental.gen.ts | 6 +- pkg/api/index.go | 1 + pkg/codegen/coremodel.go | 266 +++++----------- pkg/codegen/pluggen.go | 299 ++++++++++++++++-- pkg/codegen/tmpl.go | 65 ++++ pkg/codegen/tmpl/addenda.tmpl | 64 ++++ pkg/codegen/tmpl/autogen_header.tmpl | 28 ++ pkg/codegen/tmpl/coremodel_imports.tmpl | 10 + pkg/codegen/tmpl/coremodel_registry.tmpl | 58 ++++ pkg/codegen/tmpl/cuetsy_multi.tmpl | 7 + pkg/codegen/tmpl/plugin_lineage_binding.tmpl | 12 + pkg/codegen/tmpl/plugin_lineage_file.tmpl | 58 ++++ pkg/codegen/tmpl/plugin_registry.tmpl | 31 ++ pkg/codegen/tmpl/plugin_registry_ref.tmpl | 16 + pkg/codegen/util_go.go | 67 ++++ pkg/coremodel/dashboard/dashboard_gen.go | 74 +++-- pkg/coremodel/pluginmeta/pluginmeta_gen.go | 36 ++- pkg/framework/coremodel/registry/provide.go | 4 +- .../coremodel/registry/registry_gen.go | 11 +- pkg/framework/coremodel/slot.go | 16 + pkg/plugins/pfs/corelist/loadlist_gen.go | 86 +++++ pkg/plugins/pfs/corelist/new.go | 30 ++ pkg/plugins/pfs/errors.go | 3 + pkg/plugins/pfs/pfs.go | 34 +- public/app/plugins/gen.go | 50 ++- .../app/plugins/panel/annolist/models.gen.ts | 10 +- .../app/plugins/panel/barchart/models.gen.ts | 10 +- .../app/plugins/panel/bargauge/models.gen.ts | 10 +- .../app/plugins/panel/dashlist/models.gen.ts | 10 +- public/app/plugins/panel/gauge/models.gen.ts | 10 +- .../app/plugins/panel/histogram/models.gen.ts | 10 +- public/app/plugins/panel/news/models.gen.ts | 10 +- .../app/plugins/panel/piechart/models.gen.ts | 10 +- public/app/plugins/panel/stat/models.gen.ts | 10 +- public/app/plugins/panel/text/models.gen.ts | 10 +- 36 files changed, 1112 insertions(+), 322 deletions(-) create mode 100644 pkg/codegen/tmpl.go create mode 100644 pkg/codegen/tmpl/addenda.tmpl create mode 100644 pkg/codegen/tmpl/autogen_header.tmpl create mode 100644 pkg/codegen/tmpl/coremodel_imports.tmpl create mode 100644 pkg/codegen/tmpl/coremodel_registry.tmpl create mode 100644 pkg/codegen/tmpl/cuetsy_multi.tmpl create mode 100644 pkg/codegen/tmpl/plugin_lineage_binding.tmpl create mode 100644 pkg/codegen/tmpl/plugin_lineage_file.tmpl create mode 100644 pkg/codegen/tmpl/plugin_registry.tmpl create mode 100644 pkg/codegen/tmpl/plugin_registry_ref.tmpl create mode 100644 pkg/codegen/util_go.go create mode 100644 pkg/plugins/pfs/corelist/loadlist_gen.go create mode 100644 pkg/plugins/pfs/corelist/new.go diff --git a/.gitignore b/.gitignore index e261c77df42..6a68d6553f4 100644 --- a/.gitignore +++ b/.gitignore @@ -169,6 +169,8 @@ compilation-stats.json !pkg/services/featuremgmt/toggles_gen.go !pkg/coremodel/**/*_gen.go !pkg/framework/**/*_gen.go +!pkg/plugins/pfs/**/*_gen.go +!public/app/plugins/**/*_gen.go # Auto-generated internationalization files public/locales/_build/ diff --git a/packages/grafana-schema/src/schema/dashboard/dashboard_experimental.gen.ts b/packages/grafana-schema/src/schema/dashboard/dashboard_experimental.gen.ts index 943619a1a6b..6ae3b7b6769 100644 --- a/packages/grafana-schema/src/schema/dashboard/dashboard_experimental.gen.ts +++ b/packages/grafana-schema/src/schema/dashboard/dashboard_experimental.gen.ts @@ -1,8 +1,10 @@ // This file is autogenerated. DO NOT EDIT. // -// Run "make gen-cue" from repository root to regenerate. +// Generated by pkg/framework/coremodel/gen.go // -// Derived from the Thema lineage at pkg/coremodel/dashboard +// Derived from the Thema lineage declared in pkg/coremodel/dashboard/coremodel.cue +// +// Run `make gen-cue` from repository root to regenerate. // This model is a WIP and not yet canonical. Consequently, its members are diff --git a/pkg/api/index.go b/pkg/api/index.go index 844f39ecc1b..e054aac9fff 100644 --- a/pkg/api/index.go +++ b/pkg/api/index.go @@ -173,6 +173,7 @@ func (hs *HTTPServer) ReqCanAdminTeams(c *models.ReqContext) bool { return c.OrgRole == org.RoleAdmin || (hs.Cfg.EditorsCanAdmin && c.OrgRole == org.RoleEditor) } +//nolint:gocyclo func (hs *HTTPServer) getNavTree(c *models.ReqContext, hasEditPerm bool, prefs *pref.Preference) ([]*dtos.NavLink, error) { hasAccess := ac.HasAccess(hs.AccessControl, c) var navTree []*dtos.NavLink diff --git a/pkg/codegen/coremodel.go b/pkg/codegen/coremodel.go index 5266d379d20..55e276deb5b 100644 --- a/pkg/codegen/coremodel.go +++ b/pkg/codegen/coremodel.go @@ -5,17 +5,14 @@ import ( "errors" "fmt" "go/ast" - "go/format" - "go/parser" - "go/token" "io" "os" "path/filepath" "regexp" "strings" "testing/fstest" - "text/template" + cerrors "cuelang.org/go/cue/errors" "cuelang.org/go/pkg/encoding/yaml" "github.com/deepmap/oapi-codegen/pkg/codegen" "github.com/getkin/kin-openapi/openapi3" @@ -23,7 +20,6 @@ import ( "github.com/grafana/grafana/pkg/cuectx" "github.com/grafana/thema" "github.com/grafana/thema/encoding/openapi" - "golang.org/x/tools/imports" ) // ExtractedLineage contains the results of statically analyzing a Grafana @@ -94,12 +90,14 @@ func ExtractLineage(path string, lib thema.Library) (*ExtractedLineage, error) { }, } - ec.RelativePath, err = filepath.Rel(groot, filepath.Dir(path)) + // ec.RelativePath, err = filepath.Rel(groot, filepath.Dir(path)) + ec.RelativePath, err = filepath.Rel(groot, path) if err != nil { // should be unreachable, since we rootclimbed to find groot above panic(err) } - ec.Lineage, err = cuectx.LoadGrafanaInstancesWithThema(ec.RelativePath, fs, lib) + ec.RelativePath = filepath.ToSlash(ec.RelativePath) + ec.Lineage, err = cuectx.LoadGrafanaInstancesWithThema(filepath.Dir(ec.RelativePath), fs, lib) if err != nil { return ec, err } @@ -116,7 +114,7 @@ func (ls *ExtractedLineage) toTemplateObj() tplVars { return tplVars{ Name: lin.Name(), LineagePath: ls.RelativePath, - PkgPath: filepath.ToSlash(filepath.Join("github.com/grafana/grafana", ls.RelativePath)), + PkgPath: filepath.ToSlash(filepath.Join("github.com/grafana/grafana", filepath.Dir(ls.RelativePath))), TitleName: strings.Title(lin.Name()), // nolint LatestSeqv: sch.Version()[0], LatestSchv: sch.Version()[1], @@ -170,12 +168,19 @@ func (ls *ExtractedLineage) GenerateGoCoremodel(path string) (WriteDiffer, error return nil, fmt.Errorf("loading generated openapi failed; %w", err) } + var importbuf bytes.Buffer + if err = tmpls.Lookup("coremodel_imports.tmpl").Execute(&importbuf, tvars_coremodel_imports{ + PackageName: lin.Name(), + }); err != nil { + return nil, fmt.Errorf("error executing imports template: %w", err) + } + gostr, err := codegen.Generate(oT, lin.Name(), codegen.Options{ GenerateTypes: true, SkipPrune: true, SkipFmt: true, UserTemplates: map[string]string{ - "imports.tmpl": fmt.Sprintf(tmplImports, ls.RelativePath), + "imports.tmpl": importbuf.String(), "typedef.tmpl": tmplTypedef, }, }) @@ -183,34 +188,34 @@ func (ls *ExtractedLineage) GenerateGoCoremodel(path string) (WriteDiffer, error return nil, fmt.Errorf("openapi generation failed: %w", err) } - vars := ls.toTemplateObj() - var buuf bytes.Buffer - err = tmplAddenda.Execute(&buuf, vars) - if err != nil { - panic(err) + buf := new(bytes.Buffer) + if err = tmpls.Lookup("autogen_header.tmpl").Execute(buf, tvars_autogen_header{ + LineagePath: ls.RelativePath, + GeneratorPath: "pkg/framework/coremodel/gen.go", // FIXME hardcoding is not OK + }); err != nil { + return nil, fmt.Errorf("error executing header template: %w", err) } - fset := token.NewFileSet() - fname := fmt.Sprintf("%s_gen.go", lin.Name()) - gf, err := parser.ParseFile(fset, fname, gostr+buuf.String(), parser.ParseComments) - if err != nil { - return nil, fmt.Errorf("generated go file parsing failed: %w", err) - } - ast.Walk(prefixDropper(strings.Title(lin.Name())), gf) + fmt.Fprint(buf, "\n", gostr) - var buf bytes.Buffer - err = format.Node(&buf, fset, gf) + vars := ls.toTemplateObj() + err = tmpls.Lookup("addenda.tmpl").Execute(buf, vars) if err != nil { - return nil, fmt.Errorf("ast printing failed: %w", err) + panic(err) } - byt, err := imports.Process(fname, buf.Bytes(), nil) + fullp := filepath.Join(path, fmt.Sprintf("%s_gen.go", lin.Name())) + byt, err := postprocessGoFile(genGoFile{ + path: fullp, + walker: makePrefixDropper(strings.Title(lin.Name()), "Model"), + in: buf.Bytes(), + }) if err != nil { - return nil, fmt.Errorf("goimports processing failed: %w", err) + return nil, err } wd := NewWriteDiffer() - wd[filepath.Join(path, fname)] = byt + wd[fullp] = byt return wd, nil } @@ -238,7 +243,7 @@ func (ls *ExtractedLineage) GenerateTypescriptCoremodel(path string) (WriteDiffe top, err := cuetsy.GenerateSingleAST(strings.Title(ls.Lineage.Name()), schv, cuetsy.TypeInterface) if err != nil { - return nil, fmt.Errorf("cuetsy top gen failed: %w", err) + return nil, fmt.Errorf("cuetsy top gen failed: %s", cerrors.Details(err, nil)) } // TODO until cuetsy can toposort its outputs, put the top/parent type at the bottom of the file. @@ -250,7 +255,12 @@ func (ls *ExtractedLineage) GenerateTypescriptCoremodel(path string) (WriteDiffe var strb strings.Builder var str string fpath := ls.Lineage.Name() + ".gen.ts" - strb.WriteString(fmt.Sprintf(genHeader, ls.RelativePath)) + if err := tmpls.Lookup("autogen_header.tmpl").Execute(&strb, tvars_autogen_header{ + LineagePath: ls.RelativePath, + GeneratorPath: "pkg/framework/coremodel/gen.go", // FIXME hardcoding is not OK + }); err != nil { + return nil, fmt.Errorf("error executing header template: %w", err) + } if !ls.IsCanonical { fpath = fmt.Sprintf("%s_experimental.gen.ts", ls.Lineage.Name()) @@ -273,16 +283,34 @@ func (ls *ExtractedLineage) GenerateTypescriptCoremodel(path string) (WriteDiffe return wd, nil } -type prefixDropper string +type prefixDropper struct { + str string + base string + rxp *regexp.Regexp + rxpsuff *regexp.Regexp +} + +func makePrefixDropper(str, base string) prefixDropper { + return prefixDropper{ + str: str, + base: base, + rxpsuff: regexp.MustCompile(fmt.Sprintf(`%s([a-zA-Z_]*)`, str)), + rxp: regexp.MustCompile(fmt.Sprintf(`%s([\s.,;-])`, str)), + } +} func (d prefixDropper) Visit(n ast.Node) ast.Visitor { - asstr := string(d) switch x := n.(type) { case *ast.Ident: - if x.Name != asstr { - x.Name = strings.TrimPrefix(x.Name, asstr) + if x.Name != d.str { + x.Name = strings.TrimPrefix(x.Name, d.str) } else { - x.Name = "Model" + x.Name = d.base + } + case *ast.CommentGroup: + for _, c := range x.List { + c.Text = d.rxp.ReplaceAllString(c.Text, d.base+"$1") + c.Text = d.rxpsuff.ReplaceAllString(c.Text, "$1") } } return d @@ -297,177 +325,33 @@ func GenerateCoremodelRegistry(path string, ecl []*ExtractedLineage) (WriteDiffe cml = append(cml, ec.toTemplateObj()) } - var buf bytes.Buffer - err := tmplRegistry.Execute(&buf, struct { - Coremodels []tplVars - }{ + buf := new(bytes.Buffer) + if err := tmpls.Lookup("coremodel_registry.tmpl").Execute(buf, tvars_coremodel_registry{ + Header: tvars_autogen_header{ + GeneratorPath: "pkg/framework/coremodel/gen.go", // FIXME hardcoding is not OK + }, Coremodels: cml, - }) - if err != nil { - return nil, fmt.Errorf("failed generating template: %w", err) + }); err != nil { + return nil, fmt.Errorf("failed executing coremodel registry template: %w", err) } - byt, err := imports.Process(path, buf.Bytes(), nil) + byt, err := postprocessGoFile(genGoFile{ + path: path, + in: buf.Bytes(), + }) if err != nil { - return nil, fmt.Errorf("goimports processing failed: %w", err) + return nil, err } - wd := NewWriteDiffer() wd[path] = byt return wd, nil } -var genHeader = `// This file is autogenerated. DO NOT EDIT. -// -// Run "make gen-cue" from repository root to regenerate. -// -// Derived from the Thema lineage at %s - -` - -var tmplImports = genHeader + `package {{ .PackageName }} - -import ( - "embed" - "path/filepath" - - "github.com/grafana/grafana/pkg/cuectx" - "github.com/grafana/grafana/pkg/framework/coremodel" - "github.com/grafana/thema" -) -` - -var tmplAddenda = template.Must(template.New("addenda").Parse(` -//go:embed coremodel.cue -var cueFS embed.FS - -// codegen ensures that this is always the latest Thema schema version -var currentVersion = thema.SV({{ .LatestSeqv }}, {{ .LatestSchv }}) - -// Lineage returns the Thema lineage representing a Grafana {{ .Name }}. -// -// The lineage is the canonical specification of the current {{ .Name }} schema, -// all prior schema versions, and the mappings that allow migration between -// schema versions. -{{- if .IsComposed }}// -// This is the base variant of the schema. It does not include any composed -// plugin schemas.{{ end }} -func Lineage(lib thema.Library, opts ...thema.BindOption) (thema.Lineage, error) { - return cuectx.LoadGrafanaInstancesWithThema(filepath.Join("pkg", "coremodel", "{{ .Name }}"), cueFS, lib, opts...) -} - -var _ thema.LineageFactory = Lineage -var _ coremodel.Interface = &Coremodel{} - -// Coremodel contains the foundational schema declaration for {{ .Name }}s. -// It implements coremodel.Interface. -type Coremodel struct { - lin thema.Lineage -} - -// Lineage returns the canonical {{ .Name }} Lineage. -func (c *Coremodel) Lineage() thema.Lineage { - return c.lin -} - -// CurrentSchema returns the current (latest) {{ .Name }} 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 {{ .Name }} 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(lib thema.Library) (*Coremodel, error) { - lin, err := Lineage(lib) - if err != nil { - return nil, err - } - - return &Coremodel{ - lin: lin, - }, nil -} -`)) - var tmplTypedef = `{{range .Types}} -{{ with .Schema.Description }}{{ . }}{{ else }}// {{.TypeName}} defines model for {{.JsonName}}.{{ end }} +{{ with .Schema.Description }}{{ . }}{{ else }}// {{.TypeName}} is the Go representation of a {{.JsonName}}.{{ end }} // // 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 {{.TypeName}} {{if and (opts.AliasTypes) (.CanAlias)}}={{end}} {{.Schema.TypeDecl}} {{end}} ` - -var tmplRegistry = template.Must(template.New("registry").Parse(` -// This file is autogenerated. DO NOT EDIT. -// -// Generated by pkg/framework/coremodel/gen.go -// Run "make gen-cue" from repository root to regenerate. - -package registry - -import ( - "fmt" - "sync" - - "github.com/google/wire" - {{range .Coremodels }} - "{{ .PkgPath }}"{{end}} - "github.com/grafana/grafana/pkg/cuectx" - "github.com/grafana/grafana/pkg/framework/coremodel" - "github.com/grafana/thema" -) - -// Base is a registry of coremodel.Interface. It provides two modes for accessing -// coremodels: individually via literal named methods, or as a slice returned from All(). -// -// Prefer the individual named methods for use cases where the particular coremodel(s) that -// are needed are known to the caller. For example, a dashboard linter can know that it -// specifically wants the dashboard coremodel. -// -// Prefer All() when performing operations generically across all coremodels. For example, -// a validation HTTP middleware for any coremodel-schematized object type. -type Base struct { - all []coremodel.Interface - {{- range .Coremodels }} - {{ .Name }} *{{ .Name }}.Coremodel{{end}} -} - -// type guards -var ( -{{- range .Coremodels }} - _ coremodel.Interface = &{{ .Name }}.Coremodel{}{{end}} -) - -{{range .Coremodels }} -// {{ .TitleName }} returns the {{ .Name }} coremodel. The return value is guaranteed to -// implement coremodel.Interface. -func (s *Base) {{ .TitleName }}() *{{ .Name }}.Coremodel { - return s.{{ .Name }} -} -{{end}} - -func doProvideBase(lib thema.Library) *Base { - var err error - reg := &Base{} - -{{range .Coremodels }} - reg.{{ .Name }}, err = {{ .Name }}.New(lib) - if err != nil { - panic(fmt.Sprintf("error while initializing {{ .Name }} coremodel: %s", err)) - } - reg.all = append(reg.all, reg.{{ .Name }}) -{{end}} - - return reg -} -`)) diff --git a/pkg/codegen/pluggen.go b/pkg/codegen/pluggen.go index 7c4c6ebc3c8..91be7911a69 100644 --- a/pkg/codegen/pluggen.go +++ b/pkg/codegen/pluggen.go @@ -4,15 +4,20 @@ import ( "bytes" "fmt" "io/fs" + "path" "path/filepath" "sort" "strings" - "text/template" "cuelang.org/go/cue/ast" + "cuelang.org/go/pkg/encoding/yaml" + "github.com/deepmap/oapi-codegen/pkg/codegen" + "github.com/getkin/kin-openapi/openapi3" "github.com/grafana/cuetsy" + "github.com/grafana/grafana/pkg/framework/coremodel" "github.com/grafana/grafana/pkg/plugins/pfs" "github.com/grafana/thema" + "github.com/grafana/thema/encoding/openapi" ) // CUE import paths, mapped to corresponding TS import paths. An empty value @@ -92,13 +97,20 @@ type PluginTreeOrErr struct { } // PluginTree is a pfs.Tree. It exists so we can add methods for code generation to it. +// +// It is, for now, tailored specifically to Grafana core's codegen needs. type PluginTree pfs.Tree func (pt *PluginTree) GenerateTS(path string) (WriteDiffer, error) { t := (*pfs.Tree)(pt) // TODO replace with cuetsy's TS AST - f := &tsFile{} + f := &tvars_cuetsy_multi{ + Header: tvars_autogen_header{ + GeneratorPath: "public/app/plugins/gen.go", // FIXME hardcoding is not OK + LineagePath: "models.cue", + }, + } pi := t.RootPlugin() slotimps := pi.SlotImplementations() @@ -120,24 +132,18 @@ func (pt *PluginTree) GenerateTS(path string) (WriteDiffer, error) { ModelName: slotname, } - // TODO this is hardcoded for now, but should ultimately be a property of - // whether the slot is a grouped lineage: - // https://github.com/grafana/thema/issues/62 - switch slotname { - case "Panel", "DSConfig": + if isGroupLineage(slotname) { b, err := cuetsy.Generate(sch.UnwrapCUE(), cuetsy.Config{}) if err != nil { return nil, fmt.Errorf("%s: error translating %s lineage to TypeScript: %w", path, slotname, err) } sec.Body = string(b) - case "Query": + } else { a, err := cuetsy.GenerateSingleAST(strings.Title(lin.Name()), sch.UnwrapCUE(), cuetsy.TypeInterface) if err != nil { return nil, fmt.Errorf("%s: error translating %s lineage to TypeScript: %w", path, slotname, err) } sec.Body = fmt.Sprint(a) - default: - panic("unrecognized slot name: " + slotname) } f.Sections = append(f.Sections, sec) @@ -145,7 +151,7 @@ func (pt *PluginTree) GenerateTS(path string) (WriteDiffer, error) { wd := NewWriteDiffer() var buf bytes.Buffer - err := tsSectionTemplate.Execute(&buf, f) + err := tmpls.Lookup("cuetsy_multi.tmpl").Execute(&buf, f) if err != nil { return nil, fmt.Errorf("%s: error executing plugin TS generator template: %w", path, err) } @@ -153,6 +159,260 @@ func (pt *PluginTree) GenerateTS(path string) (WriteDiffer, error) { return wd, nil } +func isGroupLineage(slotname string) bool { + sl, has := coremodel.AllSlots()[slotname] + if !has { + panic("unknown slotname name: " + slotname) + } + return sl.IsGroup() +} + +type GoGenConfig struct { + // Types indicates whether corresponding Go types should be generated from the + // latest version in the lineage(s). + Types bool + + // ThemaBindings indicates whether Thema bindings (an implementation of + // ["github.com/grafana/thema".LineageFactory]) should be generated for + // lineage(s). + ThemaBindings bool + + // DocPathPrefix allows the caller to optionally specify a path to be prefixed + // onto paths generated for documentation. This is useful for io/fs-based code + // generators, which typically only have knowledge of paths relative to the fs.FS + // root, typically an encapsulated subpath, but docs are easier to understand when + // paths are relative to a repository root. + // + // Note that all paths are normalized to use slashes, regardless of the + // OS running the code generator. + DocPathPrefix string +} + +func (pt *PluginTree) GenerateGo(path string, cfg GoGenConfig) (WriteDiffer, error) { + t := (*pfs.Tree)(pt) + wd := NewWriteDiffer() + + all := t.SubPlugins() + if all == nil { + all = make(map[string]pfs.PluginInfo) + } + all[""] = t.RootPlugin() + for subpath, plug := range all { + fullp := filepath.Join(path, subpath) + if cfg.Types { + gwd, err := genGoTypes(plug, path, subpath, cfg.DocPathPrefix) + if err != nil { + return nil, fmt.Errorf("error generating go types for %s: %w", fullp, err) + } + if err = wd.Merge(gwd); err != nil { + return nil, fmt.Errorf("error merging file set to generate for %s: %w", fullp, err) + } + } + if cfg.ThemaBindings { + twd, err := genThemaBindings(plug, path, subpath, cfg.DocPathPrefix) + if err != nil { + return nil, fmt.Errorf("error generating thema bindings for %s: %w", fullp, err) + } + if err = wd.Merge(twd); err != nil { + return nil, fmt.Errorf("error merging file set to generate for %s: %w", fullp, err) + } + } + } + + return wd, nil +} + +func genGoTypes(plug pfs.PluginInfo, path, subpath, prefix string) (WriteDiffer, error) { + wd := NewWriteDiffer() + for slotname, lin := range plug.SlotImplementations() { + lowslot := strings.ToLower(slotname) + lib := lin.Library() + sch := thema.SchemaP(lin, thema.LatestVersion(lin)) + + // FIXME gotta hack this out of thema in order to deal with our custom imports :scream: + f, err := openapi.GenerateSchema(sch, nil) + if err != nil { + return nil, fmt.Errorf("thema openapi generation failed: %w", err) + } + + str, err := yaml.Marshal(lib.Context().BuildFile(f)) + if err != nil { + return nil, fmt.Errorf("cue-yaml marshaling failed: %w", err) + } + + loader := openapi3.NewLoader() + oT, err := loader.LoadFromData([]byte(str)) + if err != nil { + return nil, fmt.Errorf("loading generated openapi failed; %w", err) + } + + buf := new(bytes.Buffer) + if err = tmpls.Lookup("autogen_header.tmpl").Execute(buf, tvars_autogen_header{ + GeneratorPath: "public/app/plugins/gen.go", // FIXME hardcoding is not OK + LineagePath: filepath.ToSlash(filepath.Join(prefix, subpath, "models.cue")), + LineageCUEPath: slotname, + GenLicense: true, + }); err != nil { + return nil, fmt.Errorf("error generating file header: %w", err) + } + + cgopt := codegen.Options{ + GenerateTypes: true, + SkipPrune: true, + SkipFmt: true, + UserTemplates: map[string]string{ + "imports.tmpl": "package {{ .PackageName }}", + "typedef.tmpl": tmplTypedef, + }, + } + if isGroupLineage(slotname) { + cgopt.ExcludeSchemas = []string{lin.Name()} + } + + gostr, err := codegen.Generate(oT, lin.Name(), cgopt) + if err != nil { + return nil, fmt.Errorf("openapi generation failed: %w", err) + } + fmt.Fprint(buf, gostr) + + finalpath := filepath.Join(path, subpath, fmt.Sprintf("types_%s_gen.go", lowslot)) + byt, err := postprocessGoFile(genGoFile{ + path: finalpath, + walker: makePrefixDropper(strings.Title(lin.Name()), slotname), + in: buf.Bytes(), + }) + if err != nil { + return nil, err + } + + wd[finalpath] = byt + } + + return wd, nil +} + +func genThemaBindings(plug pfs.PluginInfo, path, subpath, prefix string) (WriteDiffer, error) { + wd := NewWriteDiffer() + bindings := make([]tvars_plugin_lineage_binding, 0) + for slotname, lin := range plug.SlotImplementations() { + lv := thema.LatestVersion(lin) + bindings = append(bindings, tvars_plugin_lineage_binding{ + SlotName: slotname, + LatestMajv: lv[0], + LatestMinv: lv[1], + }) + } + + buf := new(bytes.Buffer) + if err := tmpls.Lookup("plugin_lineage_file.tmpl").Execute(buf, tvars_plugin_lineage_file{ + PackageName: sanitizePluginId(plug.Meta().Id), + PluginType: string(plug.Meta().Type), + PluginID: plug.Meta().Id, + SlotImpls: bindings, + HasModels: len(bindings) != 0, + Header: tvars_autogen_header{ + GeneratorPath: "public/app/plugins/gen.go", // FIXME hardcoding is not OK + GenLicense: true, + LineagePath: filepath.Join(prefix, subpath), + }, + }); err != nil { + return nil, fmt.Errorf("error executing plugin lineage file template: %w", err) + } + + fullpath := filepath.Join(path, subpath, "pfs_gen.go") + if byt, err := postprocessGoFile(genGoFile{ + path: fullpath, + in: buf.Bytes(), + }); err != nil { + return nil, err + } else { + wd[fullpath] = byt + } + + return wd, nil +} + +// Plugin IDs are allowed to contain characters that aren't allowed in CUE +// package names, Go package names, TS or Go type names, etc. +// TODO expose this as standard +func sanitizePluginId(s string) string { + return strings.Map(func(r rune) rune { + switch { + case r >= 'a' && r <= 'z': + fallthrough + case r >= 'A' && r <= 'Z': + fallthrough + case r >= '0' && r <= '9': + fallthrough + case r == '_': + return r + case r == '-': + return '_' + default: + return -1 + } + }, s) +} + +// FIXME unexport this and refactor, this is way too one-off to be in here +func GenPluginTreeList(trees []TreeAndPath, prefix, target string, ref bool) (WriteDiffer, error) { + buf := new(bytes.Buffer) + vars := tvars_plugin_registry{ + Header: tvars_autogen_header{ + GenLicense: true, + }, + Plugins: make([]struct { + PkgName, Path, ImportPath string + NoAlias bool + }, 0, len(trees)), + } + + type tpl struct { + PkgName, Path, ImportPath string + NoAlias bool + } + + // No sub-plugin support here. If we never allow subplugins in core, that's probably fine. + // But still worth noting. + for _, pt := range trees { + rp := (*pfs.Tree)(pt.Tree).RootPlugin() + vars.Plugins = append(vars.Plugins, tpl{ + PkgName: sanitizePluginId(rp.Meta().Id), + NoAlias: sanitizePluginId(rp.Meta().Id) != filepath.Base(pt.Path), + ImportPath: filepath.ToSlash(filepath.Join(prefix, pt.Path)), + Path: path.Join(append(strings.Split(prefix, "/")[3:], pt.Path)...), + }) + } + + tmplname := "plugin_registry.tmpl" + if ref { + tmplname = "plugin_registry_ref.tmpl" + } + + if err := tmpls.Lookup(tmplname).Execute(buf, vars); err != nil { + return nil, fmt.Errorf("failed executing plugin registry template: %w", err) + } + + byt, err := postprocessGoFile(genGoFile{ + path: target, + in: buf.Bytes(), + }) + if err != nil { + return nil, fmt.Errorf("error postprocessing plugin registry: %w", err) + } + + wd := NewWriteDiffer() + wd[target] = byt + return wd, nil +} + +// FIXME unexport this and refactor, this is way too one-off to be in here +type TreeAndPath struct { + Tree *PluginTree + // path relative to path prefix UUUGHHH (basically {panel,datasource}/} + Path string +} + // TODO convert this to use cuetsy ts types, once import * form is supported func convertImport(im *ast.ImportSpec) *tsImport { var err error @@ -182,11 +442,6 @@ func convertImport(im *ast.ImportSpec) *tsImport { return tsim } -type tsFile struct { - Imports []*tsImport - Sections []tsSection -} - type tsSection struct { V thema.SyntacticVersion ModelName string @@ -197,15 +452,3 @@ type tsImport struct { Ident string Pkg string } - -var tsSectionTemplate = template.Must(template.New("cuetsymulti").Parse(`//~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ -// This file is autogenerated. DO NOT EDIT. -// -// To regenerate, run "make gen-cue" from the repository root. -//~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ -{{range .Imports}} -import * as {{.Ident}} from '{{.Pkg}}';{{end}} -{{range .Sections}}{{if ne .ModelName "" }} -export const {{.ModelName}}ModelVersion = Object.freeze([{{index .V 0}}, {{index .V 1}}]); -{{end}} -{{.Body}}{{end}}`)) diff --git a/pkg/codegen/tmpl.go b/pkg/codegen/tmpl.go new file mode 100644 index 00000000000..c2eac90d329 --- /dev/null +++ b/pkg/codegen/tmpl.go @@ -0,0 +1,65 @@ +package codegen + +import ( + "embed" + "text/template" + "time" +) + +// All the parsed templates in the tmpl subdirectory +var tmpls *template.Template + +func init() { + base := template.New("codegen").Funcs(template.FuncMap{ + "now": time.Now, + }) + tmpls = template.Must(base.ParseFS(tmplFS, "tmpl/*.tmpl")) +} + +//go:embed tmpl/*.tmpl +var tmplFS embed.FS + +// The following group of types, beginning with tvars_*, all contain the set +// of variables expected by the corresponding named template file under tmpl/ +type ( + tvars_autogen_header struct { + GeneratorPath string + LineagePath string + LineageCUEPath string + GenLicense bool + } + tvars_coremodel_registry struct { + Header tvars_autogen_header + Coremodels []tplVars + } + tvars_coremodel_imports struct { + PackageName string + } + tvars_plugin_lineage_binding struct { + SlotName string + LatestMajv, LatestMinv uint + } + tvars_plugin_lineage_file struct { + PackageName string + PluginID string + PluginType string + HasModels bool + RootCUE bool + SlotImpls []tvars_plugin_lineage_binding + Header tvars_autogen_header + } + tvars_cuetsy_multi struct { + Header tvars_autogen_header + Imports []*tsImport + Sections []tsSection + } + tvars_plugin_registry struct { + Header tvars_autogen_header + Plugins []struct { + PkgName string + Path string + ImportPath string + NoAlias bool + } + } +) diff --git a/pkg/codegen/tmpl/addenda.tmpl b/pkg/codegen/tmpl/addenda.tmpl new file mode 100644 index 00000000000..e8b99ae2f8b --- /dev/null +++ b/pkg/codegen/tmpl/addenda.tmpl @@ -0,0 +1,64 @@ + + +//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({{ .LatestSeqv }}, {{ .LatestSchv }}) + +// Lineage returns the Thema lineage representing a Grafana {{ .Name }}. +// +// The lineage is the canonical specification of the current {{ .Name }} schema, +// all prior schema versions, and the mappings that allow migration between +// schema versions. +{{- if .IsComposed }}// +// This is the base variant of the schema. It does not include any composed +// plugin schemas.{{ end }} +func Lineage(lib thema.Library, opts ...thema.BindOption) (thema.Lineage, error) { + return cuectx.LoadGrafanaInstancesWithThema(filepath.Join("pkg", "coremodel", "{{ .Name }}"), cueFS, lib, opts...) +} + +var _ thema.LineageFactory = Lineage +var _ coremodel.Interface = &Coremodel{} + +// Coremodel contains the foundational schema declaration for {{ .Name }}s. +// It implements coremodel.Interface. +type Coremodel struct { + lin thema.Lineage +} + +// Lineage returns the canonical {{ .Name }} Lineage. +func (c *Coremodel) Lineage() thema.Lineage { + return c.lin +} + +// CurrentSchema returns the current (latest) {{ .Name }} 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 {{ .Name }} 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(lib thema.Library) (*Coremodel, error) { + lin, err := Lineage(lib) + if err != nil { + return nil, err + } + + return &Coremodel{ + lin: lin, + }, nil +} diff --git a/pkg/codegen/tmpl/autogen_header.tmpl b/pkg/codegen/tmpl/autogen_header.tmpl new file mode 100644 index 00000000000..c6af378dc5b --- /dev/null +++ b/pkg/codegen/tmpl/autogen_header.tmpl @@ -0,0 +1,28 @@ +{{ if .GenLicense -}} +// Copyright {{ now.Year }} Grafana Labs +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +{{ end -}} +// This file is autogenerated. DO NOT EDIT. +{{- if ne .GeneratorPath "" }} +// +// Generated by {{ .GeneratorPath }} +{{- end }} +{{- if ne .LineagePath "" }} +// +// Derived from the Thema lineage declared in {{ .LineagePath }}{{ if ne .LineageCUEPath "" }} at CUE path "{{ .LineageCUEPath }}"{{ end }} +{{- end }} +// +// Run `make gen-cue` from repository root to regenerate. + diff --git a/pkg/codegen/tmpl/coremodel_imports.tmpl b/pkg/codegen/tmpl/coremodel_imports.tmpl new file mode 100644 index 00000000000..40e4aa45211 --- /dev/null +++ b/pkg/codegen/tmpl/coremodel_imports.tmpl @@ -0,0 +1,10 @@ +package {{ .PackageName }} + +import ( + "embed" + "path/filepath" + + "github.com/grafana/grafana/pkg/cuectx" + "github.com/grafana/grafana/pkg/framework/coremodel" + "github.com/grafana/thema" +) diff --git a/pkg/codegen/tmpl/coremodel_registry.tmpl b/pkg/codegen/tmpl/coremodel_registry.tmpl new file mode 100644 index 00000000000..07c4bc27197 --- /dev/null +++ b/pkg/codegen/tmpl/coremodel_registry.tmpl @@ -0,0 +1,58 @@ +{{ template "autogen_header.tmpl" .Header }} +package registry + +import ( + "fmt" + "sync" + + "github.com/google/wire" + {{range .Coremodels }} + "{{ .PkgPath }}"{{end}} + "github.com/grafana/grafana/pkg/cuectx" + "github.com/grafana/grafana/pkg/framework/coremodel" + "github.com/grafana/thema" +) + +// Base is a registry of coremodel.Interface. It provides two modes for accessing +// coremodels: individually via literal named methods, or as a slice returned from All(). +// +// Prefer the individual named methods for use cases where the particular coremodel(s) that +// are needed are known to the caller. For example, a dashboard linter can know that it +// specifically wants the dashboard coremodel. +// +// Prefer All() when performing operations generically across all coremodels. For example, +// a validation HTTP middleware for any coremodel-schematized object type. +type Base struct { + all []coremodel.Interface + {{- range .Coremodels }} + {{ .Name }} *{{ .Name }}.Coremodel{{end}} +} + +// type guards +var ( +{{- range .Coremodels }} + _ coremodel.Interface = &{{ .Name }}.Coremodel{}{{end}} +) + +{{range .Coremodels }} +// {{ .TitleName }} returns the {{ .Name }} coremodel. The return value is guaranteed to +// implement coremodel.Interface. +func (b *Base) {{ .TitleName }}() *{{ .Name }}.Coremodel { + return b.{{ .Name }} +} +{{end}} + +func doProvideBase(lib thema.Library) *Base { + var err error + reg := &Base{} + +{{range .Coremodels }} + reg.{{ .Name }}, err = {{ .Name }}.New(lib) + if err != nil { + panic(fmt.Sprintf("error while initializing {{ .Name }} coremodel: %s", err)) + } + reg.all = append(reg.all, reg.{{ .Name }}) +{{end}} + + return reg +} diff --git a/pkg/codegen/tmpl/cuetsy_multi.tmpl b/pkg/codegen/tmpl/cuetsy_multi.tmpl new file mode 100644 index 00000000000..2aebe8d5e9b --- /dev/null +++ b/pkg/codegen/tmpl/cuetsy_multi.tmpl @@ -0,0 +1,7 @@ +{{ template "autogen_header.tmpl" .Header -}} +{{range .Imports}} +import * as {{.Ident}} from '{{.Pkg}}';{{end}} +{{range .Sections}}{{if ne .ModelName "" }} +export const {{.ModelName}}ModelVersion = Object.freeze([{{index .V 0}}, {{index .V 1}}]); +{{end}} +{{.Body}}{{end}} diff --git a/pkg/codegen/tmpl/plugin_lineage_binding.tmpl b/pkg/codegen/tmpl/plugin_lineage_binding.tmpl new file mode 100644 index 00000000000..41f9b6bbfc8 --- /dev/null +++ b/pkg/codegen/tmpl/plugin_lineage_binding.tmpl @@ -0,0 +1,12 @@ +// 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{{ .SlotName }} = thema.SV({{ .LatestSeqv }}, {{ .LatestSchv }}) + +// {{ .SlotName }}Lineage returns the Thema lineage for the {{ .PluginID }} {{ .PluginType }} plugin's +// {{ .SlotName }} ["github.com/grafana/grafana/pkg/framework/coremodel".Slot] implementation. +func {{ .SlotName }}Lineage(lib thema.Library, opts ...thema.BindOption) (thema.Lineage, error) { + return cuectx.LoadGrafanaInstancesWithThema(filepath.Join("public", "app", "{{ .Name }}"), cueFS, lib, opts...) +} diff --git a/pkg/codegen/tmpl/plugin_lineage_file.tmpl b/pkg/codegen/tmpl/plugin_lineage_file.tmpl new file mode 100644 index 00000000000..5212a0b503f --- /dev/null +++ b/pkg/codegen/tmpl/plugin_lineage_file.tmpl @@ -0,0 +1,58 @@ +{{ template "autogen_header.tmpl" .Header -}} +package {{ .PackageName }} + +import ( + "embed" + "fmt" + "path/filepath" + "sync" + + "github.com/grafana/grafana/pkg/cuectx" + "github.com/grafana/grafana/pkg/plugins/pfs" + "github.com/grafana/thema" +) + +var parseOnce sync.Once +var ptree *pfs.Tree + +//go:embed plugin.{{ if .RootCUE }}cue{{ else }}json{{ end }}{{ if .HasModels }} models.cue{{ end }} +var plugFS embed.FS + +// PluginTree returns the plugin tree representing the statically analyzable contents of the {{ .PluginID }} plugin. +func PluginTree(lib *thema.Library) *pfs.Tree { + var err error + if lib == nil { + parseOnce.Do(func() { + ptree, err = pfs.ParsePluginFS(plugFS, cuectx.ProvideThemaLibrary()) + }) + } else { + ptree, err = pfs.ParsePluginFS(plugFS, cuectx.ProvideThemaLibrary()) + } + + if err != nil { + // Even the most rudimentary testing in CI ensures this is unreachable + panic(fmt.Errorf("error parsing plugin fs tree: %w", err)) + } + + return ptree +} + +{{ $pluginfo := . }}{{ range $slot := .SlotImpls }} +// {{ .SlotName }}Lineage returns the Thema lineage for the {{ $pluginfo.PluginID }} {{ $pluginfo.PluginType }} plugin's +// {{ .SlotName }} ["github.com/grafana/grafana/pkg/framework/coremodel".Slot] implementation. +func {{ .SlotName }}Lineage(lib *thema.Library, opts ...thema.BindOption) (thema.Lineage, error) { + t := PluginTree(lib) + lin, has := t.RootPlugin().SlotImplementations()["{{ .SlotName }}"] + if !has { + panic("unreachable: lineage for {{ .SlotName }} does not exist, but code is only generated for existing lineages") + } + return lin, nil +} + +// The current schema version of the {{ .SlotName }} slot implementation. +// +// Code generation ensures that this is always the version number for the latest schema +// in the {{ .SlotName }} Thema lineage. +var currentVersion{{ .SlotName }} = thema.SV({{ .LatestMajv }}, {{ .LatestMinv }}) + +{{ end }} diff --git a/pkg/codegen/tmpl/plugin_registry.tmpl b/pkg/codegen/tmpl/plugin_registry.tmpl new file mode 100644 index 00000000000..4983a8a285e --- /dev/null +++ b/pkg/codegen/tmpl/plugin_registry.tmpl @@ -0,0 +1,31 @@ +{{ template "autogen_header.tmpl" .Header -}} +package corelist + +import ( + "fmt" + "io/fs" + "sync" + "github.com/grafana/grafana" + "github.com/grafana/grafana/pkg/plugins/pfs" + "github.com/grafana/thema" +) + +func makeTreeOrPanic(path string, pkgname string, lib thema.Library) *pfs.Tree { + sub, err := fs.Sub(grafana.CueSchemaFS, path) + if err != nil { + panic("could not create fs sub to " + path) + } + tree, err := pfs.ParsePluginFS(sub, lib) + if err != nil { + panic(fmt.Sprintf("error parsing plugin metadata for %s: %s", pkgname, err)) + } + return tree +} + +func coreTreeList(lib thema.Library) pfs.TreeList{ + return pfs.TreeList{ + {{- range .Plugins }} + makeTreeOrPanic("{{ .Path }}", "{{ .PkgName }}", lib), + {{- end }} + } +} diff --git a/pkg/codegen/tmpl/plugin_registry_ref.tmpl b/pkg/codegen/tmpl/plugin_registry_ref.tmpl new file mode 100644 index 00000000000..b4a9be5ffc7 --- /dev/null +++ b/pkg/codegen/tmpl/plugin_registry_ref.tmpl @@ -0,0 +1,16 @@ +{{ template "autogen_header.tmpl" .Header -}} +package registry + +import ( + "github.com/grafana/grafana/pkg/plugins/pfs" + "github.com/grafana/thema" + {{ range .Plugins }} + {{ if .NoAlias }}{{ .PkgName }} {{end}}"{{ .Path }}"{{ end }} +) + +func coreTreeLoaders() []func(*thema.Library) *pfs.Tree{ + return []func(*thema.Library) *pfs.Tree{ + {{- range .Plugins }} + {{ .PkgName }}.PluginTree,{{ end }} + } +} diff --git a/pkg/codegen/util_go.go b/pkg/codegen/util_go.go new file mode 100644 index 00000000000..525b764298c --- /dev/null +++ b/pkg/codegen/util_go.go @@ -0,0 +1,67 @@ +package codegen + +import ( + "bytes" + "fmt" + "go/ast" + "go/format" + "go/parser" + "go/token" + "os" + "path/filepath" + "strings" + + "golang.org/x/tools/imports" +) + +type genGoFile struct { + path string + walker ast.Visitor + in []byte +} + +func postprocessGoFile(cfg genGoFile) ([]byte, error) { + fname := filepath.Base(cfg.path) + buf := new(bytes.Buffer) + fset := token.NewFileSet() + gf, err := parser.ParseFile(fset, fname, string(cfg.in), parser.ParseComments) + if err != nil { + return nil, fmt.Errorf("error parsing generated file: %w", err) + } + + if cfg.walker != nil { + ast.Walk(cfg.walker, gf) + + err = format.Node(buf, fset, gf) + if err != nil { + return nil, fmt.Errorf("error formatting Go AST: %w", err) + } + } else { + buf = bytes.NewBuffer(cfg.in) + } + + byt, err := imports.Process(fname, buf.Bytes(), nil) + if err != nil { + return nil, fmt.Errorf("goimports processing failed: %w", err) + } + + // Compare imports before and after; warn about performance if some were added + gfa, _ := parser.ParseFile(fset, fname, string(byt), parser.ParseComments) + imap := make(map[string]bool) + for _, im := range gf.Imports { + imap[im.Path.Value] = true + } + var added []string + for _, im := range gfa.Imports { + if !imap[im.Path.Value] { + added = append(added, im.Path.Value) + } + } + + if len(added) != 0 { + // TODO improve the guidance in this error if/when we better abstract over imports to generate + fmt.Fprintf(os.Stderr, "The following imports were added by goimports while generating %s: \n\t%s\nRelying on goimports to find imports significantly slows down code generation. Consider adding these to the relevant template.\n", cfg.path, strings.Join(added, "\n\t")) + } + + return byt, nil +} diff --git a/pkg/coremodel/dashboard/dashboard_gen.go b/pkg/coremodel/dashboard/dashboard_gen.go index 6c26cbad5ff..fb377496dd7 100644 --- a/pkg/coremodel/dashboard/dashboard_gen.go +++ b/pkg/coremodel/dashboard/dashboard_gen.go @@ -1,8 +1,10 @@ // This file is autogenerated. DO NOT EDIT. // -// Run "make gen-cue" from repository root to regenerate. +// Generated by pkg/framework/coremodel/gen.go // -// Derived from the Thema lineage at pkg/coremodel/dashboard +// Derived from the Thema lineage declared in pkg/coremodel/dashboard/coremodel.cue +// +// Run `make gen-cue` from repository root to regenerate. package dashboard @@ -15,7 +17,7 @@ import ( "github.com/grafana/thema" ) -// Defines values for DashboardGraphTooltip. +// Defines values for GraphTooltip. const ( GraphTooltipN0 GraphTooltip = 0 @@ -24,14 +26,14 @@ const ( GraphTooltipN2 GraphTooltip = 2 ) -// Defines values for DashboardStyle. +// Defines values for Style. const ( StyleDark Style = "dark" StyleLight Style = "light" ) -// Defines values for DashboardTimezone. +// Defines values for Timezone. const ( TimezoneBrowser Timezone = "browser" @@ -40,7 +42,7 @@ const ( TimezoneUtc Timezone = "utc" ) -// Defines values for DashboardDashboardCursorSync. +// Defines values for DashboardCursorSync. const ( DashboardCursorSyncN0 DashboardCursorSync = 0 @@ -49,14 +51,14 @@ const ( DashboardCursorSyncN2 DashboardCursorSync = 2 ) -// Defines values for DashboardDashboardLinkType. +// Defines values for DashboardLinkType. const ( DashboardLinkTypeDashboards DashboardLinkType = "dashboards" DashboardLinkTypeLink DashboardLinkType = "link" ) -// Defines values for DashboardFieldColorModeId. +// Defines values for FieldColorModeId. const ( FieldColorModeIdContinuousGrYlRd FieldColorModeId = "continuous-GrYlRd" @@ -69,7 +71,7 @@ const ( FieldColorModeIdThresholds FieldColorModeId = "thresholds" ) -// Defines values for DashboardFieldColorSeriesByMode. +// Defines values for FieldColorSeriesByMode. const ( FieldColorSeriesByModeLast FieldColorSeriesByMode = "last" @@ -78,43 +80,43 @@ const ( FieldColorSeriesByModeMin FieldColorSeriesByMode = "min" ) -// Defines values for DashboardGraphPanelType. +// Defines values for GraphPanelType. const ( GraphPanelTypeGraph GraphPanelType = "graph" ) -// Defines values for DashboardHeatmapPanelType. +// Defines values for HeatmapPanelType. const ( HeatmapPanelTypeHeatmap HeatmapPanelType = "heatmap" ) -// Defines values for DashboardPanelRepeatDirection. +// Defines values for PanelRepeatDirection. const ( PanelRepeatDirectionH PanelRepeatDirection = "h" PanelRepeatDirectionV PanelRepeatDirection = "v" ) -// Defines values for DashboardRowPanelType. +// Defines values for RowPanelType. const ( RowPanelTypeRow RowPanelType = "row" ) -// Defines values for DashboardThresholdsConfigMode. +// Defines values for ThresholdsConfigMode. const ( ThresholdsConfigModeAbsolute ThresholdsConfigMode = "absolute" ThresholdsConfigModePercentage ThresholdsConfigMode = "percentage" ) -// Defines values for DashboardThresholdsMode. +// Defines values for ThresholdsMode. const ( ThresholdsModeAbsolute ThresholdsMode = "absolute" ThresholdsModePercentage ThresholdsMode = "percentage" ) -// Defines values for DashboardVariableModelType. +// Defines values for VariableModelType. const ( VariableModelTypeAdhoc VariableModelType = "adhoc" @@ -133,7 +135,7 @@ const ( VariableModelTypeTextbox VariableModelType = "textbox" ) -// Defines values for DashboardVariableType. +// Defines values for VariableType. const ( VariableTypeAdhoc VariableType = "adhoc" @@ -152,7 +154,7 @@ const ( VariableTypeTextbox VariableType = "textbox" ) -// Dashboard defines model for dashboard. +// Model is the Go representation of a dashboard. // // 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. @@ -243,7 +245,7 @@ type Model struct { WeekStart *string `json:"weekStart,omitempty"` } -// DashboardGraphTooltip defines model for Dashboard.GraphTooltip. +// GraphTooltip is the Go representation of a Model.GraphTooltip. // // 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. @@ -294,7 +296,7 @@ type AnnotationQuery struct { // Schema for panel targets is specified by datasource // plugins. We use a placeholder definition, which the Go // schema loader either left open/as-is with the Base - // variant of the Dashboard and Panel families, or filled + // variant of the Model and Panel families, or filled // with types derived from plugins in the Instance variant. // When working directly from CUE, importers can extend this // type directly to achieve the same effect. @@ -310,7 +312,7 @@ type AnnotationQuery struct { // Equivalent Go types at stable import paths are provided in https://github.com/grafana/grok. type DashboardCursorSync int -// FROM public/app/features/dashboard/state/DashboardModels.ts - ish +// FROM public/app/features/dashboard/state/Models.ts - ish // TODO docs // // THIS TYPE IS INTENDED FOR INTERNAL USE BY THE GRAFANA BACKEND, AND IS SUBJECT TO BREAKING CHANGES. @@ -328,7 +330,7 @@ type DashboardLink struct { Url *string `json:"url,omitempty"` } -// DashboardDashboardLinkType defines model for DashboardDashboardLink.Type. +// DashboardLinkType is the Go representation of a DashboardLink.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. @@ -361,7 +363,7 @@ type FieldColorModeId string // Equivalent Go types at stable import paths are provided in https://github.com/grafana/grok. type FieldColorSeriesByMode string -// DashboardGraphPanel defines model for dashboard.GraphPanel. +// GraphPanel is the Go representation of a dashboard.GraphPanel. // // 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. @@ -376,7 +378,7 @@ type GraphPanel struct { // Equivalent Go types at stable import paths are provided in https://github.com/grafana/grok. type GraphPanelType string -// DashboardGridPos defines model for dashboard.GridPos. +// GridPos is the Go representation of a dashboard.GridPos. // // 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. @@ -397,7 +399,7 @@ type GridPos struct { Y int `json:"y"` } -// DashboardHeatmapPanel defines model for dashboard.HeatmapPanel. +// HeatmapPanel is the Go representation of a dashboard.HeatmapPanel. // // 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. @@ -405,13 +407,13 @@ type HeatmapPanel struct { Type HeatmapPanelType `json:"type"` } -// DashboardHeatmapPanelType defines model for DashboardHeatmapPanel.Type. +// HeatmapPanelType is the Go representation of a HeatmapPanel.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. type HeatmapPanelType string -// Dashboard panels. Panels are canonically defined inline +// Model panels. Panels are canonically defined inline // because they share a version timeline with the dashboard // schema; they do not evolve independently. // @@ -588,7 +590,7 @@ type RowPanel struct { Type RowPanelType `json:"type"` } -// DashboardRowPanelType defines model for DashboardRowPanel.Type. +// RowPanelType is the Go representation of a RowPanel.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. @@ -597,7 +599,7 @@ type RowPanelType string // Schema for panel targets is specified by datasource // plugins. We use a placeholder definition, which the Go // schema loader either left open/as-is with the Base -// variant of the Dashboard and Panel families, or filled +// variant of the Model and Panel families, or filled // with types derived from plugins in the Instance variant. // When working directly from CUE, importers can extend this // type directly to achieve the same effect. @@ -624,7 +626,7 @@ type Threshold struct { Value *float32 `json:"value,omitempty"` } -// DashboardThresholdsConfig defines model for dashboard.ThresholdsConfig. +// ThresholdsConfig is the Go representation of a dashboard.ThresholdsConfig. // // 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. @@ -647,13 +649,13 @@ type ThresholdsConfig struct { } `json:"steps"` } -// DashboardThresholdsConfigMode defines model for DashboardThresholdsConfig.Mode. +// ThresholdsConfigMode is the Go representation of a ThresholdsConfig.Mode. // // 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 ThresholdsConfigMode string -// DashboardThresholdsMode defines model for dashboard.ThresholdsMode. +// ThresholdsMode is the Go representation of a dashboard.ThresholdsMode. // // 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. @@ -682,7 +684,7 @@ type VariableModel struct { Type VariableModelType `json:"type"` } -// DashboardVariableModelType defines model for DashboardVariableModel.Type. +// VariableModelType is the Go representation of a VariableModel.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. @@ -699,7 +701,11 @@ type VariableType string //go:embed coremodel.cue var cueFS embed.FS -// codegen ensures that this is always the latest Thema schema version +// 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 dashboard. diff --git a/pkg/coremodel/pluginmeta/pluginmeta_gen.go b/pkg/coremodel/pluginmeta/pluginmeta_gen.go index 5ab11498282..5fe83adee33 100644 --- a/pkg/coremodel/pluginmeta/pluginmeta_gen.go +++ b/pkg/coremodel/pluginmeta/pluginmeta_gen.go @@ -1,8 +1,10 @@ // This file is autogenerated. DO NOT EDIT. // -// Run "make gen-cue" from repository root to regenerate. +// Generated by pkg/framework/coremodel/gen.go // -// Derived from the Thema lineage at pkg/coremodel/pluginmeta +// Derived from the Thema lineage declared in pkg/coremodel/pluginmeta/coremodel.cue +// +// Run `make gen-cue` from repository root to regenerate. package pluginmeta @@ -15,7 +17,7 @@ import ( "github.com/grafana/thema" ) -// Defines values for PluginmetaCategory. +// Defines values for Category. const ( CategoryCloud Category = "cloud" @@ -32,7 +34,7 @@ const ( CategoryTsdb Category = "tsdb" ) -// Defines values for PluginmetaType. +// Defines values for Type. const ( TypeApp Type = "app" @@ -45,7 +47,7 @@ const ( TypeSecretsmanager Type = "secretsmanager" ) -// Defines values for PluginmetaDependencyType. +// Defines values for DependencyType. const ( DependencyTypeApp DependencyType = "app" @@ -54,7 +56,7 @@ const ( DependencyTypePanel DependencyType = "panel" ) -// Defines values for PluginmetaIncludeRole. +// Defines values for IncludeRole. const ( IncludeRoleAdmin IncludeRole = "Admin" @@ -63,7 +65,7 @@ const ( IncludeRoleViewer IncludeRole = "Viewer" ) -// Defines values for PluginmetaIncludeType. +// Defines values for IncludeType. const ( IncludeTypeApp IncludeType = "app" @@ -80,7 +82,7 @@ const ( IncludeTypeSecretsmanager IncludeType = "secretsmanager" ) -// Defines values for PluginmetaReleaseState. +// Defines values for ReleaseState. const ( ReleaseStateAlpha ReleaseState = "alpha" @@ -91,7 +93,7 @@ const ( ReleaseStateStable ReleaseState = "stable" ) -// Pluginmeta defines model for pluginmeta. +// 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. @@ -287,7 +289,7 @@ type Category string // Equivalent Go types at stable import paths are provided in https://github.com/grafana/grok. type Type string -// PluginmetaBuildInfo defines model for pluginmeta.BuildInfo. +// 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. @@ -307,7 +309,7 @@ type BuildInfo struct { Time *int64 `json:"time,omitempty"` } -// PluginmetaDependencies defines model for pluginmeta.Dependencies. +// 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. @@ -338,7 +340,7 @@ type Dependency struct { Version string `json:"version"` } -// PluginmetaDependencyType defines model for PluginmetaDependency.Type. +// 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. @@ -383,13 +385,13 @@ type Include struct { Uid *string `json:"uid,omitempty"` } -// PluginmetaIncludeRole defines model for PluginmetaInclude.Role. +// 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. type IncludeRole string -// PluginmetaIncludeType defines model for PluginmetaInclude.Type. +// 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. @@ -546,7 +548,11 @@ type URLParam struct { //go:embed coremodel.cue var cueFS embed.FS -// codegen ensures that this is always the latest Thema schema version +// 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. diff --git a/pkg/framework/coremodel/registry/provide.go b/pkg/framework/coremodel/registry/provide.go index a4b0db4b63f..7e56ca7482b 100644 --- a/pkg/framework/coremodel/registry/provide.go +++ b/pkg/framework/coremodel/registry/provide.go @@ -54,6 +54,6 @@ func provideBase(lib *thema.Library) *Base { // // The returned slice is sorted lexicographically by coremodel name. It should // not be modified. -func (s *Base) All() []coremodel.Interface { - return s.all +func (b *Base) All() []coremodel.Interface { + return b.all } diff --git a/pkg/framework/coremodel/registry/registry_gen.go b/pkg/framework/coremodel/registry/registry_gen.go index d434dfd636b..c198ef8647f 100644 --- a/pkg/framework/coremodel/registry/registry_gen.go +++ b/pkg/framework/coremodel/registry/registry_gen.go @@ -1,7 +1,8 @@ // This file is autogenerated. DO NOT EDIT. // // Generated by pkg/framework/coremodel/gen.go -// Run "make gen-cue" from repository root to regenerate. +// +// Run `make gen-cue` from repository root to regenerate. package registry @@ -37,14 +38,14 @@ var ( // Dashboard returns the dashboard coremodel. The return value is guaranteed to // implement coremodel.Interface. -func (s *Base) Dashboard() *dashboard.Coremodel { - return s.dashboard +func (b *Base) Dashboard() *dashboard.Coremodel { + return b.dashboard } // Pluginmeta returns the pluginmeta coremodel. The return value is guaranteed to // implement coremodel.Interface. -func (s *Base) Pluginmeta() *pluginmeta.Coremodel { - return s.pluginmeta +func (b *Base) Pluginmeta() *pluginmeta.Coremodel { + return b.pluginmeta } func doProvideBase(lib thema.Library) *Base { diff --git a/pkg/framework/coremodel/slot.go b/pkg/framework/coremodel/slot.go index e8eb0d1a734..69c212b3c06 100644 --- a/pkg/framework/coremodel/slot.go +++ b/pkg/framework/coremodel/slot.go @@ -42,6 +42,22 @@ func (s Slot) ForPluginType(plugintype string) (may, must bool) { return } +// IsGroup indicates whether the slot specifies a group lineage - one in which +// each top-level key represents a distinct schema for objects that are expected +// to exist in the wild, but objects corresponding to the root of the schema are not +// expected to exist. +func (s Slot) IsGroup() bool { + // TODO rely on first-class Thema properties for this, one they exist - https://github.com/grafana/thema/issues/62 + switch s.name { + case "Panel", "DSOptions": + return true + case "Query": + return false + default: + panic("unreachable - unknown slot name " + s.name) + } +} + func AllSlots() map[string]*Slot { fw := CUEFramework() slots := make(map[string]*Slot) diff --git a/pkg/plugins/pfs/corelist/loadlist_gen.go b/pkg/plugins/pfs/corelist/loadlist_gen.go new file mode 100644 index 00000000000..523c3262f28 --- /dev/null +++ b/pkg/plugins/pfs/corelist/loadlist_gen.go @@ -0,0 +1,86 @@ +// Copyright 2022 Grafana Labs +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +// This file is autogenerated. DO NOT EDIT. +// +// Run `make gen-cue` from repository root to regenerate. + +package corelist + +import ( + "fmt" + "io/fs" + + "github.com/grafana/grafana" + "github.com/grafana/grafana/pkg/plugins/pfs" + "github.com/grafana/thema" +) + +func makeTreeOrPanic(path string, pkgname string, lib thema.Library) *pfs.Tree { + sub, err := fs.Sub(grafana.CueSchemaFS, path) + if err != nil { + panic("could not create fs sub to " + path) + } + tree, err := pfs.ParsePluginFS(sub, lib) + if err != nil { + panic(fmt.Sprintf("error parsing plugin metadata for %s: %s", pkgname, err)) + } + return tree +} + +func coreTreeList(lib thema.Library) pfs.TreeList { + return pfs.TreeList{ + makeTreeOrPanic("public/app/plugins/datasource/alertmanager", "alertmanager", lib), + makeTreeOrPanic("public/app/plugins/datasource/cloud-monitoring", "stackdriver", lib), + makeTreeOrPanic("public/app/plugins/datasource/cloudwatch", "cloudwatch", lib), + makeTreeOrPanic("public/app/plugins/datasource/dashboard", "dashboard", lib), + makeTreeOrPanic("public/app/plugins/datasource/elasticsearch", "elasticsearch", lib), + makeTreeOrPanic("public/app/plugins/datasource/grafana", "grafana", lib), + makeTreeOrPanic("public/app/plugins/datasource/grafana-azure-monitor-datasource", "grafana_azure_monitor_datasource", lib), + makeTreeOrPanic("public/app/plugins/datasource/graphite", "graphite", lib), + makeTreeOrPanic("public/app/plugins/datasource/jaeger", "jaeger", lib), + makeTreeOrPanic("public/app/plugins/datasource/loki", "loki", lib), + makeTreeOrPanic("public/app/plugins/datasource/mssql", "mssql", lib), + makeTreeOrPanic("public/app/plugins/datasource/mysql", "mysql", lib), + makeTreeOrPanic("public/app/plugins/datasource/postgres", "postgres", lib), + makeTreeOrPanic("public/app/plugins/datasource/prometheus", "prometheus", lib), + makeTreeOrPanic("public/app/plugins/datasource/tempo", "tempo", lib), + makeTreeOrPanic("public/app/plugins/datasource/testdata", "testdata", lib), + makeTreeOrPanic("public/app/plugins/datasource/zipkin", "zipkin", lib), + makeTreeOrPanic("public/app/plugins/panel/alertGroups", "alertGroups", lib), + makeTreeOrPanic("public/app/plugins/panel/alertlist", "alertlist", lib), + makeTreeOrPanic("public/app/plugins/panel/annolist", "annolist", lib), + makeTreeOrPanic("public/app/plugins/panel/barchart", "barchart", lib), + makeTreeOrPanic("public/app/plugins/panel/bargauge", "bargauge", lib), + makeTreeOrPanic("public/app/plugins/panel/dashlist", "dashlist", lib), + makeTreeOrPanic("public/app/plugins/panel/debug", "debug", lib), + makeTreeOrPanic("public/app/plugins/panel/gauge", "gauge", lib), + makeTreeOrPanic("public/app/plugins/panel/geomap", "geomap", lib), + makeTreeOrPanic("public/app/plugins/panel/gettingstarted", "gettingstarted", lib), + makeTreeOrPanic("public/app/plugins/panel/graph", "graph", lib), + makeTreeOrPanic("public/app/plugins/panel/histogram", "histogram", lib), + makeTreeOrPanic("public/app/plugins/panel/icon", "icon", lib), + makeTreeOrPanic("public/app/plugins/panel/live", "live", lib), + makeTreeOrPanic("public/app/plugins/panel/logs", "logs", lib), + makeTreeOrPanic("public/app/plugins/panel/news", "news", lib), + makeTreeOrPanic("public/app/plugins/panel/nodeGraph", "nodeGraph", lib), + makeTreeOrPanic("public/app/plugins/panel/piechart", "piechart", lib), + makeTreeOrPanic("public/app/plugins/panel/stat", "stat", lib), + makeTreeOrPanic("public/app/plugins/panel/table-old", "table_old", lib), + makeTreeOrPanic("public/app/plugins/panel/text", "text", lib), + makeTreeOrPanic("public/app/plugins/panel/traces", "traces", lib), + makeTreeOrPanic("public/app/plugins/panel/welcome", "welcome", lib), + makeTreeOrPanic("public/app/plugins/panel/xychart", "xychart", lib), + } +} diff --git a/pkg/plugins/pfs/corelist/new.go b/pkg/plugins/pfs/corelist/new.go new file mode 100644 index 00000000000..1ac92961078 --- /dev/null +++ b/pkg/plugins/pfs/corelist/new.go @@ -0,0 +1,30 @@ +package corelist + +import ( + "sync" + + "github.com/grafana/grafana/pkg/cuectx" + "github.com/grafana/grafana/pkg/plugins/pfs" + "github.com/grafana/thema" +) + +var coreTrees pfs.TreeList +var coreOnce sync.Once + +// New returns a pfs.TreeList containing the plugin trees for all core plugins +// in the current version of Grafana. +// +// Go code within the grafana codebase should only ever call this with nil. +func New(lib *thema.Library) pfs.TreeList { + var tl pfs.TreeList + if lib == nil { + coreOnce.Do(func() { + coreTrees = coreTreeList(cuectx.ProvideThemaLibrary()) + }) + tl = make(pfs.TreeList, len(coreTrees)) + copy(tl, coreTrees) + } else { + return coreTreeList(*lib) + } + return tl +} diff --git a/pkg/plugins/pfs/errors.go b/pkg/plugins/pfs/errors.go index d347480dc45..4885d13f1e3 100644 --- a/pkg/plugins/pfs/errors.go +++ b/pkg/plugins/pfs/errors.go @@ -17,6 +17,9 @@ var ErrInvalidRootFile = errors.New("plugin.json is invalid") // - A required slot for its type is not implemented (e.g. panel plugin does not implemented Panel) var ErrImplementedSlots = errors.New("slot implementation not allowed for this plugin type") +// ErrInvalidCUE indicates that a plugin's model.cue file contained invalid CUE. +var ErrInvalidCUE = errors.New("CUE syntax error") + // ErrInvalidLineage indicates that the plugin contains an invalid lineage // declaration, according to Thema's validation rules in // ["github.com/grafana/thema".BindLineage]. diff --git a/pkg/plugins/pfs/pfs.go b/pkg/plugins/pfs/pfs.go index ae84518088a..dd1fe0543b7 100644 --- a/pkg/plugins/pfs/pfs.go +++ b/pkg/plugins/pfs/pfs.go @@ -133,7 +133,28 @@ func (t *Tree) RootPlugin() PluginInfo { // SubPlugins returned a map of the PluginInfos for subplugins // within the tree, if any, keyed by subpath. func (t *Tree) SubPlugins() map[string]PluginInfo { - panic("TODO") + // TODO implement these once ParsePluginFS descends + return nil +} + +// TreeList is a slice of validated plugin fs Trees with helper methods +// for filtering to particular subsets of its members. +type TreeList []*Tree + +// LineagesForSlot returns the set of plugin-defined lineages that implement a +// particular named Grafana slot (See ["github.com/grafana/grafana/pkg/framework/coremodel".Slot]). +func (tl TreeList) LineagesForSlot(slotname string) map[string]thema.Lineage { + m := make(map[string]thema.Lineage) + for _, tree := range tl { + rootp := tree.RootPlugin() + rid := rootp.Meta().Id + + if lin, has := rootp.SlotImplementations()[slotname]; has { + m[rid] = lin + } + } + + return m } // PluginInfo represents everything knowable about a single plugin from static @@ -166,8 +187,8 @@ func (pi PluginInfo) Meta() pluginmeta.Model { // ParsePluginFS takes an fs.FS and checks that it represents exactly one valid // plugin fs tree, with the fs.FS root as the root of the tree. // -// It does not descend into subdirectories to search for additional -// plugin.json files. +// It does not descend into subdirectories to search for additional plugin.json +// files. // TODO no descent is ok for core plugins, but won't cut it in general func ParsePluginFS(f fs.FS, lib thema.Library) (*Tree, error) { if f == nil { @@ -234,6 +255,9 @@ func ParsePluginFS(f fs.FS, lib thema.Library) (*Tree, error) { } val := ctx.BuildInstance(bi) + if val.Err() != nil { + return nil, ewrap(fmt.Errorf("models.cue is invalid CUE: %w", val.Err()), ErrInvalidCUE) + } for _, s := range allslots { iv := val.LookupPath(cue.ParsePath(s.slot.Name())) lin, err := bindSlotLineage(iv, s.slot, r.meta, lib) @@ -249,7 +273,7 @@ func ParsePluginFS(f fs.FS, lib thema.Library) (*Tree, error) { return tree, nil } -func bindSlotLineage(v cue.Value, s *coremodel.Slot, meta pluginmeta.Model, lib thema.Library) (thema.Lineage, error) { +func bindSlotLineage(v cue.Value, s *coremodel.Slot, meta pluginmeta.Model, lib thema.Library, opts ...thema.BindOption) (thema.Lineage, error) { accept, required := s.ForPluginType(string(meta.Type)) exists := v.Exists() @@ -269,7 +293,7 @@ func bindSlotLineage(v cue.Value, s *coremodel.Slot, meta pluginmeta.Model, lib // TODO make this opt real in thema, then uncomment to enforce joinSchema // lin, err := thema.BindLineage(iv, lib, thema.SatisfiesJoinSchema(s.MetaSchema())) - lin, err := thema.BindLineage(v, lib) + lin, err := thema.BindLineage(v, lib, opts...) if err != nil { return nil, ewrap(fmt.Errorf("%s: invalid thema lineage for slot %s: %w", meta.Id, s.Name(), err), ErrInvalidLineage) } diff --git a/public/app/plugins/gen.go b/public/app/plugins/gen.go index 23e9c811337..7a57abe2fda 100644 --- a/public/app/plugins/gen.go +++ b/public/app/plugins/gen.go @@ -9,6 +9,7 @@ import ( "os" "path/filepath" "sort" + "strings" "github.com/grafana/grafana/pkg/codegen" "github.com/grafana/grafana/pkg/cuectx" @@ -29,6 +30,8 @@ var skipPlugins = map[string]bool{ "opentsdb": true, // plugin.json fails validation (defaultMatchFormat) } +const sep = string(filepath.Separator) + // Generate TypeScript for all plugin models.cue func main() { if len(os.Args) > 1 { @@ -41,15 +44,17 @@ func main() { fmt.Fprintf(os.Stderr, "could not get working directory: %s", err) os.Exit(1) } + grootp := strings.Split(cwd, sep) + groot := filepath.Join(sep, filepath.Join(grootp[:len(grootp)-3]...)) wd := codegen.NewWriteDiffer() lib := cuectx.ProvideThemaLibrary() type ptreepath struct { - fullpath string - tree *codegen.PluginTree + Path string + Tree *codegen.PluginTree } - var ptrees []ptreepath + var ptrees []codegen.TreeAndPath for _, typ := range []string{"datasource", "panel"} { dir := filepath.Join(cwd, typ) treeor, err := codegen.ExtractPluginTrees(os.DirFS(dir), lib) @@ -64,9 +69,9 @@ func main() { } if option.Tree != nil { - ptrees = append(ptrees, ptreepath{ - fullpath: filepath.Join(typ, name), - tree: option.Tree, + ptrees = append(ptrees, codegen.TreeAndPath{ + Path: filepath.Join(typ, name), + Tree: option.Tree, }) } else if !errors.Is(option.Err, pfs.ErrNoRootFile) { fmt.Fprintf(os.Stderr, "error parsing plugin directory %s: %s\n", filepath.Join(dir, name), option.Err) @@ -79,18 +84,39 @@ func main() { // having multiple core plugins with errors can cause confusing error // flip-flopping sort.Slice(ptrees, func(i, j int) bool { - return ptrees[i].fullpath < ptrees[j].fullpath + return ptrees[i].Path < ptrees[j].Path }) + var wdm codegen.WriteDiffer for _, ptp := range ptrees { - twd, err := ptp.tree.GenerateTS(ptp.fullpath) + wdm, err = ptp.Tree.GenerateTS(ptp.Path) + if err != nil { + fmt.Fprintf(os.Stderr, "generating typescript failed for %s: %s\n", ptp.Path, err) + os.Exit(1) + } + wd.Merge(wdm) + + relp, _ := filepath.Rel(groot, ptp.Path) + wdm, err = ptp.Tree.GenerateGo(ptp.Path, codegen.GoGenConfig{ + Types: isDatasource(ptp.Tree), + // TODO false until we decide on a consistent codegen format for core and external plugins + ThemaBindings: false, + DocPathPrefix: relp, + }) if err != nil { - fmt.Fprintf(os.Stderr, "generating typescript failed for %s: %s\n", ptp.fullpath, err) + fmt.Fprintf(os.Stderr, "generating Go failed for %s: %s\n", ptp.Path, err) os.Exit(1) } - wd.Merge(twd) + wd.Merge(wdm) } + wdm, err = codegen.GenPluginTreeList(ptrees, "github.com/grafana/grafana/public/app/plugins", filepath.Join(groot, "pkg", "plugins", "pfs", "corelist", "loadlist_gen.go"), false) + if err != nil { + fmt.Fprintf(os.Stderr, "generating plugin loader registry failed: %s\n", err) + os.Exit(1) + } + wd.Merge(wdm) + if _, set := os.LookupEnv("CODEGEN_VERIFY"); set { err = wd.Verify() if err != nil { @@ -105,3 +131,7 @@ func main() { } } } + +func isDatasource(pt *codegen.PluginTree) bool { + return string((*pfs.Tree)(pt).RootPlugin().Meta().Type) == "datasource" +} diff --git a/public/app/plugins/panel/annolist/models.gen.ts b/public/app/plugins/panel/annolist/models.gen.ts index 833aff1f0bf..a5a5a4fcd69 100644 --- a/public/app/plugins/panel/annolist/models.gen.ts +++ b/public/app/plugins/panel/annolist/models.gen.ts @@ -1,8 +1,11 @@ -//~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ // This file is autogenerated. DO NOT EDIT. // -// To regenerate, run "make gen-cue" from the repository root. -//~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +// Generated by public/app/plugins/gen.go +// +// Derived from the Thema lineage declared in models.cue +// +// Run `make gen-cue` from repository root to regenerate. + export const PanelModelVersion = Object.freeze([0, 0]); @@ -33,3 +36,4 @@ export const defaultPanelOptions: Partial = { showUser: true, tags: [], }; + diff --git a/public/app/plugins/panel/barchart/models.gen.ts b/public/app/plugins/panel/barchart/models.gen.ts index 1a994174207..46522acf1e9 100644 --- a/public/app/plugins/panel/barchart/models.gen.ts +++ b/public/app/plugins/panel/barchart/models.gen.ts @@ -1,8 +1,11 @@ -//~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ // This file is autogenerated. DO NOT EDIT. // -// To regenerate, run "make gen-cue" from the repository root. -//~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +// Generated by public/app/plugins/gen.go +// +// Derived from the Thema lineage declared in models.cue +// +// Run `make gen-cue` from repository root to regenerate. + import * as ui from '@grafana/schema'; @@ -45,3 +48,4 @@ export const defaultPanelFieldConfig: Partial = { gradientMode: ui.GraphGradientMode.None, lineWidth: 1, }; + diff --git a/public/app/plugins/panel/bargauge/models.gen.ts b/public/app/plugins/panel/bargauge/models.gen.ts index 91151539de1..c57f0927b40 100644 --- a/public/app/plugins/panel/bargauge/models.gen.ts +++ b/public/app/plugins/panel/bargauge/models.gen.ts @@ -1,8 +1,11 @@ -//~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ // This file is autogenerated. DO NOT EDIT. // -// To regenerate, run "make gen-cue" from the repository root. -//~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +// Generated by public/app/plugins/gen.go +// +// Derived from the Thema lineage declared in models.cue +// +// Run `make gen-cue` from repository root to regenerate. + import * as ui from '@grafana/schema'; @@ -22,3 +25,4 @@ export const defaultPanelOptions: Partial = { minVizWidth: 0, showUnfilled: true, }; + diff --git a/public/app/plugins/panel/dashlist/models.gen.ts b/public/app/plugins/panel/dashlist/models.gen.ts index 2ec2afdf367..8c1bfc0d543 100644 --- a/public/app/plugins/panel/dashlist/models.gen.ts +++ b/public/app/plugins/panel/dashlist/models.gen.ts @@ -1,8 +1,11 @@ -//~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ // This file is autogenerated. DO NOT EDIT. // -// To regenerate, run "make gen-cue" from the repository root. -//~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +// Generated by public/app/plugins/gen.go +// +// Derived from the Thema lineage declared in models.cue +// +// Run `make gen-cue` from repository root to regenerate. + export const PanelModelVersion = Object.freeze([0, 0]); @@ -35,3 +38,4 @@ export const defaultPanelOptions: Partial = { showStarred: true, tags: [], }; + diff --git a/public/app/plugins/panel/gauge/models.gen.ts b/public/app/plugins/panel/gauge/models.gen.ts index 73af2acdc52..cd4e38d0b80 100644 --- a/public/app/plugins/panel/gauge/models.gen.ts +++ b/public/app/plugins/panel/gauge/models.gen.ts @@ -1,8 +1,11 @@ -//~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ // This file is autogenerated. DO NOT EDIT. // -// To regenerate, run "make gen-cue" from the repository root. -//~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +// Generated by public/app/plugins/gen.go +// +// Derived from the Thema lineage declared in models.cue +// +// Run `make gen-cue` from repository root to regenerate. + import * as ui from '@grafana/schema'; @@ -18,3 +21,4 @@ export const defaultPanelOptions: Partial = { showThresholdLabels: false, showThresholdMarkers: true, }; + diff --git a/public/app/plugins/panel/histogram/models.gen.ts b/public/app/plugins/panel/histogram/models.gen.ts index 278cc9e939c..61eabbee500 100644 --- a/public/app/plugins/panel/histogram/models.gen.ts +++ b/public/app/plugins/panel/histogram/models.gen.ts @@ -1,8 +1,11 @@ -//~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ // This file is autogenerated. DO NOT EDIT. // -// To regenerate, run "make gen-cue" from the repository root. -//~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +// Generated by public/app/plugins/gen.go +// +// Derived from the Thema lineage declared in models.cue +// +// Run `make gen-cue` from repository root to regenerate. + import * as ui from '@grafana/schema'; @@ -30,3 +33,4 @@ export const defaultPanelFieldConfig: Partial = { gradientMode: ui.GraphGradientMode.None, lineWidth: 1, }; + diff --git a/public/app/plugins/panel/news/models.gen.ts b/public/app/plugins/panel/news/models.gen.ts index 1d87060b735..a8ce1fc724b 100644 --- a/public/app/plugins/panel/news/models.gen.ts +++ b/public/app/plugins/panel/news/models.gen.ts @@ -1,8 +1,11 @@ -//~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ // This file is autogenerated. DO NOT EDIT. // -// To regenerate, run "make gen-cue" from the repository root. -//~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +// Generated by public/app/plugins/gen.go +// +// Derived from the Thema lineage declared in models.cue +// +// Run `make gen-cue` from repository root to regenerate. + export const PanelModelVersion = Object.freeze([0, 0]); @@ -16,3 +19,4 @@ export interface PanelOptions { export const defaultPanelOptions: Partial = { showImage: true, }; + diff --git a/public/app/plugins/panel/piechart/models.gen.ts b/public/app/plugins/panel/piechart/models.gen.ts index 5b53d0a4fa9..db9696368b0 100644 --- a/public/app/plugins/panel/piechart/models.gen.ts +++ b/public/app/plugins/panel/piechart/models.gen.ts @@ -1,8 +1,11 @@ -//~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ // This file is autogenerated. DO NOT EDIT. // -// To regenerate, run "make gen-cue" from the repository root. -//~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +// Generated by public/app/plugins/gen.go +// +// Derived from the Thema lineage declared in models.cue +// +// Run `make gen-cue` from repository root to regenerate. + import * as ui from '@grafana/schema'; @@ -44,3 +47,4 @@ export const defaultPanelOptions: Partial = { }; export interface PanelFieldConfig extends ui.HideableFieldConfig {} + diff --git a/public/app/plugins/panel/stat/models.gen.ts b/public/app/plugins/panel/stat/models.gen.ts index f1289056903..3da1887896f 100644 --- a/public/app/plugins/panel/stat/models.gen.ts +++ b/public/app/plugins/panel/stat/models.gen.ts @@ -1,8 +1,11 @@ -//~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ // This file is autogenerated. DO NOT EDIT. // -// To regenerate, run "make gen-cue" from the repository root. -//~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +// Generated by public/app/plugins/gen.go +// +// Derived from the Thema lineage declared in models.cue +// +// Run `make gen-cue` from repository root to regenerate. + import * as ui from '@grafana/schema'; @@ -22,3 +25,4 @@ export const defaultPanelOptions: Partial = { justifyMode: ui.BigValueJustifyMode.Auto, textMode: ui.BigValueTextMode.Auto, }; + diff --git a/public/app/plugins/panel/text/models.gen.ts b/public/app/plugins/panel/text/models.gen.ts index 23ee8a95cfc..818d7667422 100644 --- a/public/app/plugins/panel/text/models.gen.ts +++ b/public/app/plugins/panel/text/models.gen.ts @@ -1,8 +1,11 @@ -//~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ // This file is autogenerated. DO NOT EDIT. // -// To regenerate, run "make gen-cue" from the repository root. -//~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +// Generated by public/app/plugins/gen.go +// +// Derived from the Thema lineage declared in models.cue +// +// Run `make gen-cue` from repository root to regenerate. + export const PanelModelVersion = Object.freeze([0, 0]); @@ -52,3 +55,4 @@ export const defaultPanelOptions: Partial = { For markdown syntax help: [commonmark.org/help](https://commonmark.org/help/)`, mode: TextMode.Markdown, }; +