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:
@@ -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
|
||||
}
|
||||
@@ -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.
|
||||
|
||||
@@ -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"
|
||||
)
|
||||
@@ -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
|
||||
}
|
||||
@@ -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}}
|
||||
@@ -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...)
|
||||
}
|
||||
@@ -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 }}
|
||||
@@ -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 }}
|
||||
}
|
||||
}
|
||||
@@ -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 }}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user