schema: Generate Go and Typescript from Thema coremodels (#49193)
* Add go code generator for coremodels * Just generate the entire coremodel for now Maybe we'll need more flexibility as more coremodels are added, but for now this is fine. * Add note on type comment about stability, grodkit * Remove local replace directive for thema * Generate typescript from coremodel * Update pkg/coremodel/dashboard/addenda.go Co-authored-by: Ryan McKinley <ryantxu@gmail.com> * Update cuetsy to new release * Update thema to latest * Fix enum generation for FieldColorModeId * Put main generated object at the end of the file * Tweaks to generated Go output * Retweak back to var * Add generated coremodel test * Remove local replace statement again * Add Make target and call into cuetsy cmd from gen * Rename and comment linsrc for readability * Move key codegen bits into reusable package * Move body of cuetsifier into codegen pkg Also genericize the diffing output into reusable WriteDiffer. * Refactor coremodel generator to use WriteDiffer * Add gen-cue step to CI * Whip all the codegen automation into shape * Add simplistic coremodel canonicality controls * Remove erroneously committed test * Bump thema version * Remove dead code * Improve wording of non-canonicality comment Co-authored-by: Ryan McKinley <ryantxu@gmail.com>
This commit is contained in:
co-authored by
Ryan McKinley
parent
a641949a05
commit
be06d37a20
@@ -1,365 +1,30 @@
|
||||
package commands
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
gerrors "errors"
|
||||
"fmt"
|
||||
"io"
|
||||
"io/fs"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
"text/template"
|
||||
|
||||
"cuelang.org/go/cue"
|
||||
"cuelang.org/go/cue/ast"
|
||||
"cuelang.org/go/cue/cuecontext"
|
||||
"cuelang.org/go/cue/errors"
|
||||
cload "cuelang.org/go/cue/load"
|
||||
"cuelang.org/go/cue/parser"
|
||||
"github.com/google/go-cmp/cmp"
|
||||
"github.com/grafana/cuetsy"
|
||||
"github.com/grafana/grafana/pkg/cmd/grafana-cli/utils"
|
||||
"github.com/grafana/grafana/pkg/schema/load"
|
||||
"github.com/grafana/grafana/pkg/codegen"
|
||||
)
|
||||
|
||||
// FIXME almost this whole file is a sloppy, one-off hack that just goes around actually making
|
||||
// the API we need. Parts need to be factored out appropriately.
|
||||
|
||||
var ctx = cuecontext.New()
|
||||
|
||||
// The only import statement we currently allow in any models.cue file
|
||||
const allowedImport = "github.com/grafana/grafana/packages/grafana-schema/src/schema"
|
||||
|
||||
var importMap = map[string]string{
|
||||
allowedImport: "@grafana/schema",
|
||||
}
|
||||
|
||||
// Hard-coded list of paths to skip. Remove a particular file as we're ready
|
||||
// to rely on the TypeScript auto-generated by cuetsy for that particular file.
|
||||
var skipPaths = []string{
|
||||
"public/app/plugins/panel/barchart/models.cue",
|
||||
"public/app/plugins/panel/canvas/models.cue",
|
||||
"public/app/plugins/panel/histogram/models.cue",
|
||||
"public/app/plugins/panel/heatmap-new/models.cue",
|
||||
"public/app/plugins/panel/candlestick/models.cue",
|
||||
"public/app/plugins/panel/state-timeline/models.cue",
|
||||
"public/app/plugins/panel/status-history/models.cue",
|
||||
"public/app/plugins/panel/table/models.cue",
|
||||
"public/app/plugins/panel/timeseries/models.cue",
|
||||
// All the cue files in this dir have to be individually excluded, even
|
||||
// though the generator currently smooshes them all together
|
||||
"packages/grafana-schema/src/schema/graph.cue",
|
||||
"packages/grafana-schema/src/schema/legend.cue",
|
||||
"packages/grafana-schema/src/schema/mudball.cue",
|
||||
"packages/grafana-schema/src/schema/table.cue",
|
||||
"packages/grafana-schema/src/schema/text.cue",
|
||||
"packages/grafana-schema/src/schema/tooltip.cue",
|
||||
}
|
||||
|
||||
const prefix = "/"
|
||||
|
||||
//nolint: gocyclo
|
||||
// TODO remove this whole thing
|
||||
func (cmd Command) generateTypescript(c utils.CommandLine) error {
|
||||
root := c.String("grafana-root")
|
||||
if root == "" {
|
||||
return gerrors.New("must provide path to the root of a Grafana repository checkout")
|
||||
}
|
||||
|
||||
var fspaths load.BaseLoadPaths
|
||||
var err error
|
||||
|
||||
fspaths.BaseCueFS, err = populateMapFSFromRoot(paths.BaseCueFS, root, "")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
fspaths.DistPluginCueFS, err = populateMapFSFromRoot(paths.DistPluginCueFS, root, "")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
overlay, err := defaultOverlay(fspaths)
|
||||
wd, err := codegen.CuetsifyPlugins(ctx, root)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// Prep the cue load config
|
||||
clcfg := &cload.Config{
|
||||
Overlay: overlay,
|
||||
// FIXME these module paths won't work for things not under our cue.mod - AKA third-party plugins
|
||||
// ModuleRoot: prefix,
|
||||
Module: "github.com/grafana/grafana",
|
||||
if c.Bool("diff") {
|
||||
return wd.Verify()
|
||||
}
|
||||
|
||||
// FIXME hardcoding paths to exclude is not the way to handle this
|
||||
excl := map[string]bool{
|
||||
"cue.mod": true,
|
||||
"cue/scuemata": true,
|
||||
"packages/grafana-schema/src/scuemata/dashboard": true,
|
||||
"packages/grafana-schema/src/scuemata/dashboard/dist": true,
|
||||
}
|
||||
|
||||
exclude := func(path string) bool {
|
||||
dir := filepath.Dir(path)
|
||||
if excl[dir] {
|
||||
return true
|
||||
}
|
||||
for _, p := range skipPaths {
|
||||
if path == p {
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
return false
|
||||
}
|
||||
|
||||
outfiles := make(map[string][]byte)
|
||||
|
||||
cuetsify := func(in fs.FS) error {
|
||||
seen := make(map[string]bool)
|
||||
return fs.WalkDir(in, ".", func(path string, d fs.DirEntry, err error) error {
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
dir := filepath.Dir(path)
|
||||
|
||||
if d.IsDir() || filepath.Ext(d.Name()) != ".cue" || seen[dir] || exclude(path) {
|
||||
return nil
|
||||
}
|
||||
seen[dir] = true
|
||||
clcfg.Dir = dir
|
||||
// FIXME Horrible hack to figure out the identifier used for
|
||||
// imported packages - intercept the parser called by the loader to
|
||||
// look at the ast.Files on their way in to building.
|
||||
// Much better if we could work backwards from the cue.Value,
|
||||
// maybe even directly in cuetsy itself, and figure out when a
|
||||
// referenced object is "out of bounds".
|
||||
// var imports sync.Map
|
||||
var imports []*ast.ImportSpec
|
||||
clcfg.ParseFile = func(name string, src interface{}) (*ast.File, error) {
|
||||
f, err := parser.ParseFile(name, src, parser.ParseComments)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
imports = append(imports, f.Imports...)
|
||||
return f, nil
|
||||
}
|
||||
|
||||
// FIXME loading in this way causes all files in a dir to be loaded
|
||||
// as a single cue.Instance or cue.Value, which makes it quite
|
||||
// difficult to map them _back_ onto the original file and generate
|
||||
// discrete .gen.ts files for each .cue input. However, going one
|
||||
// .cue file at a time and passing it as the first arg to
|
||||
// load.Instances() means that the other files are ignored
|
||||
// completely, causing references between these files to be
|
||||
// unresolved, and thus encounter a different kind of error.
|
||||
insts := cload.Instances(nil, clcfg)
|
||||
if len(insts) > 1 {
|
||||
panic("extra instances")
|
||||
}
|
||||
bi := insts[0]
|
||||
|
||||
v := ctx.BuildInstance(bi)
|
||||
if v.Err() != nil {
|
||||
return v.Err()
|
||||
}
|
||||
|
||||
var b []byte
|
||||
f := &tsFile{}
|
||||
seen := make(map[string]bool)
|
||||
// FIXME explicitly mapping path patterns to conversion patterns
|
||||
// is exactly what we want to avoid
|
||||
switch {
|
||||
// panel plugin models.cue files
|
||||
case strings.Contains(path, "public/app/plugins"):
|
||||
for _, im := range imports {
|
||||
ip := strings.Trim(im.Path.Value, "\"")
|
||||
if ip != allowedImport {
|
||||
// TODO make a specific error type for this
|
||||
return errors.Newf(im.Pos(), "import %q not allowed, panel plugins may only import from %q", ip, allowedImport)
|
||||
}
|
||||
// TODO this approach will silently swallow the unfixable
|
||||
// error case where multiple files in the same dir import
|
||||
// the same package to a different ident
|
||||
if !seen[ip] {
|
||||
seen[ip] = true
|
||||
f.Imports = append(f.Imports, convertImport(im))
|
||||
}
|
||||
}
|
||||
|
||||
// val := v.LookupPath(cue.ParsePath("Panel.lineages[0][0]"))
|
||||
// Extract the latest schema and its version number. (All of this goes away with Thema, whew)
|
||||
f.V = &tsModver{}
|
||||
lins := v.LookupPath(cue.ParsePath("Panel.lineages"))
|
||||
f.V.Lin, _ = lins.Len().Int64()
|
||||
f.V.Lin = f.V.Lin - 1
|
||||
schs := lins.LookupPath(cue.MakePath(cue.Index(int(f.V.Lin))))
|
||||
f.V.Sch, _ = schs.Len().Int64()
|
||||
f.V.Sch = f.V.Sch - 1
|
||||
latest := schs.LookupPath(cue.MakePath(cue.Index(int(f.V.Sch))))
|
||||
|
||||
b, err = cuetsy.Generate(latest, cuetsy.Config{})
|
||||
default:
|
||||
b, err = cuetsy.Generate(v, cuetsy.Config{})
|
||||
}
|
||||
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
f.Body = string(b)
|
||||
|
||||
var buf bytes.Buffer
|
||||
err = tsTemplate.Execute(&buf, f)
|
||||
outfiles[strings.Replace(path, ".cue", ".gen.ts", -1)] = buf.Bytes()
|
||||
return err
|
||||
})
|
||||
}
|
||||
|
||||
err = cuetsify(fspaths.BaseCueFS)
|
||||
if err != nil {
|
||||
return gerrors.New(errors.Details(err, nil))
|
||||
}
|
||||
err = cuetsify(fspaths.DistPluginCueFS)
|
||||
if err != nil {
|
||||
return gerrors.New(errors.Details(err, nil))
|
||||
}
|
||||
|
||||
diff := c.Bool("diff")
|
||||
var derr bool
|
||||
for of, b := range outfiles {
|
||||
p := filepath.Join(root, of)
|
||||
if diff {
|
||||
if _, err := os.Stat(p); err != nil {
|
||||
if errors.Is(err, os.ErrNotExist) {
|
||||
fmt.Printf("%s: no generated code file to compare against\n", p)
|
||||
derr = true
|
||||
continue
|
||||
}
|
||||
return fmt.Errorf("%s: %w", p, err)
|
||||
}
|
||||
|
||||
f, err := os.Open(filepath.Clean(p))
|
||||
if err != nil {
|
||||
return fmt.Errorf("%s: %w", p, err)
|
||||
}
|
||||
|
||||
ob, err := io.ReadAll(f)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
dstr := cmp.Diff(string(ob), string(b))
|
||||
if dstr != "" {
|
||||
derr = true
|
||||
fmt.Printf("%s would have changed:\n%s\n", p, dstr)
|
||||
}
|
||||
} else {
|
||||
err := os.WriteFile(p, b, 0644)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if derr {
|
||||
return errors.New("some files changed")
|
||||
}
|
||||
|
||||
return nil
|
||||
return wd.Write()
|
||||
}
|
||||
|
||||
func convertImport(im *ast.ImportSpec) *tsImport {
|
||||
tsim := &tsImport{
|
||||
Pkg: importMap[allowedImport],
|
||||
}
|
||||
if im.Name != nil && im.Name.String() != "" {
|
||||
tsim.Ident = im.Name.String()
|
||||
} else {
|
||||
sl := strings.Split(im.Path.Value, "/")
|
||||
final := sl[len(sl)-1]
|
||||
if idx := strings.Index(final, ":"); idx != -1 {
|
||||
tsim.Pkg = final[idx:]
|
||||
} else {
|
||||
tsim.Pkg = final
|
||||
}
|
||||
}
|
||||
return tsim
|
||||
}
|
||||
|
||||
func defaultOverlay(p load.BaseLoadPaths) (map[string]cload.Source, error) {
|
||||
overlay := make(map[string]cload.Source)
|
||||
|
||||
if err := toOverlay(prefix, p.BaseCueFS, overlay); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if err := toOverlay(prefix, p.DistPluginCueFS, overlay); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return overlay, nil
|
||||
}
|
||||
|
||||
func toOverlay(prefix string, vfs fs.FS, overlay map[string]cload.Source) error {
|
||||
if !filepath.IsAbs(prefix) {
|
||||
return fmt.Errorf("must provide absolute path prefix when generating cue overlay, got %q", prefix)
|
||||
}
|
||||
err := fs.WalkDir(vfs, ".", func(path string, d fs.DirEntry, err error) error {
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if d.IsDir() {
|
||||
return nil
|
||||
}
|
||||
|
||||
f, err := vfs.Open(path)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer func(f fs.File) {
|
||||
err := f.Close()
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
}(f)
|
||||
|
||||
b, err := io.ReadAll(f)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
overlay[filepath.Join(prefix, path)] = cload.FromBytes(b)
|
||||
return nil
|
||||
})
|
||||
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
type tsFile struct {
|
||||
V *tsModver
|
||||
Imports []*tsImport
|
||||
Body string
|
||||
}
|
||||
|
||||
type tsModver struct {
|
||||
Lin, Sch int64
|
||||
}
|
||||
|
||||
type tsImport struct {
|
||||
Ident string
|
||||
Pkg string
|
||||
}
|
||||
|
||||
var tsTemplate = template.Must(template.New("cuetsygen").Parse(
|
||||
`//~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||
// This file was autogenerated by cuetsy. DO NOT EDIT!
|
||||
//~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||
{{range .Imports}}
|
||||
import * as {{.Ident}} from '{{.Pkg}}';{{end}}
|
||||
{{if .V}}
|
||||
export const modelVersion = Object.freeze([{{ .V.Lin }}, {{ .V.Sch }}]);
|
||||
{{end}}
|
||||
{{.Body}}`))
|
||||
|
||||
@@ -0,0 +1,399 @@
|
||||
package codegen
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"errors"
|
||||
"fmt"
|
||||
"go/ast"
|
||||
"go/format"
|
||||
"go/parser"
|
||||
"go/token"
|
||||
"io/ioutil"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"regexp"
|
||||
"strings"
|
||||
"testing/fstest"
|
||||
"text/template"
|
||||
|
||||
"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/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
|
||||
// directory for a Thema lineage.
|
||||
type ExtractedLineage struct {
|
||||
Lineage thema.Lineage
|
||||
// Absolute path to the coremodel's lineage.cue file.
|
||||
LineagePath string
|
||||
// Path to the coremodel's lineage.cue file relative to repo root.
|
||||
RelativePath string
|
||||
// Indicates whether the coremodel is considered canonical or not. Generated
|
||||
// code from not-yet-canonical coremodels should include appropriate caveats in
|
||||
// documentation and possibly be hidden from external public API surface areas.
|
||||
IsCanonical bool
|
||||
}
|
||||
|
||||
// ExtractLineage loads a Grafana Thema lineage from the filesystem.
|
||||
//
|
||||
// The provided path must be the absolute path to the file containing the
|
||||
// lineage to be loaded.
|
||||
//
|
||||
// This loading approach is intended primarily for use with code generators, or
|
||||
// other use cases external to grafana-server backend. For code within
|
||||
// grafana-server, prefer lineage loaders provided in e.g. pkg/coremodel/*.
|
||||
func ExtractLineage(path string, lib thema.Library) (*ExtractedLineage, error) {
|
||||
if !filepath.IsAbs(path) {
|
||||
return nil, fmt.Errorf("must provide an absolute path, got %q", path)
|
||||
}
|
||||
|
||||
ec := &ExtractedLineage{
|
||||
LineagePath: path,
|
||||
}
|
||||
|
||||
var find func(path string) (string, error)
|
||||
find = func(path string) (string, error) {
|
||||
parent := filepath.Dir(path)
|
||||
if parent == path {
|
||||
return "", errors.New("grafana root directory could not be found")
|
||||
}
|
||||
fp := filepath.Join(path, "go.mod")
|
||||
if _, err := os.Stat(fp); err == nil {
|
||||
return path, nil
|
||||
}
|
||||
return find(parent)
|
||||
}
|
||||
groot, err := find(path)
|
||||
if err != nil {
|
||||
return ec, err
|
||||
}
|
||||
|
||||
f, err := os.Open(ec.LineagePath)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("could not open lineage file at %s: %w", path, err)
|
||||
}
|
||||
|
||||
byt, err := ioutil.ReadAll(f)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
fs := fstest.MapFS{
|
||||
"lineage.cue": &fstest.MapFile{
|
||||
Data: byt,
|
||||
},
|
||||
}
|
||||
|
||||
ec.RelativePath, err = filepath.Rel(groot, filepath.Dir(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)
|
||||
if err != nil {
|
||||
return ec, err
|
||||
}
|
||||
ec.IsCanonical = isCanonical(ec.Lineage.Name())
|
||||
return ec, nil
|
||||
}
|
||||
|
||||
func isCanonical(name string) bool {
|
||||
return canonicalCoremodels[name]
|
||||
}
|
||||
|
||||
// FIXME specificying coremodel canonicality DOES NOT belong here - it should be part of the coremodel declaration.
|
||||
var canonicalCoremodels = map[string]bool{
|
||||
"dashboard": false,
|
||||
}
|
||||
|
||||
// GenerateGoCoremodel generates a standard Go coremodel from a Thema lineage.
|
||||
//
|
||||
// The provided path must be a directory. Generated code files will be written
|
||||
// to that path. The final element of the path must match the Lineage.Name().
|
||||
func (ls *ExtractedLineage) GenerateGoCoremodel(path string) (WriteDiffer, error) {
|
||||
lin, lib := ls.Lineage, ls.Lineage.Library()
|
||||
_, name := filepath.Split(path)
|
||||
if name != lin.Name() {
|
||||
return nil, fmt.Errorf("lineage name %q must match final element of path, got %q", lin.Name(), path)
|
||||
}
|
||||
|
||||
sch := thema.SchemaP(lin, thema.LatestVersion(lin))
|
||||
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)
|
||||
}
|
||||
|
||||
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),
|
||||
"typedef.tmpl": tmplTypedef,
|
||||
},
|
||||
})
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("openapi generation failed: %w", err)
|
||||
}
|
||||
|
||||
vars := goPkg{
|
||||
Name: lin.Name(),
|
||||
LineagePath: ls.RelativePath,
|
||||
LatestSeqv: sch.Version()[0],
|
||||
LatestSchv: sch.Version()[1],
|
||||
}
|
||||
var buuf bytes.Buffer
|
||||
err = tmplAddenda.Execute(&buuf, vars)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
|
||||
fset := token.NewFileSet()
|
||||
gf, err := parser.ParseFile(fset, "coremodel_gen.go", gostr+buuf.String(), parser.ParseComments)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("generated go file parsing failed: %w", err)
|
||||
}
|
||||
m := makeReplacer(lin.Name())
|
||||
ast.Walk(m, 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("coremodel_gen.go", buf.Bytes(), nil)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("goimports processing failed: %w", err)
|
||||
}
|
||||
|
||||
// Generate the assignability test. TODO do this in a framework test instead
|
||||
var buf3 bytes.Buffer
|
||||
err = tmplAssignableTest.Execute(&buf3, vars)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed generating assignability test file: %w", err)
|
||||
}
|
||||
|
||||
wd := NewWriteDiffer()
|
||||
wd[filepath.Join(path, "coremodel_gen.go")] = byt
|
||||
wd[filepath.Join(path, "coremodel_gen_test.go")] = buf3.Bytes()
|
||||
|
||||
return wd, nil
|
||||
}
|
||||
|
||||
type goPkg struct {
|
||||
Name string
|
||||
LineagePath string
|
||||
LatestSeqv, LatestSchv uint
|
||||
IsComposed bool
|
||||
}
|
||||
|
||||
func (ls *ExtractedLineage) GenerateTypescriptCoremodel(path string) (WriteDiffer, error) {
|
||||
_, name := filepath.Split(path)
|
||||
if name != ls.Lineage.Name() {
|
||||
return nil, fmt.Errorf("lineage name %q must match final element of path, got %q", ls.Lineage.Name(), path)
|
||||
}
|
||||
|
||||
schv := thema.SchemaP(ls.Lineage, thema.LatestVersion(ls.Lineage)).UnwrapCUE()
|
||||
|
||||
parts, err := cuetsy.GenerateAST(schv, cuetsy.Config{})
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("cuetsy parts gen failed: %w", err)
|
||||
}
|
||||
|
||||
top, err := cuetsy.GenerateSingleAST(string(makeReplacer(ls.Lineage.Name())), schv, cuetsy.TypeInterface)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("cuetsy top gen failed: %w", err)
|
||||
}
|
||||
|
||||
// TODO until cuetsy can toposort its outputs, put the top/parent type at the bottom of the file.
|
||||
parts.Nodes = append(parts.Nodes, top.T, top.D)
|
||||
// parts.Nodes = append([]ts.Decl{top.T, top.D}, parts.Nodes...)
|
||||
|
||||
var strb strings.Builder
|
||||
var str string
|
||||
fpath := ls.Lineage.Name() + ".gen.ts"
|
||||
strb.WriteString(fmt.Sprintf(genHeader, ls.RelativePath))
|
||||
|
||||
if !ls.IsCanonical {
|
||||
fpath = fmt.Sprintf("%s_experimental.gen.ts", ls.Lineage.Name())
|
||||
strb.WriteString(`
|
||||
// This model is a WIP and not yet canonical. Consequently, its members are
|
||||
// not exported to exclude it from grafana-schema's public API surface.
|
||||
|
||||
`)
|
||||
strb.WriteString(fmt.Sprint(parts))
|
||||
// TODO replace this regexp with cuetsy config for whether members are exported
|
||||
re := regexp.MustCompile(`(?m)^export `)
|
||||
str = re.ReplaceAllLiteralString(strb.String(), "")
|
||||
} else {
|
||||
strb.WriteString(fmt.Sprint(parts))
|
||||
str = strb.String()
|
||||
}
|
||||
|
||||
wd := NewWriteDiffer()
|
||||
wd[filepath.Join(path, fpath)] = []byte(str)
|
||||
return wd, nil
|
||||
}
|
||||
|
||||
type modelReplacer string
|
||||
|
||||
func makeReplacer(name string) modelReplacer {
|
||||
return modelReplacer(fmt.Sprintf("%s%s", string(strings.ToUpper(name)[0]), name[1:]))
|
||||
}
|
||||
|
||||
func (m modelReplacer) Visit(n ast.Node) ast.Visitor {
|
||||
switch x := n.(type) {
|
||||
case *ast.Ident:
|
||||
x.Name = m.replacePrefix(x.Name)
|
||||
}
|
||||
return m
|
||||
}
|
||||
|
||||
func (m modelReplacer) replacePrefix(str string) string {
|
||||
if len(str) >= len(m) && str[:len(m)] == string(m) {
|
||||
return strings.Replace(str, string(m), "Model", 1)
|
||||
}
|
||||
return str
|
||||
}
|
||||
|
||||
var genHeader = `// This file is autogenerated. DO NOT EDIT.
|
||||
//
|
||||
// To regenerate, run "make gen-cue" from repository root.
|
||||
//
|
||||
// Derived from the Thema lineage at %s
|
||||
|
||||
`
|
||||
|
||||
var tmplImports = genHeader + `package {{ .PackageName }}
|
||||
|
||||
import (
|
||||
"embed"
|
||||
"bytes"
|
||||
"compress/gzip"
|
||||
"context"
|
||||
"encoding/base64"
|
||||
"encoding/json"
|
||||
"encoding/xml"
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
"io/ioutil"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"path"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/deepmap/oapi-codegen/pkg/runtime"
|
||||
openapi_types "github.com/deepmap/oapi-codegen/pkg/types"
|
||||
"github.com/getkin/kin-openapi/openapi3"
|
||||
"github.com/grafana/thema"
|
||||
"github.com/grafana/grafana/pkg/cuectx"
|
||||
)
|
||||
`
|
||||
|
||||
var tmplAddenda = template.Must(template.New("addenda").Parse(`
|
||||
//go:embed lineage.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", "dashboard"), cueFS, lib, opts...)
|
||||
}
|
||||
|
||||
var _ thema.LineageFactory = Lineage
|
||||
|
||||
// Coremodel contains the foundational schema declaration for {{ .Name }}s.
|
||||
type Coremodel struct {
|
||||
lin thema.Lineage
|
||||
}
|
||||
|
||||
// Lineage returns the canonical dashboard 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{}
|
||||
}
|
||||
|
||||
func ProvideCoremodel(lib thema.Library) (*Coremodel, error) {
|
||||
lin, err := Lineage(lib)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return &Coremodel{
|
||||
lin: lin,
|
||||
}, nil
|
||||
}
|
||||
`))
|
||||
|
||||
var tmplAssignableTest = template.Must(template.New("addenda").Parse(fmt.Sprintf(genHeader, "{{ .LineagePath }}") + `package {{ .Name }}
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/grafana/grafana/pkg/cuectx"
|
||||
"github.com/grafana/thema"
|
||||
)
|
||||
|
||||
func TestSchemaAssignability(t *testing.T) {
|
||||
lin, err := Lineage(cuectx.ProvideThemaLibrary())
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
sch := thema.SchemaP(lin, currentVersion)
|
||||
|
||||
err = thema.AssignableTo(sch, &Model{})
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
}
|
||||
`))
|
||||
|
||||
var tmplTypedef = `{{range .Types}}
|
||||
{{ with .Schema.Description }}{{ . }}{{ else }}// {{.TypeName}} defines model for {{.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}}
|
||||
`
|
||||
@@ -0,0 +1,134 @@
|
||||
package codegen
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"sort"
|
||||
|
||||
"github.com/google/go-cmp/cmp"
|
||||
"github.com/hashicorp/go-multierror"
|
||||
"golang.org/x/sync/errgroup"
|
||||
)
|
||||
|
||||
// WriteDiffer is a pseudo-filesystem that supports batch-writing its contents
|
||||
// to the real filesystem, or batch-comparing its contents to the real
|
||||
// filesystem. Its intended use is for idiomatic `go generate`-style code
|
||||
// generators, where it is expected that the results of codegen are committed to
|
||||
// version control.
|
||||
//
|
||||
// In such cases, the normal behavior of a generator is to write files to disk,
|
||||
// but in CI, that behavior should change to verify that what is already on disk
|
||||
// is identical to the results of code generation. This allows CI to ensure that
|
||||
// the results of code generation are always up to date. WriteDiffer supports
|
||||
// these related behaviors through its Write() and Verify() methods, respectively.
|
||||
//
|
||||
// Note that the statelessness of WriteDiffer means that, if a particular input
|
||||
// to the code generator goes away, it will not notice generated files left
|
||||
// behind if their inputs are removed.
|
||||
// TODO introduce a search/match system
|
||||
type WriteDiffer map[string][]byte
|
||||
|
||||
func NewWriteDiffer() WriteDiffer {
|
||||
return WriteDiffer(make(map[string][]byte))
|
||||
}
|
||||
|
||||
type writeSlice []struct {
|
||||
path string
|
||||
contents []byte
|
||||
}
|
||||
|
||||
// Verify checks the contents of each file against the filesystem. It emits an error
|
||||
// if any of its contained files differ.
|
||||
func (wd WriteDiffer) Verify() error {
|
||||
var result error
|
||||
|
||||
for _, item := range wd.toSlice() {
|
||||
if _, err := os.Stat(item.path); err != nil {
|
||||
if errors.Is(err, os.ErrNotExist) {
|
||||
result = multierror.Append(result, fmt.Errorf("%s: generated file should exist, but does not", item.path))
|
||||
} else {
|
||||
result = multierror.Append(result, fmt.Errorf("%s: could not stat generated file: %w", item.path, err))
|
||||
}
|
||||
continue
|
||||
}
|
||||
|
||||
f, err := os.Open(filepath.Clean(item.path))
|
||||
if err != nil {
|
||||
result = multierror.Append(result, fmt.Errorf("%s: %w", item.path, err))
|
||||
continue
|
||||
}
|
||||
|
||||
ob, err := io.ReadAll(f)
|
||||
if err != nil {
|
||||
result = multierror.Append(result, fmt.Errorf("%s: %w", item.path, err))
|
||||
continue
|
||||
}
|
||||
dstr := cmp.Diff(string(ob), string(item.contents))
|
||||
if dstr != "" {
|
||||
result = multierror.Append(result, fmt.Errorf("%s would have changed:\n\n%s", item.path, dstr))
|
||||
}
|
||||
}
|
||||
|
||||
return result
|
||||
}
|
||||
|
||||
// Write writes all of the files to their indicated paths.
|
||||
func (wd WriteDiffer) Write() error {
|
||||
g, _ := errgroup.WithContext(context.TODO())
|
||||
g.SetLimit(12)
|
||||
|
||||
for _, item := range wd.toSlice() {
|
||||
it := item
|
||||
g.Go(func() error {
|
||||
err := os.MkdirAll(filepath.Dir(it.path), os.ModePerm)
|
||||
if err != nil {
|
||||
return fmt.Errorf("%s: failed to ensure parent directory exists: %w", it.path, err)
|
||||
}
|
||||
|
||||
if err := os.WriteFile(it.path, it.contents, 0644); err != nil {
|
||||
return fmt.Errorf("%s: error while writing file: %w", it.path, err)
|
||||
}
|
||||
return nil
|
||||
})
|
||||
}
|
||||
|
||||
return g.Wait()
|
||||
}
|
||||
|
||||
func (wd WriteDiffer) toSlice() writeSlice {
|
||||
sl := make(writeSlice, 0, len(wd))
|
||||
type ws struct {
|
||||
path string
|
||||
contents []byte
|
||||
}
|
||||
|
||||
for k, v := range wd {
|
||||
sl = append(sl, ws{
|
||||
path: k,
|
||||
contents: v,
|
||||
})
|
||||
}
|
||||
|
||||
sort.Slice(sl, func(i, j int) bool {
|
||||
return sl[i].path < sl[j].path
|
||||
})
|
||||
|
||||
return sl
|
||||
}
|
||||
|
||||
// Merge combines all the entries from the provided WriteDiffer into the callee
|
||||
// WriteDiffer. Duplicate paths result in an error.
|
||||
func (wd WriteDiffer) Merge(wd2 WriteDiffer) error {
|
||||
for k, v := range wd2 {
|
||||
if _, has := wd[k]; has {
|
||||
return fmt.Errorf("path %s already exists in write differ", k)
|
||||
}
|
||||
wd[k] = v
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
@@ -0,0 +1,343 @@
|
||||
package codegen
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
gerrors "errors"
|
||||
"fmt"
|
||||
"io"
|
||||
"io/fs"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
"testing/fstest"
|
||||
"text/template"
|
||||
|
||||
"cuelang.org/go/cue"
|
||||
"cuelang.org/go/cue/ast"
|
||||
"cuelang.org/go/cue/errors"
|
||||
cload "cuelang.org/go/cue/load"
|
||||
"cuelang.org/go/cue/parser"
|
||||
"github.com/grafana/cuetsy"
|
||||
"github.com/grafana/grafana/pkg/schema/load"
|
||||
)
|
||||
|
||||
// The only import statement we currently allow in any models.cue file
|
||||
const allowedImport = "github.com/grafana/grafana/packages/grafana-schema/src/schema"
|
||||
|
||||
var importMap = map[string]string{
|
||||
allowedImport: "@grafana/schema",
|
||||
}
|
||||
|
||||
// Hard-coded list of paths to skip. Remove a particular file as we're ready
|
||||
// to rely on the TypeScript auto-generated by cuetsy for that particular file.
|
||||
var skipPaths = []string{
|
||||
"public/app/plugins/panel/barchart/models.cue",
|
||||
"public/app/plugins/panel/canvas/models.cue",
|
||||
"public/app/plugins/panel/histogram/models.cue",
|
||||
"public/app/plugins/panel/heatmap-new/models.cue",
|
||||
"public/app/plugins/panel/candlestick/models.cue",
|
||||
"public/app/plugins/panel/state-timeline/models.cue",
|
||||
"public/app/plugins/panel/status-history/models.cue",
|
||||
"public/app/plugins/panel/table/models.cue",
|
||||
"public/app/plugins/panel/timeseries/models.cue",
|
||||
}
|
||||
|
||||
const prefix = "/"
|
||||
|
||||
var paths = load.GetDefaultLoadPaths()
|
||||
|
||||
// CuetsifyPlugins runs cuetsy against plugins' models.cue files.
|
||||
func CuetsifyPlugins(ctx *cue.Context, root string) (WriteDiffer, error) {
|
||||
// TODO this whole func has a lot of old, crufty behavior from the scuemata era; needs TLC
|
||||
var fspaths load.BaseLoadPaths
|
||||
var err error
|
||||
|
||||
fspaths.BaseCueFS, err = populateMapFSFromRoot(paths.BaseCueFS, root, "")
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
fspaths.DistPluginCueFS, err = populateMapFSFromRoot(paths.DistPluginCueFS, root, "")
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
overlay, err := defaultOverlay(fspaths)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// Prep the cue load config
|
||||
clcfg := &cload.Config{
|
||||
Overlay: overlay,
|
||||
// FIXME these module paths won't work for things not under our cue.mod - AKA third-party plugins
|
||||
ModuleRoot: prefix,
|
||||
Module: "github.com/grafana/grafana",
|
||||
}
|
||||
|
||||
// FIXME hardcoding paths to exclude is not the way to handle this
|
||||
excl := map[string]bool{
|
||||
"cue.mod": true,
|
||||
"cue/scuemata": true,
|
||||
"packages/grafana-schema/src/scuemata/dashboard": true,
|
||||
"packages/grafana-schema/src/scuemata/dashboard/dist": true,
|
||||
}
|
||||
|
||||
exclude := func(path string) bool {
|
||||
dir := filepath.Dir(path)
|
||||
if excl[dir] {
|
||||
return true
|
||||
}
|
||||
for _, p := range skipPaths {
|
||||
if path == p {
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
return false
|
||||
}
|
||||
|
||||
outfiles := NewWriteDiffer()
|
||||
|
||||
cuetsify := func(in fs.FS) error {
|
||||
seen := make(map[string]bool)
|
||||
return fs.WalkDir(in, ".", func(path string, d fs.DirEntry, err error) error {
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
dir := filepath.Dir(path)
|
||||
|
||||
if d.IsDir() || filepath.Ext(d.Name()) != ".cue" || seen[dir] || exclude(path) {
|
||||
return nil
|
||||
}
|
||||
seen[dir] = true
|
||||
clcfg.Dir = filepath.Join(root, dir)
|
||||
// FIXME Horrible hack to figure out the identifier used for
|
||||
// imported packages - intercept the parser called by the loader to
|
||||
// look at the ast.Files on their way in to building.
|
||||
// Much better if we could work backwards from the cue.Value,
|
||||
// maybe even directly in cuetsy itself, and figure out when a
|
||||
// referenced object is "out of bounds".
|
||||
// var imports sync.Map
|
||||
var imports []*ast.ImportSpec
|
||||
clcfg.ParseFile = func(name string, src interface{}) (*ast.File, error) {
|
||||
f, err := parser.ParseFile(name, src, parser.ParseComments)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
imports = append(imports, f.Imports...)
|
||||
return f, nil
|
||||
}
|
||||
if strings.Contains(path, "public/app/plugins") {
|
||||
clcfg.Package = "grafanaschema"
|
||||
} else {
|
||||
clcfg.Package = ""
|
||||
}
|
||||
|
||||
// FIXME loading in this way causes all files in a dir to be loaded
|
||||
// as a single cue.Instance or cue.Value, which makes it quite
|
||||
// difficult to map them _back_ onto the original file and generate
|
||||
// discrete .gen.ts files for each .cue input. However, going one
|
||||
// .cue file at a time and passing it as the first arg to
|
||||
// load.Instances() means that the other files are ignored
|
||||
// completely, causing references between these files to be
|
||||
// unresolved, and thus encounter a different kind of error.
|
||||
insts := cload.Instances(nil, clcfg)
|
||||
if len(insts) > 1 {
|
||||
panic("extra instances")
|
||||
}
|
||||
bi := insts[0]
|
||||
|
||||
v := ctx.BuildInstance(bi)
|
||||
if v.Err() != nil {
|
||||
return v.Err()
|
||||
}
|
||||
|
||||
var b []byte
|
||||
f := &tsFile{}
|
||||
seen := make(map[string]bool)
|
||||
// FIXME explicitly mapping path patterns to conversion patterns
|
||||
// is exactly what we want to avoid
|
||||
switch {
|
||||
// panel plugin models.cue files
|
||||
case strings.Contains(path, "public/app/plugins"):
|
||||
for _, im := range imports {
|
||||
ip := strings.Trim(im.Path.Value, "\"")
|
||||
if ip != allowedImport {
|
||||
// TODO make a specific error type for this
|
||||
return errors.Newf(im.Pos(), "import %q not allowed, panel plugins may only import from %q", ip, allowedImport)
|
||||
}
|
||||
// TODO this approach will silently swallow the unfixable
|
||||
// error case where multiple files in the same dir import
|
||||
// the same package to a different ident
|
||||
if !seen[ip] {
|
||||
seen[ip] = true
|
||||
f.Imports = append(f.Imports, convertImport(im))
|
||||
}
|
||||
}
|
||||
|
||||
// Extract the latest schema and its version number. (All of this goes away with Thema, whew)
|
||||
f.V = &tsModver{}
|
||||
lins := v.LookupPath(cue.ParsePath("Panel.lineages"))
|
||||
f.V.Lin, _ = lins.Len().Int64()
|
||||
f.V.Lin = f.V.Lin - 1
|
||||
schs := lins.LookupPath(cue.MakePath(cue.Index(int(f.V.Lin))))
|
||||
f.V.Sch, _ = schs.Len().Int64()
|
||||
f.V.Sch = f.V.Sch - 1
|
||||
latest := schs.LookupPath(cue.MakePath(cue.Index(int(f.V.Sch))))
|
||||
|
||||
b, err = cuetsy.Generate(latest, cuetsy.Config{})
|
||||
default:
|
||||
b, err = cuetsy.Generate(v, cuetsy.Config{})
|
||||
}
|
||||
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
f.Body = string(b)
|
||||
|
||||
var buf bytes.Buffer
|
||||
err = tsTemplate.Execute(&buf, f)
|
||||
outfiles[filepath.Join(root, strings.Replace(path, ".cue", ".gen.ts", -1))] = buf.Bytes()
|
||||
return err
|
||||
})
|
||||
}
|
||||
|
||||
err = cuetsify(fspaths.BaseCueFS)
|
||||
if err != nil {
|
||||
return nil, gerrors.New(errors.Details(err, nil))
|
||||
}
|
||||
err = cuetsify(fspaths.DistPluginCueFS)
|
||||
if err != nil {
|
||||
return nil, gerrors.New(errors.Details(err, nil))
|
||||
}
|
||||
|
||||
return outfiles, nil
|
||||
}
|
||||
|
||||
func convertImport(im *ast.ImportSpec) *tsImport {
|
||||
tsim := &tsImport{
|
||||
Pkg: importMap[allowedImport],
|
||||
}
|
||||
if im.Name != nil && im.Name.String() != "" {
|
||||
tsim.Ident = im.Name.String()
|
||||
} else {
|
||||
sl := strings.Split(im.Path.Value, "/")
|
||||
final := sl[len(sl)-1]
|
||||
if idx := strings.Index(final, ":"); idx != -1 {
|
||||
tsim.Pkg = final[idx:]
|
||||
} else {
|
||||
tsim.Pkg = final
|
||||
}
|
||||
}
|
||||
return tsim
|
||||
}
|
||||
|
||||
func defaultOverlay(p load.BaseLoadPaths) (map[string]cload.Source, error) {
|
||||
overlay := make(map[string]cload.Source)
|
||||
|
||||
if err := toOverlay(prefix, p.BaseCueFS, overlay); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if err := toOverlay(prefix, p.DistPluginCueFS, overlay); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return overlay, nil
|
||||
}
|
||||
|
||||
func toOverlay(prefix string, vfs fs.FS, overlay map[string]cload.Source) error {
|
||||
if !filepath.IsAbs(prefix) {
|
||||
return fmt.Errorf("must provide absolute path prefix when generating cue overlay, got %q", prefix)
|
||||
}
|
||||
err := fs.WalkDir(vfs, ".", func(path string, d fs.DirEntry, err error) error {
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if d.IsDir() {
|
||||
return nil
|
||||
}
|
||||
|
||||
f, err := vfs.Open(path)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer func(f fs.File) {
|
||||
err := f.Close()
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
}(f)
|
||||
|
||||
b, err := io.ReadAll(f)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
overlay[filepath.Join(prefix, path)] = cload.FromBytes(b)
|
||||
return nil
|
||||
})
|
||||
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// Helper function that populates an fs.FS by walking over a virtual filesystem,
|
||||
// and reading files from disk corresponding to each file encountered.
|
||||
func populateMapFSFromRoot(in fs.FS, root, join string) (fs.FS, error) {
|
||||
out := make(fstest.MapFS)
|
||||
err := fs.WalkDir(in, ".", func(path string, d fs.DirEntry, err error) error {
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if d.IsDir() {
|
||||
return nil
|
||||
}
|
||||
// Ignore gosec warning G304. The input set here is necessarily
|
||||
// constrained to files specified in embed.go
|
||||
// nolint:gosec
|
||||
b, err := os.Open(filepath.Join(root, join, path))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
byt, err := io.ReadAll(b)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
out[path] = &fstest.MapFile{Data: byt}
|
||||
return nil
|
||||
})
|
||||
return out, err
|
||||
}
|
||||
|
||||
type tsFile struct {
|
||||
V *tsModver
|
||||
Imports []*tsImport
|
||||
Body string
|
||||
}
|
||||
|
||||
type tsModver struct {
|
||||
Lin, Sch int64
|
||||
}
|
||||
|
||||
type tsImport struct {
|
||||
Ident string
|
||||
Pkg string
|
||||
}
|
||||
|
||||
var tsTemplate = template.Must(template.New("cuetsygen").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}}
|
||||
{{if .V}}
|
||||
export const modelVersion = Object.freeze([{{ .V.Lin }}, {{ .V.Sch }}]);
|
||||
{{end}}
|
||||
{{.Body}}`))
|
||||
@@ -0,0 +1,10 @@
|
||||
package dashboard
|
||||
|
||||
// HandoffSchemaVersion is the minimum schemaVersion for dashboards at which the
|
||||
// Thema-based dashboard schema is possibly valid
|
||||
//
|
||||
// schemaVersion is the original version numbering system for dashboards. If a
|
||||
// dashboard is below this schemaVersion, it is necessary for the frontend
|
||||
// typescript dashboard migration logic to first run and get it past this
|
||||
// number, after which Thema can take over.
|
||||
const HandoffSchemaVersion = 36
|
||||
@@ -1,40 +0,0 @@
|
||||
package dashboard
|
||||
|
||||
import (
|
||||
"github.com/grafana/thema"
|
||||
)
|
||||
|
||||
// Coremodel contains the foundational schema declaration for dashboards.
|
||||
type Coremodel struct {
|
||||
lin thema.Lineage
|
||||
}
|
||||
|
||||
// Lineage returns the canonical dashboard Lineage.
|
||||
func (c *Coremodel) Lineage() thema.Lineage {
|
||||
return c.lin
|
||||
}
|
||||
|
||||
func (c *Coremodel) CurrentSchema() thema.Schema {
|
||||
sch, err := c.lin.Schema(currentVersion)
|
||||
if err != nil {
|
||||
// Only reachable if our own schema currentVersion does not exist, which
|
||||
// can really only happen transitionally during development
|
||||
panic(err)
|
||||
}
|
||||
return sch
|
||||
}
|
||||
|
||||
func (c *Coremodel) GoType() interface{} {
|
||||
return &model{}
|
||||
}
|
||||
|
||||
func ProvideCoremodel(lib thema.Library) (*Coremodel, error) {
|
||||
lin, err := Lineage(lib)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return &Coremodel{
|
||||
lin: lin,
|
||||
}, nil
|
||||
}
|
||||
@@ -0,0 +1,751 @@
|
||||
// This file is autogenerated. DO NOT EDIT.
|
||||
//
|
||||
// To regenerate, run "make gen-cue" from repository root.
|
||||
//
|
||||
// Derived from the Thema lineage at pkg/coremodel/dashboard
|
||||
|
||||
package dashboard
|
||||
|
||||
import (
|
||||
"embed"
|
||||
"path/filepath"
|
||||
|
||||
"github.com/grafana/grafana/pkg/cuectx"
|
||||
"github.com/grafana/thema"
|
||||
)
|
||||
|
||||
// Defines values for DashboardGraphTooltip.
|
||||
const (
|
||||
ModelGraphTooltipN0 ModelGraphTooltip = 0
|
||||
|
||||
ModelGraphTooltipN1 ModelGraphTooltip = 1
|
||||
|
||||
ModelGraphTooltipN2 ModelGraphTooltip = 2
|
||||
)
|
||||
|
||||
// Defines values for DashboardStyle.
|
||||
const (
|
||||
ModelStyleDark ModelStyle = "dark"
|
||||
|
||||
ModelStyleLight ModelStyle = "light"
|
||||
)
|
||||
|
||||
// Defines values for DashboardTimezone.
|
||||
const (
|
||||
ModelTimezoneBrowser ModelTimezone = "browser"
|
||||
|
||||
ModelTimezoneEmpty ModelTimezone = ""
|
||||
|
||||
ModelTimezoneUtc ModelTimezone = "utc"
|
||||
)
|
||||
|
||||
// Defines values for DashboardDashboardCursorSync.
|
||||
const (
|
||||
ModelDashboardCursorSyncN0 ModelDashboardCursorSync = 0
|
||||
|
||||
ModelDashboardCursorSyncN1 ModelDashboardCursorSync = 1
|
||||
|
||||
ModelDashboardCursorSyncN2 ModelDashboardCursorSync = 2
|
||||
)
|
||||
|
||||
// Defines values for DashboardDashboardLinkType.
|
||||
const (
|
||||
ModelDashboardLinkTypeDashboards ModelDashboardLinkType = "dashboards"
|
||||
|
||||
ModelDashboardLinkTypeLink ModelDashboardLinkType = "link"
|
||||
)
|
||||
|
||||
// Defines values for DashboardFieldColorModeId.
|
||||
const (
|
||||
ModelFieldColorModeIdContinuousGrYlRd ModelFieldColorModeId = "continuous-GrYlRd"
|
||||
|
||||
ModelFieldColorModeIdFixed ModelFieldColorModeId = "fixed"
|
||||
|
||||
ModelFieldColorModeIdPaletteClassic ModelFieldColorModeId = "palette-classic"
|
||||
|
||||
ModelFieldColorModeIdPaletteSaturated ModelFieldColorModeId = "palette-saturated"
|
||||
|
||||
ModelFieldColorModeIdThresholds ModelFieldColorModeId = "thresholds"
|
||||
)
|
||||
|
||||
// Defines values for DashboardFieldColorSeriesByMode.
|
||||
const (
|
||||
ModelFieldColorSeriesByModeLast ModelFieldColorSeriesByMode = "last"
|
||||
|
||||
ModelFieldColorSeriesByModeMax ModelFieldColorSeriesByMode = "max"
|
||||
|
||||
ModelFieldColorSeriesByModeMin ModelFieldColorSeriesByMode = "min"
|
||||
)
|
||||
|
||||
// Defines values for DashboardGraphPanelType.
|
||||
const (
|
||||
ModelGraphPanelTypeGraph ModelGraphPanelType = "graph"
|
||||
)
|
||||
|
||||
// Defines values for DashboardHeatmapPanelType.
|
||||
const (
|
||||
ModelHeatmapPanelTypeHeatmap ModelHeatmapPanelType = "heatmap"
|
||||
)
|
||||
|
||||
// Defines values for DashboardPanelRepeatDirection.
|
||||
const (
|
||||
ModelPanelRepeatDirectionH ModelPanelRepeatDirection = "h"
|
||||
|
||||
ModelPanelRepeatDirectionV ModelPanelRepeatDirection = "v"
|
||||
)
|
||||
|
||||
// Defines values for DashboardRowPanelType.
|
||||
const (
|
||||
ModelRowPanelTypeRow ModelRowPanelType = "row"
|
||||
)
|
||||
|
||||
// Defines values for DashboardThresholdsConfigMode.
|
||||
const (
|
||||
ModelThresholdsConfigModeAbsolute ModelThresholdsConfigMode = "absolute"
|
||||
|
||||
ModelThresholdsConfigModePercentage ModelThresholdsConfigMode = "percentage"
|
||||
)
|
||||
|
||||
// Defines values for DashboardThresholdsMode.
|
||||
const (
|
||||
ModelThresholdsModeAbsolute ModelThresholdsMode = "absolute"
|
||||
|
||||
ModelThresholdsModePercentage ModelThresholdsMode = "percentage"
|
||||
)
|
||||
|
||||
// Defines values for DashboardVariableModelType.
|
||||
const (
|
||||
ModelVariableModelTypeAdhoc ModelVariableModelType = "adhoc"
|
||||
|
||||
ModelVariableModelTypeConstant ModelVariableModelType = "constant"
|
||||
|
||||
ModelVariableModelTypeCustom ModelVariableModelType = "custom"
|
||||
|
||||
ModelVariableModelTypeDatasource ModelVariableModelType = "datasource"
|
||||
|
||||
ModelVariableModelTypeInterval ModelVariableModelType = "interval"
|
||||
|
||||
ModelVariableModelTypeQuery ModelVariableModelType = "query"
|
||||
|
||||
ModelVariableModelTypeSystem ModelVariableModelType = "system"
|
||||
|
||||
ModelVariableModelTypeTextbox ModelVariableModelType = "textbox"
|
||||
)
|
||||
|
||||
// Defines values for DashboardVariableType.
|
||||
const (
|
||||
ModelVariableTypeAdhoc ModelVariableType = "adhoc"
|
||||
|
||||
ModelVariableTypeConstant ModelVariableType = "constant"
|
||||
|
||||
ModelVariableTypeCustom ModelVariableType = "custom"
|
||||
|
||||
ModelVariableTypeDatasource ModelVariableType = "datasource"
|
||||
|
||||
ModelVariableTypeInterval ModelVariableType = "interval"
|
||||
|
||||
ModelVariableTypeQuery ModelVariableType = "query"
|
||||
|
||||
ModelVariableTypeSystem ModelVariableType = "system"
|
||||
|
||||
ModelVariableTypeTextbox ModelVariableType = "textbox"
|
||||
)
|
||||
|
||||
// Dashboard defines model for 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.
|
||||
type Model struct {
|
||||
Annotations *struct {
|
||||
List []ModelAnnotationQuery `json:"list"`
|
||||
} `json:"annotations,omitempty"`
|
||||
|
||||
// Description of dashboard.
|
||||
Description *string `json:"description,omitempty"`
|
||||
|
||||
// Whether a dashboard is editable or not.
|
||||
Editable bool `json:"editable"`
|
||||
|
||||
// TODO docs
|
||||
FiscalYearStartMonth *int `json:"fiscalYearStartMonth,omitempty"`
|
||||
GnetId *string `json:"gnetId,omitempty"`
|
||||
GraphTooltip ModelGraphTooltip `json:"graphTooltip"`
|
||||
|
||||
// Unique numeric identifier for the dashboard.
|
||||
// TODO must isolate or remove identifiers local to a Grafana instance...?
|
||||
Id *int64 `json:"id,omitempty"`
|
||||
|
||||
// TODO docs
|
||||
Links *[]ModelDashboardLink `json:"links,omitempty"`
|
||||
|
||||
// TODO docs
|
||||
LiveNow *bool `json:"liveNow,omitempty"`
|
||||
Panels *[]interface{} `json:"panels,omitempty"`
|
||||
|
||||
// TODO docs
|
||||
Refresh *interface{} `json:"refresh,omitempty"`
|
||||
|
||||
// Version of the JSON schema, incremented each time a Grafana update brings
|
||||
// changes to said schema.
|
||||
// TODO this is the existing schema numbering system. It will be replaced by Thema's themaVersion
|
||||
SchemaVersion int `json:"schemaVersion"`
|
||||
|
||||
// Theme of dashboard.
|
||||
Style ModelStyle `json:"style"`
|
||||
|
||||
// Tags associated with dashboard.
|
||||
Tags *[]string `json:"tags,omitempty"`
|
||||
Templating *struct {
|
||||
List []ModelVariableModel `json:"list"`
|
||||
} `json:"templating,omitempty"`
|
||||
|
||||
// Time range for dashboard, e.g. last 6 hours, last 7 days, etc
|
||||
Time *struct {
|
||||
From string `json:"from"`
|
||||
To string `json:"to"`
|
||||
} `json:"time,omitempty"`
|
||||
|
||||
// TODO docs
|
||||
// TODO this appears to be spread all over in the frontend. Concepts will likely need tidying in tandem with schema changes
|
||||
Timepicker *struct {
|
||||
// Whether timepicker is collapsed or not.
|
||||
Collapse bool `json:"collapse"`
|
||||
|
||||
// Whether timepicker is enabled or not.
|
||||
Enable bool `json:"enable"`
|
||||
|
||||
// Whether timepicker is visible or not.
|
||||
Hidden bool `json:"hidden"`
|
||||
|
||||
// Selectable intervals for auto-refresh.
|
||||
RefreshIntervals []string `json:"refresh_intervals"`
|
||||
} `json:"timepicker,omitempty"`
|
||||
|
||||
// Timezone of dashboard,
|
||||
Timezone *ModelTimezone `json:"timezone,omitempty"`
|
||||
|
||||
// Title of dashboard.
|
||||
Title *string `json:"title,omitempty"`
|
||||
|
||||
// Unique dashboard identifier that can be generated by anyone. string (8-40)
|
||||
Uid *string `json:"uid,omitempty"`
|
||||
|
||||
// Version of the dashboard, incremented each time the dashboard is updated.
|
||||
Version *int `json:"version,omitempty"`
|
||||
|
||||
// TODO docs
|
||||
WeekStart *string `json:"weekStart,omitempty"`
|
||||
}
|
||||
|
||||
// DashboardGraphTooltip defines model for Dashboard.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.
|
||||
type ModelGraphTooltip int
|
||||
|
||||
// Theme of 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.
|
||||
type ModelStyle string
|
||||
|
||||
// Timezone of 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.
|
||||
type ModelTimezone string
|
||||
|
||||
// TODO docs
|
||||
// FROM: AnnotationQuery in grafana-data/src/types/annotations.ts
|
||||
//
|
||||
// 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 ModelAnnotationQuery struct {
|
||||
BuiltIn int `json:"builtIn"`
|
||||
|
||||
// Datasource to use for annotation.
|
||||
Datasource struct {
|
||||
Type *string `json:"type,omitempty"`
|
||||
Uid *string `json:"uid,omitempty"`
|
||||
} `json:"datasource"`
|
||||
|
||||
// Whether annotation is enabled.
|
||||
Enable bool `json:"enable"`
|
||||
|
||||
// Whether to hide annotation.
|
||||
Hide *bool `json:"hide,omitempty"`
|
||||
|
||||
// Annotation icon color.
|
||||
IconColor *string `json:"iconColor,omitempty"`
|
||||
|
||||
// Name of annotation.
|
||||
Name *string `json:"name,omitempty"`
|
||||
|
||||
// Query for annotation data.
|
||||
RawQuery *string `json:"rawQuery,omitempty"`
|
||||
ShowIn int `json:"showIn"`
|
||||
|
||||
// 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
|
||||
// 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.
|
||||
Target *ModelTarget `json:"target,omitempty"`
|
||||
Type string `json:"type"`
|
||||
}
|
||||
|
||||
// 0 for no shared crosshair or tooltip (default).
|
||||
// 1 for shared crosshair.
|
||||
// 2 for shared crosshair AND shared tooltip.
|
||||
//
|
||||
// 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 ModelDashboardCursorSync int
|
||||
|
||||
// FROM public/app/features/dashboard/state/DashboardModels.ts - ish
|
||||
// TODO docs
|
||||
//
|
||||
// THIS TYPE IS INTENDED FOR INTERNAL USE BY THE GRAFANA BACKEND, AND IS SUBJECT TO BREAKING CHANGES.
|
||||
// Equivalent Go types at stable import paths are provided in https://github.com/grafana/grok.
|
||||
type ModelDashboardLink struct {
|
||||
AsDropdown bool `json:"asDropdown"`
|
||||
Icon *string `json:"icon,omitempty"`
|
||||
IncludeVars bool `json:"includeVars"`
|
||||
KeepTime bool `json:"keepTime"`
|
||||
Tags []string `json:"tags"`
|
||||
TargetBlank bool `json:"targetBlank"`
|
||||
Title string `json:"title"`
|
||||
Tooltip *string `json:"tooltip,omitempty"`
|
||||
Type ModelDashboardLinkType `json:"type"`
|
||||
Url *string `json:"url,omitempty"`
|
||||
}
|
||||
|
||||
// DashboardDashboardLinkType defines model for DashboardDashboardLink.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 ModelDashboardLinkType string
|
||||
|
||||
// TODO docs
|
||||
//
|
||||
// THIS TYPE IS INTENDED FOR INTERNAL USE BY THE GRAFANA BACKEND, AND IS SUBJECT TO BREAKING CHANGES.
|
||||
// Equivalent Go types at stable import paths are provided in https://github.com/grafana/grok.
|
||||
type ModelFieldColor struct {
|
||||
// Stores the fixed color value if mode is fixed
|
||||
FixedColor *string `json:"fixedColor,omitempty"`
|
||||
|
||||
// The main color scheme mode
|
||||
Mode interface{} `json:"mode"`
|
||||
|
||||
// TODO docs
|
||||
SeriesBy *ModelFieldColorSeriesByMode `json:"seriesBy,omitempty"`
|
||||
}
|
||||
|
||||
// TODO docs
|
||||
//
|
||||
// THIS TYPE IS INTENDED FOR INTERNAL USE BY THE GRAFANA BACKEND, AND IS SUBJECT TO BREAKING CHANGES.
|
||||
// Equivalent Go types at stable import paths are provided in https://github.com/grafana/grok.
|
||||
type ModelFieldColorModeId string
|
||||
|
||||
// TODO docs
|
||||
//
|
||||
// THIS TYPE IS INTENDED FOR INTERNAL USE BY THE GRAFANA BACKEND, AND IS SUBJECT TO BREAKING CHANGES.
|
||||
// Equivalent Go types at stable import paths are provided in https://github.com/grafana/grok.
|
||||
type ModelFieldColorSeriesByMode string
|
||||
|
||||
// DashboardGraphPanel defines model for 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.
|
||||
type ModelGraphPanel struct {
|
||||
// Support for legacy graph and heatmap panels.
|
||||
Type ModelGraphPanelType `json:"type"`
|
||||
}
|
||||
|
||||
// Support for legacy graph and heatmap panels.
|
||||
//
|
||||
// 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 ModelGraphPanelType string
|
||||
|
||||
// DashboardHeatmapPanel defines model for 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.
|
||||
type ModelHeatmapPanel struct {
|
||||
Type ModelHeatmapPanelType `json:"type"`
|
||||
}
|
||||
|
||||
// DashboardHeatmapPanelType defines model for DashboardHeatmapPanel.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 ModelHeatmapPanelType string
|
||||
|
||||
// Dashboard panels. Panels are canonically defined inline
|
||||
// because they share a version timeline with the dashboard
|
||||
// schema; they do not evolve independently.
|
||||
//
|
||||
// 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 ModelPanel struct {
|
||||
// The datasource used in all targets.
|
||||
Datasource *struct {
|
||||
Type *string `json:"type,omitempty"`
|
||||
Uid *string `json:"uid,omitempty"`
|
||||
} `json:"datasource,omitempty"`
|
||||
|
||||
// Description.
|
||||
Description *string `json:"description,omitempty"`
|
||||
FieldConfig struct {
|
||||
Defaults struct {
|
||||
// TODO docs
|
||||
Color *ModelFieldColor `json:"color,omitempty"`
|
||||
|
||||
// custom is specified by the PanelFieldConfig field
|
||||
// in panel plugin schemas.
|
||||
Custom *map[string]interface{} `json:"custom,omitempty"`
|
||||
|
||||
// Significant digits (for display)
|
||||
Decimals *float32 `json:"decimals,omitempty"`
|
||||
|
||||
// Human readable field metadata
|
||||
Description *string `json:"description,omitempty"`
|
||||
|
||||
// The display value for this field. This supports template variables blank is auto
|
||||
DisplayName *string `json:"displayName,omitempty"`
|
||||
|
||||
// This can be used by data sources that return and explicit naming structure for values and labels
|
||||
// When this property is configured, this value is used rather than the default naming strategy.
|
||||
DisplayNameFromDS *string `json:"displayNameFromDS,omitempty"`
|
||||
|
||||
// True if data source field supports ad-hoc filters
|
||||
Filterable *bool `json:"filterable,omitempty"`
|
||||
|
||||
// // The behavior when clicking on a result
|
||||
Links *[]interface{} `json:"links,omitempty"`
|
||||
|
||||
// Convert input values into a display string
|
||||
//
|
||||
// TODO this one corresponds to a complex type with
|
||||
// generics on the typescript side. Ouch. Will
|
||||
// either need special care, or we'll just need to
|
||||
// accept a very loosely specified schema. It's very
|
||||
// unlikely we'll be able to translate cue to
|
||||
// typescript generics in the general case, though
|
||||
// this particular one *may* be able to work.
|
||||
Mappings *[]map[string]interface{} `json:"mappings,omitempty"`
|
||||
Max *float32 `json:"max,omitempty"`
|
||||
Min *float32 `json:"min,omitempty"`
|
||||
|
||||
// Alternative to empty string
|
||||
NoValue *string `json:"noValue,omitempty"`
|
||||
|
||||
// An explict path to the field in the datasource. When the frame meta includes a path,
|
||||
// This will default to `${frame.meta.path}/${field.name}
|
||||
//
|
||||
// When defined, this value can be used as an identifier within the datasource scope, and
|
||||
// may be used to update the results
|
||||
Path *string `json:"path,omitempty"`
|
||||
Thresholds *ModelThresholdsConfig `json:"thresholds,omitempty"`
|
||||
|
||||
// Numeric Options
|
||||
Unit *string `json:"unit,omitempty"`
|
||||
|
||||
// True if data source can write a value to the path. Auth/authz are supported separately
|
||||
Writeable *bool `json:"writeable,omitempty"`
|
||||
} `json:"defaults"`
|
||||
Overrides []struct {
|
||||
Matcher struct {
|
||||
Id string `json:"id"`
|
||||
Options *interface{} `json:"options,omitempty"`
|
||||
} `json:"matcher"`
|
||||
Properties []struct {
|
||||
Id string `json:"id"`
|
||||
Value *interface{} `json:"value,omitempty"`
|
||||
} `json:"properties"`
|
||||
} `json:"overrides"`
|
||||
} `json:"fieldConfig"`
|
||||
|
||||
// Grid position.
|
||||
GridPos *struct {
|
||||
// Panel
|
||||
H int `json:"h"`
|
||||
|
||||
// true if fixed
|
||||
Static *bool `json:"static,omitempty"`
|
||||
|
||||
// Panel
|
||||
W int `json:"w"`
|
||||
|
||||
// Panel x
|
||||
X int `json:"x"`
|
||||
|
||||
// Panel y
|
||||
Y int `json:"y"`
|
||||
} `json:"gridPos,omitempty"`
|
||||
|
||||
// TODO docs
|
||||
Id *int `json:"id,omitempty"`
|
||||
|
||||
// TODO docs
|
||||
// TODO tighter constraint
|
||||
Interval *string `json:"interval,omitempty"`
|
||||
|
||||
// Panel links.
|
||||
// TODO fill this out - seems there are a couple variants?
|
||||
Links *[]ModelDashboardLink `json:"links,omitempty"`
|
||||
|
||||
// TODO docs
|
||||
MaxDataPoints *float32 `json:"maxDataPoints,omitempty"`
|
||||
|
||||
// options is specified by the PanelOptions field in panel
|
||||
// plugin schemas.
|
||||
Options map[string]interface{} `json:"options"`
|
||||
|
||||
// FIXME this almost certainly has to be changed in favor of scuemata versions
|
||||
PluginVersion *string `json:"pluginVersion,omitempty"`
|
||||
|
||||
// Name of template variable to repeat for.
|
||||
Repeat *string `json:"repeat,omitempty"`
|
||||
|
||||
// Direction to repeat in if 'repeat' is set.
|
||||
// "h" for horizontal, "v" for vertical.
|
||||
RepeatDirection ModelPanelRepeatDirection `json:"repeatDirection"`
|
||||
|
||||
// TODO docs
|
||||
Tags *[]string `json:"tags,omitempty"`
|
||||
|
||||
// TODO docs
|
||||
Targets *[]ModelTarget `json:"targets,omitempty"`
|
||||
|
||||
// TODO docs
|
||||
Thresholds *[]interface{} `json:"thresholds,omitempty"`
|
||||
|
||||
// TODO docs
|
||||
// TODO tighter constraint
|
||||
TimeFrom *string `json:"timeFrom,omitempty"`
|
||||
|
||||
// TODO docs
|
||||
TimeRegions *[]interface{} `json:"timeRegions,omitempty"`
|
||||
|
||||
// TODO docs
|
||||
// TODO tighter constraint
|
||||
TimeShift *string `json:"timeShift,omitempty"`
|
||||
|
||||
// Panel title.
|
||||
Title *string `json:"title,omitempty"`
|
||||
Transformations []struct {
|
||||
Id string `json:"id"`
|
||||
Options map[string]interface{} `json:"options"`
|
||||
} `json:"transformations"`
|
||||
|
||||
// Whether to display the panel without a background.
|
||||
Transparent bool `json:"transparent"`
|
||||
|
||||
// The panel plugin type id. May not be empty.
|
||||
Type string `json:"type"`
|
||||
}
|
||||
|
||||
// Direction to repeat in if 'repeat' is set.
|
||||
// "h" for horizontal, "v" for vertical.
|
||||
//
|
||||
// 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 ModelPanelRepeatDirection string
|
||||
|
||||
// Row panel
|
||||
//
|
||||
// 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 ModelRowPanel struct {
|
||||
Collapsed bool `json:"collapsed"`
|
||||
|
||||
// Name of default datasource.
|
||||
Datasource *struct {
|
||||
Type *string `json:"type,omitempty"`
|
||||
Uid *string `json:"uid,omitempty"`
|
||||
} `json:"datasource,omitempty"`
|
||||
GridPos *struct {
|
||||
// Panel
|
||||
H int `json:"h"`
|
||||
|
||||
// true if fixed
|
||||
Static *bool `json:"static,omitempty"`
|
||||
|
||||
// Panel
|
||||
W int `json:"w"`
|
||||
|
||||
// Panel x
|
||||
X int `json:"x"`
|
||||
|
||||
// Panel y
|
||||
Y int `json:"y"`
|
||||
} `json:"gridPos,omitempty"`
|
||||
Id int `json:"id"`
|
||||
Panels []interface{} `json:"panels"`
|
||||
|
||||
// Name of template variable to repeat for.
|
||||
Repeat *string `json:"repeat,omitempty"`
|
||||
Title *string `json:"title,omitempty"`
|
||||
Type ModelRowPanelType `json:"type"`
|
||||
}
|
||||
|
||||
// DashboardRowPanelType defines model for DashboardRowPanel.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 ModelRowPanelType 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
|
||||
// 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.
|
||||
//
|
||||
// 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 ModelTarget map[string]interface{}
|
||||
|
||||
// TODO docs
|
||||
//
|
||||
// THIS TYPE IS INTENDED FOR INTERNAL USE BY THE GRAFANA BACKEND, AND IS SUBJECT TO BREAKING CHANGES.
|
||||
// Equivalent Go types at stable import paths are provided in https://github.com/grafana/grok.
|
||||
type ModelThreshold struct {
|
||||
// TODO docs
|
||||
Color string `json:"color"`
|
||||
|
||||
// TODO docs
|
||||
// TODO are the values here enumerable into a disjunction?
|
||||
// Some seem to be listed in typescript comment
|
||||
State *string `json:"state,omitempty"`
|
||||
|
||||
// TODO docs
|
||||
// FIXME the corresponding typescript field is required/non-optional, but nulls currently appear here when serializing -Infinity to JSON
|
||||
Value *float32 `json:"value,omitempty"`
|
||||
}
|
||||
|
||||
// DashboardThresholdsConfig defines model for 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.
|
||||
type ModelThresholdsConfig struct {
|
||||
Mode ModelThresholdsConfigMode `json:"mode"`
|
||||
|
||||
// Must be sorted by 'value', first value is always -Infinity
|
||||
Steps []struct {
|
||||
// TODO docs
|
||||
Color string `json:"color"`
|
||||
|
||||
// TODO docs
|
||||
// TODO are the values here enumerable into a disjunction?
|
||||
// Some seem to be listed in typescript comment
|
||||
State *string `json:"state,omitempty"`
|
||||
|
||||
// TODO docs
|
||||
// FIXME the corresponding typescript field is required/non-optional, but nulls currently appear here when serializing -Infinity to JSON
|
||||
Value *float32 `json:"value,omitempty"`
|
||||
} `json:"steps"`
|
||||
}
|
||||
|
||||
// DashboardThresholdsConfigMode defines model for DashboardThresholdsConfig.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 ModelThresholdsConfigMode string
|
||||
|
||||
// DashboardThresholdsMode defines model for 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.
|
||||
type ModelThresholdsMode string
|
||||
|
||||
// TODO docs
|
||||
// FIXME this is extremely underspecfied; wasn't obvious which typescript types corresponded to it
|
||||
//
|
||||
// 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 ModelTransformation struct {
|
||||
Id string `json:"id"`
|
||||
Options map[string]interface{} `json:"options"`
|
||||
}
|
||||
|
||||
// FROM: packages/grafana-data/src/types/templateVars.ts
|
||||
// TODO docs
|
||||
// TODO what about what's in public/app/features/types.ts?
|
||||
// TODO there appear to be a lot of different kinds of [template] vars here? if so need a disjunction
|
||||
//
|
||||
// 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 ModelVariableModel struct {
|
||||
Label *string `json:"label,omitempty"`
|
||||
Name string `json:"name"`
|
||||
Type ModelVariableModelType `json:"type"`
|
||||
}
|
||||
|
||||
// DashboardVariableModelType defines model for DashboardVariableModel.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 ModelVariableModelType string
|
||||
|
||||
// FROM: packages/grafana-data/src/types/templateVars.ts
|
||||
// TODO docs
|
||||
// TODO this implies some wider pattern/discriminated union, probably?
|
||||
//
|
||||
// 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 ModelVariableType string
|
||||
|
||||
//go:embed lineage.cue
|
||||
var cueFS embed.FS
|
||||
|
||||
// codegen 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.
|
||||
//
|
||||
// The lineage is the canonical specification of the current dashboard schema,
|
||||
// all prior schema versions, and the mappings that allow migration between
|
||||
// schema versions.
|
||||
func Lineage(lib thema.Library, opts ...thema.BindOption) (thema.Lineage, error) {
|
||||
return cuectx.LoadGrafanaInstancesWithThema(filepath.Join("pkg", "coremodel", "dashboard"), cueFS, lib, opts...)
|
||||
}
|
||||
|
||||
var _ thema.LineageFactory = Lineage
|
||||
|
||||
// Coremodel contains the foundational schema declaration for dashboards.
|
||||
type Coremodel struct {
|
||||
lin thema.Lineage
|
||||
}
|
||||
|
||||
// Lineage returns the canonical dashboard Lineage.
|
||||
func (c *Coremodel) Lineage() thema.Lineage {
|
||||
return c.lin
|
||||
}
|
||||
|
||||
// CurrentSchema returns the current (latest) dashboard 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{}
|
||||
}
|
||||
|
||||
func ProvideCoremodel(lib thema.Library) (*Coremodel, error) {
|
||||
lin, err := Lineage(lib)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return &Coremodel{
|
||||
lin: lin,
|
||||
}, nil
|
||||
}
|
||||
@@ -0,0 +1,28 @@
|
||||
// This file is autogenerated. DO NOT EDIT.
|
||||
//
|
||||
// To regenerate, run "make gen-cue" from repository root.
|
||||
//
|
||||
// Derived from the Thema lineage at pkg/coremodel/dashboard
|
||||
|
||||
package dashboard
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/grafana/grafana/pkg/cuectx"
|
||||
"github.com/grafana/thema"
|
||||
)
|
||||
|
||||
func TestSchemaAssignability(t *testing.T) {
|
||||
lin, err := Lineage(cuectx.ProvideThemaLibrary())
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
sch := thema.SchemaP(lin, currentVersion)
|
||||
|
||||
err = thema.AssignableTo(sch, &Model{})
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
}
|
||||
@@ -136,7 +136,7 @@ seqs: [
|
||||
#VariableType: "query" | "adhoc" | "constant" | "datasource" | "interval" | "textbox" | "custom" | "system" @cuetsy(kind="type")
|
||||
|
||||
// TODO docs
|
||||
#FieldColorModeId: "thresholds" | "palette-classic" | "palette-saturated" | "continuous-GrYlRd" | "fixed" @cuetsy(kind="enum")
|
||||
#FieldColorModeId: "thresholds" | "palette-classic" | "palette-saturated" | "continuous-GrYlRd" | "fixed" @cuetsy(kind="enum",memberNames="Thresholds|PaletteClassic|PaletteSaturated|ContinuousGrYlRd|Fixed")
|
||||
|
||||
// TODO docs
|
||||
#FieldColorSeriesByMode: "min" | "max" | "last" @cuetsy(kind="type")
|
||||
|
||||
@@ -1,117 +0,0 @@
|
||||
package dashboard
|
||||
|
||||
import (
|
||||
"embed"
|
||||
"path/filepath"
|
||||
|
||||
"github.com/grafana/thema"
|
||||
|
||||
"github.com/grafana/grafana/pkg/cuectx"
|
||||
)
|
||||
|
||||
var (
|
||||
//go:embed lineage.cue
|
||||
cueFS embed.FS
|
||||
|
||||
// TODO: this should be generated by Thema.
|
||||
currentVersion = thema.SV(0, 0)
|
||||
)
|
||||
|
||||
// HandoffSchemaVersion is the minimum schemaVersion for dashboards at which the
|
||||
// Thema-based dashboard schema is known to be valid.
|
||||
//
|
||||
// schemaVersion is the original version numbering system for dashboards. If a
|
||||
// dashboard is below this schemaVersion, it is necessary for the frontend
|
||||
// typescript dashboard migration logic to first run and get it past this
|
||||
// number, after which Thema can take over.
|
||||
const HandoffSchemaVersion = 36
|
||||
|
||||
// Lineage returns the Thema lineage representing Grafana dashboards. The
|
||||
// lineage is the canonical specification of the current datasource schema, all
|
||||
// prior schema versions, and the mappings that allow migration between schema
|
||||
// versions.
|
||||
//
|
||||
// This is the base variant of the schema, which does not include any composed
|
||||
// plugin schemas.
|
||||
func Lineage(lib thema.Library, opts ...thema.BindOption) (thema.Lineage, error) {
|
||||
return cuectx.LoadGrafanaInstancesWithThema(filepath.Join("pkg", "coremodel", "dashboard"), cueFS, lib, opts...)
|
||||
}
|
||||
|
||||
// Model is a dummy struct stand-in for dashboards.
|
||||
//
|
||||
// It exists solely to trick compgen into accepting the dashboard coremodel as valid.
|
||||
type Model struct{}
|
||||
|
||||
// model is a hacky Go struct representing a dashboard.
|
||||
//
|
||||
// This exists solely because the coremodel framework enforces that there is a Go struct to which
|
||||
// all valid Thema schema instances can be assigned, per Thema's assignability checker. See
|
||||
// https://github.com/grafana/thema/blob/main/docs/invariants.md#go-assignability for rules.
|
||||
//
|
||||
// DO NOT RELY ON THIS FOR ANYTHING REAL. It is unclear whether we will ever attempt to have a correct, complete
|
||||
// Go struct representation of dashboards, let alone compress it into a single struct.
|
||||
type model struct {
|
||||
Title string `json:"title"`
|
||||
Description string `json:"description"`
|
||||
GnetId string `json:"gnetId"`
|
||||
Tags []string `json:"tags"`
|
||||
Style string `json:"style"`
|
||||
Timezone string `json:"timezone"`
|
||||
Editable bool `json:"editable"`
|
||||
GraphTooltip uint8 `json:"graphTooltip"`
|
||||
Time struct {
|
||||
From string `json:"from"`
|
||||
To string `json:"to"`
|
||||
} `json:"time"`
|
||||
Timepicker struct {
|
||||
Collapse bool `json:"collapse"`
|
||||
Enable bool `json:"enable"`
|
||||
Hidden bool `json:"hidden"`
|
||||
RefreshIntervals []string `json:"refresh_intervals"`
|
||||
} `json:"timepicker"`
|
||||
Templating struct {
|
||||
List []interface{} `json:"list"`
|
||||
} `json:"templating"`
|
||||
Annotations struct {
|
||||
List []struct {
|
||||
Name string `json:"name"`
|
||||
Type string `json:"type"`
|
||||
BuiltIn uint8 `json:"builtIn"`
|
||||
Datasource struct {
|
||||
Type string `json:"type"`
|
||||
Uid string `json:"uid"`
|
||||
} `json:"datasource"`
|
||||
Enable bool `json:"enable"`
|
||||
Hide bool `json:"hide,omitempty"`
|
||||
IconColor string `json:"iconColor"`
|
||||
RawQuery string `json:"rawQuery,omitempty"`
|
||||
ShowIn int `json:"showIn"`
|
||||
Target interface{} `json:"target"`
|
||||
} `json:"list"`
|
||||
} `json:"annotations"`
|
||||
Refresh interface{} `json:"refresh"` // (bool|string)
|
||||
SchemaVersion int `json:"schemaVersion"`
|
||||
Links []struct {
|
||||
Title string `json:"title"`
|
||||
Type string `json:"type"`
|
||||
Icon string `json:"icon,omitempty"`
|
||||
Tooltip string `json:"tooltip,omitempty"`
|
||||
Url string `json:"url,omitempty"`
|
||||
Tags []string `json:"tags"`
|
||||
AsDropdown bool `json:"asDropdown"`
|
||||
TargetBlank bool `json:"targetBlank"`
|
||||
IncludeVars bool `json:"includeVars"`
|
||||
KeepTime bool `json:"keepTime"`
|
||||
} `json:"links"`
|
||||
Panels []interface{} `json:"panels"`
|
||||
FiscalYearStartMonth uint8 `json:"fiscalYearStartMonth"`
|
||||
LiveNow bool `json:"liveNow"`
|
||||
WeekStart string `json:"weekStart"`
|
||||
|
||||
// //
|
||||
|
||||
Uid string `json:"uid"`
|
||||
// OrgId int64 `json:"orgId"`
|
||||
Id int64 `json:"id,omitempty"`
|
||||
Version int `json:"version"`
|
||||
}
|
||||
@@ -0,0 +1,93 @@
|
||||
// go:build ignore
|
||||
//go:build ignore
|
||||
// +build ignore
|
||||
|
||||
package main
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"io/ioutil"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
|
||||
"cuelang.org/go/cue/cuecontext"
|
||||
gcgen "github.com/grafana/grafana/pkg/codegen"
|
||||
"github.com/grafana/thema"
|
||||
)
|
||||
|
||||
var lib = thema.NewLibrary(cuecontext.New())
|
||||
|
||||
const sep = string(filepath.Separator)
|
||||
|
||||
// Generate Go and Typescript implementations for all coremodels, and populate the
|
||||
// coremodel static registry.
|
||||
func main() {
|
||||
if len(os.Args) > 1 {
|
||||
fmt.Fprintf(os.Stderr, "coremodel code generator does not currently accept any arguments\n, got %q", os.Args)
|
||||
os.Exit(1)
|
||||
}
|
||||
|
||||
cwd, err := os.Getwd()
|
||||
if err != nil {
|
||||
fmt.Fprintf(os.Stderr, "could not get working directory: %s", err)
|
||||
os.Exit(1)
|
||||
}
|
||||
|
||||
// TODO this binds us to only having coremodels in a single directory. If we need more, compgen is the way
|
||||
grootp := strings.Split(cwd, sep)
|
||||
groot := filepath.Join(sep, filepath.Join(grootp[:len(grootp)-3]...))
|
||||
|
||||
cmroot := filepath.Join(groot, "pkg", "coremodel")
|
||||
tsroot := filepath.Join(groot, "packages", "grafana-schema", "src", "schema")
|
||||
|
||||
items, err := ioutil.ReadDir(cmroot)
|
||||
if err != nil {
|
||||
fmt.Fprintf(os.Stderr, "could not read coremodels parent dir %s: %s\n", cmroot, err)
|
||||
os.Exit(1)
|
||||
}
|
||||
|
||||
var lins []*gcgen.ExtractedLineage
|
||||
for _, item := range items {
|
||||
if item.IsDir() {
|
||||
lin, err := gcgen.ExtractLineage(filepath.Join(cmroot, item.Name(), "lineage.cue"), lib)
|
||||
if err != nil {
|
||||
fmt.Fprintf(os.Stderr, "could not process coremodel dir %s: %s\n", cmroot, err)
|
||||
os.Exit(1)
|
||||
}
|
||||
|
||||
lins = append(lins, lin)
|
||||
}
|
||||
}
|
||||
|
||||
wd := gcgen.NewWriteDiffer()
|
||||
for _, ls := range lins {
|
||||
wdg, err := ls.GenerateGoCoremodel(filepath.Join(cmroot, ls.Lineage.Name()))
|
||||
if err != nil {
|
||||
fmt.Fprintf(os.Stderr, "failed to generate Go for %s: %s\n", ls.Lineage.Name(), err)
|
||||
os.Exit(1)
|
||||
}
|
||||
wd.Merge(wdg)
|
||||
|
||||
wdt, err := ls.GenerateTypescriptCoremodel(filepath.Join(tsroot, ls.Lineage.Name()))
|
||||
if err != nil {
|
||||
fmt.Fprintf(os.Stderr, "failed to generate TypeScript for %s: %s\n", ls.Lineage.Name(), err)
|
||||
os.Exit(1)
|
||||
}
|
||||
wd.Merge(wdt)
|
||||
}
|
||||
|
||||
if _, set := os.LookupEnv("CODEGEN_VERIFY"); set {
|
||||
err = wd.Verify()
|
||||
if err != nil {
|
||||
fmt.Fprintf(os.Stderr, "generated code is not up to date:\n%s\nrun `make gen-cue` to regenerate\n\n", err)
|
||||
os.Exit(1)
|
||||
}
|
||||
} else {
|
||||
err = wd.Write()
|
||||
if err != nil {
|
||||
fmt.Fprintf(os.Stderr, "error while writing generated code to disk:\n%s\n", err)
|
||||
os.Exit(1)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,5 +1,9 @@
|
||||
package coremodel
|
||||
|
||||
// Generates all code derived from coremodel Thema lineages that's used directly
|
||||
// by both the frontend and backend.
|
||||
//go:generate go run gen.go
|
||||
|
||||
import (
|
||||
"github.com/grafana/thema"
|
||||
)
|
||||
|
||||
Reference in New Issue
Block a user