diff --git a/.github/CODEOWNERS b/.github/CODEOWNERS index 78df7648155f2..a7115b5455af8 100644 --- a/.github/CODEOWNERS +++ b/.github/CODEOWNERS @@ -218,7 +218,15 @@ lerna.json @grafana/frontend-ops # Grafana Partnerships Team /pkg/infra/httpclient/httpclientprovider/sigv4_middleware.go @grafana/grafana-partnerships-team -# Schema framework and code generation +# Kind system and code generation +embed.go @grafana/grafana-as-code +/kinds/ @grafana/grafana-as-code /pkg/codegen @grafana/grafana-as-code -/pkg/framework/coremodel @grafana/grafana-as-code +/pkg/kindsys @grafana/grafana-as-code +/pkg/kinds/*/*_gen.go @grafana/grafana-as-code +/pkg/registry/corekind @grafana/grafana-as-code /public/app/plugins/*gen.go @grafana/grafana-as-code + +# Specific core kinds +/kinds/raw/ @grafana/grafana-edge-squad +/kinds/structured/dashboard @grafana/dashboards-squad diff --git a/.gitignore b/.gitignore index a77f35f874761..c7f6ce8ec9e8e 100644 --- a/.gitignore +++ b/.gitignore @@ -170,14 +170,8 @@ compilation-stats.json # auto generated frontend docs /docs/sources/packages_api -# auto generated Go files -*_gen.go -!pkg/services/featuremgmt/toggles_gen.go -!pkg/coremodel/**/*_gen.go -!pkg/framework/**/*_gen.go -!pkg/plugins/pfs/**/*_gen.go -!public/app/plugins/**/*_gen.go -!pkg/services/publicdashboards/*_gen.go +# wire generated files +**/wire_gen.go # Auto-generated internationalization files public/locales/_build/ diff --git a/LICENSING.md b/LICENSING.md index fdb32e2906c0f..7b64973e012a7 100644 --- a/LICENSING.md +++ b/LICENSING.md @@ -17,10 +17,11 @@ packages/grafana-toolkit/ packages/grafana-ui/ packages/jaeger-ui-components/ packaging/ -pkg/coremodel/ -pkg/framework/coremodel/ +kinds/ +pkg/kinds/ +pkg/kindsys/ +pkg/registry/corekind/ grafana-mixin/ -cue/ public/app/plugins/datasource/tempo public/img/icons/solid/ public/img/icons/unicons/ diff --git a/Makefile b/Makefile index 41475f2c01c01..df5598c1bf94e 100644 --- a/Makefile +++ b/Makefile @@ -66,6 +66,7 @@ openapi3-gen: swagger-api-spec ## Generates OpenApi 3 specs from the Swagger 2 a ##@ Building gen-cue: ## Do all CUE/Thema code generation @echo "generate code from .cue files" + go generate ./kinds/gen.go go generate ./pkg/framework/coremodel go generate ./public/app/plugins diff --git a/embed.go b/embed.go index 779d369af34a7..c135376178198 100644 --- a/embed.go +++ b/embed.go @@ -6,5 +6,5 @@ import ( // CueSchemaFS embeds all schema-related CUE files in the Grafana project. // -//go:embed cue.mod/module.cue packages/grafana-schema/src/schema/*.cue public/app/plugins/*/*/*.cue public/app/plugins/*/*/plugin.json pkg/framework/coremodel/*.cue +//go:embed cue.mod/module.cue kinds/*/*.cue kinds/*/*/*.cue packages/grafana-schema/src/schema/*.cue public/app/plugins/*/*/*.cue public/app/plugins/*/*/plugin.json pkg/framework/coremodel/*.cue pkg/kindsys/*.cue var CueSchemaFS embed.FS diff --git a/go.mod b/go.mod index b3c2668a26e13..5020e35ccb139 100644 --- a/go.mod +++ b/go.mod @@ -61,7 +61,7 @@ require ( github.com/grafana/grafana-aws-sdk v0.11.0 github.com/grafana/grafana-azure-sdk-go v1.3.1 github.com/grafana/grafana-plugin-sdk-go v0.142.0 - github.com/grafana/thema v0.0.0-20220929145912-2c7c4a7bb20b + github.com/grafana/thema v0.0.0-20221107225215-00ad2949c7bc github.com/grpc-ecosystem/go-grpc-middleware v1.3.0 github.com/hashicorp/go-hclog v1.0.0 github.com/hashicorp/go-plugin v1.4.3 @@ -110,7 +110,7 @@ require ( golang.org/x/exp v0.0.0-20220613132600-b0d781184e0d golang.org/x/net v0.0.0-20220909164309-bea034e7d591 // indirect golang.org/x/oauth2 v0.0.0-20220630143837-2104d58473e0 - golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4 + golang.org/x/sync v0.1.0 golang.org/x/time v0.0.0-20220609170525-579cf78fd858 golang.org/x/tools v0.1.12 gonum.org/v1/gonum v0.11.0 @@ -250,11 +250,13 @@ require ( github.com/bufbuild/connect-go v1.0.0 github.com/dlmiddlecote/sqlstats v1.0.2 github.com/drone/drone-cli v1.6.1 - github.com/getkin/kin-openapi v0.94.0 + github.com/getkin/kin-openapi v0.103.0 github.com/golang-migrate/migrate/v4 v4.7.0 github.com/google/go-github/v45 v45.2.0 + github.com/grafana/codejen v0.0.2 github.com/grafana/dskit v0.0.0-20211011144203-3a88ec0b675f github.com/jmoiron/sqlx v1.3.5 + github.com/kr/pretty v0.3.0 github.com/matryer/is v1.4.0 github.com/parca-dev/parca v0.12.1 github.com/urfave/cli v1.22.9 @@ -285,11 +287,13 @@ require ( github.com/gosimple/unidecode v1.0.1 // indirect github.com/hashicorp/go-immutable-radix v1.3.1 // indirect github.com/hashicorp/memberlist v0.4.0 // indirect + github.com/invopop/yaml v0.1.0 // indirect github.com/kr/text v0.2.0 // indirect github.com/mattn/go-colorable v0.1.12 // indirect github.com/mattn/go-ieproxy v0.0.3 // indirect github.com/mitchellh/mapstructure v1.4.3 // indirect github.com/rivo/uniseg v0.2.0 // indirect + github.com/rogpeppe/go-internal v1.8.1 // indirect github.com/russross/blackfriday/v2 v2.1.0 // indirect github.com/segmentio/asm v1.1.4 // indirect go.starlark.net v0.0.0-20221020143700-22309ac47eac // indirect diff --git a/go.sum b/go.sum index 7c1c6710824a9..ddf28204f3271 100644 --- a/go.sum +++ b/go.sum @@ -847,8 +847,9 @@ github.com/gdamore/encoding v1.0.0/go.mod h1:alR0ol34c49FCSBLjhosxzcPHQbf2trDkoo github.com/gdamore/tcell v1.3.0/go.mod h1:Hjvr+Ofd+gLglo7RYKxxnzCBmev3BzsS67MebKS4zMM= github.com/getkin/kin-openapi v0.53.0/go.mod h1:7Yn5whZr5kJi6t+kShccXS8ae1APpYTW6yheSwk8Yi4= github.com/getkin/kin-openapi v0.61.0/go.mod h1:7Yn5whZr5kJi6t+kShccXS8ae1APpYTW6yheSwk8Yi4= -github.com/getkin/kin-openapi v0.94.0 h1:bAxg2vxgnHHHoeefVdmGbR+oxtJlcv5HsJJa3qmAHuo= github.com/getkin/kin-openapi v0.94.0/go.mod h1:LWZfzOd7PRy8GJ1dJ6mCU6tNdSfOwRac1BUPam4aw6Q= +github.com/getkin/kin-openapi v0.103.0 h1:F5wAtaQvPWxKCAYZ69LgHAThgu16p4u41VQtbn1U8LA= +github.com/getkin/kin-openapi v0.103.0/go.mod h1:w4lRPHiyOdwGbOkLIyk+P0qCwlu7TXPCHD/64nSXzgE= github.com/getsentry/raven-go v0.2.0/go.mod h1:KungGk8q33+aIAZUIVWZDr2OfAEBsO49PX4NzFV5kcQ= github.com/getsentry/sentry-go v0.13.0 h1:20dgTiUSfxRB/EhMPtxcL9ZEbM1ZdR+W/7f7NWD+xWo= github.com/getsentry/sentry-go v0.13.0/go.mod h1:EOsfu5ZdvKPfeHYV6pTVQnsjfp30+XA7//UooKNumH0= @@ -1347,6 +1348,8 @@ github.com/gosimple/slug v1.12.0 h1:xzuhj7G7cGtd34NXnW/yF0l+AGNfWqwgh/IXgFy7dnc= github.com/gosimple/slug v1.12.0/go.mod h1:UiRaFH+GEilHstLUmcBgWcI42viBN7mAb818JrYOeFQ= github.com/gosimple/unidecode v1.0.1 h1:hZzFTMMqSswvf0LBJZCZgThIZrpDHFXux9KeGmn6T/o= github.com/gosimple/unidecode v1.0.1/go.mod h1:CP0Cr1Y1kogOtx0bJblKzsVWrqYaqfNOnHzpgWw4Awc= +github.com/grafana/codejen v0.0.2 h1:Ssp27X7SOnYxaPUTByW/6201tNV5Q60l1BSF+s3lRP8= +github.com/grafana/codejen v0.0.2/go.mod h1:zmwwM/DRyQB7pfuBjTWII3CWtxcXh8LTwAYGfDfpR6s= github.com/grafana/cuetsy v0.1.1 h1:+1jaDDYCpvKlcOWJgBRbkc5+VZIClCEn5mbI+4PLZqM= github.com/grafana/cuetsy v0.1.1/go.mod h1:4KWkUOslwvRTpEv7wdQG0jDFTuJmU+0L9x0h4kWxa2A= github.com/grafana/dskit v0.0.0-20211011144203-3a88ec0b675f h1:FvvSVEbnGeM2bUivGmsiXTi8URJyBU7TcFEEoRe5wWI= @@ -1366,8 +1369,8 @@ github.com/grafana/prometheus-alertmanager v0.24.1-0.20221012142027-823cd9150293 github.com/grafana/prometheus-alertmanager v0.24.1-0.20221012142027-823cd9150293/go.mod h1:HVHqK+BVPa/tmL8EMhLCCrPt2a1GdJpEyxr5hgur2UI= github.com/grafana/saml v0.4.9-0.20220727151557-61cd9c9353fc h1:1PY8n+rXuBNr3r1JQhoytWDCpc+pq+BibxV0SZv+Cr4= github.com/grafana/saml v0.4.9-0.20220727151557-61cd9c9353fc/go.mod h1:9Zh6dWPtB3MSzTRt8fIFH60Z351QQ+s7hCU3J/tTlA4= -github.com/grafana/thema v0.0.0-20220929145912-2c7c4a7bb20b h1:OEGzlaj04LE6Eq7aGMOh0bCplGW5rXNeSSSwgamPBEY= -github.com/grafana/thema v0.0.0-20220929145912-2c7c4a7bb20b/go.mod h1:i3/NX50sNrwsPSAQAj56ckjQTb4biaYG/6y+zyKgpb0= +github.com/grafana/thema v0.0.0-20221107225215-00ad2949c7bc h1:Icv777/PBaqhLmbSBSDaajDl424cbmh5ee77Du2rUFE= +github.com/grafana/thema v0.0.0-20221107225215-00ad2949c7bc/go.mod h1:wnIJykzNiNVANl6g/Z4nkXxoMqaaH1LoG0IPNW++BEk= github.com/grafana/xorm v0.8.3-0.20220614223926-2fcda7565af6 h1:I9dh1MXGX0wGyxdV/Sl7+ugnki4Dfsy8lv2s5Yf887o= github.com/grafana/xorm v0.8.3-0.20220614223926-2fcda7565af6/go.mod h1:ZkJLEYLoVyg7amJK/5r779bHyzs2AU8f8VMiP6BM7uY= github.com/gregjones/httpcache v0.0.0-20180305231024-9cad4c3443a7/go.mod h1:FecbI9+v66THATjSRHfNgh1IVFe/9kFxbXtjV0ctIMA= @@ -1552,6 +1555,8 @@ github.com/influxdata/roaring v0.4.13-0.20180809181101-fc520f41fab6/go.mod h1:bS github.com/influxdata/tdigest v0.0.0-20181121200506-bf2b5ad3c0a9/go.mod h1:Js0mqiSBE6Ffsg94weZZ2c+v/ciT8QRHFOap7EKDrR0= github.com/influxdata/tdigest v0.0.2-0.20210216194612-fc98d27c9e8b/go.mod h1:Z0kXnxzbTC2qrx4NaIzYkE1k66+6oEDQTvL95hQFh5Y= github.com/influxdata/usage-client v0.0.0-20160829180054-6d3895376368/go.mod h1:Wbbw6tYNvwa5dlB6304Sd+82Z3f7PmVZHVKU637d4po= +github.com/invopop/yaml v0.1.0 h1:YW3WGUoJEXYfzWBjn00zIlrw7brGVD0fUKRYDPAPhrc= +github.com/invopop/yaml v0.1.0/go.mod h1:2XuRLgs/ouIrW3XNzuNj7J3Nvu/Dig5MXvbCEdiBN3Q= github.com/j-keck/arping v0.0.0-20160618110441-2cf9dc699c56/go.mod h1:ymszkNOg6tORTn+6F6j+Jc8TOr5osrynvN6ivFWZ2GA= github.com/jackc/chunkreader v1.0.0/go.mod h1:RT6O25fNZIuasFJRyZ4R/Y2BbhasbmZXF9QQ7T3kePo= github.com/jackc/chunkreader/v2 v2.0.0/go.mod h1:odVSm741yZoC3dpHEUXIqA9tQRhFrgOHwnPIn9lDKlk= @@ -2888,8 +2893,9 @@ golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJ golang.org/x/sync v0.0.0-20201207232520-09787c993a3a/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20210220032951-036812b2e83c/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20220601150217-0de741cfad7f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= -golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4 h1:uVc8UZUe6tr40fFVnUP5Oj+veunVezqYl9z7DYw9xzw= golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.1.0 h1:wsuoTGHzEhffawBOhz5CYhcrV4IdKZbEyZjBMuTp12o= +golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sys v0.0.0-20170830134202-bb24a47a89ea/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20180823144017-11551d06cbcc/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= @@ -3376,6 +3382,7 @@ gopkg.in/yaml.v3 v3.0.0-20200603094226-e3079894b1e8/go.mod h1:K4uyk7z7BCEPqu6E+C gopkg.in/yaml.v3 v3.0.0-20200605160147-a5ece683394c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= gopkg.in/yaml.v3 v3.0.0-20200615113413-eeeca48fe776/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= +gopkg.in/yaml.v3 v3.0.0/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= gotest.tools v2.2.0+incompatible h1:VsBPFP1AI068pPrMxtb/S8Zkgf9xEmTLJjfM+P5UIEo= diff --git a/kinds/gen.go b/kinds/gen.go new file mode 100644 index 0000000000000..8fd79172c7fa1 --- /dev/null +++ b/kinds/gen.go @@ -0,0 +1,157 @@ +//go:build ignore +// +build ignore + +//go:generate go run gen.go + +package main + +import ( + "context" + "fmt" + "io/fs" + "os" + "path/filepath" + "sort" + "strings" + + "cuelang.org/go/cue/errors" + "github.com/grafana/codejen" + "github.com/grafana/grafana/pkg/codegen" + "github.com/grafana/grafana/pkg/cuectx" + "github.com/grafana/grafana/pkg/kindsys" +) + +const sep = string(filepath.Separator) + +func main() { + if len(os.Args) > 1 { + fmt.Fprintf(os.Stderr, "plugin thema code generator does not currently accept any arguments\n, got %q", os.Args) + os.Exit(1) + } + + // Core kinds composite code generator. Produces all generated code in + // grafana/grafana that derives from raw and structured core kinds. + coreKindsGen := codejen.JennyListWithNamer[*codegen.DeclForGen](func(decl *codegen.DeclForGen) string { + return decl.Meta.Common().MachineName + }) + + // All the jennies that comprise the core kinds generator pipeline + coreKindsGen.Append( + codegen.GoTypesJenny(kindsys.GoCoreKindParentPath, nil), + codegen.CoreStructuredKindJenny(kindsys.GoCoreKindParentPath, nil), + codegen.RawKindJenny(kindsys.GoCoreKindParentPath, nil), + codegen.BaseCoreRegistryJenny(filepath.Join("pkg", "registry", "corekind"), kindsys.GoCoreKindParentPath), + codegen.TSTypesJenny(kindsys.TSCoreKindParentPath, &codegen.TSTypesGeneratorConfig{ + GenDirName: func(decl *codegen.DeclForGen) string { + // FIXME this hardcodes always generating to experimental dir. OK for now, but need generator fanout + return filepath.Join(decl.Meta.Common().MachineName, "x") + }, + }), + codegen.TSVeneerIndexJenny(filepath.Join("packages", "grafana-schema", "src")), + ) + + coreKindsGen.AddPostprocessors(codegen.SlashHeaderMapper("kinds/gen.go")) + + cwd, err := os.Getwd() + if err != nil { + fmt.Fprintf(os.Stderr, "could not get working directory: %s", err) + os.Exit(1) + } + grootp := strings.Split(cwd, sep) + groot := filepath.Join(sep, filepath.Join(grootp[:len(grootp)-1]...)) + + rt := cuectx.GrafanaThemaRuntime() + var all []*codegen.DeclForGen + + // structured kinddirs first + f := os.DirFS(filepath.Join(groot, kindsys.CoreStructuredDeclParentPath)) + kinddirs := elsedie(fs.ReadDir(f, "."))("error reading structured fs root directory") + for _, ent := range kinddirs { + if !ent.IsDir() { + continue + } + rel := filepath.Join(kindsys.CoreStructuredDeclParentPath, ent.Name()) + decl, err := kindsys.LoadCoreKind[kindsys.CoreStructuredMeta](rel, rt.Context(), nil) + if err != nil { + die(fmt.Errorf("%s is not a valid kind: %s", rel, errors.Details(err, nil))) + } + if decl.Meta.MachineName != ent.Name() { + die(fmt.Errorf("%s: kind's machine name (%s) must equal parent dir name (%s)", rel, decl.Meta.Name, ent.Name())) + } + + all = append(all, elsedie(codegen.ForGen(rt, decl.Some()))(rel)) + } + + // now raw kinddirs + f = os.DirFS(filepath.Join(groot, kindsys.RawDeclParentPath)) + kinddirs = elsedie(fs.ReadDir(f, "."))("error reading raw fs root directory") + for _, ent := range kinddirs { + if !ent.IsDir() { + continue + } + rel := filepath.Join(kindsys.RawDeclParentPath, ent.Name()) + decl, err := kindsys.LoadCoreKind[kindsys.RawMeta](rel, rt.Context(), nil) + if err != nil { + die(fmt.Errorf("%s is not a valid kind: %s", rel, errors.Details(err, nil))) + } + if decl.Meta.MachineName != ent.Name() { + die(fmt.Errorf("%s: kind's machine name (%s) must equal parent dir name (%s)", rel, decl.Meta.Name, ent.Name())) + } + dfg, _ := codegen.ForGen(nil, decl.Some()) + all = append(all, dfg) + } + + sort.Slice(all, func(i, j int) bool { + return nameFor(all[i].Meta) < nameFor(all[j].Meta) + }) + + jfs, err := coreKindsGen.GenerateFS(all) + if err != nil { + die(fmt.Errorf("core kinddirs codegen failed: %w", err)) + } + // for _, f := range jfs.AsFiles() { + // fmt.Println(filepath.Join(groot, f.RelativePath)) + // } + + if _, set := os.LookupEnv("CODEGEN_VERIFY"); set { + if err = jfs.Verify(context.Background(), groot); err != nil { + die(fmt.Errorf("generated code is out of sync with inputs:\n%s\nrun `make gen-cue` to regenerate", err)) + } + } else if err = jfs.Write(context.Background(), groot); err != nil { + die(fmt.Errorf("error while writing generated code to disk:\n%s", err)) + } +} + +func nameFor(m kindsys.SomeKindMeta) string { + switch x := m.(type) { + case kindsys.RawMeta: + return x.Name + case kindsys.CoreStructuredMeta: + return x.Name + case kindsys.CustomStructuredMeta: + return x.Name + case kindsys.ComposableMeta: + return x.Name + default: + // unreachable so long as all the possibilities in KindMetas have switch branches + panic("unreachable") + } +} + +func elsedie[T any](t T, err error) func(msg string) T { + if err != nil { + return func(msg string) T { + fmt.Fprintf(os.Stderr, "%s: %s\n", msg, err) + os.Exit(1) + return t + } + } + return func(msg string) T { + return t + } +} + +func die(err error) { + fmt.Fprint(os.Stderr, err, "\n") + os.Exit(1) +} diff --git a/kinds/raw/constraint.cue b/kinds/raw/constraint.cue new file mode 100644 index 0000000000000..bcd29fd1f3779 --- /dev/null +++ b/kinds/raw/constraint.cue @@ -0,0 +1,7 @@ +package kind + +import "github.com/grafana/grafana/pkg/kindsys" + +// In each child directory, the set of .cue files with 'package kind' +// must be an instance of kindsys.#Raw - a declaration of a raw kind. +kindsys.#Raw diff --git a/kinds/raw/svg/svg_kind.cue b/kinds/raw/svg/svg_kind.cue new file mode 100644 index 0000000000000..0fff1c1186cab --- /dev/null +++ b/kinds/raw/svg/svg_kind.cue @@ -0,0 +1,4 @@ +package kind + +name: "SVG" +extensions: ["svg"] diff --git a/kinds/structured/constraint.cue b/kinds/structured/constraint.cue new file mode 100644 index 0000000000000..e3a5937767ffe --- /dev/null +++ b/kinds/structured/constraint.cue @@ -0,0 +1,8 @@ +package kind + +import "github.com/grafana/grafana/pkg/kindsys" + +// In each child directory, the set of .cue files with 'package kind' +// must be an instance of kindsys.#CoreStructured - a declaration of a +// structured kind. +kindsys.#CoreStructured diff --git a/pkg/coremodel/dashboard/coremodel.cue b/kinds/structured/dashboard/dashboard_kind.cue similarity index 96% rename from pkg/coremodel/dashboard/coremodel.cue rename to kinds/structured/dashboard/dashboard_kind.cue index 7fc09b3cb7890..c0e9ddc692762 100644 --- a/pkg/coremodel/dashboard/coremodel.cue +++ b/kinds/structured/dashboard/dashboard_kind.cue @@ -1,18 +1,15 @@ -package dashboard +package kind -import ( - "strings" +import "strings" - "github.com/grafana/thema" -) +name: "Dashboard" +maturity: "merged" -thema.#Lineage -name: "dashboard" -seqs: [ +lineage: seqs: [ { schemas: [ {// 0.0 - @grafana(TSVeneer="type") + @grafana(TSVeneer="type") // Unique numeric identifier for the dashboard. // TODO must isolate or remove identifiers local to a Grafana instance...? @@ -84,12 +81,13 @@ seqs: [ /////////////////////////////////////// // Definitions (referenced above) are declared below + // TODO docs #AnnotationTarget: { - limit: int64 + limit: int64 matchAny: bool tags: [...string] type: string - } + } @cuetsy(kind="interface") @grafanamaturity(NeedsExpertReview) // TODO docs // FROM: AnnotationQuery in grafana-data/src/types/annotations.ts @@ -111,9 +109,9 @@ seqs: [ iconColor?: string @grafanamaturity(NeedsExpertReview) type: string | *"dashboard" @grafanamaturity(NeedsExpertReview) // Query for annotation data. - rawQuery?: string @grafanamaturity(NeedsExpertReview) - showIn: uint8 | *0 @grafanamaturity(NeedsExpertReview) - target?: #AnnotationTarget @grafanamaturity(NeedsExpertReview) + rawQuery?: string @grafanamaturity(NeedsExpertReview) + showIn: uint8 | *0 @grafanamaturity(NeedsExpertReview) + target?: #AnnotationTarget @grafanamaturity(NeedsExpertReview) } @cuetsy(kind="interface") // FROM: packages/grafana-data/src/types/templateVars.ts @@ -237,7 +235,7 @@ seqs: [ #SpecialValueMap: { type: #MappingType & "special" options: { - match: "true" | "false" + match: "true" | "false" pattern: string result: #ValueMappingResult } @@ -376,7 +374,7 @@ seqs: [ // Human readable field metadata description?: string @grafanamaturity(NeedsExpertReview) - // An explict path to the field in the datasource. When the frame meta includes a path, + // An explicit 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 diff --git a/pkg/coremodel/playlist/coremodel.cue b/kinds/structured/playlist/playlist_kind.cue similarity index 94% rename from pkg/coremodel/playlist/coremodel.cue rename to kinds/structured/playlist/playlist_kind.cue index f12338f1d617f..6b3865b9d8560 100644 --- a/pkg/coremodel/playlist/coremodel.cue +++ b/kinds/structured/playlist/playlist_kind.cue @@ -1,12 +1,9 @@ -package playlist +package kind -import ( - "github.com/grafana/thema" -) +name: "Playlist" +maturity: "merged" -thema.#Lineage -name: "playlist" -seqs: [ +lineage: seqs: [ { schemas: [ {//0.0 diff --git a/packages/grafana-schema/src/index.gen.ts b/packages/grafana-schema/src/index.gen.ts index 6b17d832f7af1..459c68aa6d7dc 100644 --- a/packages/grafana-schema/src/index.gen.ts +++ b/packages/grafana-schema/src/index.gen.ts @@ -1,11 +1,15 @@ -// This file is autogenerated. DO NOT EDIT. +// THIS FILE IS GENERATED. EDITING IS FUTILE. // -// Generated by pkg/framework/coremodel/gen.go +// Generated by: +// kinds/gen.go +// Using jennies: +// TSVeneerIndexJenny // -// Run `make gen-cue` from repository root to regenerate. +// Run 'make gen-cue' from repository root to regenerate. -// Raw generated types from dashboard entity type. +// Raw generated types from Dashboard kind. export type { + AnnotationTarget, AnnotationQuery, VariableModel, DashboardLink, @@ -30,10 +34,11 @@ export type { DashboardCursorSync, MatcherConfig, RowPanel -} from './raw/dashboard/x/dashboard.gen'; +} from './raw/dashboard/x/dashboard_types.gen'; -// Raw generated default consts from dashboard entity type. +// Raw generated default consts from dashboard kind. export { + defaultAnnotationTarget, defaultAnnotationQuery, defaultDashboardLink, defaultGridPos, @@ -41,10 +46,10 @@ export { defaultDashboardCursorSync, defaultMatcherConfig, defaultRowPanel -} from './raw/dashboard/x/dashboard.gen'; +} from './raw/dashboard/x/dashboard_types.gen'; -// The following exported declarations correspond to types in the dashboard@0.0 schema with -// attribute @grafana(TSVeneer="type"). (lineage declared in file: pkg/coremodel/dashboard/coremodel.cue) +// The following exported declarations correspond to types in the dashboard@0.0 kind's +// schema with attribute @grafana(TSVeneer="type"). // // The handwritten file for these type and default veneers is expected to be at // packages/grafana-schema/src/veneer/dashboard.types.ts. @@ -59,8 +64,8 @@ export type { FieldConfig } from './veneer/dashboard.types'; -// The following exported declarations correspond to types in the dashboard@0.0 schema with -// attribute @grafana(TSVeneer="type"). (lineage declared in file: pkg/coremodel/dashboard/coremodel.cue) +// The following exported declarations correspond to types in the dashboard@0.0 kind's +// schema with attribute @grafana(TSVeneer="type"). // // The handwritten file for these type and default veneers is expected to be at // packages/grafana-schema/src/veneer/dashboard.types.ts. @@ -75,11 +80,11 @@ export { defaultFieldConfig } from './veneer/dashboard.types'; -// Raw generated types from playlist entity type. +// Raw generated types from Playlist kind. export type { Playlist, PlaylistItem -} from './raw/playlist/x/playlist.gen'; +} from './raw/playlist/x/playlist_types.gen'; -// Raw generated default consts from playlist entity type. -export { defaultPlaylist } from './raw/playlist/x/playlist.gen'; +// Raw generated default consts from playlist kind. +export { defaultPlaylist } from './raw/playlist/x/playlist_types.gen'; diff --git a/packages/grafana-schema/src/raw/dashboard/x/dashboard.gen.ts b/packages/grafana-schema/src/raw/dashboard/x/dashboard_types.gen.ts similarity index 96% rename from packages/grafana-schema/src/raw/dashboard/x/dashboard.gen.ts rename to packages/grafana-schema/src/raw/dashboard/x/dashboard_types.gen.ts index 50571f93aa5fe..7bcf55da26038 100644 --- a/packages/grafana-schema/src/raw/dashboard/x/dashboard.gen.ts +++ b/packages/grafana-schema/src/raw/dashboard/x/dashboard_types.gen.ts @@ -1,10 +1,25 @@ -// This file is autogenerated. DO NOT EDIT. +// THIS FILE IS GENERATED. EDITING IS FUTILE. // -// Generated by pkg/framework/coremodel/gen.go +// Generated by: +// kinds/gen.go +// Using jennies: +// TSTypesJenny // -// Derived from the Thema lineage declared in pkg/coremodel/dashboard/coremodel.cue -// -// Run `make gen-cue` from repository root to regenerate. +// Run 'make gen-cue' from repository root to regenerate. + +/** + * TODO docs + */ +export interface AnnotationTarget { + limit: number; + matchAny: boolean; + tags: Array; + type: string; +} + +export const defaultAnnotationTarget: Partial = { + tags: [], +}; /** * TODO docs @@ -40,12 +55,7 @@ export interface AnnotationQuery { */ rawQuery?: string; showIn: number; - target?: { - limit: number; - matchAny: boolean; - tags: Array; - type: string; - }; + target?: AnnotationTarget; type: string; } @@ -493,7 +503,7 @@ export interface FieldConfig { */ noValue?: string; /** - * An explict path to the field in the datasource. When the frame meta includes a path, + * An explicit 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 diff --git a/packages/grafana-schema/src/raw/playlist/x/playlist.gen.ts b/packages/grafana-schema/src/raw/playlist/x/playlist_types.gen.ts similarity index 86% rename from packages/grafana-schema/src/raw/playlist/x/playlist.gen.ts rename to packages/grafana-schema/src/raw/playlist/x/playlist_types.gen.ts index 3a562777c0c28..1e1ceeabd8d60 100644 --- a/packages/grafana-schema/src/raw/playlist/x/playlist.gen.ts +++ b/packages/grafana-schema/src/raw/playlist/x/playlist_types.gen.ts @@ -1,10 +1,11 @@ -// This file is autogenerated. DO NOT EDIT. +// THIS FILE IS GENERATED. EDITING IS FUTILE. // -// Generated by pkg/framework/coremodel/gen.go +// Generated by: +// kinds/gen.go +// Using jennies: +// TSTypesJenny // -// Derived from the Thema lineage declared in pkg/coremodel/playlist/coremodel.cue -// -// Run `make gen-cue` from repository root to regenerate. +// Run 'make gen-cue' from repository root to regenerate. export interface PlaylistItem { /** diff --git a/packages/grafana-schema/src/schema/mudball.gen.ts b/packages/grafana-schema/src/schema/mudball.gen.ts index f29d781111da8..803c8aced88ee 100644 --- a/packages/grafana-schema/src/schema/mudball.gen.ts +++ b/packages/grafana-schema/src/schema/mudball.gen.ts @@ -1,4 +1,4 @@ -//~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +//~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ ~~~~~~~~~~~~~~~~~~~ // This file is autogenerated. DO NOT EDIT. // // To regenerate, run "make gen-cue" from the repository root. diff --git a/packages/grafana-schema/src/veneer/dashboard.types.ts b/packages/grafana-schema/src/veneer/dashboard.types.ts index 4dc0c1b02973d..013d76db03933 100644 --- a/packages/grafana-schema/src/veneer/dashboard.types.ts +++ b/packages/grafana-schema/src/veneer/dashboard.types.ts @@ -1,4 +1,4 @@ -import * as raw from '../raw/dashboard/x/dashboard.gen'; +import * as raw from '../raw/dashboard/x/dashboard_types.gen'; export interface Dashboard extends raw.Dashboard { panels?: Array< diff --git a/pkg/api/dashboard.go b/pkg/api/dashboard.go index 2f3f335d389ea..926af720944f1 100644 --- a/pkg/api/dashboard.go +++ b/pkg/api/dashboard.go @@ -16,9 +16,8 @@ import ( "github.com/grafana/grafana/pkg/api/response" "github.com/grafana/grafana/pkg/components/dashdiffs" "github.com/grafana/grafana/pkg/components/simplejson" - "github.com/grafana/grafana/pkg/coremodel/dashboard" - "github.com/grafana/grafana/pkg/cuectx" "github.com/grafana/grafana/pkg/infra/metrics" + "github.com/grafana/grafana/pkg/kinds/dashboard" "github.com/grafana/grafana/pkg/models" "github.com/grafana/grafana/pkg/services/accesscontrol" "github.com/grafana/grafana/pkg/services/alerting" @@ -365,21 +364,22 @@ func (hs *HTTPServer) PostDashboard(c *models.ReqContext) response.Response { } if hs.Features.IsEnabled(featuremgmt.FlagValidateDashboardsOnSave) { - cm := hs.Coremodels.Dashboard() + kind := hs.Kinds.Dashboard() + dashbytes, err := cmd.Dashboard.Bytes() + if err != nil { + return response.Error(http.StatusBadRequest, "unable to parse dashboard", err) + } // Ideally, coremodel validation calls would be integrated into the web // framework. But this does the job for now. schv, err := cmd.Dashboard.Get("schemaVersion").Int() // Only try to validate if the schemaVersion is at least the handoff version // (the minimum schemaVersion against which the dashboard schema is known to - // work), or if schemaVersion is absent (which will happen once the Thema - // schema becomes canonical). + // work), or if schemaVersion is absent (which will happen once the kind schema + // becomes canonical). if err != nil || schv >= dashboard.HandoffSchemaVersion { - // Can't fail, web.Bind() already ensured it's valid JSON - b, _ := cmd.Dashboard.Bytes() - v, _ := cuectx.JSONtoCUE("dashboard.json", b) - if _, err := cm.CurrentSchema().Validate(v); err != nil { + if _, _, err := kind.JSONValueMux(dashbytes); err != nil { return response.Error(http.StatusBadRequest, "invalid dashboard json", err) } } @@ -772,7 +772,7 @@ func (hs *HTTPServer) ValidateDashboard(c *models.ReqContext) response.Response return response.Error(http.StatusBadRequest, "bad request data", err) } - cm := hs.Coremodels.Dashboard() + dk := hs.Kinds.Dashboard() dashboardBytes := []byte(cmd.Dashboard) // POST api receives dashboard as a string of json (so line numbers for errors stay consistent), @@ -793,8 +793,7 @@ func (hs *HTTPServer) ValidateDashboard(c *models.ReqContext) response.Response // work), or if schemaVersion is absent (which will happen once the Thema // schema becomes canonical). if err != nil || schemaVersion >= dashboard.HandoffSchemaVersion { - v, _ := cuectx.JSONtoCUE("dashboard.json", dashboardBytes) - _, validationErr := cm.CurrentSchema().Validate(v) + _, _, validationErr := dk.JSONValueMux(dashboardBytes) if validationErr == nil { isValid = true diff --git a/pkg/api/dashboard_test.go b/pkg/api/dashboard_test.go index b4fc7c52e3c44..bd4a7f08b3dd3 100644 --- a/pkg/api/dashboard_test.go +++ b/pkg/api/dashboard_test.go @@ -17,11 +17,11 @@ import ( "github.com/grafana/grafana/pkg/api/response" "github.com/grafana/grafana/pkg/api/routing" "github.com/grafana/grafana/pkg/components/simplejson" - "github.com/grafana/grafana/pkg/framework/coremodel/registry" "github.com/grafana/grafana/pkg/infra/db" "github.com/grafana/grafana/pkg/infra/usagestats" "github.com/grafana/grafana/pkg/models" "github.com/grafana/grafana/pkg/plugins" + "github.com/grafana/grafana/pkg/registry/corekind" accesscontrolmock "github.com/grafana/grafana/pkg/services/accesscontrol/mock" "github.com/grafana/grafana/pkg/services/alerting" "github.com/grafana/grafana/pkg/services/annotations/annotationstest" @@ -65,7 +65,7 @@ func TestGetHomeDashboard(t *testing.T) { SQLStore: mockstore.NewSQLStoreMock(), preferenceService: prefService, dashboardVersionService: dashboardVersionService, - Coremodels: registry.NewBase(nil), + Kinds: corekind.NewBase(nil), } tests := []struct { @@ -149,7 +149,7 @@ func TestDashboardAPIEndpoint(t *testing.T) { Features: featuremgmt.WithFeatures(), DashboardService: dashboardService, dashboardVersionService: fakeDashboardVersionService, - Coremodels: registry.NewBase(nil), + Kinds: corekind.NewBase(nil), } setUp := func() { @@ -271,7 +271,7 @@ func TestDashboardAPIEndpoint(t *testing.T) { DashboardService: dashboardService, dashboardVersionService: fakeDashboardVersionService, Features: featuremgmt.WithFeatures(), - Coremodels: registry.NewBase(nil), + Kinds: corekind.NewBase(nil), } setUp := func() { @@ -968,7 +968,7 @@ func TestDashboardAPIEndpoint(t *testing.T) { AccessControl: accesscontrolmock.New(), DashboardService: dashboardService, Features: featuremgmt.WithFeatures(), - Coremodels: registry.NewBase(nil), + Kinds: corekind.NewBase(nil), } hs.callGetDashboard(sc) @@ -1023,7 +1023,7 @@ func getDashboardShouldReturn200WithConfig(t *testing.T, sc *scenarioContext, pr ), DashboardService: dashboardService, Features: featuremgmt.WithFeatures(), - Coremodels: registry.NewBase(nil), + Kinds: corekind.NewBase(nil), } hs.callGetDashboard(sc) @@ -1089,7 +1089,7 @@ func postDashboardScenario(t *testing.T, desc string, url string, routePattern s DashboardService: dashboardService, folderService: folderService, Features: featuremgmt.WithFeatures(), - Coremodels: registry.NewBase(nil), + Kinds: corekind.NewBase(nil), } sc := setupScenarioContext(t, url) @@ -1121,7 +1121,7 @@ func postValidateScenario(t *testing.T, desc string, url string, routePattern st LibraryElementService: &mockLibraryElementService{}, SQLStore: sqlmock, Features: featuremgmt.WithFeatures(), - Coremodels: registry.NewBase(nil), + Kinds: corekind.NewBase(nil), } sc := setupScenarioContext(t, url) @@ -1158,7 +1158,7 @@ func postDiffScenario(t *testing.T, desc string, url string, routePattern string SQLStore: sqlmock, dashboardVersionService: fakeDashboardVersionService, Features: featuremgmt.WithFeatures(), - Coremodels: registry.NewBase(nil), + Kinds: corekind.NewBase(nil), } sc := setupScenarioContext(t, url) @@ -1197,7 +1197,7 @@ func restoreDashboardVersionScenario(t *testing.T, desc string, url string, rout SQLStore: sqlStore, Features: featuremgmt.WithFeatures(), dashboardVersionService: fakeDashboardVersionService, - Coremodels: registry.NewBase(nil), + Kinds: corekind.NewBase(nil), } sc := setupScenarioContext(t, url) diff --git a/pkg/api/http_server.go b/pkg/api/http_server.go index 8ecd9639cf4e8..9392c03a911ac 100644 --- a/pkg/api/http_server.go +++ b/pkg/api/http_server.go @@ -29,7 +29,6 @@ import ( "github.com/grafana/grafana/pkg/api/routing" httpstatic "github.com/grafana/grafana/pkg/api/static" "github.com/grafana/grafana/pkg/components/simplejson" - "github.com/grafana/grafana/pkg/framework/coremodel/registry" "github.com/grafana/grafana/pkg/infra/kvstore" "github.com/grafana/grafana/pkg/infra/localcache" "github.com/grafana/grafana/pkg/infra/log" @@ -41,6 +40,7 @@ import ( "github.com/grafana/grafana/pkg/models" "github.com/grafana/grafana/pkg/plugins" "github.com/grafana/grafana/pkg/plugins/plugincontext" + "github.com/grafana/grafana/pkg/registry/corekind" "github.com/grafana/grafana/pkg/services/accesscontrol" "github.com/grafana/grafana/pkg/services/alerting" "github.com/grafana/grafana/pkg/services/annotations" @@ -192,7 +192,7 @@ type HTTPServer struct { dashboardVersionService dashver.Service PublicDashboardsApi *publicdashboardsApi.Api starService star.Service - Coremodels *registry.Base + Kinds *corekind.Base playlistService playlist.Service apiKeyService apikey.Service kvStore kvstore.KVStore @@ -241,7 +241,7 @@ func ProvideHTTPServer(opts ServerOptions, cfg *setting.Cfg, routeRegister routi avatarCacheServer *avatar.AvatarCacheServer, preferenceService pref.Service, teamsPermissionsService accesscontrol.TeamPermissionsService, folderPermissionsService accesscontrol.FolderPermissionsService, dashboardPermissionsService accesscontrol.DashboardPermissionsService, dashboardVersionService dashver.Service, - starService star.Service, csrfService csrf.Service, coremodels *registry.Base, + starService star.Service, csrfService csrf.Service, basekinds *corekind.Base, playlistService playlist.Service, apiKeyService apikey.Service, kvStore kvstore.KVStore, secretsMigrator secrets.Migrator, secretsPluginManager plugins.SecretsPluginManager, secretsService secrets.Service, secretsPluginMigrator spm.SecretMigrationProvider, secretsStore secretsKV.SecretsKVStore, @@ -337,7 +337,7 @@ func ProvideHTTPServer(opts ServerOptions, cfg *setting.Cfg, routeRegister routi dashboardPermissionsService: dashboardPermissionsService, dashboardVersionService: dashboardVersionService, starService: starService, - Coremodels: coremodels, + Kinds: basekinds, playlistService: playlistService, apiKeyService: apiKeyService, kvStore: kvStore, diff --git a/pkg/cmd/grafana-cli/runner/wire.go b/pkg/cmd/grafana-cli/runner/wire.go index 498197990695e..366e01376755d 100644 --- a/pkg/cmd/grafana-cli/runner/wire.go +++ b/pkg/cmd/grafana-cli/runner/wire.go @@ -8,7 +8,7 @@ import ( "github.com/google/wire" "github.com/grafana/grafana/pkg/tsdb/parca" - phlare "github.com/grafana/grafana/pkg/tsdb/phlare" + "github.com/grafana/grafana/pkg/tsdb/phlare" sdkhttpclient "github.com/grafana/grafana-plugin-sdk-go/backend/httpclient" "github.com/grafana/grafana/pkg/api" @@ -17,7 +17,6 @@ import ( "github.com/grafana/grafana/pkg/bus" "github.com/grafana/grafana/pkg/cuectx" "github.com/grafana/grafana/pkg/expr" - cmreg "github.com/grafana/grafana/pkg/framework/coremodel/registry" "github.com/grafana/grafana/pkg/infra/db" "github.com/grafana/grafana/pkg/infra/db/dbtest" "github.com/grafana/grafana/pkg/infra/httpclient" @@ -47,6 +46,7 @@ import ( managerStore "github.com/grafana/grafana/pkg/plugins/manager/store" "github.com/grafana/grafana/pkg/plugins/plugincontext" "github.com/grafana/grafana/pkg/plugins/repo" + "github.com/grafana/grafana/pkg/registry/corekind" "github.com/grafana/grafana/pkg/services/accesscontrol" "github.com/grafana/grafana/pkg/services/accesscontrol/acimpl" "github.com/grafana/grafana/pkg/services/accesscontrol/ossaccesscontrol" @@ -305,7 +305,7 @@ var wireSet = wire.NewSet( avatar.ProvideAvatarCacheServer, authproxy.ProvideAuthProxy, statscollector.ProvideService, - cmreg.CoremodelSet, + corekind.KindSet, cuectx.GrafanaCUEContext, cuectx.GrafanaThemaRuntime, csrf.ProvideCSRFFilter, diff --git a/pkg/codegen/astmanip_test.go b/pkg/codegen/astmanip_test.go index 0f44a112f3999..e09e1071b1686 100644 --- a/pkg/codegen/astmanip_test.go +++ b/pkg/codegen/astmanip_test.go @@ -29,7 +29,7 @@ type FooThing struct { }`, out: `package foo -type Model struct { +type Foo struct { Id int64 Ref Thing } @@ -52,7 +52,7 @@ type FooThing struct { }`, out: `package foo -type Model struct { +type Foo struct { Id int64 Ref *Thing } @@ -77,7 +77,7 @@ type FooThing struct { }`, out: `package foo -type Model struct { +type Foo struct { Id int64 Ref []Thing PRef []*Thing @@ -104,7 +104,7 @@ type FooThing struct { }`, out: `package foo -type Model struct { +type Foo struct { Id int64 KeyRef map[Thing]string ValRef map[string]Thing @@ -132,7 +132,7 @@ type FooThing struct { }`, out: `package foo -type Model struct { +type Foo struct { Id int64 KeyRef map[*Thing]string ValRef map[string]*Thing @@ -154,7 +154,7 @@ type Foo struct { }`, out: `package foo -type Model struct { +type Foo struct { Id int64 FooRef []string } @@ -235,6 +235,37 @@ type Thing string // of objects, only types, so we shouldn't encounter this case. skip: true, }, + "comments": { + in: `package foo + +// Foo is a thing. It should be Foo still. +type Foo struct { + Id int64 + Ref FooThing +} + +// FooThing is also a thing. We want [FooThing] to be known properly. +// Even if FooThing +// were not a FooThing, in our minds, forever shall it be FooThing. +type FooThing struct { + Id int64 +}`, + out: `package foo + +// Foo is a thing. It should be Foo still. +type Foo struct { + Id int64 + Ref Thing +} + +// Thing is also a thing. We want [Thing] to be known properly. +// Even if Thing +// were not a Thing, in our minds, forever shall it be Thing. +type Thing struct { + Id int64 +} +`, + }, } for name, it := range tt { @@ -250,7 +281,7 @@ type Thing string t.Fatal(err) } - drop := makePrefixDropper("Foo", "Model") + drop := PrefixDropper("Foo") astutil.Apply(inf, drop, nil) buf := new(bytes.Buffer) err = format.Node(buf, fset, inf) diff --git a/pkg/codegen/coremodel.go b/pkg/codegen/coremodel.go index cfdc78e1e3044..d3b8ae924649a 100644 --- a/pkg/codegen/coremodel.go +++ b/pkg/codegen/coremodel.go @@ -4,11 +4,9 @@ import ( "bytes" "errors" "fmt" - "go/ast" "io" "os" "path/filepath" - "regexp" "strings" "testing/fstest" @@ -21,7 +19,6 @@ import ( "github.com/grafana/grafana/pkg/cuectx" "github.com/grafana/thema" "github.com/grafana/thema/encoding/openapi" - "golang.org/x/tools/go/ast/astutil" ) // CoremodelDeclaration contains the results of statically analyzing a Grafana @@ -218,7 +215,7 @@ func (cd *CoremodelDeclaration) GenerateGoCoremodel(path string) (WriteDiffer, e fullp := filepath.Join(path, fmt.Sprintf("%s_gen.go", lin.Name())) byt, err := postprocessGoFile(genGoFile{ path: fullp, - walker: makePrefixDropper(strings.Title(lin.Name()), "Model"), + walker: PrefixDropper(strings.Title(lin.Name())), in: buf.Bytes(), }) if err != nil { @@ -273,117 +270,6 @@ func (cd *CoremodelDeclaration) GenerateTypescriptCoremodel() (*tsast.File, erro return tf, nil } -type prefixDropper struct { - str string - base string - rxp *regexp.Regexp - rxpsuff *regexp.Regexp -} - -func makePrefixDropper(str, base string) astutil.ApplyFunc { - return (&prefixDropper{ - str: str, - base: base, - rxpsuff: regexp.MustCompile(fmt.Sprintf(`%s([a-zA-Z_]*)`, str)), - rxp: regexp.MustCompile(fmt.Sprintf(`%s([\s.,;-])`, str)), - }).applyfunc -} - -func depoint(e ast.Expr) ast.Expr { - if star, is := e.(*ast.StarExpr); is { - return star.X - } - return e -} - -func (d prefixDropper) applyfunc(c *astutil.Cursor) bool { - n := c.Node() - - // fmt.Printf("%T %s\n", c.Node(), ast.Print(nil, c.Node())) - switch x := n.(type) { - case *ast.ValueSpec: - // fmt.Printf("%T %s\n", c.Node(), ast.Print(nil, c.Node())) - d.handleExpr(x.Type) - for _, id := range x.Names { - d.do(id) - } - case *ast.TypeSpec: - // Always do typespecs - d.do(x.Name) - case *ast.Field: - // Don't rename struct fields. We just want to rename type declarations, and - // field value specifications that reference those types. - d.handleExpr(x.Type) - // return false - - case *ast.CommentGroup: - for _, c := range x.List { - c.Text = d.rxp.ReplaceAllString(c.Text, d.base+"$1") - c.Text = d.rxpsuff.ReplaceAllString(c.Text, "$1") - } - } - return true -} - -func (d prefixDropper) handleExpr(e ast.Expr) { - // Deref a StarExpr, if there is one - expr := depoint(e) - switch x := expr.(type) { - case *ast.Ident: - d.do(x) - case *ast.ArrayType: - if id, is := depoint(x.Elt).(*ast.Ident); is { - d.do(id) - } - case *ast.MapType: - if id, is := depoint(x.Key).(*ast.Ident); is { - d.do(id) - } - if id, is := depoint(x.Value).(*ast.Ident); is { - d.do(id) - } - } -} - -func (d prefixDropper) do(n *ast.Ident) { - if n.Name != d.str { - n.Name = strings.TrimPrefix(n.Name, d.str) - } else { - n.Name = d.base - } -} - -// GenerateCoremodelRegistry produces Go files that define a registry with -// references to all the Go code that is expected to be generated from the -// provided lineages. -func GenerateCoremodelRegistry(path string, ecl []*CoremodelDeclaration) (WriteDiffer, error) { - var cml []tplVars - for _, ec := range ecl { - cml = append(cml, ec.toTemplateObj()) - } - - buf := new(bytes.Buffer) - if err := tmpls.Lookup("coremodel_registry.tmpl").Execute(buf, tvars_coremodel_registry{ - Header: tvars_autogen_header{ - GeneratorPath: "pkg/framework/coremodel/gen.go", // FIXME hardcoding is not OK - }, - Coremodels: cml, - }); err != nil { - return nil, fmt.Errorf("failed executing coremodel registry template: %w", err) - } - - byt, err := postprocessGoFile(genGoFile{ - path: path, - in: buf.Bytes(), - }) - if err != nil { - return nil, err - } - wd := NewWriteDiffer() - wd[path] = byt - return wd, nil -} - var tmplTypedef = `{{range .Types}} {{ with .Schema.Description }}{{ . }}{{ else }}// {{.TypeName}} is the Go representation of a {{.JsonName}}.{{ end }} // diff --git a/pkg/codegen/generators.go b/pkg/codegen/generators.go new file mode 100644 index 0000000000000..c6090dfe4f5fc --- /dev/null +++ b/pkg/codegen/generators.go @@ -0,0 +1,61 @@ +package codegen + +import ( + "bytes" + "fmt" + + "github.com/grafana/codejen" + "github.com/grafana/grafana/pkg/kindsys" + "github.com/grafana/thema" +) + +type OneToOne codejen.OneToOne[*DeclForGen] +type OneToMany codejen.OneToMany[*DeclForGen] +type ManyToOne codejen.ManyToOne[*DeclForGen] +type ManyToMany codejen.ManyToMany[*DeclForGen] + +// ForGen is a codejen input transformer that converts a pure kindsys.SomeDecl into +// a DeclForGen by binding its contained lineage. +func ForGen(rt *thema.Runtime, decl *kindsys.SomeDecl) (*DeclForGen, error) { + lin, err := decl.BindKindLineage(rt) + if err != nil { + return nil, err + } + + return &DeclForGen{ + SomeDecl: decl, + lin: lin, + }, nil +} + +// DeclForGen wraps [kindsys.SomeDecl] to provide trivial caching of +// the lineage declared by the kind (nil for raw kinds). +type DeclForGen struct { + *kindsys.SomeDecl + lin thema.Lineage +} + +func (decl *DeclForGen) Lineage() thema.Lineage { + return decl.lin +} + +func SlashHeaderMapper(maingen string) codejen.FileMapper { + return func(f codejen.File) (codejen.File, error) { + b := new(bytes.Buffer) + fmt.Fprintf(b, headerTmpl, maingen, f.FromString()) + fmt.Fprint(b, string(f.Data)) + f.Data = b.Bytes() + return f, nil + } +} + +var headerTmpl = `// THIS FILE IS GENERATED. EDITING IS FUTILE. +// +// Generated by: +// %s +// Using jennies: +// %s +// +// Run 'make gen-cue' from repository root to regenerate. + +` diff --git a/pkg/codegen/jenny_basecorereg.go b/pkg/codegen/jenny_basecorereg.go new file mode 100644 index 0000000000000..96298b61cef63 --- /dev/null +++ b/pkg/codegen/jenny_basecorereg.go @@ -0,0 +1,62 @@ +package codegen + +import ( + "bytes" + "fmt" + "path/filepath" + + "github.com/grafana/codejen" +) + +// BaseCoreRegistryJenny generates a static registry for core kinds that +// only initializes their [kindsys.Interface]. No slot kinds are composed. +// +// Path should be the relative path to the directory that will contain the +// generated registry. kindrelroot should be the repo-root-relative path to the +// parent directory to all directories that contain generated kind bindings +// (e.g. pkg/kind). +func BaseCoreRegistryJenny(path, kindrelroot string) ManyToOne { + return &genBaseRegistry{ + path: path, + kindrelroot: kindrelroot, + } +} + +type genBaseRegistry struct { + path string + kindrelroot string +} + +func (gen *genBaseRegistry) JennyName() string { + return "BaseCoreRegistryJenny" +} + +func (gen *genBaseRegistry) Generate(decls []*DeclForGen) (*codejen.File, error) { + var numRaw int + for _, k := range decls { + if k.IsRaw() { + numRaw++ + } + } + + buf := new(bytes.Buffer) + if err := tmpls.Lookup("kind_registry.tmpl").Execute(buf, tvars_kind_registry{ + NumRaw: numRaw, + NumStructured: len(decls) - numRaw, + PackageName: filepath.Base(gen.path), + KindPackagePrefix: filepath.ToSlash(filepath.Join("github.com/grafana/grafana", gen.kindrelroot)), + Kinds: decls, + }); err != nil { + return nil, fmt.Errorf("failed executing kind registry template: %w", err) + } + + b, err := postprocessGoFile(genGoFile{ + path: gen.path, + in: buf.Bytes(), + }) + if err != nil { + return nil, err + } + + return codejen.NewFile(filepath.Join(gen.path, "base_gen.go"), b, gen), nil +} diff --git a/pkg/codegen/jenny_corestructkind.go b/pkg/codegen/jenny_corestructkind.go new file mode 100644 index 0000000000000..0d1fad63e17ac --- /dev/null +++ b/pkg/codegen/jenny_corestructkind.go @@ -0,0 +1,71 @@ +package codegen + +import ( + "bytes" + "fmt" + "path/filepath" + + "github.com/grafana/codejen" +) + +// CoreStructuredKindJenny generates the implementation of +// [kindsys.Structured] for the provided kind declaration. +// +// gokindsdir should be the relative path to the parent directory that contains +// all generated kinds. +// +// This generator only has output for core structured kinds. +func CoreStructuredKindJenny(gokindsdir string, cfg *CoreStructuredKindGeneratorConfig) OneToOne { + if cfg == nil { + cfg = new(CoreStructuredKindGeneratorConfig) + } + if cfg.GenDirName == nil { + cfg.GenDirName = func(decl *DeclForGen) string { + return decl.Meta.Common().MachineName + } + } + + return &genCoreStructuredKind{ + gokindsdir: gokindsdir, + cfg: cfg, + } +} + +// CoreStructuredKindGeneratorConfig holds configuration options for [CoreStructuredKindJenny]. +type CoreStructuredKindGeneratorConfig struct { + // GenDirName returns the name of the directory in which the file should be + // generated. Defaults to DeclForGen.Lineage().Name() if nil. + GenDirName func(*DeclForGen) string +} + +type genCoreStructuredKind struct { + gokindsdir string + cfg *CoreStructuredKindGeneratorConfig +} + +var _ OneToOne = &genCoreStructuredKind{} + +func (gen *genCoreStructuredKind) JennyName() string { + return "CoreStructuredKindJenny" +} + +func (gen *genCoreStructuredKind) Generate(decl *DeclForGen) (*codejen.File, error) { + if !decl.IsCoreStructured() { + return nil, nil + } + + path := filepath.Join(gen.gokindsdir, gen.cfg.GenDirName(decl), decl.Meta.Common().MachineName+"_kind_gen.go") + buf := new(bytes.Buffer) + if err := tmpls.Lookup("kind_corestructured.tmpl").Execute(buf, decl); err != nil { + return nil, fmt.Errorf("failed executing kind_corestructured template for %s: %w", path, err) + } + b, err := postprocessGoFile(genGoFile{ + path: path, + in: buf.Bytes(), + }) + if err != nil { + return nil, err + } + + return codejen.NewFile(path, b, gen), nil +} diff --git a/pkg/codegen/jenny_gotypes.go b/pkg/codegen/jenny_gotypes.go new file mode 100644 index 0000000000000..5d933ee6857aa --- /dev/null +++ b/pkg/codegen/jenny_gotypes.go @@ -0,0 +1,98 @@ +package codegen + +import ( + "fmt" + "path/filepath" + + "github.com/grafana/codejen" + "github.com/grafana/thema" + "github.com/grafana/thema/encoding/gocode" + "golang.org/x/tools/go/ast/astutil" +) + +// GoTypesJenny creates a [OneToOne] that produces Go types for the latest +// Thema schema in a structured kind's lineage. +// +// At minimum, a gokindsdir must be provided. This should be the path to the parent +// directory of the directory in which the types should be generated, relative +// to the project root. For example, if the types for a kind named "foo" +// should live at pkg/kind/foo/foo_gen.go, relpath should be "pkg/kind". +// +// This generator is a no-op for raw kinds. +func GoTypesJenny(gokindsdir string, cfg *GoTypesGeneratorConfig) OneToOne { + if cfg == nil { + cfg = new(GoTypesGeneratorConfig) + } + if cfg.GenDirName == nil { + cfg.GenDirName = func(decl *DeclForGen) string { + return decl.Meta.Common().MachineName + } + } + + return &genGoTypes{ + gokindsdir: gokindsdir, + cfg: cfg, + } +} + +// GoTypesGeneratorConfig holds configuration options for [GoTypesJenny]. +type GoTypesGeneratorConfig struct { + // Apply is an optional AST manipulation func that, if provided, will be run + // against the generated Go file prior to running it through goimports. + Apply astutil.ApplyFunc + + // GenDirName returns the name of the parent directory in which the type file + // should be generated. If nil, the DeclForGen.Lineage().Name() will be used. + GenDirName func(*DeclForGen) string + + // Version of the schema to generate. If nil, latest is generated. + Version *thema.SyntacticVersion +} + +type genGoTypes struct { + gokindsdir string + cfg *GoTypesGeneratorConfig +} + +func (gen *genGoTypes) JennyName() string { + return "GoTypesJenny" +} + +func (gen *genGoTypes) Generate(decl *DeclForGen) (*codejen.File, error) { + if decl.IsRaw() { + return nil, nil + } + + var sch thema.Schema + var err error + + lin := decl.Lineage() + if gen.cfg.Version == nil { + sch = lin.Latest() + } else { + sch, err = lin.Schema(*gen.cfg.Version) + if err != nil { + return nil, fmt.Errorf("error in configured version for %s generator: %w", *gen.cfg.Version, err) + } + } + + // always drop prefixes. + var appf []astutil.ApplyFunc + if gen.cfg.Apply != nil { + appf = append(appf, gen.cfg.Apply) + } + appf = append(appf, PrefixDropper(decl.Meta.Common().Name)) + + pdir := gen.cfg.GenDirName(decl) + fpath := filepath.Join(gen.gokindsdir, pdir, lin.Name()+"_types_gen.go") + // TODO allow using name instead of machine name in thema generator + b, err := gocode.GenerateTypesOpenAPI(sch, &gocode.TypeConfigOpenAPI{ + PackageName: filepath.Base(pdir), + ApplyFuncs: appf, + }) + if err != nil { + return nil, err + } + + return codejen.NewFile(fpath, b, gen), nil +} diff --git a/pkg/codegen/jenny_rawkind.go b/pkg/codegen/jenny_rawkind.go new file mode 100644 index 0000000000000..45c1675a01bf0 --- /dev/null +++ b/pkg/codegen/jenny_rawkind.go @@ -0,0 +1,68 @@ +package codegen + +import ( + "bytes" + "fmt" + "path/filepath" + + "github.com/grafana/codejen" +) + +// RawKindJenny generates the implementation of [kindsys.Raw] for the +// provided kind declaration. +// +// gokindsdir should be the relative path to the parent directory that contains +// all generated kinds. +// +// This generator only has output for raw kinds. +func RawKindJenny(gokindsdir string, cfg *RawKindGeneratorConfig) OneToOne { + if cfg == nil { + cfg = new(RawKindGeneratorConfig) + } + if cfg.GenDirName == nil { + cfg.GenDirName = func(decl *DeclForGen) string { + return decl.Meta.Common().MachineName + } + } + + return &genRawKind{ + gokindsdir: gokindsdir, + cfg: cfg, + } +} + +type genRawKind struct { + gokindsdir string + cfg *RawKindGeneratorConfig +} + +type RawKindGeneratorConfig struct { + // GenDirName returns the name of the directory in which the file should be + // generated. Defaults to DeclForGen.Lineage().Name() if nil. + GenDirName func(*DeclForGen) string +} + +func (gen *genRawKind) JennyName() string { + return "RawKindJenny" +} + +func (gen *genRawKind) Generate(decl *DeclForGen) (*codejen.File, error) { + if !decl.IsRaw() { + return nil, nil + } + + path := filepath.Join(gen.gokindsdir, gen.cfg.GenDirName(decl), decl.Meta.Common().MachineName+"_kind_gen.go") + buf := new(bytes.Buffer) + if err := tmpls.Lookup("kind_raw.tmpl").Execute(buf, decl); err != nil { + return nil, fmt.Errorf("failed executing kind_raw template for %s: %w", path, err) + } + b, err := postprocessGoFile(genGoFile{ + path: path, + in: buf.Bytes(), + }) + if err != nil { + return nil, err + } + + return codejen.NewFile(path, b, gen), nil +} diff --git a/pkg/codegen/jenny_tstypes.go b/pkg/codegen/jenny_tstypes.go new file mode 100644 index 0000000000000..110ec60ff1a8c --- /dev/null +++ b/pkg/codegen/jenny_tstypes.go @@ -0,0 +1,85 @@ +package codegen + +import ( + "fmt" + "path/filepath" + + "github.com/grafana/codejen" + "github.com/grafana/thema" + "github.com/grafana/thema/encoding/typescript" +) + +// TSTypesJenny creates a [OneToOne] that produces TypeScript types and +// defaults for the latest Thema schema in a structured kind's lineage. +// +// At minimum, a tskindsdir must be provided. This should be the path to the parent +// directory of the directory in which the types should be generated, relative +// to the project root. For example, if the types for a kind named "foo" +// should live at packages/grafana-schema/src/raw/foo, relpath should be "pkg/kind". +// +// This generator is a no-op for raw kinds. +func TSTypesJenny(tskindsdir string, cfg *TSTypesGeneratorConfig) OneToOne { + if cfg == nil { + cfg = new(TSTypesGeneratorConfig) + } + if cfg.GenDirName == nil { + cfg.GenDirName = func(decl *DeclForGen) string { + return decl.Meta.Common().MachineName + } + } + + return &genTSTypes{ + tskindsdir: tskindsdir, + cfg: cfg, + } +} + +// TSTypesGeneratorConfig holds configuration options for [TSTypesJenny]. +type TSTypesGeneratorConfig struct { + // GenDirName returns the name of the parent directory in which the type file + // should be generated. If nil, the DeclForGen.Lineage().Name() will be used. + GenDirName func(*DeclForGen) string + + // Version of the schema to generate. If nil, latest is generated. + Version *thema.SyntacticVersion +} + +type genTSTypes struct { + tskindsdir string + cfg *TSTypesGeneratorConfig +} + +func (gen *genTSTypes) JennyName() string { + return "TSTypesJenny" +} + +func (gen *genTSTypes) Generate(decl *DeclForGen) (*codejen.File, error) { + if decl.IsRaw() { + return nil, nil + } + var sch thema.Schema + var err error + + lin := decl.Lineage() + if gen.cfg.Version == nil { + sch = lin.Latest() + } else { + sch, err = lin.Schema(*gen.cfg.Version) + if err != nil { + return nil, fmt.Errorf("error in configured version for %s generator: %w", *gen.cfg.Version, err) + } + } + + // TODO allow using name instead of machine name in thema generator + f, err := typescript.GenerateTypes(sch, &typescript.TypeConfig{ + RootName: decl.Meta.Common().Name, + Group: decl.Meta.Common().LineageIsGroup, + }) + if err != nil { + return nil, err + } + return codejen.NewFile( + filepath.Join(gen.tskindsdir, gen.cfg.GenDirName(decl), lin.Name()+"_types.gen.ts"), + []byte(f.String()), + gen), nil +} diff --git a/pkg/codegen/jenny_tsveneerindex.go b/pkg/codegen/jenny_tsveneerindex.go new file mode 100644 index 0000000000000..c213a20ddfdd6 --- /dev/null +++ b/pkg/codegen/jenny_tsveneerindex.go @@ -0,0 +1,325 @@ +package codegen + +import ( + "fmt" + "path/filepath" + "sort" + "strings" + + "cuelang.org/go/cue" + "cuelang.org/go/cue/errors" + "github.com/grafana/codejen" + "github.com/grafana/cuetsy/ts" + "github.com/grafana/cuetsy/ts/ast" + "github.com/grafana/grafana/pkg/kindsys" + "github.com/grafana/thema" + "github.com/grafana/thema/encoding/typescript" +) + +// TSVeneerIndexJenny generates an index.gen.ts file with references to all +// generated TS types. Elements with the attribute @grafana(TSVeneer="type") are +// exported from a handwritten file, rather than the raw generated types. +// +// The provided dir is the path, relative to the grafana root, to the directory +// that should contain the generated index. +// +// Implicitly depends on output patterns in TSTypesJenny. +// TODO this is wasteful; share-nothing generator model entails re-running the cuetsy gen that TSTypesJenny already did +func TSVeneerIndexJenny(dir string) ManyToOne { + return &genTSVeneerIndex{ + dir: dir, + } +} + +type genTSVeneerIndex struct { + dir string +} + +func (gen *genTSVeneerIndex) JennyName() string { + return "TSVeneerIndexJenny" +} + +func (gen *genTSVeneerIndex) Generate(decls []*DeclForGen) (*codejen.File, error) { + tsf := new(ast.File) + for _, decl := range decls { + if decl.IsRaw() { + continue + } + + sch := decl.Lineage().Latest() + f, err := typescript.GenerateTypes(sch, &typescript.TypeConfig{ + RootName: decl.Meta.Common().Name, + Group: decl.Meta.Common().LineageIsGroup, + }) + if err != nil { + return nil, fmt.Errorf("%s: %w", decl.Meta.Common().Name, err) + } + elems, err := gen.extractTSIndexVeneerElements(decl, f) + if err != nil { + return nil, fmt.Errorf("%s: %w", decl.Meta.Common().Name, err) + } + tsf.Nodes = append(tsf.Nodes, elems...) + } + + return codejen.NewFile(filepath.Join(gen.dir, "index.gen.ts"), []byte(tsf.String()), gen), nil +} + +func (gen *genTSVeneerIndex) extractTSIndexVeneerElements(decl *DeclForGen, tf *ast.File) ([]ast.Decl, error) { + lin := decl.Lineage() + sch := thema.SchemaP(lin, thema.LatestVersion(lin)) + comm := decl.Meta.Common() + + // Check the root, then walk the tree + rootv := sch.UnwrapCUE() + + var raw, custom, rawD, customD ast.Idents + + var terr errors.Error + visit := func(p cue.Path, wv cue.Value) bool { + var name string + sels := p.Selectors() + switch len(sels) { + case 0: + name = strings.Title(lin.Name()) + fallthrough + case 1: + // Only deal with subpaths that are definitions, for now + // TODO incorporate smarts about grouped lineages here + if name == "" { + if !sels[0].IsDefinition() { + return false + } + // It might seem to make sense that we'd strip replaceout the leading # here for + // definitions. However, cuetsy's tsast actually has the # still present in its + // Ident types, stripping it replaceout on the fly when stringifying. + name = sels[0].String() + } + + // Search the generated TS AST for the type and default decl nodes + pair := findDeclNode(name, tf) + if pair.T == nil { + // No generated type for this item, skip it + return false + } + + cust, perr := getCustomVeneerAttr(wv) + if perr != nil { + terr = errors.Append(terr, errors.Promote(perr, fmt.Sprintf("%s: ", p.String()))) + } + var has bool + for _, tgt := range cust { + has = has || tgt.target == "type" + } + if has { + custom = append(custom, *pair.T) + if pair.D != nil { + customD = append(customD, *pair.D) + } + } else { + raw = append(raw, *pair.T) + if pair.D != nil { + rawD = append(rawD, *pair.D) + } + } + } + + return true + } + walk(rootv, visit, nil) + + if len(errors.Errors(terr)) != 0 { + return nil, terr + } + + vpath := fmt.Sprintf("v%v", thema.LatestVersion(lin)[0]) + if decl.Meta.Common().Maturity.Less(kindsys.MaturityStable) { + vpath = "x" + } + + ret := make([]ast.Decl, 0) + if len(raw) > 0 { + ret = append(ret, ast.ExportSet{ + CommentList: []ast.Comment{ts.CommentFromString(fmt.Sprintf("Raw generated types from %s kind.", comm.Name), 80, false)}, + TypeOnly: true, + Exports: raw, + From: ast.Str{Value: fmt.Sprintf("./raw/%s/%s/%s_types.gen", comm.MachineName, vpath, comm.MachineName)}, + }) + } + if len(rawD) > 0 { + ret = append(ret, ast.ExportSet{ + CommentList: []ast.Comment{ts.CommentFromString(fmt.Sprintf("Raw generated default consts from %s kind.", lin.Name()), 80, false)}, + TypeOnly: false, + Exports: rawD, + From: ast.Str{Value: fmt.Sprintf("./raw/%s/%s/%s_types.gen", comm.MachineName, vpath, comm.MachineName)}, + }) + } + vtfile := fmt.Sprintf("./veneer/%s.types", lin.Name()) + customstr := fmt.Sprintf(`// The following exported declarations correspond to types in the %s@%s kind's +// schema with attribute @grafana(TSVeneer="type"). +// +// The handwritten file for these type and default veneers is expected to be at +// %s.ts. +// This re-export declaration enforces that the handwritten veneer file exists, +// and exports all the symbols in the list. +// +// TODO generate code such that tsc enforces type compatibility between raw and veneer decls`, + lin.Name(), thema.LatestVersion(lin), filepath.ToSlash(filepath.Join(gen.dir, vtfile))) + + customComments := []ast.Comment{{Text: customstr}} + if len(custom) > 0 { + ret = append(ret, ast.ExportSet{ + CommentList: customComments, + TypeOnly: true, + Exports: custom, + From: ast.Str{Value: vtfile}, + }) + } + if len(customD) > 0 { + ret = append(ret, ast.ExportSet{ + CommentList: customComments, + TypeOnly: false, + Exports: customD, + From: ast.Str{Value: vtfile}, + }) + } + + // TODO emit a decl in the index.gen.ts that ensures any custom veneer types are "compatible" with current version raw types + return ret, nil +} + +type declPair struct { + T, D *ast.Ident +} + +type tsVeneerAttr struct { + target string +} + +func findDeclNode(name string, tf *ast.File) declPair { + var p declPair + for _, decl := range tf.Nodes { + // Peer through export keywords + if ex, is := decl.(ast.ExportKeyword); is { + decl = ex.Decl + } + + switch x := decl.(type) { + case ast.TypeDecl: + if x.Name.Name == name { + p.T = &x.Name + } + case ast.VarDecl: + if x.Names.Idents[0].Name == "default"+name { + p.D = &x.Names.Idents[0] + } + } + } + return p +} + +func walk(v cue.Value, before func(cue.Path, cue.Value) bool, after func(cue.Path, cue.Value)) { + innerWalk(cue.MakePath(), v, before, after) +} + +func innerWalk(p cue.Path, v cue.Value, before func(cue.Path, cue.Value) bool, after func(cue.Path, cue.Value)) { + switch v.Kind() { + default: + if before != nil && !before(p, v) { + return + } + case cue.StructKind: + if before != nil && !before(p, v) { + return + } + iter, err := v.Fields(cue.All()) + if err != nil { + panic(err) + } + + for iter.Next() { + innerWalk(appendPath(p, iter.Selector()), iter.Value(), before, after) + } + if lv := v.LookupPath(cue.MakePath(cue.AnyString)); lv.Exists() { + innerWalk(appendPath(p, cue.AnyString), lv, before, after) + } + case cue.ListKind: + if before != nil && !before(p, v) { + return + } + list, err := v.List() + if err != nil { + panic(err) + } + for i := 0; list.Next(); i++ { + innerWalk(appendPath(p, cue.Index(i)), list.Value(), before, after) + } + if lv := v.LookupPath(cue.MakePath(cue.AnyIndex)); lv.Exists() { + innerWalk(appendPath(p, cue.AnyString), lv, before, after) + } + } + if after != nil { + after(p, v) + } +} + +func appendPath(p cue.Path, sel cue.Selector) cue.Path { + return cue.MakePath(append(p.Selectors(), sel)...) +} + +func getCustomVeneerAttr(v cue.Value) ([]tsVeneerAttr, error) { + var attrs []tsVeneerAttr + for _, a := range v.Attributes(cue.ValueAttr) { + if a.Name() != "grafana" { + continue + } + for i := 0; i < a.NumArgs(); i++ { + key, av := a.Arg(i) + if key != "TSVeneer" { + return nil, valError(v, "attribute 'grafana' only allows the arg 'TSVeneer'") + } + + aterr := valError(v, "@grafana(TSVeneer=\"x\") requires one or more of the following separated veneer types for x: %s", allowedTSVeneersString()) + var some bool + for _, tgt := range strings.Split(av, "|") { + some = true + if !allowedTSVeneers[tgt] { + return nil, aterr + } + attrs = append(attrs, tsVeneerAttr{ + target: tgt, + }) + } + if !some { + return nil, aterr + } + } + } + + sort.Slice(attrs, func(i, j int) bool { + return attrs[i].target < attrs[j].target + }) + + return attrs, nil +} + +var allowedTSVeneers = map[string]bool{ + "type": true, +} + +func allowedTSVeneersString() string { + var list []string + for tgt := range allowedTSVeneers { + list = append(list, tgt) + } + sort.Strings(list) + + return strings.Join(list, "|") +} + +func valError(v cue.Value, format string, args ...interface{}) error { + s := v.Source() + if s == nil { + return fmt.Errorf(format, args...) + } + return errors.Newf(s.Pos(), format, args...) +} diff --git a/pkg/codegen/pluggen.go b/pkg/codegen/pluggen.go index 55bc8d9e775c8..34beeac4b2d78 100644 --- a/pkg/codegen/pluggen.go +++ b/pkg/codegen/pluggen.go @@ -209,7 +209,7 @@ func (pt *PluginTree) GenerateGo(path string, cfg GoGenConfig) (WriteDiffer, err for subpath, plug := range all { fullp := filepath.Join(path, subpath) if cfg.Types { - gwd, err := genGoTypes(plug, path, subpath, cfg.DocPathPrefix) + gwd, err := pgenGoTypes(plug, path, subpath, cfg.DocPathPrefix) if err != nil { return nil, fmt.Errorf("error generating go types for %s: %w", fullp, err) } @@ -218,7 +218,7 @@ func (pt *PluginTree) GenerateGo(path string, cfg GoGenConfig) (WriteDiffer, err } } if cfg.ThemaBindings { - twd, err := genThemaBindings(plug, path, subpath, cfg.DocPathPrefix) + twd, err := pgenThemaBindings(plug, path, subpath, cfg.DocPathPrefix) if err != nil { return nil, fmt.Errorf("error generating thema bindings for %s: %w", fullp, err) } @@ -231,7 +231,7 @@ func (pt *PluginTree) GenerateGo(path string, cfg GoGenConfig) (WriteDiffer, err return wd, nil } -func genGoTypes(plug pfs.PluginInfo, path, subpath, prefix string) (WriteDiffer, error) { +func pgenGoTypes(plug pfs.PluginInfo, path, subpath, prefix string) (WriteDiffer, error) { wd := NewWriteDiffer() for slotname, lin := range plug.SlotImplementations() { lowslot := strings.ToLower(slotname) @@ -287,7 +287,7 @@ func genGoTypes(plug pfs.PluginInfo, path, subpath, prefix string) (WriteDiffer, finalpath := filepath.Join(path, subpath, fmt.Sprintf("types_%s_gen.go", lowslot)) byt, err := postprocessGoFile(genGoFile{ path: finalpath, - walker: makePrefixDropper(strings.Title(lin.Name()), slotname), + walker: PrefixDropper(strings.Title(lin.Name())), in: buf.Bytes(), }) if err != nil { @@ -300,7 +300,7 @@ func genGoTypes(plug pfs.PluginInfo, path, subpath, prefix string) (WriteDiffer, return wd, nil } -func genThemaBindings(plug pfs.PluginInfo, path, subpath, prefix string) (WriteDiffer, error) { +func pgenThemaBindings(plug pfs.PluginInfo, path, subpath, prefix string) (WriteDiffer, error) { wd := NewWriteDiffer() bindings := make([]tvars_plugin_lineage_binding, 0) for slotname, lin := range plug.SlotImplementations() { diff --git a/pkg/codegen/tmpl.go b/pkg/codegen/tmpl.go index 8c20a30eba9a7..5b4f8212f4c9b 100644 --- a/pkg/codegen/tmpl.go +++ b/pkg/codegen/tmpl.go @@ -29,9 +29,12 @@ type ( LineageCUEPath string GenLicense bool } - tvars_coremodel_registry struct { - Header tvars_autogen_header - Coremodels []tplVars + tvars_kind_registry struct { + // Header tvars_autogen_header + NumRaw, NumStructured int + PackageName string + KindPackagePrefix string + Kinds []*DeclForGen } tvars_coremodel_imports struct { PackageName string diff --git a/pkg/codegen/tmpl/coremodel_registry.tmpl b/pkg/codegen/tmpl/coremodel_registry.tmpl deleted file mode 100644 index 04efd5bb3c3be..0000000000000 --- a/pkg/codegen/tmpl/coremodel_registry.tmpl +++ /dev/null @@ -1,58 +0,0 @@ -{{ 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(rt *thema.Runtime) *Base { - var err error - reg := &Base{} - -{{range .Coremodels }} - reg.{{ .Name }}, err = {{ .Name }}.New(rt) - if err != nil { - panic(fmt.Sprintf("error while initializing {{ .Name }} coremodel: %s", err)) - } - reg.all = append(reg.all, reg.{{ .Name }}) -{{end}} - - return reg -} diff --git a/pkg/codegen/tmpl/kind_corestructured.tmpl b/pkg/codegen/tmpl/kind_corestructured.tmpl new file mode 100644 index 0000000000000..d617ccfd2b696 --- /dev/null +++ b/pkg/codegen/tmpl/kind_corestructured.tmpl @@ -0,0 +1,95 @@ +package {{ .Meta.MachineName }} + +import ( + "github.com/grafana/grafana/pkg/kindsys" + "github.com/grafana/thema" + "github.com/grafana/thema/vmux" +) + +// rootrel is the relative path from the grafana repository root to the +// directory containing the .cue files in which this kind is declared. Necessary +// for runtime errors related to the declaration and/or lineage to provide +// a real path to the correct .cue file. +const rootrel string = "kinds/structured/{{ .Meta.MachineName }}" + +// TODO standard generated docs +type Kind struct { + lin thema.ConvergentLineage[*{{ .Meta.Name }}] + jendec vmux.Endec + valmux vmux.ValueMux[*{{ .Meta.Name }}] + decl kindsys.Decl[kindsys.CoreStructuredMeta] +} + +// type guard +var _ kindsys.Structured = &Kind{} + +// TODO standard generated docs +func NewKind(rt *thema.Runtime, opts ...thema.BindOption) (*Kind, error) { + decl, err := kindsys.LoadCoreKind[kindsys.CoreStructuredMeta](rootrel, rt.Context(), nil) + if err != nil { + return nil, err + } + k := &Kind{ + decl: *decl, + } + + lin, err := decl.Some().BindKindLineage(rt, opts...) + if err != nil { + return nil, err + } + + // Get the thema.Schema that the meta says is in the current version (which + // codegen ensures is always the latest) + cursch := thema.SchemaP(lin, k.decl.Meta.CurrentVersion) + tsch, err := thema.BindType[*{{ .Meta.Name }}](cursch, &{{ .Meta.Name }}{}) + if err != nil { + // Should be unreachable, modulo bugs in the Thema->Go code generator + return nil, err + } + + k.jendec = vmux.NewJSONEndec("{{ .Meta.MachineName }}.json") + k.lin = tsch.ConvergentLineage() + k.valmux = vmux.NewValueMux(k.lin.TypedSchema(), k.jendec) + return k, nil +} + +// TODO standard generated docs +func (k *Kind) Name() string { + return "{{ .Meta.MachineName }}" +} + +// TODO standard generated docs +func (k *Kind) MachineName() string { + return "{{ .Meta.MachineName }}" +} + +// TODO standard generated docs +func (k *Kind) Lineage() thema.Lineage { + return k.lin +} + +// TODO standard generated docs +func (k *Kind) ConvergentLineage() thema.ConvergentLineage[*{{ .Meta.Name }}] { + return k.lin +} + +// JSONValueMux is a version multiplexer that maps a []byte containing JSON data +// at any schematized dashboard version to an instance of {{ .Meta.Name }}. +// +// Validation and translation errors emitted from this func will identify the +// input bytes as "dashboard.json". +// +// This is a thin wrapper around Thema's [vmux.ValueMux]. +func (k *Kind) JSONValueMux(b []byte) (*{{ .Meta.Name }}, thema.TranslationLacunas, error) { + return k.valmux(b) +} + +// TODO standard generated docs +func (k *Kind) Maturity() kindsys.Maturity { + return k.decl.Meta.Maturity +} + +// TODO standard generated docs +func (k *Kind) Meta() kindsys.CoreStructuredMeta { + return k.decl.Meta +} diff --git a/pkg/codegen/tmpl/kind_raw.tmpl b/pkg/codegen/tmpl/kind_raw.tmpl new file mode 100644 index 0000000000000..479915c124e4b --- /dev/null +++ b/pkg/codegen/tmpl/kind_raw.tmpl @@ -0,0 +1,47 @@ +package {{ .Meta.MachineName }} + +import ( + "github.com/grafana/grafana/pkg/kindsys" + "github.com/grafana/thema" + "github.com/grafana/thema/vmux" +) + +// TODO standard generated docs +type Kind struct { + decl kindsys.Decl[kindsys.RawMeta] +} + +// type guard +var _ kindsys.Raw = &Kind{} + +// TODO standard generated docs +func NewKind() (*Kind, error) { + decl, err := kindsys.LoadCoreKind[kindsys.RawMeta]("kinds/raw/{{ .Meta.MachineName }}", nil, nil) + if err != nil { + return nil, err + } + + return &Kind{ + decl: *decl, + }, nil +} + +// TODO standard generated docs +func (k *Kind) Name() string { + return "{{ .Meta.Name }}" +} + +// TODO standard generated docs +func (k *Kind) MachineName() string { + return "{{ .Meta.MachineName }}" +} + +// TODO standard generated docs +func (k *Kind) Maturity() kindsys.Maturity { + return k.decl.Meta.Maturity +} + +// TODO standard generated docs +func (k *Kind) Meta() kindsys.RawMeta { + return k.decl.Meta +} diff --git a/pkg/codegen/tmpl/kind_registry.tmpl b/pkg/codegen/tmpl/kind_registry.tmpl new file mode 100644 index 0000000000000..e323903a9f9ea --- /dev/null +++ b/pkg/codegen/tmpl/kind_registry.tmpl @@ -0,0 +1,60 @@ +package {{ .PackageName }} + +import ( + "fmt" + "sync" + + {{range .Kinds }} + "{{ $.KindPackagePrefix }}/{{ .Meta.MachineName }}"{{end}} + "github.com/grafana/grafana/pkg/cuectx" + "github.com/grafana/grafana/pkg/kindsys" + "github.com/grafana/thema" +) + +// Base is a registry of kindsys.Interface. It provides two modes for accessing +// kinds: individually via literal named methods, or as a slice returned from +// an All*() method. +// +// Prefer the individual named methods for use cases where the particular kind(s) that +// are needed are known to the caller. For example, a dashboard linter can know that it +// specifically wants the dashboard kind. +// +// Prefer All*() methods when performing operations generically across all kinds. +// For example, a validation HTTP middleware for any kind-schematized object type. +type Base struct { + all []kindsys.Interface + numRaw, numStructured int + {{- range .Kinds }} + {{ .Meta.MachineName }} *{{ .Meta.MachineName }}.Kind{{end}} +} + +// type guards +var ( +{{- range .Kinds }} + _ kindsys.{{ if .IsRaw }}Raw{{ else }}Structured{{ end }} = &{{ .Meta.MachineName }}.Kind{}{{end}} +) + +{{range .Kinds }} +// {{ .Meta.Name }} returns the [kindsys.Interface] implementation for the {{ .Meta.MachineName }} kind. +func (b *Base) {{ .Meta.Name }}() *{{ .Meta.MachineName }}.Kind { + return b.{{ .Meta.MachineName }} +} +{{end}} + +func doNewBase(rt *thema.Runtime) *Base { + var err error + reg := &Base{ + numRaw: {{ .NumRaw }}, + numStructured: {{ .NumStructured }}, + } + +{{range .Kinds }} + reg.{{ .Meta.MachineName }}, err = {{ .Meta.MachineName }}.NewKind({{ if .IsCoreStructured }}rt{{ end }}) + if err != nil { + panic(fmt.Sprintf("error while initializing the {{ .Meta.MachineName }} Kind: %s", err)) + } + reg.all = append(reg.all, reg.{{ .Meta.MachineName }}) +{{end}} + + return reg +} diff --git a/pkg/codegen/util_go.go b/pkg/codegen/util_go.go index 9033e29b96880..3c8b05f3a6416 100644 --- a/pkg/codegen/util_go.go +++ b/pkg/codegen/util_go.go @@ -3,11 +3,13 @@ package codegen import ( "bytes" "fmt" + "go/ast" "go/format" "go/parser" "go/token" "os" "path/filepath" + "regexp" "strings" "golang.org/x/tools/go/ast/astutil" @@ -65,3 +67,84 @@ func postprocessGoFile(cfg genGoFile) ([]byte, error) { return byt, nil } + +type prefixmod struct { + str string + base string + rxp *regexp.Regexp + rxpsuff *regexp.Regexp +} + +// PrefixDropper returns an astutil.ApplyFunc that removes the provided prefix +// string when it appears as a leading sequence in type names, var names, and +// comments in a generated Go file. +func PrefixDropper(prefix string) astutil.ApplyFunc { + return (&prefixmod{ + str: prefix, + rxpsuff: regexp.MustCompile(fmt.Sprintf(`%s([a-zA-Z_]+)`, prefix)), + rxp: regexp.MustCompile(fmt.Sprintf(`%s([\s.,;-])`, prefix)), + }).applyfunc +} + +func depoint(e ast.Expr) ast.Expr { + if star, is := e.(*ast.StarExpr); is { + return star.X + } + return e +} + +func (d prefixmod) applyfunc(c *astutil.Cursor) bool { + n := c.Node() + + switch x := n.(type) { + case *ast.ValueSpec: + d.handleExpr(x.Type) + for _, id := range x.Names { + d.do(id) + } + case *ast.TypeSpec: + // Always do typespecs + d.do(x.Name) + case *ast.Field: + // Don't rename struct fields. We just want to rename type declarations, and + // field value specifications that reference those types. + d.handleExpr(x.Type) + + case *ast.CommentGroup: + for _, c := range x.List { + c.Text = d.rxpsuff.ReplaceAllString(c.Text, "$1") + if d.base != "" { + c.Text = d.rxp.ReplaceAllString(c.Text, d.base+"$1") + } + } + } + return true +} + +func (d prefixmod) handleExpr(e ast.Expr) { + // Deref a StarExpr, if there is one + expr := depoint(e) + switch x := expr.(type) { + case *ast.Ident: + d.do(x) + case *ast.ArrayType: + if id, is := depoint(x.Elt).(*ast.Ident); is { + d.do(id) + } + case *ast.MapType: + if id, is := depoint(x.Key).(*ast.Ident); is { + d.do(id) + } + if id, is := depoint(x.Value).(*ast.Ident); is { + d.do(id) + } + } +} + +func (d prefixmod) do(n *ast.Ident) { + if n.Name != d.str { + n.Name = strings.TrimPrefix(n.Name, d.str) + } else if d.base != "" { + n.Name = d.base + } +} diff --git a/pkg/coremodel/playlist/playlist_gen.go b/pkg/coremodel/playlist/playlist_gen.go deleted file mode 100644 index 79502c2b80ed8..0000000000000 --- a/pkg/coremodel/playlist/playlist_gen.go +++ /dev/null @@ -1,136 +0,0 @@ -// This file is autogenerated. DO NOT EDIT. -// -// Generated by pkg/framework/coremodel/gen.go -// -// Derived from the Thema lineage declared in pkg/coremodel/playlist/coremodel.cue -// -// Run `make gen-cue` from repository root to regenerate. - -package playlist - -import ( - "embed" - "path/filepath" - - "github.com/grafana/grafana/pkg/cuectx" - "github.com/grafana/grafana/pkg/framework/coremodel" - "github.com/grafana/thema" -) - -// Defines values for PlaylistItemType. -const ( - PlaylistItemTypeDashboardById PlaylistItemType = "dashboard_by_id" - - PlaylistItemTypeDashboardByTag PlaylistItemType = "dashboard_by_tag" - - PlaylistItemTypeDashboardByUid PlaylistItemType = "dashboard_by_uid" -) - -// Model is the Go representation of a playlist. -// -// 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 { - // Interval sets the time between switching views in a playlist. - // FIXME: Is this based on a standardized format or what options are available? Can datemath be used? - Interval string `json:"interval"` - - // The ordered list of items that the playlist will iterate over. - // FIXME! This should not be optional, but changing it makes the godegen awkward - Items *[]PlaylistItem `json:"items,omitempty"` - - // Name of the playlist. - Name string `json:"name"` - - // Unique playlist identifier. Generated on creation, either by the - // creator of the playlist of by the application. - Uid string `json:"uid"` -} - -// PlaylistItem is the Go representation of a playlist.Item. -// -// 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 PlaylistItem struct { - // Title is an unused property -- it will be removed in the future - Title *string `json:"title,omitempty"` - - // Type of the item. - Type PlaylistItemType `json:"type"` - - // Value depends on type and describes the playlist item. - // - // - dashboard_by_id: The value is an internal numerical identifier set by Grafana. This - // is not portable as the numerical identifier is non-deterministic between different instances. - // Will be replaced by dashboard_by_uid in the future. (deprecated) - // - dashboard_by_tag: The value is a tag which is set on any number of dashboards. All - // dashboards behind the tag will be added to the playlist. - // - dashboard_by_uid: The value is the dashboard UID - Value string `json:"value"` -} - -// Type of the item. -// -// 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 PlaylistItemType string - -//go:embed coremodel.cue -var cueFS embed.FS - -// The current version of the coremodel schema, as declared in coremodel.cue. -// This version determines what schema version is returned from [Coremodel.CurrentSchema], -// and which schema version is used for code generation within the grafana/grafana repository. -// -// The code generator ensures that this is always the latest Thema schema version. -var currentVersion = thema.SV(0, 0) - -// Lineage returns the Thema lineage representing a Grafana playlist. -// -// The lineage is the canonical specification of the current playlist schema, -// all prior schema versions, and the mappings that allow migration between -// schema versions. -func Lineage(rt *thema.Runtime, opts ...thema.BindOption) (thema.Lineage, error) { - return cuectx.LoadGrafanaInstancesWithThema(filepath.Join("pkg", "coremodel", "playlist"), cueFS, rt, opts...) -} - -var _ thema.LineageFactory = Lineage -var _ coremodel.Interface = &Coremodel{} - -// Coremodel contains the foundational schema declaration for playlists. -// It implements coremodel.Interface. -type Coremodel struct { - lin thema.Lineage -} - -// Lineage returns the canonical playlist Lineage. -func (c *Coremodel) Lineage() thema.Lineage { - return c.lin -} - -// CurrentSchema returns the current (latest) playlist 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 playlist coremodel. -// -// Note that this function does not cache, and initially loading a Thema lineage -// can be expensive. As such, the Grafana backend should prefer to access this -// coremodel through a registry (pkg/framework/coremodel/registry), which does cache. -func New(rt *thema.Runtime) (*Coremodel, error) { - lin, err := Lineage(rt) - if err != nil { - return nil, err - } - - return &Coremodel{ - lin: lin, - }, nil -} diff --git a/pkg/cuectx/ctx.go b/pkg/cuectx/ctx.go index d2865caf2abc0..7b29041eb3c81 100644 --- a/pkg/cuectx/ctx.go +++ b/pkg/cuectx/ctx.go @@ -5,15 +5,19 @@ package cuectx import ( + "fmt" "io/fs" "path/filepath" "testing/fstest" "cuelang.org/go/cue" + "cuelang.org/go/cue/build" "cuelang.org/go/cue/cuecontext" + "github.com/grafana/grafana" "github.com/grafana/thema" "github.com/grafana/thema/load" "github.com/grafana/thema/vmux" + "github.com/yalue/merged_fs" ) var ctx = cuecontext.New() @@ -84,16 +88,16 @@ func LoadGrafanaInstancesWithThema(path string, cueFS fs.FS, rt *thema.Runtime, return lin, nil } -// prefixWithGrafanaCUE constructs an fs.FS that merges the provided fs.FS with one -// containing grafana's cue.mod at the root. The provided prefix should be the +// prefixWithGrafanaCUE constructs an fs.FS that merges the provided fs.FS with +// the embedded FS containing Grafana's core CUE files, [grafana.CueSchemaFS]. +// The provided prefix should be the relative path from the grafana repository +// root to the directory root of the provided inputfs. // -// The returned fs.FS is suitable for passing to a CUE loader, such as -// cuelang.org/cue/load.Instances or -// github.com/grafana/thema/load.InstancesWithThema. +// The returned fs.FS is suitable for passing to a CUE loader, such as [load.InstancesWithThema]. func prefixWithGrafanaCUE(prefix string, inputfs fs.FS) (fs.FS, error) { m := fstest.MapFS{ // fstest can recognize only forward slashes. - filepath.ToSlash(filepath.Join("cue.mod", "module.cue")): &fstest.MapFile{Data: []byte(`module: "github.com/grafana/grafana"`)}, + // filepath.ToSlash(filepath.Join("cue.mod", "module.cue")): &fstest.MapFile{Data: []byte(`module: "github.com/grafana/grafana"`)}, } prefix = filepath.FromSlash(prefix) @@ -114,6 +118,81 @@ func prefixWithGrafanaCUE(prefix string, inputfs fs.FS) (fs.FS, error) { m[filepath.ToSlash(filepath.Join(prefix, path))] = &fstest.MapFile{Data: b} return nil }) + if err != nil { + return nil, err + } + return merged_fs.NewMergedFS(m, grafana.CueSchemaFS), nil +} + +// BuildGrafanaInstance wraps [load.InstancesWithThema] to load a +// [*build.Instance] corresponding to a particular path within the +// github.com/grafana/grafana CUE module, then builds that into a [cue.Value], +// checks it for errors and returns. +// +// This allows resolution of imports within the grafana or thema CUE modules to +// work correctly and consistently by relying on the embedded FS at +// [grafana.CueSchemaFS] and [thema.CueFS]. +// +// relpath should be a relative path path within [grafana.CueSchemaFS] to be +// loaded. Optionally, the caller may provide an additional fs.FS via the +// overlay parameter, which will be merged with [grafana.CueSchemaFS] at +// relpath, and loaded. +// +// pkg, if non-empty, is set as the value of +// ["cuelang.org/go/cue/load".Config.Package]. If the CUE package to be loaded +// is the same as the parent directory name, it should be omitted. +// +// NOTE this function will be removed in favor of a more generic loader +func BuildGrafanaInstance(relpath string, pkg string, ctx *cue.Context, overlay fs.FS) (cue.Value, error) { + // notes about how this crap needs to work + // + // Within grafana/grafana, need: + // - pass in an fs.FS that, in its root, contains the .cue files to load + // - has no cue.mod + // - gets prefixed with the appropriate path within grafana/grafana + // - and merged with all the other .cue files from grafana/grafana + if ctx == nil { + ctx = GrafanaCUEContext() + } + relpath = filepath.ToSlash(relpath) + + var v cue.Value + var f fs.FS = grafana.CueSchemaFS + var err error + if overlay != nil { + f, err = prefixWithGrafanaCUE(relpath, overlay) + if err != nil { + return v, err + } + } + + var bi *build.Instance + if pkg != "" { + bi, err = load.InstancesWithThema(f, relpath, load.Package(pkg)) + } else { + bi, err = load.InstancesWithThema(f, relpath) + } + if err != nil { + return v, err + } + + v = ctx.BuildInstance(bi) + if v.Err() != nil { + return v, fmt.Errorf("%s not a valid CUE instance: %w", relpath, v.Err()) + } + return v, nil +} - return m, err +// TODO docs +// NOTE this function will be removed in favor of a more generic loader +func LoadInstanceWithGrafana(ifs fs.FS, prefix string) (*build.Instance, error) { + // notes about how this crap needs to work + // + // Need a prefixing instance loader that: + // - can take multiple fs.FS, each one representing a CUE module (nesting?) + // - reconcile at most one of the provided fs with cwd + // - behavior must differ depending on whether cwd is in a cue module + // - behavior should(?) be controllable depending on + + panic("TODO") } diff --git a/pkg/framework/coremodel/gen.go b/pkg/framework/coremodel/gen.go index c56b5983353ef..fb84467c031a2 100644 --- a/pkg/framework/coremodel/gen.go +++ b/pkg/framework/coremodel/gen.go @@ -7,21 +7,12 @@ package main import ( "fmt" "os" - "path" "path/filepath" - "sort" - "strings" - "cuelang.org/go/cue" "cuelang.org/go/cue/cuecontext" - "cuelang.org/go/cue/errors" "cuelang.org/go/cue/load" "github.com/grafana/cuetsy" - "github.com/grafana/cuetsy/ts" - "github.com/grafana/cuetsy/ts/ast" gcgen "github.com/grafana/grafana/pkg/codegen" - "github.com/grafana/grafana/pkg/cuectx" - "github.com/grafana/thema" ) const sep = string(filepath.Separator) @@ -45,74 +36,11 @@ func init() { // Generate Go and Typescript implementations for all coremodels, and populate the // coremodel static registry. func main() { - rt := cuectx.GrafanaThemaRuntime() 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) } - - items, err := os.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.CoremodelDeclaration - for _, item := range items { - if item.IsDir() { - lin, err := gcgen.ExtractLineage(filepath.Join(cmroot, item.Name(), "coremodel.cue"), rt) - if err != nil { - fmt.Fprintf(os.Stderr, "could not process coremodel dir %s: %s\n", filepath.Join(cmroot, item.Name()), err) - os.Exit(1) - } - - lins = append(lins, lin) - } - } - sort.Slice(lins, func(i, j int) bool { - return lins[i].Lineage.Name() < lins[j].Lineage.Name() - }) - - // The typescript veneer index.gen.ts file, which we'll build up over time - // from the exported types. - tsvidx := new(ast.File) wd := gcgen.NewWriteDiffer() - for _, ls := range lins { - gofiles, 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(gofiles) - - // Only generate TS for API types - if ls.IsAPIType { - tsf, err := ls.GenerateTypescriptCoremodel() - if err != nil { - fmt.Fprintf(os.Stderr, "error generating TypeScript for %s: %s\n", ls.Lineage.Name(), err) - os.Exit(1) - } - tsf.Doc = mkTSHeader(ls) - wd[filepath.FromSlash(filepath.Join(tsroot, rawTSGenPath(ls)))] = []byte(tsf.String()) - - decls, err := extractTSIndexVeneerElements(ls, tsf) - if err != nil { - fmt.Fprintf(os.Stderr, "error generating TypeScript veneer for %s: %s\n", ls.Lineage.Name(), errors.Details(err, nil)) - os.Exit(1) - } - tsvidx.Nodes = append(tsvidx.Nodes, decls...) - } - } - - tsvidx.Doc = mkTSHeader(nil) - wd[filepath.Join(tsroot, "index.gen.ts")] = []byte(tsvidx.String()) - - regfiles, err := gcgen.GenerateCoremodelRegistry(filepath.Join(groot, "pkg", "framework", "coremodel", "registry", "registry_gen.go"), lins) - if err != nil { - fmt.Fprintf(os.Stderr, "failed to generate coremodel registry: %s\n", err) - os.Exit(1) - } - wd.Merge(regfiles) // TODO generating these is here temporarily until we make a more permanent home wdsh, err := genSharedSchemas(groot) @@ -137,25 +65,6 @@ func main() { } } -// generates the path relative to packages/grafana-schema/src at which the raw -// type definitions should be exported for the latest schema of this type -func rawTSGenPath(cm *gcgen.CoremodelDeclaration) string { - return fmt.Sprintf("raw/%s/%s/%s.gen.ts", cm.Lineage.Name(), cm.PathVersion(), cm.Lineage.Name()) -} - -func mkTSHeader(cm *gcgen.CoremodelDeclaration) *ast.Comment { - v := gcgen.HeaderVars{ - GeneratorPath: "pkg/framework/coremodel/gen.go", - } - if cm != nil { - v.LineagePath = cm.RelativePath - } - v.GeneratorPath = "pkg/framework/coremodel/gen.go" - return &ast.Comment{ - Text: strings.TrimSpace(gcgen.GenGrafanaHeader(v)), - } -} - func genSharedSchemas(groot string) (gcgen.WriteDiffer, error) { abspath := filepath.Join(groot, "packages", "grafana-schema", "src", "schema") cfg := &load.Config{ @@ -183,7 +92,7 @@ func genSharedSchemas(groot string) (gcgen.WriteDiffer, error) { } wd := gcgen.NewWriteDiffer() - wd[filepath.Join(abspath, "mudball.gen.ts")] = append([]byte(`//~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + wd[filepath.Join(abspath, "mudball.gen.ts")] = append([]byte(`//~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ ~~~~~~~~~~~~~~~~~~~ // This file is autogenerated. DO NOT EDIT. // // To regenerate, run "make gen-cue" from the repository root. @@ -191,259 +100,3 @@ func genSharedSchemas(groot string) (gcgen.WriteDiffer, error) { `), b...) return wd, nil } - -// TODO make this more generic and reusable -func extractTSIndexVeneerElements(cm *gcgen.CoremodelDeclaration, tf *ast.File) ([]ast.Decl, error) { - lin := cm.Lineage - sch := thema.SchemaP(lin, thema.LatestVersion(lin)) - - // Check the root, then walk the tree - rootv := sch.UnwrapCUE() - - var raw, custom, rawD, customD ast.Idents - - var terr errors.Error - visit := func(p cue.Path, wv cue.Value) bool { - var name string - sels := p.Selectors() - switch len(sels) { - case 0: - name = strings.Title(cm.Lineage.Name()) - fallthrough - case 1: - // Only deal with subpaths that are definitions, for now - // TODO incorporate smarts about grouped lineages here - if name == "" { - if !sels[0].IsDefinition() { - return false - } - // It might seem to make sense that we'd strip out the leading # here for - // definitions. However, cuetsy's tsast actually has the # still present in its - // Ident types, stripping it out on the fly when stringifying. - name = sels[0].String() - } - - // Search the generated TS AST for the type and default decl nodes - pair := findDeclNode(name, tf) - if pair.T == nil { - // No generated type for this item, skip it - return false - } - - cust, perr := getCustomVeneerAttr(wv) - if perr != nil { - terr = errors.Append(terr, errors.Promote(perr, fmt.Sprintf("%s: ", p.String()))) - } - var has bool - for _, tgt := range cust { - has = has || tgt.target == "type" - } - if has { - custom = append(custom, *pair.T) - if pair.D != nil { - customD = append(customD, *pair.D) - } - } else { - raw = append(raw, *pair.T) - if pair.D != nil { - rawD = append(rawD, *pair.D) - } - } - } - - return true - } - walk(rootv, visit, nil) - - if len(errors.Errors(terr)) != 0 { - return nil, terr - } - - ret := make([]ast.Decl, 0) - if len(raw) > 0 { - ret = append(ret, ast.ExportSet{ - CommentList: []ast.Comment{ts.CommentFromString(fmt.Sprintf("Raw generated types from %s entity type.", cm.Lineage.Name()), 80, false)}, - TypeOnly: true, - Exports: raw, - From: ast.Str{Value: fmt.Sprintf("./raw/%s/%s/%s.gen", cm.Lineage.Name(), cm.PathVersion(), cm.Lineage.Name())}, - }) - } - if len(rawD) > 0 { - ret = append(ret, ast.ExportSet{ - CommentList: []ast.Comment{ts.CommentFromString(fmt.Sprintf("Raw generated default consts from %s entity type.", cm.Lineage.Name()), 80, false)}, - TypeOnly: false, - Exports: rawD, - From: ast.Str{Value: fmt.Sprintf("./raw/%s/%s/%s.gen", cm.Lineage.Name(), cm.PathVersion(), cm.Lineage.Name())}, - }) - } - vtfile := fmt.Sprintf("./veneer/%s.types", cm.Lineage.Name()) - customstr := fmt.Sprintf(`// The following exported declarations correspond to types in the %s@%s schema with -// attribute @grafana(TSVeneer="type"). (lineage declared in file: %s) -// -// The handwritten file for these type and default veneers is expected to be at -// %s.ts. -// This re-export declaration enforces that the handwritten veneer file exists, -// and exports all the symbols in the list. -// -// TODO generate code such that tsc enforces type compatibility between raw and veneer decls`, - cm.Lineage.Name(), thema.LatestVersion(cm.Lineage), cm.RelativePath, filepath.Clean(path.Join("packages", "grafana-schema", "src", vtfile))) - - customComments := []ast.Comment{{Text: customstr}} - if len(custom) > 0 { - ret = append(ret, ast.ExportSet{ - CommentList: customComments, - TypeOnly: true, - Exports: custom, - From: ast.Str{Value: vtfile}, - }) - } - if len(customD) > 0 { - ret = append(ret, ast.ExportSet{ - CommentList: customComments, - TypeOnly: false, - Exports: customD, - From: ast.Str{Value: vtfile}, - }) - } - - // TODO emit a decl in the index.gen.ts that ensures any custom veneer types are "compatible" with current version raw types - return ret, nil -} - -type declPair struct { - T, D *ast.Ident -} - -func findDeclNode(name string, tf *ast.File) declPair { - var p declPair - for _, decl := range tf.Nodes { - // Peer through export keywords - if ex, is := decl.(ast.ExportKeyword); is { - decl = ex.Decl - } - - switch x := decl.(type) { - case ast.TypeDecl: - if x.Name.Name == name { - p.T = &x.Name - } - case ast.VarDecl: - if x.Names.Idents[0].Name == "default"+name { - p.D = &x.Names.Idents[0] - } - } - } - return p -} - -type tsVeneerAttr struct { - target string -} - -func walk(v cue.Value, before func(cue.Path, cue.Value) bool, after func(cue.Path, cue.Value)) { - innerWalk(cue.MakePath(), v, before, after) -} - -func innerWalk(p cue.Path, v cue.Value, before func(cue.Path, cue.Value) bool, after func(cue.Path, cue.Value)) { - // switch v.IncompleteKind() { - switch v.Kind() { - default: - if before != nil && !before(p, v) { - return - } - case cue.StructKind: - if before != nil && !before(p, v) { - return - } - iter, err := v.Fields(cue.All()) - if err != nil { - panic(err) - } - - for iter.Next() { - innerWalk(appendPath(p, iter.Selector()), iter.Value(), before, after) - } - if lv := v.LookupPath(cue.MakePath(cue.AnyString)); lv.Exists() { - innerWalk(appendPath(p, cue.AnyString), lv, before, after) - } - case cue.ListKind: - if before != nil && !before(p, v) { - return - } - list, err := v.List() - if err != nil { - panic(err) - } - for i := 0; list.Next(); i++ { - innerWalk(appendPath(p, cue.Index(i)), list.Value(), before, after) - } - if lv := v.LookupPath(cue.MakePath(cue.AnyIndex)); lv.Exists() { - innerWalk(appendPath(p, cue.AnyString), lv, before, after) - } - } - if after != nil { - after(p, v) - } -} - -func appendPath(p cue.Path, sel cue.Selector) cue.Path { - return cue.MakePath(append(p.Selectors(), sel)...) -} - -var allowedTSVeneers = map[string]bool{ - "type": true, -} - -func allowedTSVeneersString() string { - var list []string - for tgt := range allowedTSVeneers { - list = append(list, tgt) - } - sort.Strings(list) - - return strings.Join(list, "|") -} - -func getCustomVeneerAttr(v cue.Value) ([]tsVeneerAttr, error) { - var attrs []tsVeneerAttr - for _, a := range v.Attributes(cue.ValueAttr) { - if a.Name() != "grafana" { - continue - } - for i := 0; i < a.NumArgs(); i++ { - key, av := a.Arg(i) - if key != "TSVeneer" { - return nil, valError(v, "attribute 'grafana' only allows the arg 'TSVeneer'") - } - - aterr := valError(v, "@grafana(TSVeneer=\"x\") requires one or more of the following separated veneer types for x: %s", allowedTSVeneersString()) - var some bool - for _, tgt := range strings.Split(av, "|") { - some = true - if !allowedTSVeneers[tgt] { - return nil, aterr - } - attrs = append(attrs, tsVeneerAttr{ - target: tgt, - }) - } - if !some { - return nil, aterr - } - } - } - - sort.Slice(attrs, func(i, j int) bool { - return attrs[i].target < attrs[j].target - }) - - return attrs, nil -} - -func valError(v cue.Value, format string, args ...interface{}) error { - s := v.Source() - if s == nil { - return fmt.Errorf(format, args...) - } - return errors.Newf(s.Pos(), format, args...) -} diff --git a/pkg/framework/coremodel/registry/assignability_test.go b/pkg/framework/coremodel/registry/assignability_test.go deleted file mode 100644 index 8d83e6284d706..0000000000000 --- a/pkg/framework/coremodel/registry/assignability_test.go +++ /dev/null @@ -1,22 +0,0 @@ -package registry_test - -import ( - "testing" - - "github.com/grafana/grafana/pkg/framework/coremodel/registry" - "github.com/grafana/thema" -) - -func TestSchemaAssignability(t *testing.T) { - reg := registry.NewBase(nil) - - for _, cm := range reg.All() { - tcm := cm - t.Run(tcm.Lineage().Name(), func(t *testing.T) { - err := thema.AssignableTo(tcm.CurrentSchema(), tcm.GoType()) - if err != nil { - t.Fatal(err) - } - }) - } -} diff --git a/pkg/framework/coremodel/registry/provide.go b/pkg/framework/coremodel/registry/provide.go deleted file mode 100644 index e64e314987f55..0000000000000 --- a/pkg/framework/coremodel/registry/provide.go +++ /dev/null @@ -1,48 +0,0 @@ -package registry - -import ( - "sync" - - "github.com/google/wire" - "github.com/grafana/grafana/pkg/cuectx" - "github.com/grafana/grafana/pkg/framework/coremodel" - "github.com/grafana/thema" -) - -// CoremodelSet contains all of the wire-style providers related to coremodels. -var CoremodelSet = wire.NewSet( - NewBase, -) - -var ( - baseOnce sync.Once - defaultBase *Base -) - -// NewBase provides a registry of all coremodels, without any composition of -// plugin-defined schemas. -// -// All calling code within grafana/grafana is expected to use Grafana's -// singleton [thema.Runtime], returned from [cuectx.GrafanaThemaRuntime]. If nil -// is passed, the singleton will be used. -func NewBase(rt *thema.Runtime) *Base { - allrt := cuectx.GrafanaThemaRuntime() - if rt == nil || rt == allrt { - baseOnce.Do(func() { - defaultBase = doProvideBase(allrt) - }) - return defaultBase - } - - return doProvideBase(rt) -} - -// All returns a slice of all registered coremodels. -// -// Prefer this method when operating generically across all coremodels. -// -// The returned slice is sorted lexicographically by coremodel name. It should -// not be modified. -func (b *Base) All() []coremodel.Interface { - return b.all -} diff --git a/pkg/framework/coremodel/registry/registry_gen.go b/pkg/framework/coremodel/registry/registry_gen.go deleted file mode 100644 index 7daa559cb5895..0000000000000 --- a/pkg/framework/coremodel/registry/registry_gen.go +++ /dev/null @@ -1,83 +0,0 @@ -// This file is autogenerated. DO NOT EDIT. -// -// Generated by pkg/framework/coremodel/gen.go -// -// Run `make gen-cue` from repository root to regenerate. - -package registry - -import ( - "fmt" - - "github.com/grafana/grafana/pkg/coremodel/dashboard" - "github.com/grafana/grafana/pkg/coremodel/playlist" - "github.com/grafana/grafana/pkg/coremodel/pluginmeta" - "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 - dashboard *dashboard.Coremodel - playlist *playlist.Coremodel - pluginmeta *pluginmeta.Coremodel -} - -// type guards -var ( - _ coremodel.Interface = &dashboard.Coremodel{} - _ coremodel.Interface = &playlist.Coremodel{} - _ coremodel.Interface = &pluginmeta.Coremodel{} -) - -// Dashboard returns the dashboard coremodel. The return value is guaranteed to -// implement coremodel.Interface. -func (b *Base) Dashboard() *dashboard.Coremodel { - return b.dashboard -} - -// Playlist returns the playlist coremodel. The return value is guaranteed to -// implement coremodel.Interface. -func (b *Base) Playlist() *playlist.Coremodel { - return b.playlist -} - -// Pluginmeta returns the pluginmeta coremodel. The return value is guaranteed to -// implement coremodel.Interface. -func (b *Base) Pluginmeta() *pluginmeta.Coremodel { - return b.pluginmeta -} - -func doProvideBase(rt *thema.Runtime) *Base { - var err error - reg := &Base{} - - reg.dashboard, err = dashboard.New(rt) - if err != nil { - panic(fmt.Sprintf("error while initializing dashboard coremodel: %s", err)) - } - reg.all = append(reg.all, reg.dashboard) - - reg.playlist, err = playlist.New(rt) - if err != nil { - panic(fmt.Sprintf("error while initializing playlist coremodel: %s", err)) - } - reg.all = append(reg.all, reg.playlist) - - reg.pluginmeta, err = pluginmeta.New(rt) - if err != nil { - panic(fmt.Sprintf("error while initializing pluginmeta coremodel: %s", err)) - } - reg.all = append(reg.all, reg.pluginmeta) - - return reg -} diff --git a/pkg/framework/coremodel/slot/doc.go b/pkg/framework/coremodel/slot/doc.go deleted file mode 100644 index 6c1a39f1f62c6..0000000000000 --- a/pkg/framework/coremodel/slot/doc.go +++ /dev/null @@ -1,2 +0,0 @@ -// Package Slot exposes Grafana's coremodel composition Slot definitions for use in Go. -package slot diff --git a/pkg/coremodel/dashboard/addenda.go b/pkg/kinds/dashboard/addenda.go similarity index 100% rename from pkg/coremodel/dashboard/addenda.go rename to pkg/kinds/dashboard/addenda.go diff --git a/pkg/kinds/dashboard/dashboard_kind_gen.go b/pkg/kinds/dashboard/dashboard_kind_gen.go new file mode 100644 index 0000000000000..6bdb47dacf34b --- /dev/null +++ b/pkg/kinds/dashboard/dashboard_kind_gen.go @@ -0,0 +1,104 @@ +// THIS FILE IS GENERATED. EDITING IS FUTILE. +// +// Generated by: +// kinds/gen.go +// Using jennies: +// CoreStructuredKindJenny +// +// Run 'make gen-cue' from repository root to regenerate. + +package dashboard + +import ( + "github.com/grafana/grafana/pkg/kindsys" + "github.com/grafana/thema" + "github.com/grafana/thema/vmux" +) + +// rootrel is the relative path from the grafana repository root to the +// directory containing the .cue files in which this kind is declared. Necessary +// for runtime errors related to the declaration and/or lineage to provide +// a real path to the correct .cue file. +const rootrel string = "kinds/structured/dashboard" + +// TODO standard generated docs +type Kind struct { + lin thema.ConvergentLineage[*Dashboard] + jendec vmux.Endec + valmux vmux.ValueMux[*Dashboard] + decl kindsys.Decl[kindsys.CoreStructuredMeta] +} + +// type guard +var _ kindsys.Structured = &Kind{} + +// TODO standard generated docs +func NewKind(rt *thema.Runtime, opts ...thema.BindOption) (*Kind, error) { + decl, err := kindsys.LoadCoreKind[kindsys.CoreStructuredMeta](rootrel, rt.Context(), nil) + if err != nil { + return nil, err + } + k := &Kind{ + decl: *decl, + } + + lin, err := decl.Some().BindKindLineage(rt, opts...) + if err != nil { + return nil, err + } + + // Get the thema.Schema that the meta says is in the current version (which + // codegen ensures is always the latest) + cursch := thema.SchemaP(lin, k.decl.Meta.CurrentVersion) + tsch, err := thema.BindType[*Dashboard](cursch, &Dashboard{}) + if err != nil { + // Should be unreachable, modulo bugs in the Thema->Go code generator + return nil, err + } + + k.jendec = vmux.NewJSONEndec("dashboard.json") + k.lin = tsch.ConvergentLineage() + k.valmux = vmux.NewValueMux(k.lin.TypedSchema(), k.jendec) + return k, nil +} + +// TODO standard generated docs +func (k *Kind) Name() string { + return "dashboard" +} + +// TODO standard generated docs +func (k *Kind) MachineName() string { + return "dashboard" +} + +// TODO standard generated docs +func (k *Kind) Lineage() thema.Lineage { + return k.lin +} + +// TODO standard generated docs +func (k *Kind) ConvergentLineage() thema.ConvergentLineage[*Dashboard] { + return k.lin +} + +// JSONValueMux is a version multiplexer that maps a []byte containing JSON data +// at any schematized dashboard version to an instance of Dashboard. +// +// Validation and translation errors emitted from this func will identify the +// input bytes as "dashboard.json". +// +// This is a thin wrapper around Thema's [vmux.ValueMux]. +func (k *Kind) JSONValueMux(b []byte) (*Dashboard, thema.TranslationLacunas, error) { + return k.valmux(b) +} + +// TODO standard generated docs +func (k *Kind) Maturity() kindsys.Maturity { + return k.decl.Meta.Maturity +} + +// TODO standard generated docs +func (k *Kind) Meta() kindsys.CoreStructuredMeta { + return k.decl.Meta +} diff --git a/pkg/coremodel/dashboard/dashboard_gen.go b/pkg/kinds/dashboard/dashboard_types_gen.go similarity index 62% rename from pkg/coremodel/dashboard/dashboard_gen.go rename to pkg/kinds/dashboard/dashboard_types_gen.go index 68aab0d37fe88..e4d63c1a16b13 100644 --- a/pkg/coremodel/dashboard/dashboard_gen.go +++ b/pkg/kinds/dashboard/dashboard_types_gen.go @@ -1,22 +1,14 @@ -// This file is autogenerated. DO NOT EDIT. +// THIS FILE IS GENERATED. EDITING IS FUTILE. // -// Generated by pkg/framework/coremodel/gen.go +// Generated by: +// kinds/gen.go +// Using jennies: +// GoTypesJenny // -// Derived from the Thema lineage declared in pkg/coremodel/dashboard/coremodel.cue -// -// Run `make gen-cue` from repository root to regenerate. +// Run 'make gen-cue' from repository root to regenerate. package dashboard -import ( - "embed" - "path/filepath" - - "github.com/grafana/grafana/pkg/cuectx" - "github.com/grafana/grafana/pkg/framework/coremodel" - "github.com/grafana/thema" -) - // Defines values for GraphTooltip. const ( GraphTooltipN0 GraphTooltip = 0 @@ -207,11 +199,8 @@ const ( VariableTypeTextbox VariableType = "textbox" ) -// Model is the Go representation of a dashboard. -// -// THIS TYPE IS INTENDED FOR INTERNAL USE BY THE GRAFANA BACKEND, AND IS SUBJECT TO BREAKING CHANGES. -// Equivalent Go types at stable import paths are provided in https://github.com/grafana/grok. -type Model struct { +// Dashboard defines model for dashboard. +type Dashboard struct { Annotations *struct { // TODO docs List []AnnotationQuery `json:"list"` @@ -298,29 +287,17 @@ type Model struct { WeekStart *string `json:"weekStart,omitempty"` } -// GraphTooltip is the Go representation of a Model.GraphTooltip. -// -// THIS TYPE IS INTENDED FOR INTERNAL USE BY THE GRAFANA BACKEND, AND IS SUBJECT TO BREAKING CHANGES. -// Equivalent Go types at stable import paths are provided in https://github.com/grafana/grok. +// GraphTooltip defines model for Dashboard.GraphTooltip. type GraphTooltip 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 Style 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 Timezone 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 AnnotationQuery struct { BuiltIn int `json:"builtIn"` @@ -343,16 +320,15 @@ type AnnotationQuery struct { Name *string `json:"name,omitempty"` // Query for annotation data. - RawQuery *string `json:"rawQuery,omitempty"` - ShowIn int `json:"showIn"` - Target *AnnotationTarget `json:"target,omitempty"` - Type string `json:"type"` + RawQuery *string `json:"rawQuery,omitempty"` + ShowIn int `json:"showIn"` + + // TODO docs + Target *AnnotationTarget `json:"target,omitempty"` + Type string `json:"type"` } -// AnnotationTarget is the Go representation of a dashboard.AnnotationTarget. -// -// 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. +// TODO docs type AnnotationTarget struct { Limit int64 `json:"limit"` MatchAny bool `json:"matchAny"` @@ -363,16 +339,10 @@ type AnnotationTarget struct { // 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 DashboardCursorSync int // FROM public/app/features/dashboard/state/Models.ts - ish // TODO docs -// -// THIS TYPE IS INTENDED FOR INTERNAL USE BY THE GRAFANA BACKEND, AND IS SUBJECT TO BREAKING CHANGES. -// Equivalent Go types at stable import paths are provided in https://github.com/grafana/grok. type DashboardLink struct { AsDropdown bool `json:"asDropdown"` Icon *string `json:"icon,omitempty"` @@ -386,25 +356,16 @@ type DashboardLink struct { Url *string `json:"url,omitempty"` } -// DashboardLinkType is the Go representation of a DashboardLink.Type. -// -// THIS TYPE IS INTENDED FOR INTERNAL USE BY THE GRAFANA BACKEND, AND IS SUBJECT TO BREAKING CHANGES. -// Equivalent Go types at stable import paths are provided in https://github.com/grafana/grok. +// DashboardLinkType defines model for DashboardLink.Type. type DashboardLinkType string -// DynamicConfigValue is the Go representation of a dashboard.DynamicConfigValue. -// -// 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. +// DynamicConfigValue defines model for dashboard.DynamicConfigValue. type DynamicConfigValue struct { Id string `json:"id"` Value *interface{} `json:"value,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 FieldColor struct { // Stores the fixed color value if mode is fixed FixedColor *string `json:"fixedColor,omitempty"` @@ -417,21 +378,12 @@ type FieldColor struct { } // TODO docs -// -// THIS TYPE IS INTENDED FOR INTERNAL USE BY THE GRAFANA BACKEND, AND IS SUBJECT TO BREAKING CHANGES. -// Equivalent Go types at stable import paths are provided in https://github.com/grafana/grok. type FieldColorModeId 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 FieldColorSeriesByMode string -// FieldConfig is the Go representation of a dashboard.FieldConfig. -// -// 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. +// FieldConfig defines model for dashboard.FieldConfig. type FieldConfig struct { // TODO docs Color *FieldColor `json:"color,omitempty"` @@ -467,7 +419,7 @@ type FieldConfig struct { // 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, + // An explicit 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 @@ -482,10 +434,7 @@ type FieldConfig struct { Writeable *bool `json:"writeable,omitempty"` } -// FieldConfigSource is the Go representation of a dashboard.FieldConfigSource. -// -// 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. +// FieldConfigSource defines model for dashboard.FieldConfigSource. type FieldConfigSource struct { Defaults struct { // TODO docs @@ -522,7 +471,7 @@ type FieldConfigSource struct { // 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, + // An explicit 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 @@ -548,25 +497,16 @@ type FieldConfigSource struct { } `json:"overrides"` } -// GraphPanel is the Go representation of a dashboard.GraphPanel. -// -// THIS TYPE IS INTENDED FOR INTERNAL USE BY THE GRAFANA BACKEND, AND IS SUBJECT TO BREAKING CHANGES. -// Equivalent Go types at stable import paths are provided in https://github.com/grafana/grok. +// GraphPanel defines model for dashboard.GraphPanel. type GraphPanel struct { // Support for legacy graph and heatmap panels. Type GraphPanelType `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 GraphPanelType string -// GridPos is the Go representation of a dashboard.GridPos. -// -// THIS TYPE IS INTENDED FOR INTERNAL USE BY THE GRAFANA BACKEND, AND IS SUBJECT TO BREAKING CHANGES. -// Equivalent Go types at stable import paths are provided in https://github.com/grafana/grok. +// GridPos defines model for dashboard.GridPos. type GridPos struct { // Panel H int `json:"h"` @@ -584,41 +524,26 @@ type GridPos struct { Y int `json:"y"` } -// HeatmapPanel is the Go representation of a dashboard.HeatmapPanel. -// -// THIS TYPE IS INTENDED FOR INTERNAL USE BY THE GRAFANA BACKEND, AND IS SUBJECT TO BREAKING CHANGES. -// Equivalent Go types at stable import paths are provided in https://github.com/grafana/grok. +// HeatmapPanel defines model for dashboard.HeatmapPanel. type HeatmapPanel struct { Type HeatmapPanelType `json:"type"` } -// HeatmapPanelType is the Go representation of a HeatmapPanel.Type. -// -// THIS TYPE IS INTENDED FOR INTERNAL USE BY THE GRAFANA BACKEND, AND IS SUBJECT TO BREAKING CHANGES. -// Equivalent Go types at stable import paths are provided in https://github.com/grafana/grok. +// HeatmapPanelType defines model for HeatmapPanel.Type. type HeatmapPanelType 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 MappingType string -// MatcherConfig is the Go representation of a dashboard.MatcherConfig. -// -// 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. +// MatcherConfig defines model for dashboard.MatcherConfig. type MatcherConfig struct { Id string `json:"id"` Options *interface{} `json:"options,omitempty"` } -// Model panels. Panels are canonically defined inline +// 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 Panel struct { // The datasource used in all targets. Datasource *struct { @@ -664,7 +589,7 @@ type Panel struct { // 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, + // An explicit 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 @@ -755,15 +680,9 @@ type Panel struct { // 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 PanelRepeatDirection 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 RangeMap struct { Options struct { // to and from are `number | null` in current ts, really not sure what to do @@ -779,16 +698,10 @@ type RangeMap struct { Type RangeMapType `json:"type"` } -// RangeMapType is the Go representation of a RangeMap.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. +// RangeMapType defines model for RangeMap.Type. type RangeMapType 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 RegexMap struct { Options struct { Pattern string `json:"pattern"` @@ -802,16 +715,10 @@ type RegexMap struct { Type RegexMapType `json:"type"` } -// RegexMapType is the Go representation of a RegexMap.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. +// RegexMapType defines model for RegexMap.Type. type RegexMapType 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 RowPanel struct { Collapsed bool `json:"collapsed"` @@ -830,16 +737,10 @@ type RowPanel struct { Type RowPanelType `json:"type"` } -// RowPanelType is the Go representation of a RowPanel.Type. -// -// THIS TYPE IS INTENDED FOR INTERNAL USE BY THE GRAFANA BACKEND, AND IS SUBJECT TO BREAKING CHANGES. -// Equivalent Go types at stable import paths are provided in https://github.com/grafana/grok. +// RowPanelType defines model for RowPanel.Type. type RowPanelType 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 SpecialValueMap struct { Options struct { Match SpecialValueMapOptionsMatch `json:"match"` @@ -854,40 +755,25 @@ type SpecialValueMap struct { Type SpecialValueMapType `json:"type"` } -// SpecialValueMapOptionsMatch is the Go representation of a SpecialValueMap.Options.Match. -// -// 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. +// SpecialValueMapOptionsMatch defines model for SpecialValueMap.Options.Match. type SpecialValueMapOptionsMatch string -// SpecialValueMapType is the Go representation of a SpecialValueMap.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. +// SpecialValueMapType defines model for SpecialValueMap.Type. type SpecialValueMapType 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 SpecialValueMatch 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 Model and Panel families, or filled +// 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 Target 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 Threshold struct { // TODO docs Color string `json:"color"` @@ -902,10 +788,7 @@ type Threshold struct { Value *float32 `json:"value,omitempty"` } -// ThresholdsConfig is the Go representation of a dashboard.ThresholdsConfig. -// -// THIS TYPE IS INTENDED FOR INTERNAL USE BY THE GRAFANA BACKEND, AND IS SUBJECT TO BREAKING CHANGES. -// Equivalent Go types at stable import paths are provided in https://github.com/grafana/grok. +// ThresholdsConfig defines model for dashboard.ThresholdsConfig. type ThresholdsConfig struct { Mode ThresholdsConfigMode `json:"mode"` @@ -925,53 +808,32 @@ type ThresholdsConfig struct { } `json:"steps"` } -// ThresholdsConfigMode is the Go representation of a ThresholdsConfig.Mode. -// -// THIS TYPE IS INTENDED FOR INTERNAL USE BY THE GRAFANA BACKEND, AND IS SUBJECT TO BREAKING CHANGES. -// Equivalent Go types at stable import paths are provided in https://github.com/grafana/grok. +// ThresholdsConfigMode defines model for ThresholdsConfig.Mode. type ThresholdsConfigMode string -// ThresholdsMode is the Go representation of a dashboard.ThresholdsMode. -// -// THIS TYPE IS INTENDED FOR INTERNAL USE BY THE GRAFANA BACKEND, AND IS SUBJECT TO BREAKING CHANGES. -// Equivalent Go types at stable import paths are provided in https://github.com/grafana/grok. +// ThresholdsMode defines model for dashboard.ThresholdsMode. type ThresholdsMode 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 Transformation struct { Id string `json:"id"` Options map[string]interface{} `json:"options"` } // 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 ValueMap struct { Options map[string]interface{} `json:"options"` Type ValueMapType `json:"type"` } -// ValueMapType is the Go representation of a ValueMap.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. +// ValueMapType defines model for ValueMap.Type. type ValueMapType 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 ValueMapping 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 ValueMappingResult struct { Color *string `json:"color,omitempty"` Icon *string `json:"icon,omitempty"` @@ -983,85 +845,16 @@ type ValueMappingResult struct { // 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 VariableModel struct { Label *string `json:"label,omitempty"` Name string `json:"name"` Type VariableModelType `json:"type"` } -// VariableModelType is the Go representation of a VariableModel.Type. -// -// THIS TYPE IS INTENDED FOR INTERNAL USE BY THE GRAFANA BACKEND, AND IS SUBJECT TO BREAKING CHANGES. -// Equivalent Go types at stable import paths are provided in https://github.com/grafana/grok. +// VariableModelType defines model for VariableModel.Type. type VariableModelType 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 VariableType string - -//go:embed coremodel.cue -var cueFS embed.FS - -// The current version of the coremodel schema, as declared in coremodel.cue. -// This version determines what schema version is returned from [Coremodel.CurrentSchema], -// and which schema version is used for code generation within the grafana/grafana repository. -// -// The code generator ensures that this is always the latest Thema schema version. -var currentVersion = thema.SV(0, 0) - -// Lineage returns the Thema lineage representing a Grafana 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(rt *thema.Runtime, opts ...thema.BindOption) (thema.Lineage, error) { - return cuectx.LoadGrafanaInstancesWithThema(filepath.Join("pkg", "coremodel", "dashboard"), cueFS, rt, opts...) -} - -var _ thema.LineageFactory = Lineage -var _ coremodel.Interface = &Coremodel{} - -// Coremodel contains the foundational schema declaration for dashboards. -// It implements coremodel.Interface. -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{} -} - -// New returns a new instance of the dashboard coremodel. -// -// Note that this function does not cache, and initially loading a Thema lineage -// can be expensive. As such, the Grafana backend should prefer to access this -// coremodel through a registry (pkg/framework/coremodel/registry), which does cache. -func New(rt *thema.Runtime) (*Coremodel, error) { - lin, err := Lineage(rt) - if err != nil { - return nil, err - } - - return &Coremodel{ - lin: lin, - }, nil -} diff --git a/pkg/coremodel/dashboard/dashboards_test.go b/pkg/kinds/dashboard/dashboards_test.go similarity index 91% rename from pkg/coremodel/dashboard/dashboards_test.go rename to pkg/kinds/dashboard/dashboards_test.go index ba4fa4a7fc4f7..0115ae62fbdad 100644 --- a/pkg/coremodel/dashboard/dashboards_test.go +++ b/pkg/kinds/dashboard/dashboards_test.go @@ -10,10 +10,9 @@ import ( "testing" "cuelang.org/go/cue/errors" - "github.com/stretchr/testify/require" - - "github.com/grafana/grafana/pkg/coremodel/dashboard" "github.com/grafana/grafana/pkg/cuectx" + "github.com/grafana/grafana/pkg/kinds/dashboard" + "github.com/stretchr/testify/require" ) func TestDevenvDashboardValidity(t *testing.T) { @@ -22,7 +21,7 @@ func TestDevenvDashboardValidity(t *testing.T) { m, err := themaTestableDashboards(os.DirFS(path)) require.NoError(t, err) - cm, err := dashboard.New(cuectx.GrafanaThemaRuntime()) + dk, err := dashboard.NewKind(cuectx.GrafanaThemaRuntime()) require.NoError(t, err) for path, b := range m { @@ -31,7 +30,7 @@ func TestDevenvDashboardValidity(t *testing.T) { cv, err := cuectx.JSONtoCUE(path, b) require.NoError(t, err, "error while decoding dashboard JSON into a CUE value") - _, err = cm.CurrentSchema().Validate(cv) + _, err = dk.ConvergentLineage().TypedSchema().Validate(cv) if err != nil { // Testify trims errors to short length. We want the full text errstr := errors.Details(err, nil) diff --git a/pkg/kinds/playlist/playlist_kind_gen.go b/pkg/kinds/playlist/playlist_kind_gen.go new file mode 100644 index 0000000000000..2ae79506035ef --- /dev/null +++ b/pkg/kinds/playlist/playlist_kind_gen.go @@ -0,0 +1,104 @@ +// THIS FILE IS GENERATED. EDITING IS FUTILE. +// +// Generated by: +// kinds/gen.go +// Using jennies: +// CoreStructuredKindJenny +// +// Run 'make gen-cue' from repository root to regenerate. + +package playlist + +import ( + "github.com/grafana/grafana/pkg/kindsys" + "github.com/grafana/thema" + "github.com/grafana/thema/vmux" +) + +// rootrel is the relative path from the grafana repository root to the +// directory containing the .cue files in which this kind is declared. Necessary +// for runtime errors related to the declaration and/or lineage to provide +// a real path to the correct .cue file. +const rootrel string = "kinds/structured/playlist" + +// TODO standard generated docs +type Kind struct { + lin thema.ConvergentLineage[*Playlist] + jendec vmux.Endec + valmux vmux.ValueMux[*Playlist] + decl kindsys.Decl[kindsys.CoreStructuredMeta] +} + +// type guard +var _ kindsys.Structured = &Kind{} + +// TODO standard generated docs +func NewKind(rt *thema.Runtime, opts ...thema.BindOption) (*Kind, error) { + decl, err := kindsys.LoadCoreKind[kindsys.CoreStructuredMeta](rootrel, rt.Context(), nil) + if err != nil { + return nil, err + } + k := &Kind{ + decl: *decl, + } + + lin, err := decl.Some().BindKindLineage(rt, opts...) + if err != nil { + return nil, err + } + + // Get the thema.Schema that the meta says is in the current version (which + // codegen ensures is always the latest) + cursch := thema.SchemaP(lin, k.decl.Meta.CurrentVersion) + tsch, err := thema.BindType[*Playlist](cursch, &Playlist{}) + if err != nil { + // Should be unreachable, modulo bugs in the Thema->Go code generator + return nil, err + } + + k.jendec = vmux.NewJSONEndec("playlist.json") + k.lin = tsch.ConvergentLineage() + k.valmux = vmux.NewValueMux(k.lin.TypedSchema(), k.jendec) + return k, nil +} + +// TODO standard generated docs +func (k *Kind) Name() string { + return "playlist" +} + +// TODO standard generated docs +func (k *Kind) MachineName() string { + return "playlist" +} + +// TODO standard generated docs +func (k *Kind) Lineage() thema.Lineage { + return k.lin +} + +// TODO standard generated docs +func (k *Kind) ConvergentLineage() thema.ConvergentLineage[*Playlist] { + return k.lin +} + +// JSONValueMux is a version multiplexer that maps a []byte containing JSON data +// at any schematized dashboard version to an instance of Playlist. +// +// Validation and translation errors emitted from this func will identify the +// input bytes as "dashboard.json". +// +// This is a thin wrapper around Thema's [vmux.ValueMux]. +func (k *Kind) JSONValueMux(b []byte) (*Playlist, thema.TranslationLacunas, error) { + return k.valmux(b) +} + +// TODO standard generated docs +func (k *Kind) Maturity() kindsys.Maturity { + return k.decl.Meta.Maturity +} + +// TODO standard generated docs +func (k *Kind) Meta() kindsys.CoreStructuredMeta { + return k.decl.Meta +} diff --git a/pkg/kinds/playlist/playlist_types_gen.go b/pkg/kinds/playlist/playlist_types_gen.go new file mode 100644 index 0000000000000..d45a61f3e2c7f --- /dev/null +++ b/pkg/kinds/playlist/playlist_types_gen.go @@ -0,0 +1,59 @@ +// THIS FILE IS GENERATED. EDITING IS FUTILE. +// +// Generated by: +// kinds/gen.go +// Using jennies: +// GoTypesJenny +// +// Run 'make gen-cue' from repository root to regenerate. + +package playlist + +// Defines values for PlaylistItemType. +const ( + PlaylistItemTypeDashboardById PlaylistItemType = "dashboard_by_id" + + PlaylistItemTypeDashboardByTag PlaylistItemType = "dashboard_by_tag" + + PlaylistItemTypeDashboardByUid PlaylistItemType = "dashboard_by_uid" +) + +// Playlist defines model for playlist. +type Playlist struct { + // Interval sets the time between switching views in a playlist. + // FIXME: Is this based on a standardized format or what options are available? Can datemath be used? + Interval string `json:"interval"` + + // The ordered list of items that the playlist will iterate over. + // FIXME! This should not be optional, but changing it makes the godegen awkward + Items *[]PlaylistItem `json:"items,omitempty"` + + // Name of the playlist. + Name string `json:"name"` + + // Unique playlist identifier. Generated on creation, either by the + // creator of the playlist of by the application. + Uid string `json:"uid"` +} + +// PlaylistItem defines model for playlist.Item. +type PlaylistItem struct { + // Title is an unused property -- it will be removed in the future + Title *string `json:"title,omitempty"` + + // Type of the item. + Type PlaylistItemType `json:"type"` + + // Value depends on type and describes the playlist item. + // + // - dashboard_by_id: The value is an internal numerical identifier set by Grafana. This + // is not portable as the numerical identifier is non-deterministic between different instances. + // Will be replaced by dashboard_by_uid in the future. (deprecated) + // - dashboard_by_tag: The value is a tag which is set on any number of dashboards. All + // dashboards behind the tag will be added to the playlist. + // - dashboard_by_uid: The value is the dashboard UID + Value string `json:"value"` +} + +// Type of the item. +type PlaylistItemType string diff --git a/pkg/kinds/svg/svg_kind_gen.go b/pkg/kinds/svg/svg_kind_gen.go new file mode 100644 index 0000000000000..9722d91998bab --- /dev/null +++ b/pkg/kinds/svg/svg_kind_gen.go @@ -0,0 +1,54 @@ +// THIS FILE IS GENERATED. EDITING IS FUTILE. +// +// Generated by: +// kinds/gen.go +// Using jennies: +// RawKindJenny +// +// Run 'make gen-cue' from repository root to regenerate. + +package svg + +import ( + "github.com/grafana/grafana/pkg/kindsys" +) + +// TODO standard generated docs +type Kind struct { + decl kindsys.Decl[kindsys.RawMeta] +} + +// type guard +var _ kindsys.Raw = &Kind{} + +// TODO standard generated docs +func NewKind() (*Kind, error) { + decl, err := kindsys.LoadCoreKind[kindsys.RawMeta]("kinds/raw/svg", nil, nil) + if err != nil { + return nil, err + } + + return &Kind{ + decl: *decl, + }, nil +} + +// TODO standard generated docs +func (k *Kind) Name() string { + return "SVG" +} + +// TODO standard generated docs +func (k *Kind) MachineName() string { + return "svg" +} + +// TODO standard generated docs +func (k *Kind) Maturity() kindsys.Maturity { + return k.decl.Meta.Maturity +} + +// TODO standard generated docs +func (k *Kind) Meta() kindsys.RawMeta { + return k.decl.Meta +} diff --git a/pkg/kindsys/EXTENDING.md b/pkg/kindsys/EXTENDING.md new file mode 100644 index 0000000000000..93f36639f45e3 --- /dev/null +++ b/pkg/kindsys/EXTENDING.md @@ -0,0 +1,60 @@ +# Kind System + +This package contains Grafana's kind system, which defines the rules that govern all Grafana kind declarations, including both core and plugin kinds. It contains many contracts on which public promises of backwards compatibility are made. All changes must be considered with care. + +While this package is maintained by @grafana/grafana-as-code, contributions from others are a main goal! Any time you have the thought, "I wish this part of Grafana's codebase was consistent," rather than writing docs (that people will inevitably miss), it's worth seeing if you can express that consistency as a kindsys extension instead. + +This document is the guide to extending kindsys. But first, we have to identify kindsys's key components. + +## Elements of kindsys + +* **CUE framework** - the collection of .cue files in this directory, `pkg/kindsys`. These are schemas that define how Kinds are declared. +* **Go framework** - the Go package in this directory containing utilities for loading individual kind declarations, validating them against the CUE framework, and representing them consistently in Go. +* **Code generators** - `pkg/codegen` contains the codegen framework. Individual generators (which take one or many `pkg/kindsys.Decl`, and produce a single file) each have a `pkg/codegen/generator_*.go` file. +* **Registries** - generated lists of all or a well-defined subset of kinds that can be used in code. `pkg/registries/corekind` is a registry of all core `pkg/kindsys.Interface` implementations; `packages/grafana-schema/src/index.gen.ts` is a registry of all the TypeScript types generated from the current versions of each kind's schema. +* **Kind declarations** - the declarations of individual kinds. By kind category: + * **Core Structured** - each child directory of `kinds/structured`. + * **Raw** - each child directory of `kinds/raw`. + * **Composable** - In Grafana core, `public/app/plugins/*/*/models.cue` files. + * **Custom** - No examples in Grafana core. See [operator-app-sdk](https://github.com/grafana/operator-app-sdk) (TODO that repo is private; make it public, or point to public examples). + +The above are treated as similarly to stateless libraries - a layer beneath the main Grafana frontend and backend without dependencies on it (no storage, no API, no wire, etc.). This lack of dependencies, and their Apache v2 licensing, allow their use as libraries for external tools. + +## Extending kindsys + +Extending the kind system generally involves: + +* Introducing one or more new fields into the CUE framework +* Updating the Go framework to accommodate the new fields +* Updating the kind authoring and maturity planning docs to reflect the new extension +* (possibly) Writing one or more new code generators +* (possibly) Writing/refactoring some frontend code that depends on new codegen output +* (possibly) Writing/refactoring some backend code that depends on codegen output and/or the Go kind framework +* (possibly) Tweaking all existing kinds as-needed to accommodate the new extension + +_TODO detailed guide to the above steps_ + +The above steps certainly aren't trivial. But they all come only after figuring out a way to solve the problem you want to solve in terms of the kind system and code generation in the first place. + +_TODO brief guide on how to think in codegen_ + +## Extensions not involving kind metadata + +While the main path for extending kindsys is through adding metadata, there are some other ways of extending kindsys. + +### CUE attributes + +[CUE attributes](https://cuelang.org/docs/references/spec/#attributes) provide additional information to kind tooling. They are suitable when it is necessary for a schema author to express additional information about a particular field or definition within a schema, without actually modifying the meaning of the schema. Two such known patterns are: + +* Controlling nuanced behavior of code generators for some field or type. Example: [@cuetsy](https://github.com/grafana/cuetsy#usage) attributes, which govern TS output +* Expressing some kind of structured TODO or WIP information on a field or type that can be easily analyzed and fed into other systems. Example: a kind marked at least `stable` maturity may not have any `@grafanamaturity` attributes + +In both of these cases, attributes are a tool _for the individual kind author_ to convey something to downstream consumers of kind declarations. It is essential. While attributes allow consistency in _how_ a particular task is accomplished, they leave _when_ to apply the rule up to the judgment of the kind author. + +Attributes occupy an awkward middle ground. They are more challenging to implement than standard kind framework properties, and less consistent than general codegen transformers while still imposing a cognitive burden on kind authors. They should be the last tool you reach for - but may be the only tool available when field-level schema metadata is required. + +TODO create a general pattern for self-contained attribute parser/validators to follow + +### Codegen transformers + +TODO actually write this - use `Uid`->`UID` as example diff --git a/pkg/kindsys/errors.go b/pkg/kindsys/errors.go new file mode 100644 index 0000000000000..a02fb887ed9b4 --- /dev/null +++ b/pkg/kindsys/errors.go @@ -0,0 +1,35 @@ +package kindsys + +import "errors" + +// TODO consider rewriting with https://github.com/cockroachdb/errors + +var ( + // ErrValueNotExist indicates that a necessary CUE value did not exist. + ErrValueNotExist = errors.New("cue value does not exist") + + // ErrValueNotAKind indicates that a provided CUE value is not any variety of + // Interface. This is almost always an end-user error - they oops'd and provided the + // wrong path, file, etc. + ErrValueNotAKind = errors.New("not a kind") +) + +func ewrap(actual, is error) error { + return &errPassthrough{ + actual: actual, + is: is, + } +} + +type errPassthrough struct { + actual error + is error +} + +func (e *errPassthrough) Is(err error) bool { + return errors.Is(err, e.actual) || errors.Is(err, e.is) +} + +func (e *errPassthrough) Error() string { + return e.actual.Error() +} diff --git a/pkg/kindsys/kind.go b/pkg/kindsys/kind.go new file mode 100644 index 0000000000000..ea02870ce26e6 --- /dev/null +++ b/pkg/kindsys/kind.go @@ -0,0 +1,82 @@ +package kindsys + +import ( + "fmt" + + "github.com/grafana/thema" +) + +// TODO docs +type Maturity string + +const ( + MaturityMerged Maturity = "merged" + MaturityExperimental Maturity = "experimental" + MaturityStable Maturity = "stable" + MaturityMature Maturity = "mature" +) + +func maturityIdx(m Maturity) int { + // icky to do this globally, this is effectively setting a default + if string(m) == "" { + m = MaturityMerged + } + + for i, ms := range maturityOrder { + if m == ms { + return i + } + } + panic(fmt.Sprintf("unknown maturity milestone %s", m)) +} + +var maturityOrder = []Maturity{ + MaturityMerged, + MaturityExperimental, + MaturityStable, + MaturityMature, +} + +func (m Maturity) Less(om Maturity) bool { + return maturityIdx(m) < maturityIdx(om) +} + +// TODO docs +type Interface interface { + // TODO docs + Name() string + + // TODO docs + MachineName() string + + // TODO docs + Maturity() Maturity // TODO unclear if we want maturity for raw kinds +} + +// TODO docs +type Raw interface { + Interface + + // TODO docs + Meta() RawMeta +} + +type Structured interface { + Interface + + // TODO docs + Lineage() thema.Lineage + + // TODO docs + Meta() CoreStructuredMeta // TODO figure out how to reconcile this interface with CustomStructuredMeta +} + +// type Composable interface { +// Interface +// +// // TODO docs +// Lineage() thema.Lineage +// +// // TODO docs +// Meta() CoreStructuredMeta // TODO figure out how to reconcile this interface with CustomStructuredMeta +// } diff --git a/pkg/kindsys/kindcats.cue b/pkg/kindsys/kindcats.cue new file mode 100644 index 0000000000000..4321b45b78b57 --- /dev/null +++ b/pkg/kindsys/kindcats.cue @@ -0,0 +1,166 @@ +package kindsys + +import ( + "strings" + + "github.com/grafana/thema" +) + +// A Kind specifies a type of Grafana resource. +// +// An instance of a Kind is called an entity. An entity is a sequence of bytes - +// for example, a JSON file or HTTP request body - that conforms to the +// constraints defined in a Kind, and enforced by Grafana's entity system. +// +// Once Grafana has determined a given byte sequence to be an +// instance of a known Kind, kind-specific behaviors can be applied, +// requests can be routed, events can be triggered, etc. +// +// Classes and objects in most programming languages are analogous: +// - #Kind is like a `class` keyword +// - Each declaration of #Kind is like a class declaration +// - Byte sequences are like arguments to the class constructor +// - Entities are like objects - what's returned from the constructor +// +// There are four categories of kinds: Raw, Composable, CoreStructured, +// and CustomStructured. +#Kind: #Raw | #Composable | #CoreStructured | #CustomStructured + +// properties shared between all kind categories. +_sharedKind: { + // name is the canonical name of a Kind, as expressed in PascalCase. + // + // To ensure names are generally portable and amenable for consumption + // in various mechanical tasks, name largely follows the relatively + // strict DNS label naming standard as defined in RFC 1123: + // - Contain at most 63 characters + // - Contain only lowercase alphanumeric characters or '-' + // - Start with an uppercase alphabetic character + // - End with an alphanumeric character + name: =~"^([A-Z][a-zA-Z0-9-]{0,61}[a-zA-Z0-9])$" + + // machineName is the case-normalized (lowercase) version of [name]. This + // version of the name is preferred for use in most mechanical contexts, + // as case normalization ensures that case-insensitive and case-sensitive + // checks will never disagree on uniqueness. + // + // In addition to lowercase normalization, dashes are transformed to underscores. + machineName: strings.ToLower(strings.Replace(name, "-", "_", -1)) + + // pluralName is the pluralized form of name. Defaults to name + "s". + pluralName: =~"^([A-Z][a-zA-Z0-9-]{0,61}[a-zA-Z])$" | *(name + "s") + + // pluralMachineName is the pluralized form of [machineName]. The same case + // normalization and dash transformation is applied to [pluralName] as [machineName] + // applies to [name]. + pluralMachineName: strings.ToLower(strings.Replace(pluralName, "-", "_", -1)) + + // lineageIsGroup indicates whether the lineage in this kind is "grouped". In a + // grouped lineage, each top-level field in the schema specifies a discrete + // object that is expected to exist in the wild + // + // This field is set at the framework level, and cannot be in the declaration of + // any individual kind. + // + // This is likely to eventually become a first-class property in Thema: + // https://github.com/grafana/thema/issues/62 + lineageIsGroup: bool + + maturity: #Maturity + + // The kind system itself is not mature enough yet for any single + // kind to advance beyond "experimental" + // TODO allow more maturity stages once system is ready https://github.com/orgs/grafana/projects/133/views/8 + maturity: *"merged" | "experimental" + + // form indicates whether the kind has a schema ("structured") or not ("raw") + form: "structured" | "raw" +} + +// Maturity indicates the how far a given kind declaration is in its initial +// journey. Mature kinds still evolve, but with guarantees about compatibility. +#Maturity: "merged" | "experimental" | "stable" | "mature" + +// Structured encompasses all three of the structured kind categories, in which +// a schema specifies validity rules for the byte sequence. These represent all +// the conventional types and functional resources in Grafana, such as +// dashboards and datasources. +// +// Structured kinds may be defined either by Grafana itself (#CoreStructured), +// or by plugins (#CustomStructured). Plugin-defined kinds have a slightly +// reduced set of capabilities, due to the constraints imposed by them being run +// in separate processes, and the risks arising from executing code from +// potentially untrusted third parties. +#Structured: S={ + _sharedKind + form: "structured" + + // lineage is the Thema lineage containing all the schemas that have existed for this kind. + // It is required that lineage.name is the same as the [machineName]. + lineage: thema.#Lineage & { name: S.machineName } + + currentVersion: thema.#SyntacticVersion & (thema.#LatestVersion & {lin: lineage}).out +} + +// Raw is a category of Kind that specifies handling for a raw file, +// like an image, or an svg or parquet file. Grafana mostly acts as asset storage for raw +// kinds: the byte sequence is a black box to Grafana, and type is determined +// through metadata such as file extension. +#Raw: { + _sharedKind + form: "raw" + + // TODO docs + extensions?: [...string] + + lineageIsGroup: false + + // known TODOs + // - sanitize function + // - get summary +} + +// TODO +#CustomStructured: { + #Structured + + lineageIsGroup: false + ... +} + +// TODO +#CoreStructured: { + #Structured + + lineageIsGroup: false +} + +// Composable is a category of structured kind that provides schema elements for +// composition into CoreStructured and CustomStructured kinds. Grafana plugins +// provide composable kinds; for example, a datasource plugin provides one to +// describe the structure of its queries, which is then composed into dashboards +// and alerting rules. +// +// Each Composable is an implementation of exactly one Slot, a shared meta-schema +// defined by Grafana itself that constrains the shape of schemas declared in +// that ComposableKind. +#Composable: S={ + _sharedKind + form: "structured" + + // TODO docs + // TODO unify this with the existing slots decls in pkg/framework/coremodel + slot: "Panel" | "Query" | "DSConfig" + + // TODO unify this with the existing slots decls in pkg/framework/coremodel + lineageIsGroup: bool & [ + if slot == "Panel" { true }, + if slot == "DSConfig" { true }, + if slot == "Query" { false }, + ][0] + + // lineage is the Thema lineage containing all the schemas that have existed for this kind. + // It is required that lineage.name is the same as the [machineName]. + lineage: thema.#Lineage & { name: S.machineName } +} + diff --git a/pkg/kindsys/kindmetas.go b/pkg/kindsys/kindmetas.go new file mode 100644 index 0000000000000..946c07fbe7628 --- /dev/null +++ b/pkg/kindsys/kindmetas.go @@ -0,0 +1,74 @@ +package kindsys + +import "github.com/grafana/thema" + +// CommonMeta contains the kind metadata common to all categories of kinds. +type CommonMeta struct { + Name string `json:"name"` + PluralName string `json:"pluralName"` + MachineName string `json:"machineName"` + PluralMachineName string `json:"pluralMachineName"` + LineageIsGroup bool `json:"lineageIsGroup"` + Maturity Maturity `json:"maturity"` +} + +// TODO generate from type.cue +type RawMeta struct { + CommonMeta + Extensions []string `json:"extensions"` +} + +func (m RawMeta) _private() {} +func (m RawMeta) Common() CommonMeta { + return m.CommonMeta +} + +// TODO +type CoreStructuredMeta struct { + CommonMeta + CurrentVersion thema.SyntacticVersion `json:"currentVersion"` +} + +func (m CoreStructuredMeta) _private() {} +func (m CoreStructuredMeta) Common() CommonMeta { + return m.CommonMeta +} + +// TODO +type CustomStructuredMeta struct { + CommonMeta + CurrentVersion thema.SyntacticVersion `json:"currentVersion"` +} + +func (m CustomStructuredMeta) _private() {} +func (m CustomStructuredMeta) Common() CommonMeta { + return m.CommonMeta +} + +// TODO +type ComposableMeta struct { + CommonMeta + CurrentVersion thema.SyntacticVersion `json:"currentVersion"` +} + +func (m ComposableMeta) _private() {} +func (m ComposableMeta) Common() CommonMeta { + return m.CommonMeta +} + +// SomeKindMeta is an interface type to abstract over the different kind +// metadata struct types: [RawMeta], [CoreStructuredMeta], +// [CustomStructuredMeta]. +// +// It is the traditional interface counterpart to the generic type constraint +// KindMetas. +type SomeKindMeta interface { + _private() + Common() CommonMeta +} + +// KindMetas is a type parameter that comprises the base possible set of +// kind metadata configurations. +type KindMetas interface { + RawMeta | CoreStructuredMeta | CustomStructuredMeta | ComposableMeta +} diff --git a/pkg/kindsys/load.go b/pkg/kindsys/load.go new file mode 100644 index 0000000000000..d8d4cda1c67ef --- /dev/null +++ b/pkg/kindsys/load.go @@ -0,0 +1,242 @@ +package kindsys + +import ( + "fmt" + "io/fs" + "path/filepath" + "sync" + + "cuelang.org/go/cue" + "cuelang.org/go/cue/errors" + "github.com/grafana/grafana" + "github.com/grafana/grafana/pkg/cuectx" + "github.com/grafana/thema" + tload "github.com/grafana/thema/load" +) + +// CoreStructuredDeclParentPath is the path, relative to the repository root, where +// each child directory is expected to contain .cue files declaring one +// CoreStructured kind. +var CoreStructuredDeclParentPath = filepath.Join("kinds", "structured") + +// RawDeclParentPath is the path, relative to the repository root, where each child +// directory is expected to contain .cue files declaring one Raw kind. +var RawDeclParentPath = filepath.Join("kinds", "raw") + +// GoCoreKindParentPath is the path, relative to the repository root, to the directory +// containing one directory per kind, full of generated Go kind output: types and bindings. +var GoCoreKindParentPath = filepath.Join("pkg", "kinds") + +// TSCoreKindParentPath is the path, relative to the repository root, to the directory that +// contains one directory per kind, full of generated TS kind output: types and default consts. +var TSCoreKindParentPath = filepath.Join("packages", "grafana-schema", "src", "raw") + +var defaultFramework cue.Value +var fwOnce sync.Once + +func init() { + loadpFrameworkOnce() +} + +func loadpFrameworkOnce() { + fwOnce.Do(func() { + var err error + defaultFramework, err = doLoadFrameworkCUE(cuectx.GrafanaCUEContext()) + if err != nil { + panic(err) + } + }) +} + +var prefix = filepath.Join("/pkg", "kindsys") + +func doLoadFrameworkCUE(ctx *cue.Context) (cue.Value, error) { + var v cue.Value + var err error + + absolutePath := prefix + if !filepath.IsAbs(absolutePath) { + absolutePath, err = filepath.Abs(absolutePath) + if err != nil { + return v, err + } + } + + bi, err := tload.InstancesWithThema(grafana.CueSchemaFS, absolutePath) + if err != nil { + return v, err + } + v = ctx.BuildInstance(bi) + + if err = v.Validate(cue.Concrete(false), cue.All()); err != nil { + return cue.Value{}, fmt.Errorf("coremodel framework loaded cue.Value has err: %w", err) + } + + return v, nil +} + +// CUEFramework returns a cue.Value representing all the kind framework +// raw CUE files. +// +// For low-level use in constructing other types and APIs, while still letting +// us declare all the frameworky CUE bits in a single package. Other Go types +// make the constructs in this value easy to use. +// +// All calling code within grafana/grafana is expected to use Grafana's +// singleton [cue.Context], returned from [cuectx.GrafanaCUEContext]. If nil +// is passed, the singleton will be used. +func CUEFramework(ctx *cue.Context) cue.Value { + if ctx == nil || ctx == cuectx.GrafanaCUEContext() { + // Ensure framework is loaded, even if this func is called + // from an init() somewhere. + loadpFrameworkOnce() + return defaultFramework + } + // Error guaranteed to be nil here because erroring would have caused init() to panic + v, _ := doLoadFrameworkCUE(ctx) // nolint:errcheck + return v +} + +// ToKindMeta takes a cue.Value expected to represent a kind of the category +// specified by the type parameter and populates the Go type from the cue.Value. +func ToKindMeta[T KindMetas](v cue.Value) (T, error) { + meta := new(T) + if !v.Exists() { + return *meta, ErrValueNotExist + } + + fw := CUEFramework(v.Context()) + var kdef cue.Value + + anymeta := any(*meta).(SomeKindMeta) + switch anymeta.(type) { + case RawMeta: + kdef = fw.LookupPath(cue.MakePath(cue.Def("Raw"))) + case CoreStructuredMeta: + kdef = fw.LookupPath(cue.MakePath(cue.Def("CoreStructured"))) + case CustomStructuredMeta: + kdef = fw.LookupPath(cue.MakePath(cue.Def("CustomStructured"))) + case ComposableMeta: + kdef = fw.LookupPath(cue.MakePath(cue.Def("Composable"))) + default: + // unreachable so long as all the possibilities in KindMetas have switch branches + panic("unreachable") + } + + item := v.Unify(kdef) + if err := item.Validate(cue.Concrete(false), cue.All()); err != nil { + return *meta, ewrap(item.Err(), ErrValueNotAKind) + } + if err := item.Decode(meta); err != nil { + // Should only be reachable if CUE and Go framework types have diverged + panic(errors.Details(err, nil)) + } + + return *meta, nil +} + +// SomeDecl represents a single kind declaration, having been loaded +// and validated by a func such as [LoadCoreKind]. +// +// The underlying type of the Meta field indicates the category of +// kind. +type SomeDecl struct { + // V is the cue.Value containing the entire Kind declaration. + V cue.Value + // Meta contains the kind's metadata settings. + Meta SomeKindMeta +} + +// BindKindLineage binds the lineage for the kind declaration. nil, nil is returned +// for raw kinds. +// +// For kinds with a corresponding Go type, it is left to the caller to associate +// that Go type with the lineage returned from this function by a call to [thema.BindType]. +func (decl *SomeDecl) BindKindLineage(rt *thema.Runtime, opts ...thema.BindOption) (thema.Lineage, error) { + if rt == nil { + rt = cuectx.GrafanaThemaRuntime() + } + switch decl.Meta.(type) { + case RawMeta: + return nil, nil + case CoreStructuredMeta, CustomStructuredMeta, ComposableMeta: + return thema.BindLineage(decl.V.LookupPath(cue.MakePath(cue.Str("lineage"))), rt, opts...) + default: + panic("unreachable") + } +} + +// IsRaw indicates whether the represented kind is a raw kind. +func (decl *SomeDecl) IsRaw() bool { + _, is := decl.Meta.(RawMeta) + return is +} + +// IsCoreStructured indicates whether the represented kind is a core structured kind. +func (decl *SomeDecl) IsCoreStructured() bool { + _, is := decl.Meta.(CoreStructuredMeta) + return is +} + +// IsCustomStructured indicates whether the represented kind is a custom structured kind. +func (decl *SomeDecl) IsCustomStructured() bool { + _, is := decl.Meta.(CustomStructuredMeta) + return is +} + +// IsComposable indicates whether the represented kind is a composable kind. +func (decl *SomeDecl) IsComposable() bool { + _, is := decl.Meta.(ComposableMeta) + return is +} + +// Decl represents a single kind declaration, having been loaded +// and validated by a func such as [LoadCoreKind]. +// +// Its type parameter indicates the category of kind. +type Decl[T KindMetas] struct { + // V is the cue.Value containing the entire Kind declaration. + V cue.Value + // Meta contains the kind's metadata settings. + Meta T +} + +// Some converts the typed Decl to the equivalent typeless SomeDecl. +func (decl *Decl[T]) Some() *SomeDecl { + return &SomeDecl{ + V: decl.V, + Meta: any(decl.Meta).(SomeKindMeta), + } +} + +// LoadCoreKind loads and validates a core kind declaration of the kind category +// indicated by the type parameter. On success, it returns a [Decl] which +// contains the entire contents of the kind declaration. +// +// declpath is the path to the directory containing the core kind declaration, +// relative to the grafana/grafana root. For example, dashboards are in +// "kinds/structured/dashboard". +// +// The .cue file bytes containing the core kind declaration will be retrieved +// from the central embedded FS, [grafana.CueSchemaFS]. If desired (e.g. for +// testing), an optional fs.FS may be provided via the overlay parameter, which +// will be merged over [grafana.CueSchemaFS]. But in typical circumstances, +// overlay can and should be nil. +// +// This is a low-level function, primarily intended for use in code generation. +// For representations of core kinds that are useful in Go programs at runtime, +// see ["github.com/grafana/grafana/pkg/registry/corekind"]. +func LoadCoreKind[T RawMeta | CoreStructuredMeta](declpath string, ctx *cue.Context, overlay fs.FS) (*Decl[T], error) { + vk, err := cuectx.BuildGrafanaInstance(declpath, "kind", ctx, overlay) + if err != nil { + return nil, err + } + decl := &Decl[T]{ + V: vk, + } + decl.Meta, err = ToKindMeta[T](vk) + if err != nil { + return nil, err + } + return decl, nil +} diff --git a/pkg/registry/corekind/base.go b/pkg/registry/corekind/base.go new file mode 100644 index 0000000000000..c2540ea820f4a --- /dev/null +++ b/pkg/registry/corekind/base.go @@ -0,0 +1,76 @@ +package corekind + +import ( + "sync" + + "github.com/google/wire" + "github.com/grafana/grafana/pkg/cuectx" + "github.com/grafana/grafana/pkg/kindsys" + "github.com/grafana/thema" +) + +// KindSet contains all of the wire-style providers related to kinds. +var KindSet = wire.NewSet( + NewBase, +) + +var ( + baseOnce sync.Once + defaultBase *Base +) + +// NewBase provides a registry of all core raw and structured kinds, without any +// composition of slot kinds. +// +// All calling code within grafana/grafana is expected to use Grafana's +// singleton [thema.Runtime], returned from [cuectx.GrafanaThemaRuntime]. If nil +// is passed, the singleton will be used. +func NewBase(rt *thema.Runtime) *Base { + allrt := cuectx.GrafanaThemaRuntime() + if rt == nil || rt == allrt { + baseOnce.Do(func() { + defaultBase = doNewBase(allrt) + }) + return defaultBase + } + + return doNewBase(rt) +} + +// All returns a slice of the [kindsys.Interface] instances corresponding to all +// core raw and structured kinds. +// +// The returned slice is sorted lexicographically by kind machine name. +func (b *Base) All() []kindsys.Interface { + ret := make([]kindsys.Interface, len(b.all)) + copy(ret, b.all) + return ret +} + +// AllRaw returns a slice of the [kindsys.Raw] instances for all raw kinds. +// +// The returned slice is sorted lexicographically by kind machine name. +func (b *Base) AllRaw() []kindsys.Raw { + ret := make([]kindsys.Raw, 0, b.numRaw) + for _, k := range b.all { + if rk, is := k.(kindsys.Raw); is { + ret = append(ret, rk) + } + } + + return ret +} + +// AllStructured returns a slice of the [kindsys.Structured] instances for +// all core structured kinds. +// +// The returned slice is sorted lexicographically by kind machine name. +func (b *Base) AllStructured() []kindsys.Structured { + ret := make([]kindsys.Structured, 0, b.numStructured) + for _, k := range b.all { + if rk, is := k.(kindsys.Structured); is { + ret = append(ret, rk) + } + } + return ret +} diff --git a/pkg/registry/corekind/base_gen.go b/pkg/registry/corekind/base_gen.go new file mode 100644 index 0000000000000..b6064eaf1e143 --- /dev/null +++ b/pkg/registry/corekind/base_gen.go @@ -0,0 +1,88 @@ +// THIS FILE IS GENERATED. EDITING IS FUTILE. +// +// Generated by: +// kinds/gen.go +// Using jennies: +// BaseCoreRegistryJenny +// +// Run 'make gen-cue' from repository root to regenerate. + +package corekind + +import ( + "fmt" + + "github.com/grafana/grafana/pkg/kinds/dashboard" + "github.com/grafana/grafana/pkg/kinds/playlist" + "github.com/grafana/grafana/pkg/kinds/svg" + "github.com/grafana/grafana/pkg/kindsys" + "github.com/grafana/thema" +) + +// Base is a registry of kindsys.Interface. It provides two modes for accessing +// kinds: individually via literal named methods, or as a slice returned from +// an All*() method. +// +// Prefer the individual named methods for use cases where the particular kind(s) that +// are needed are known to the caller. For example, a dashboard linter can know that it +// specifically wants the dashboard kind. +// +// Prefer All*() methods when performing operations generically across all kinds. +// For example, a validation HTTP middleware for any kind-schematized object type. +type Base struct { + all []kindsys.Interface + numRaw, numStructured int + dashboard *dashboard.Kind + playlist *playlist.Kind + svg *svg.Kind +} + +// type guards +var ( + _ kindsys.Structured = &dashboard.Kind{} + _ kindsys.Structured = &playlist.Kind{} + _ kindsys.Raw = &svg.Kind{} +) + +// Dashboard returns the [kindsys.Interface] implementation for the dashboard kind. +func (b *Base) Dashboard() *dashboard.Kind { + return b.dashboard +} + +// Playlist returns the [kindsys.Interface] implementation for the playlist kind. +func (b *Base) Playlist() *playlist.Kind { + return b.playlist +} + +// SVG returns the [kindsys.Interface] implementation for the svg kind. +func (b *Base) SVG() *svg.Kind { + return b.svg +} + +func doNewBase(rt *thema.Runtime) *Base { + var err error + reg := &Base{ + numRaw: 1, + numStructured: 2, + } + + reg.dashboard, err = dashboard.NewKind(rt) + if err != nil { + panic(fmt.Sprintf("error while initializing the dashboard Kind: %s", err)) + } + reg.all = append(reg.all, reg.dashboard) + + reg.playlist, err = playlist.NewKind(rt) + if err != nil { + panic(fmt.Sprintf("error while initializing the playlist Kind: %s", err)) + } + reg.all = append(reg.all, reg.playlist) + + reg.svg, err = svg.NewKind() + if err != nil { + panic(fmt.Sprintf("error while initializing the svg Kind: %s", err)) + } + reg.all = append(reg.all, reg.svg) + + return reg +} diff --git a/pkg/server/wire.go b/pkg/server/wire.go index 1123c99789360..372d590faf16d 100644 --- a/pkg/server/wire.go +++ b/pkg/server/wire.go @@ -12,7 +12,6 @@ import ( "github.com/grafana/grafana/pkg/bus" "github.com/grafana/grafana/pkg/cuectx" "github.com/grafana/grafana/pkg/expr" - cmreg "github.com/grafana/grafana/pkg/framework/coremodel/registry" "github.com/grafana/grafana/pkg/infra/db" "github.com/grafana/grafana/pkg/infra/httpclient" "github.com/grafana/grafana/pkg/infra/httpclient/httpclientprovider" @@ -41,6 +40,7 @@ import ( managerStore "github.com/grafana/grafana/pkg/plugins/manager/store" "github.com/grafana/grafana/pkg/plugins/plugincontext" "github.com/grafana/grafana/pkg/plugins/repo" + "github.com/grafana/grafana/pkg/registry/corekind" "github.com/grafana/grafana/pkg/services/accesscontrol" "github.com/grafana/grafana/pkg/services/accesscontrol/acimpl" "github.com/grafana/grafana/pkg/services/accesscontrol/ossaccesscontrol" @@ -334,7 +334,7 @@ var wireBasicSet = wire.NewSet( avatar.ProvideAvatarCacheServer, authproxy.ProvideAuthProxy, statscollector.ProvideService, - cmreg.CoremodelSet, + corekind.KindSet, cuectx.GrafanaCUEContext, cuectx.GrafanaThemaRuntime, csrf.ProvideCSRFFilter, diff --git a/pkg/services/playlist/model.go b/pkg/services/playlist/model.go index 3c0f4b6ad6439..20eb8087d0067 100644 --- a/pkg/services/playlist/model.go +++ b/pkg/services/playlist/model.go @@ -3,7 +3,7 @@ package playlist import ( "errors" - "github.com/grafana/grafana/pkg/coremodel/playlist" + "github.com/grafana/grafana/pkg/kinds/playlist" ) // Typed errors @@ -22,7 +22,7 @@ type Playlist struct { OrgId int64 `json:"-" db:"org_id"` } -type PlaylistDTO = playlist.Model +type PlaylistDTO = playlist.Playlist type PlaylistItemDTO = playlist.PlaylistItem type PlaylistItemType = playlist.PlaylistItemType diff --git a/pkg/services/publicdashboards/models/models.go b/pkg/services/publicdashboards/models/models.go index 4afa351385695..16d20ab6144a6 100644 --- a/pkg/services/publicdashboards/models/models.go +++ b/pkg/services/publicdashboards/models/models.go @@ -5,7 +5,7 @@ import ( "strconv" "time" - "github.com/grafana/grafana/pkg/coremodel/dashboard" + "github.com/grafana/grafana/pkg/kinds/dashboard" "github.com/grafana/grafana/pkg/models" "github.com/grafana/grafana/pkg/tsdb/legacydata" ) diff --git a/pkg/services/publicdashboards/service/query_test.go b/pkg/services/publicdashboards/service/query_test.go index 884b82d8d5978..b64f1cb929997 100644 --- a/pkg/services/publicdashboards/service/query_test.go +++ b/pkg/services/publicdashboards/service/query_test.go @@ -8,9 +8,9 @@ import ( "github.com/grafana/grafana-plugin-sdk-go/backend" "github.com/grafana/grafana-plugin-sdk-go/data" "github.com/grafana/grafana/pkg/components/simplejson" - dashboard2 "github.com/grafana/grafana/pkg/coremodel/dashboard" "github.com/grafana/grafana/pkg/infra/db" "github.com/grafana/grafana/pkg/infra/log" + dashboard2 "github.com/grafana/grafana/pkg/kinds/dashboard" grafanamodels "github.com/grafana/grafana/pkg/models" "github.com/grafana/grafana/pkg/services/annotations" "github.com/grafana/grafana/pkg/services/annotations/annotationsimpl" @@ -1024,11 +1024,11 @@ func TestBuildAnonymousUser(t *testing.T) { sqlStore := db.InitTestDB(t) dashboardStore := dashboardsDB.ProvideDashboardStore(sqlStore, sqlStore.Cfg, featuremgmt.WithFeatures(), tagimpl.ProvideService(sqlStore, sqlStore.Cfg)) dashboard := insertTestDashboard(t, dashboardStore, "testDashie", 1, 0, true, []map[string]interface{}{}, nil) - //publicdashboardStore := database.ProvideStore(sqlStore) - //service := &PublicDashboardServiceImpl{ + // publicdashboardStore := database.ProvideStore(sqlStore) + // service := &PublicDashboardServiceImpl{ // log: log.New("test.logger"), // store: publicdashboardStore, - //} + // } t.Run("will add datasource read and query permissions to user for each datasource in dashboard", func(t *testing.T) { user := buildAnonymousUser(context.Background(), dashboard) diff --git a/pkg/services/store/kind/playlist/summary.go b/pkg/services/store/kind/playlist/summary.go index 2221f7cbe94a6..d5ca2b896ab15 100644 --- a/pkg/services/store/kind/playlist/summary.go +++ b/pkg/services/store/kind/playlist/summary.go @@ -5,7 +5,7 @@ import ( "encoding/json" "fmt" - "github.com/grafana/grafana/pkg/coremodel/playlist" + "github.com/grafana/grafana/pkg/kinds/playlist" "github.com/grafana/grafana/pkg/models" ) @@ -22,7 +22,7 @@ func GetObjectSummaryBuilder() models.ObjectSummaryBuilder { } func summaryBuilder(ctx context.Context, uid string, body []byte) (*models.ObjectSummary, []byte, error) { - obj := &playlist.Model{} + obj := &playlist.Playlist{} err := json.Unmarshal(body, obj) if err != nil { return nil, nil, err // unable to read object diff --git a/pkg/services/store/kind/playlist/summary_test.go b/pkg/services/store/kind/playlist/summary_test.go index 49b18cead8558..f6d346ea0aea0 100644 --- a/pkg/services/store/kind/playlist/summary_test.go +++ b/pkg/services/store/kind/playlist/summary_test.go @@ -5,7 +5,7 @@ import ( "encoding/json" "testing" - "github.com/grafana/grafana/pkg/coremodel/playlist" + "github.com/grafana/grafana/pkg/kinds/playlist" "github.com/stretchr/testify/require" ) @@ -16,7 +16,7 @@ func TestPlaylistSummary(t *testing.T) { _, _, err := builder(context.Background(), "abc", []byte("{invalid json")) require.Error(t, err) - playlist := playlist.Model{ + playlist := playlist.Playlist{ Interval: "30s", Name: "test", Items: &[]playlist.PlaylistItem{ diff --git a/public/app/features/playlist/types.ts b/public/app/features/playlist/types.ts index 0856917c17655..65b291c3515cb 100644 --- a/public/app/features/playlist/types.ts +++ b/public/app/features/playlist/types.ts @@ -1,4 +1,4 @@ -import { PlaylistItem as PlaylistItemFromSchema } from '@grafana/schema/src/raw/playlist/x/playlist.gen'; +import { PlaylistItem as PlaylistItemFromSchema } from '@grafana/schema'; import { DashboardQueryResult } from '../search/service'; diff --git a/public/app/plugins/gen.go b/public/app/plugins/gen.go index cbb28c1ee5319..ab9946669d61e 100644 --- a/public/app/plugins/gen.go +++ b/public/app/plugins/gen.go @@ -32,7 +32,6 @@ var skipPlugins = map[string]bool{ const sep = string(filepath.Separator) -// Generate TypeScript for all plugin models.cue func main() { if len(os.Args) > 1 { fmt.Fprintf(os.Stderr, "plugin thema code generator does not currently accept any arguments\n, got %q", os.Args)