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
This commit is contained in:
sam boyer
2022-09-14 10:15:09 -04:00
committed by GitHub
parent c69a37f8c2
commit ced53a8dc2
36 changed files with 1114 additions and 324 deletions
+77 -193
View File
@@ -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)
}
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)
}
fmt.Fprint(buf, "\n", gostr)
vars := ls.toTemplateObj()
var buuf bytes.Buffer
err = tmplAddenda.Execute(&buuf, vars)
err = tmpls.Lookup("addenda.tmpl").Execute(buf, vars)
if err != nil {
panic(err)
}
fset := token.NewFileSet()
fname := fmt.Sprintf("%s_gen.go", lin.Name())
gf, err := parser.ParseFile(fset, fname, gostr+buuf.String(), parser.ParseComments)
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("generated go file parsing failed: %w", err)
}
ast.Walk(prefixDropper(strings.Title(lin.Name())), gf)
var buf bytes.Buffer
err = format.Node(&buf, fset, gf)
if err != nil {
return nil, fmt.Errorf("ast printing failed: %w", err)
}
byt, err := imports.Process(fname, buf.Bytes(), nil)
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,
}); err != nil {
return nil, fmt.Errorf("failed executing coremodel registry template: %w", err)
}
byt, err := postprocessGoFile(genGoFile{
path: path,
in: buf.Bytes(),
})
if err != nil {
return nil, fmt.Errorf("failed generating template: %w", err)
return nil, err
}
byt, err := imports.Process(path, buf.Bytes(), nil)
if err != nil {
return nil, fmt.Errorf("goimports processing failed: %w", 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
}
`))