Kindsys: Remove defs, Slot->SchemaInterface (#61069)

* kindsys: Remove defs, Slot->SchemaInterface

* Remove excess file

* Fix up tests

* Regenerate kinds report

* Final bits of cleanup

* Stop complaining, linter

* Update pkg/kindsys/kindcat_composable.cue

Co-authored-by: Tania <yalyna.ts@gmail.com>

Co-authored-by: Tania <yalyna.ts@gmail.com>
This commit is contained in:
sam boyer 2023-01-06 12:37:32 -05:00 committed by GitHub
parent c2ad447f8c
commit 4db3b2fd5c
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
31 changed files with 423 additions and 379 deletions

View File

@ -3,5 +3,5 @@ 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.#Core - a declaration of a core kind.
kindsys.#Core
// must be an instance of kindsys.Core - a declaration of a core kind.
kindsys.Core

View File

@ -17,7 +17,7 @@ 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) {
func ForGen(rt *thema.Runtime, decl kindsys.SomeDecl) (*DeclForGen, error) {
lin, err := decl.BindKindLineage(rt)
if err != nil {
return nil, err
@ -32,7 +32,7 @@ func ForGen(rt *thema.Runtime, decl *kindsys.SomeDecl) (*DeclForGen, error) {
// DeclForGen wraps [kindsys.SomeDecl] to provide trivial caching of
// the lineage declared by the kind (nil for raw kinds).
type DeclForGen struct {
*kindsys.SomeDecl
kindsys.SomeDecl
lin thema.Lineage
}

View File

@ -34,7 +34,6 @@ func (gen *genBaseRegistry) JennyName() string {
func (gen *genBaseRegistry) Generate(decls ...*DeclForGen) (*codejen.File, error) {
buf := new(bytes.Buffer)
if err := tmpls.Lookup("kind_registry.tmpl").Execute(buf, tvars_kind_registry{
NumStructured: len(decls),
PackageName: filepath.Base(gen.path),
KindPackagePrefix: filepath.ToSlash(filepath.Join("github.com/grafana/grafana", gen.kindrelroot)),
Kinds: decls,

View File

@ -15,9 +15,9 @@ import (
// all generated kinds.
//
// This generator only has output for core structured kinds.
func CoreKindJenny(gokindsdir string, cfg *CoreStructuredKindGeneratorConfig) OneToOne {
func CoreKindJenny(gokindsdir string, cfg *CoreKindJennyConfig) OneToOne {
if cfg == nil {
cfg = new(CoreStructuredKindGeneratorConfig)
cfg = new(CoreKindJennyConfig)
}
if cfg.GenDirName == nil {
cfg.GenDirName = func(decl *DeclForGen) string {
@ -31,8 +31,8 @@ func CoreKindJenny(gokindsdir string, cfg *CoreStructuredKindGeneratorConfig) On
}
}
// CoreStructuredKindGeneratorConfig holds configuration options for [CoreKindJenny].
type CoreStructuredKindGeneratorConfig struct {
// CoreKindJennyConfig holds configuration options for [CoreKindJenny].
type CoreKindJennyConfig 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
@ -40,7 +40,7 @@ type CoreStructuredKindGeneratorConfig struct {
type coreKindJenny struct {
gokindsdir string
cfg *CoreStructuredKindGeneratorConfig
cfg *CoreKindJennyConfig
}
var _ OneToOne = &coreKindJenny{}

View File

@ -38,7 +38,6 @@ type (
}
tvars_kind_registry struct {
// Header tvars_autogen_header
NumStructured int
PackageName string
KindPackagePrefix string
Kinds []*DeclForGen

View File

@ -30,7 +30,7 @@ func NewKind(rt *thema.Runtime, opts ...thema.BindOption) (*Kind, error) {
return nil, err
}
k := &Kind{
decl: *decl,
decl: decl,
}
lin, err := decl.Some().BindKindLineage(rt, opts...)
@ -91,9 +91,8 @@ func (k *Kind) Maturity() kindsys.Maturity {
// Decl returns the [kindsys.Decl] containing both CUE and Go representations of the
// {{ .Properties.MachineName }} declaration in .cue files.
func (k *Kind) Decl() *kindsys.Decl[kindsys.CoreProperties] {
d := k.decl
return &d
func (k *Kind) Decl() kindsys.Decl[kindsys.CoreProperties] {
return k.decl
}
// Props returns a [kindsys.SomeKindProps], with underlying type [kindsys.CoreProperties],

View File

@ -39,7 +39,7 @@ func NewKind(rt *thema.Runtime, opts ...thema.BindOption) (*Kind, error) {
return nil, err
}
k := &Kind{
decl: *decl,
decl: decl,
}
lin, err := decl.Some().BindKindLineage(rt, opts...)
@ -100,9 +100,8 @@ func (k *Kind) Maturity() kindsys.Maturity {
// Decl returns the [kindsys.Decl] containing both CUE and Go representations of the
// dashboard declaration in .cue files.
func (k *Kind) Decl() *kindsys.Decl[kindsys.CoreProperties] {
d := k.decl
return &d
func (k *Kind) Decl() kindsys.Decl[kindsys.CoreProperties] {
return k.decl
}
// Props returns a [kindsys.SomeKindProps], with underlying type [kindsys.CoreProperties],

View File

@ -39,7 +39,7 @@ func NewKind(rt *thema.Runtime, opts ...thema.BindOption) (*Kind, error) {
return nil, err
}
k := &Kind{
decl: *decl,
decl: decl,
}
lin, err := decl.Some().BindKindLineage(rt, opts...)
@ -100,9 +100,8 @@ func (k *Kind) Maturity() kindsys.Maturity {
// Decl returns the [kindsys.Decl] containing both CUE and Go representations of the
// playlist declaration in .cue files.
func (k *Kind) Decl() *kindsys.Decl[kindsys.CoreProperties] {
d := k.decl
return &d
func (k *Kind) Decl() kindsys.Decl[kindsys.CoreProperties] {
return k.decl
}
// Props returns a [kindsys.SomeKindProps], with underlying type [kindsys.CoreProperties],

View File

@ -39,7 +39,7 @@ func NewKind(rt *thema.Runtime, opts ...thema.BindOption) (*Kind, error) {
return nil, err
}
k := &Kind{
decl: *decl,
decl: decl,
}
lin, err := decl.Some().BindKindLineage(rt, opts...)
@ -100,9 +100,8 @@ func (k *Kind) Maturity() kindsys.Maturity {
// Decl returns the [kindsys.Decl] containing both CUE and Go representations of the
// team declaration in .cue files.
func (k *Kind) Decl() *kindsys.Decl[kindsys.CoreProperties] {
d := k.decl
return &d
func (k *Kind) Decl() kindsys.Decl[kindsys.CoreProperties] {
return k.decl
}
// Props returns a [kindsys.SomeKindProps], with underlying type [kindsys.CoreProperties],

View File

@ -57,6 +57,9 @@ type Interface interface {
// a Decl() method through which these same properties are accessible.
Props() SomeKindProperties
// TODO docs
Lineage() thema.Lineage
// TODO remove, unnecessary with Props()
Name() string
@ -71,18 +74,19 @@ type Core interface {
Interface
// TODO docs
Lineage() thema.Lineage
// TODO docs
Decl() *Decl[CoreProperties] // TODO figure out how to reconcile this interface with CustomProperties
Decl() Decl[CoreProperties]
}
// type Composable interface {
// Interface
//
// // TODO docs
// Lineage() thema.Lineage
//
// // TODO docs
// Properties() CoreProperties // TODO figure out how to reconcile this interface with CustomProperties
// }
type Custom interface {
Interface
// TODO docs
Decl() Decl[CustomProperties]
}
type Composable interface {
Interface
// TODO docs
Decl() Decl[ComposableProperties]
}

View File

@ -0,0 +1,29 @@
package kindsys
// Composable is a category of kind that provides schema elements for
// composition into Core and Custom 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
// schemaInterface is the name of the Grafana schema interface implemented by
// this Composable kind. The set is open to ensure forward compatibility of
// Grafana and tooling with any additional schema interfaces that may be added.
// schemaInterface: or([ for k, _ in schemaInterfaces {k}, string])
schemaInterface: string
let schif = schemaInterfaces[S.schemaInterface]
// lineage is the Thema lineage containing all the schemas that have existed for this kind.
// The name of the lineage is constrained to the name of the schema interface being implemented.
// lineage: thema.#Lineage & {name: S.schemaInterface, joinSchema: schif.interface}
lineage: { joinSchema: (schif.interface | *{}) }
lineageIsGroup: schif.group | *false
}

View File

@ -7,10 +7,9 @@ package kindsys
//
// Grafana provides Kubernetes apiserver-shaped HTTP APIs for interacting with custom
// kinds - the same API patterns (and clients) used to interact with k8s CustomResources.
#Custom: S={
Custom: S={
_sharedKind
lineage: { name: S.machineName }
lineageIsGroup: false
...
}

View File

@ -22,7 +22,7 @@ import (
// Grafana provides a standard mechanism for representing its kinds as CRDs.
//
// There are three categories of kinds: Core, Custom, and Composable.
#Kind: #Composable | #Core | #Custom
Kind: Composable | Core | Custom
// properties shared between all kind categories.
_sharedKind: {
@ -86,37 +86,9 @@ _sharedKind: {
// Core specifies the kind category for core-defined arbitrary types.
// Familiar types and functional resources in Grafana, such as dashboards and
// and datasources, are represented as core kinds.
#Core: S=close({
Core: S=close({
_sharedKind
lineage: { name: S.machineName }
lineageIsGroup: false
})
// Composable is a category of kind that provides schema elements for
// composition into Core and Custom 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
// 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 }
}

View File

@ -13,12 +13,12 @@ type CommonProperties struct {
}
// CoreProperties represents the static properties in the declaration of a
// #Core kind that are representable with basic Go types. This
// Core kind that are representable with basic Go types. This
// excludes Thema schemas.
//
// When a .cue #Core declaration is loaded through the standard [LoadCoreKind],
// When a .cue Core declaration is loaded through the standard [LoadCoreKind],
// func, it is fully validated and populated according to all rules specified
// in CUE for #Core kinds.
// in CUE for Core kinds.
type CoreProperties struct {
CommonProperties
CurrentVersion thema.SyntacticVersion `json:"currentVersion"`
@ -30,7 +30,7 @@ func (m CoreProperties) Common() CommonProperties {
}
// CustomProperties represents the static properties in the declaration of a
// #Custom kind that are representable with basic Go types. This
// Custom kind that are representable with basic Go types. This
// excludes Thema schemas.
type CustomProperties struct {
CommonProperties
@ -43,7 +43,7 @@ func (m CustomProperties) Common() CommonProperties {
}
// ComposableProperties represents the static properties in the declaration of a
// #Composable kind that are representable with basic Go types. This
// Composable kind that are representable with basic Go types. This
// excludes Thema schemas.
type ComposableProperties struct {
CommonProperties

View File

@ -60,11 +60,11 @@ func doLoadFrameworkCUE(ctx *cue.Context) (cue.Value, error) {
//
// 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.
// make the constructs in the returned cue.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.
// Calling this with a nil [cue.Context] (the singleton returned from
// [cuectx.GrafanaCUEContext] is used) will memoize certain CUE operations.
// Prefer passing nil unless a different cue.Context is specifically required.
func CUEFramework(ctx *cue.Context) cue.Value {
if ctx == nil || ctx == cuectx.GrafanaCUEContext() {
// Ensure framework is loaded, even if this func is called
@ -77,9 +77,9 @@ func CUEFramework(ctx *cue.Context) cue.Value {
return v
}
// ToKindMeta takes a cue.Value expected to represent a kind of the category
// ToKindProps 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 KindProperties](v cue.Value) (T, error) {
func ToKindProps[T KindProperties](v cue.Value) (T, error) {
props := new(T)
if !v.Exists() {
return *props, ErrValueNotExist
@ -91,11 +91,11 @@ func ToKindMeta[T KindProperties](v cue.Value) (T, error) {
anyprops := any(*props).(SomeKindProperties)
switch anyprops.(type) {
case CoreProperties:
kdef = fw.LookupPath(cue.MakePath(cue.Def("Core")))
kdef = fw.LookupPath(cue.MakePath(cue.Str("Core")))
case CustomProperties:
kdef = fw.LookupPath(cue.MakePath(cue.Def("Custom")))
kdef = fw.LookupPath(cue.MakePath(cue.Str("Custom")))
case ComposableProperties:
kdef = fw.LookupPath(cue.MakePath(cue.Def("Composable")))
kdef = fw.LookupPath(cue.MakePath(cue.Str("Composable")))
default:
// unreachable so long as all the possibilities in KindProperties have switch branches
panic("unreachable")
@ -128,8 +128,9 @@ type SomeDecl struct {
// BindKindLineage binds the lineage for the kind declaration.
//
// 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) {
// 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()
}
@ -142,19 +143,19 @@ func (decl *SomeDecl) BindKindLineage(rt *thema.Runtime, opts ...thema.BindOptio
}
// IsCore indicates whether the represented kind is a core kind.
func (decl *SomeDecl) IsCore() bool {
func (decl SomeDecl) IsCore() bool {
_, is := decl.Properties.(CoreProperties)
return is
}
// IsCustom indicates whether the represented kind is a custom kind.
func (decl *SomeDecl) IsCustom() bool {
func (decl SomeDecl) IsCustom() bool {
_, is := decl.Properties.(CustomProperties)
return is
}
// IsComposable indicates whether the represented kind is a composable kind.
func (decl *SomeDecl) IsComposable() bool {
func (decl SomeDecl) IsComposable() bool {
_, is := decl.Properties.(ComposableProperties)
return is
}
@ -171,8 +172,8 @@ type Decl[T KindProperties] struct {
}
// Some converts the typed Decl to the equivalent typeless SomeDecl.
func (decl *Decl[T]) Some() *SomeDecl {
return &SomeDecl{
func (decl Decl[T]) Some() SomeDecl {
return SomeDecl{
V: decl.V,
Properties: any(decl.Properties).(SomeKindProperties),
}
@ -195,17 +196,20 @@ func (decl *Decl[T]) Some() *SomeDecl {
// 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(declpath string, ctx *cue.Context, overlay fs.FS) (*Decl[CoreProperties], error) {
func LoadCoreKind(declpath string, ctx *cue.Context, overlay fs.FS) (Decl[CoreProperties], error) {
none := Decl[CoreProperties]{}
vk, err := cuectx.BuildGrafanaInstance(ctx, declpath, "kind", overlay)
if err != nil {
return nil, err
return none, err
}
decl := &Decl[CoreProperties]{
V: vk,
}
decl.Properties, err = ToKindMeta[CoreProperties](vk)
props, err := ToKindProps[CoreProperties](vk)
if err != nil {
return nil, err
return none, err
}
return decl, nil
return Decl[CoreProperties]{
V: vk,
Properties: props,
}, nil
}

View File

@ -139,24 +139,24 @@ func buildKindStateReport() *KindStateReport {
}, "core")
}
all := kindsys.AllSlots(nil)
all := kindsys.SchemaInterfaces(nil)
// TODO this is all hacks until #59001, which will unite plugins with kindsys
for _, tree := range corelist.New(nil) {
rp := tree.RootPlugin()
for _, slot := range all {
if may, _ := slot.ForPluginType(string(rp.Meta().Type)); may {
n := fmt.Sprintf("%s-%s", strings.Title(rp.Meta().Id), slot.Name())
for _, si := range all {
if si.Should(string(rp.Meta().Type)) {
n := fmt.Sprintf("%s-%s", strings.Title(rp.Meta().Id), si.Name())
props := kindsys.ComposableProperties{
CommonProperties: kindsys.CommonProperties{
Name: n,
PluralName: n + "s",
MachineName: machinize(n),
PluralMachineName: machinize(n) + "s",
LineageIsGroup: slot.IsGroup(),
LineageIsGroup: si.IsGroup(),
Maturity: "planned",
},
}
if ck, has := rp.SlotImplementations()[slot.Name()]; has {
if ck, has := rp.SlotImplementations()[si.Name()]; has {
props.CommonProperties.Maturity = "merged"
props.CurrentVersion = ck.Latest().Version()
}

View File

@ -41,7 +41,7 @@
"pluralName": "Alertmanager-Querys",
"machineName": "alertmanager_query",
"pluralMachineName": "alertmanager_querys",
"lineageIsGroup": false,
"lineageIsGroup": true,
"maturity": "planned",
"currentVersion": [
0,
@ -113,7 +113,7 @@
"pluralName": "Cloudwatch-Querys",
"machineName": "cloudwatch_query",
"pluralMachineName": "cloudwatch_querys",
"lineageIsGroup": false,
"lineageIsGroup": true,
"maturity": "planned",
"currentVersion": [
0,
@ -149,7 +149,7 @@
"pluralName": "Dashboard-Querys",
"machineName": "dashboard_query",
"pluralMachineName": "dashboard_querys",
"lineageIsGroup": false,
"lineageIsGroup": true,
"maturity": "planned",
"currentVersion": [
0,
@ -209,7 +209,7 @@
"pluralName": "Elasticsearch-Querys",
"machineName": "elasticsearch_query",
"pluralMachineName": "elasticsearch_querys",
"lineageIsGroup": false,
"lineageIsGroup": true,
"maturity": "planned",
"currentVersion": [
0,
@ -293,7 +293,7 @@
"pluralName": "Grafana-Azure-Monitor-Datasource-Querys",
"machineName": "grafana_azure_monitor_datasource_query",
"pluralMachineName": "grafana_azure_monitor_datasource_querys",
"lineageIsGroup": false,
"lineageIsGroup": true,
"maturity": "planned",
"currentVersion": [
0,
@ -317,7 +317,7 @@
"pluralName": "Grafana-Querys",
"machineName": "grafana_query",
"pluralMachineName": "grafana_querys",
"lineageIsGroup": false,
"lineageIsGroup": true,
"maturity": "planned",
"currentVersion": [
0,
@ -353,7 +353,7 @@
"pluralName": "Graphite-Querys",
"machineName": "graphite_query",
"pluralMachineName": "graphite_querys",
"lineageIsGroup": false,
"lineageIsGroup": true,
"maturity": "planned",
"currentVersion": [
0,
@ -401,7 +401,7 @@
"pluralName": "Jaeger-Querys",
"machineName": "jaeger_query",
"pluralMachineName": "jaeger_querys",
"lineageIsGroup": false,
"lineageIsGroup": true,
"maturity": "planned",
"currentVersion": [
0,
@ -449,7 +449,7 @@
"pluralName": "Loki-Querys",
"machineName": "loki_query",
"pluralMachineName": "loki_querys",
"lineageIsGroup": false,
"lineageIsGroup": true,
"maturity": "planned",
"currentVersion": [
0,
@ -473,7 +473,7 @@
"pluralName": "Mssql-Querys",
"machineName": "mssql_query",
"pluralMachineName": "mssql_querys",
"lineageIsGroup": false,
"lineageIsGroup": true,
"maturity": "planned",
"currentVersion": [
0,
@ -497,7 +497,7 @@
"pluralName": "Mysql-Querys",
"machineName": "mysql_query",
"pluralMachineName": "mysql_querys",
"lineageIsGroup": false,
"lineageIsGroup": true,
"maturity": "planned",
"currentVersion": [
0,
@ -545,7 +545,7 @@
"pluralName": "Parca-Querys",
"machineName": "parca_query",
"pluralMachineName": "parca_querys",
"lineageIsGroup": false,
"lineageIsGroup": true,
"maturity": "planned",
"currentVersion": [
0,
@ -569,7 +569,7 @@
"pluralName": "Phlare-Querys",
"machineName": "phlare_query",
"pluralMachineName": "phlare_querys",
"lineageIsGroup": false,
"lineageIsGroup": true,
"maturity": "planned",
"currentVersion": [
0,
@ -617,7 +617,7 @@
"pluralName": "Postgres-Querys",
"machineName": "postgres_query",
"pluralMachineName": "postgres_querys",
"lineageIsGroup": false,
"lineageIsGroup": true,
"maturity": "planned",
"currentVersion": [
0,
@ -641,7 +641,7 @@
"pluralName": "Prometheus-Querys",
"machineName": "prometheus_query",
"pluralMachineName": "prometheus_querys",
"lineageIsGroup": false,
"lineageIsGroup": true,
"maturity": "planned",
"currentVersion": [
0,
@ -701,7 +701,7 @@
"pluralName": "Stackdriver-Querys",
"machineName": "stackdriver_query",
"pluralMachineName": "stackdriver_querys",
"lineageIsGroup": false,
"lineageIsGroup": true,
"maturity": "planned",
"currentVersion": [
0,
@ -761,7 +761,7 @@
"pluralName": "Tempo-Querys",
"machineName": "tempo_query",
"pluralMachineName": "tempo_querys",
"lineageIsGroup": false,
"lineageIsGroup": true,
"maturity": "planned",
"currentVersion": [
0,
@ -785,7 +785,7 @@
"pluralName": "Testdata-Querys",
"machineName": "testdata_query",
"pluralMachineName": "testdata_querys",
"lineageIsGroup": false,
"lineageIsGroup": true,
"maturity": "planned",
"currentVersion": [
0,
@ -881,7 +881,7 @@
"pluralName": "Zipkin-Querys",
"machineName": "zipkin_query",
"pluralMachineName": "zipkin_querys",
"lineageIsGroup": false,
"lineageIsGroup": true,
"maturity": "planned",
"currentVersion": [
0,

View File

@ -0,0 +1,140 @@
package kindsys
// The schema interfaces defined in this file are meta-schemas. They are shared
// contracts between the producers (composable kinds, defined in Grafana
// plugins) and consumers (core and custom Grafana kinds) of composable schemas.
//
// This contract is similar to an interface in most programming languages:
// producer and consumer implementations depend only on the schema interface
// definition, rather than the details of any particular implementation. This
// allows producers and consumers to be loosely coupled, while keeping an
// explicit contract for composition of sub-schemas from producers into the
// consumer schemas that want to use them.
//
// Schema interfaces allow schema composition to be broken down into a series of
// simple "what," "which," and "how" questions:
//
// - "What" is the subschema to be composed?
// - "How" should subschema(s) be composed into another schema to produce a unified result schema?
// - "Which" subset of known composable subschemas ("whats") should be provided in composition ("how")?
//
// On the producer side, Grafana plugin authors may provide Thema lineages
// within Composable kinds declared in .cue files adjacent to their
// plugin.json, following a pattern (see
// github.com/grafana/grafana/pkg/plugins/pfs.#GrafanaPlugin.composableKinds)
// corresponding to the name of the schema interface. Each such definition is
// an answer to "what."
//
// On the consumer side, any core or custom kind author can choose to define a
// standard Thema composition slot in its contained lineage that uses one of
// these schema interfaces as its meta-schema. The slot specification in Thema
// answers "how", for that kind.
//
// Composable kinds declared by a plugin are parsed and validated by Grafana's
// plugin system when a plugin is installed. This gives each Grafana instance a
// set of all known Composable kinds ("whats"), which can be narrowed into the
// subsets ("which") that each known Core or Custom can consume. These subsets
// are injected dynamically into the consumers, resulting in the final schema.
//
// For example, in the Thema lineage for the dashboard core kind:
// - There is a slot named `panelcfg`
// - It is constrained to accept only Thema lineages following the `panelcfg` schema interface
// - The composition logic specifies that the `panelcfg.PanelOptions` from each lineage provided
// to the dashboard lineage be one possibility for `panels[].options`
//
// (TODO actual implementation is pending https://github.com/grafana/thema/issue/8)
//
// Thus, the dashboard schema used for validation by any particular Grafana instance
// can tell the user if a particular dashboard with a `timeseries` panel has invalid
// values for `panels[].options`, even though neither the dashboard core kind, nor the
// the timeseries composable kind, are directly aware of (import) each other.
// A SchemaInterface defines a single Grafana schema interface.
SchemaInterface: {
// name is the unique identifier of the schema interface.
//
// Often used to provide namespacing of schema interface implementations
// in places where implementations must be enumerated, such as:
// - In-memory indexes in the Grafana backend
// - Documentation URLs
// - Parent directory paths or names in generated code
name: string & =~"^[A-Z][A-Za-z]{1,19}$"
// interface is the body of the SchemaInterface - the actual meta-schema that
// forms the shared contract between consumers (core & custom kind lineages)
// and producers (composable kind lineages).
interface: {}
// pluginTypes is a list of plugin types that are expected to produce composable
// kinds following this interface.
//
// Note that Grafana's plugin architecture intentionally does not enforce this.
// The worst that a violation (impl expected and absent, or impl present and not expected)
// will currently produce is a warning.
//
// TODO this relies on information in pkg/plugins/plugindef, awkward having it here
pluginTypes: [...string]
// Whether lineages implementing this are considered "grouped" or not. Generally
// this refers to whether an e.g. JSON object is ever expected to exist that
// corresponds to the whole schema, or to top-level fields within the schema.
//
// TODO see https://github.com/grafana/thema/issues/62
//
// The main effect is whether code generation should produce one type that represents
// the root schema for lineages, or only produce types for each of the top-level fields
// within the schema.
group: bool | *true
}
// The canonical list of all Grafana schema interfaces.
schemaInterfaces: [N=string]: SchemaInterface & { name: N }
schemaInterfaces: {
Panel: {
interface: {
// Defines plugin-specific options for a panel that should be persisted. Required,
// though a panel without any options may specify an empty struct.
//
// Currently mapped to #Panel.options within the dashboard schema.
PanelOptions: {}
// Plugin-specific custom field properties. Optional.
//
// Currently mapped to #Panel.fieldConfig.defaults.custom within the dashboard schema.
PanelFieldConfig?: {}
}
pluginTypes: ["panel"]
// grouped b/c separate non-cross-referring elements always occur together in larger structure (panel)
group: true
}
Query: {
// The contract for the queries schema interface is itself a pattern:
// Each of its top-level fields must be represent a distinct query type for
// the datasource plugin. The queryType field acts as a discriminator, and
// is constrained to be the same as the name of the top-level field declaring it.
interface: [QT=string]: {
queryType?: QT
}
pluginTypes: ["datasource"]
// grouped b/c separate, non-cross-referring elements are actually themselves each impls of the concept
// and it avoids us having to put more levels in the slot system (uggghhh)
group: true
}
DSOptions: {
interface: {
// Normal datasource configuration options.
Options: {}
// Sensitive datasource configuration options that require encryption.
SecureOptions: {}
}
pluginTypes: ["datasource"]
// group b/c separate, non-cross-referring elements have diff runtime representation due to encryption
group: true
}
}

View File

@ -0,0 +1,121 @@
package kindsys
import (
"fmt"
"sync"
"cuelang.org/go/cue"
"github.com/grafana/grafana/pkg/cuectx"
)
// SchemaInterface represents one of Grafana's named schema interfaces.
//
// Canonical definition of schema interfaces is done in CUE. Instances of
// this type simply represent that information in Go.
// TODO link to framework docs
type SchemaInterface struct {
name string
group bool
raw cue.Value
plugins []string
}
// Name returns the name of the SchemaInterface.
//
// The name is also used as the path at which a SchemaInterface lineage is defined in a
// plugin models.cue file.
func (s SchemaInterface) Name() string {
return s.name
}
// Interface returns the cue.Value representing the meta-schema that is the
// contract between core or custom kinds that compose the meta-schema, and the
// plugin-declared composable kinds that implement the meta-schema.
func (s SchemaInterface) Interface() cue.Value {
return s.raw.LookupPath(ip)
}
var ip = cue.ParsePath("interface")
// Should indicates whether the given plugin type is expected (but not required)
// to produce a composable kind that implements this SchemaInterface.
func (s SchemaInterface) Should(plugintype string) bool {
pt := plugintype
for _, t := range s.plugins {
if pt == t {
return true
}
}
return false
}
// IsGroup indicates whether the slot specifies a group lineage - one in which
// each top-level key represents a distinct schema for objects that are expected
// to exist in the wild, but objects corresponding to the root of the schema are not
// expected to exist.
func (s SchemaInterface) IsGroup() bool {
return s.group
}
func FindSchemaInterface(name string) (SchemaInterface, error) {
sl, has := SchemaInterfaces(nil)[name]
if !has {
return SchemaInterface{}, fmt.Errorf("unsupported slot: %s", name)
}
return sl, nil
}
var defaultIfaces map[string]SchemaInterface
var onceIfaces sync.Once
// SchemaInterfaces returns a map of all [SchemaInterface]s defined by
// Grafana's kindsys framework.
//
// 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. This is a reasonable default for external
// code, as well.
//
// TODO link to framework docs
func SchemaInterfaces(ctx *cue.Context) map[string]SchemaInterface {
if ctx == nil || ctx == cuectx.GrafanaCUEContext() {
// Ensure framework is loaded, even if this func is called
// from an init() somewhere.
onceIfaces.Do(func() {
defaultIfaces = doSchemaInterfaces(nil)
})
return defaultIfaces
}
return doSchemaInterfaces(ctx)
}
func doSchemaInterfaces(ctx *cue.Context) map[string]SchemaInterface {
fw := CUEFramework(ctx)
defs := fw.LookupPath(cue.ParsePath("schemaInterfaces"))
if !defs.Exists() {
panic("schemaInterfaces key does not exist in kindsys framework")
}
type typ struct {
Name string `json:"name"`
PluginTypes []string `json:"pluginTypes"`
Group bool `json:"group"`
}
ifaces := make(map[string]SchemaInterface)
iter, _ := defs.Fields() //nolint:errcheck
for iter.Next() {
k := iter.Selector().String()
v := &typ{}
_ = iter.Value().Decode(&v) //nolint:errcheck,gosec
ifaces[k] = SchemaInterface{
name: v.Name,
plugins: v.PluginTypes,
group: v.Group,
raw: iter.Value(),
}
}
return ifaces
}

View File

@ -1,121 +0,0 @@
package kindsys
import (
"fmt"
"cuelang.org/go/cue"
)
// Slot represents one of Grafana's named slot definitions.
// TODO link to framework docs
type Slot struct {
name string
raw cue.Value
plugins map[string]bool
}
// Name returns the name of the Slot.
//
// The name is also used as the path at which a Slot lineage is defined in a
// plugin models.cue file.
func (s Slot) Name() string {
return s.name
}
// MetaSchema returns the meta-schema that is the contract between core or
// custom kinds that compose the meta-schema, and the plugin-declared composable
// kinds that implement the meta-schema.
func (s Slot) MetaSchema() cue.Value {
return s.raw
}
// ForPluginType indicates whether for this Slot, plugins of the given type may
// provide a slot implementation (first return value), and for those types that
// may, whether they must produce one (second return value).
//
// Expected values here are those in the set of
// ["github.com/grafana/grafana/pkg/plugins/plugindef".Type], though passing
// a string not in that set will harmlessly return {false, false}. That type is
// not used here to avoid import cycles.
//
// Note that, at least for now, plugins are not required to provide any slot
// implementations, and do so by simply not containing any .cue files in the
// "grafanaplugin" package. Consequently, the "must" return value is best
// understood as, "IF a plugin provides a *.cue files, it MUST contain an
// implementation of this slot."
func (s Slot) ForPluginType(plugintype string) (may, must bool) {
must, may = s.plugins[plugintype]
return
}
// IsGroup indicates whether the slot specifies a group lineage - one in which
// each top-level key represents a distinct schema for objects that are expected
// to exist in the wild, but objects corresponding to the root of the schema are not
// expected to exist.
func (s Slot) IsGroup() bool {
// TODO rely on first-class Thema properties for this, one they exist - https://github.com/grafana/thema/issues/62
switch s.name {
case "Panel", "DSOptions":
return true
case "Query":
return false
default:
panic("unreachable - unknown slot name " + s.name)
}
}
func FindSlot(name string) (*Slot, error) {
sl, has := AllSlots(nil)[name]
if !has {
return nil, fmt.Errorf("unsupported slot: %s", name)
}
return sl, nil
}
// AllSlots returns a map of all [Slot]s defined in the Grafana kindsys
// framework.
//
// TODO cache this for core context
func AllSlots(ctx *cue.Context) map[string]*Slot {
fw := CUEFramework(ctx)
slots := make(map[string]*Slot)
// Ignore err, can only happen if we change structure of fw files, and all we'd
// do is panic and that's what the next line will do anyway. Same for similar ignored
// errors later in this func
iter, _ := fw.LookupPath(cue.ParsePath("pluginTypeMetaSchema")).Fields(cue.Optional(true))
type nameopt struct {
name string
req bool
}
plugslots := make(map[string][]nameopt)
for iter.Next() {
plugin := iter.Selector().String()
iiter, _ := iter.Value().Fields(cue.Optional(true))
for iiter.Next() {
slotname := iiter.Selector().String()
plugslots[slotname] = append(plugslots[slotname], nameopt{
name: plugin,
req: !iiter.IsOptional(),
})
}
}
iter, _ = fw.LookupPath(cue.ParsePath("slots")).Fields(cue.Optional(true))
for iter.Next() {
n := iter.Selector().String()
sl := Slot{
name: n,
raw: iter.Value(),
plugins: make(map[string]bool),
}
for _, no := range plugslots[n] {
sl.plugins[no.name] = no.req
}
slots[n] = &sl
}
return slots
}

View File

@ -1,29 +0,0 @@
package kindsys
import (
"sort"
"testing"
"cuelang.org/go/cue/cuecontext"
"github.com/stretchr/testify/require"
)
// This is a brick-dumb test that just ensures slots are being loaded correctly
// from their declarations in .cue files.
//
// If this test fails, it's either because:
// - They're not being loaded correctly - there's a bug in kindsys somewhere, fix it
// - The set of slots names has been modified - update the static list here
func TestSlotsAreLoaded(t *testing.T) {
slots := []string{"Panel", "Query", "DSOptions"}
all := AllSlots(cuecontext.New())
var loadedSlots []string
for k := range all {
loadedSlots = append(loadedSlots, k)
}
sort.Strings(slots)
sort.Strings(loadedSlots)
require.Equal(t, slots, loadedSlots, "slots loaded from cue differs from fixture set - either a bug or fixture needs updating")
}

View File

@ -1,71 +0,0 @@
package kindsys
// The slots named and specified in this file are meta-schemas that act as a
// shared contract between Grafana plugins (producers) and coremodel types
// (consumers).
//
// On the consumer side, any coremodel Thema lineage can choose to define a
// standard Thema composition slot that specifies one of these named slots as
// its meta-schema. Such a specification entails that all schemas in any lineage
// placed into that composition slot must adhere to the meta-schema.
//
// On the producer side, Grafana's plugin system enforces that certain plugin
// types are expected to provide Thema lineages for these named slots which
// adhere to the slot meta-schema.
//
// For example, the Panel slot is consumed by the dashboard coremodel, and is
// expected to be produced by panel plugins.
//
// The name given to each slot in this file must be used as the name of the
// slot in the coremodel, and the name of the field under which the lineage
// is provided in a plugin's models.cue file.
//
// Conformance to meta-schema is achieved by Thema's native lineage joinSchema,
// which Thema internals automatically enforce across all schemas in a lineage.
// Meta-schema for the Panel slot, as implemented in Grafana panel plugins.
//
// This is a grouped meta-schema, intended solely for use in composition. Object
// literals conforming to it are not expected to exist.
slots: Panel: {
// Defines plugin-specific options for a panel that should be persisted. Required,
// though a panel without any options may specify an empty struct.
//
// Currently mapped to #Panel.options within the dashboard schema.
PanelOptions: {...}
// Plugin-specific custom field properties. Optional.
//
// Currently mapped to #Panel.fieldConfig.defaults.custom within the dashboard schema.
PanelFieldConfig?: {...}
}
// Meta-schema for the Query slot, as implemented in Grafana datasource plugins.
slots: Query: {...}
// Meta-schema for the DSOptions slot, as implemented in Grafana datasource plugins.
//
// This is a grouped meta-schema, intended solely for use in composition. Object
// literals conforming to it are not expected to exist.
slots: DSOptions: {
// Normal datasource configuration options.
Options: {...}
// Sensitive datasource configuration options that require encryption.
SecureOptions: {...}
}
// pluginTypeMetaSchema defines which plugin types should use which metaschemas
// as joinSchema for the lineages declared at which paths.
pluginTypeMetaSchema: [string]: {...}
pluginTypeMetaSchema: {
// Panel plugins are expected to provide a lineage at path Panel conforming to
// the Panel joinSchema.
panel: {
Panel: slots.Panel
}
// Datasource plugins are expected to provide lineages at paths Query and
// DSOptions, conforming to those joinSchemas respectively.
datasource: {
Query: slots.Query
DSOptions: slots.DSOptions
}
}

View File

@ -37,7 +37,7 @@ func (j *pgoJenny) Generate(decl *pfs.PluginDecl) (*codejen.File, error) {
}
pluginfolder := filepath.Base(decl.PluginPath)
slotname := strings.ToLower(decl.Slot.Name())
slotname := strings.ToLower(decl.SchemaInterface.Name())
filename := fmt.Sprintf("types_%s_gen.go", slotname)
f.RelativePath = filepath.Join(j.root, pluginfolder, filename)
f.From = append(f.From, j)

View File

@ -40,7 +40,7 @@ func (j *ptsJenny) Generate(decl *pfs.PluginDecl) (*codejen.File, error) {
}
}
slotname := decl.Slot.Name()
slotname := decl.SchemaInterface.Name()
v := decl.Lineage.Latest().Version()
tsf.Nodes = append(tsf.Nodes, tsast.Raw{

View File

@ -3,7 +3,7 @@ package grafanaplugin
import "github.com/grafana/thema"
Query: thema.#Lineage & {
name: "missing_slot_impl"
name: "missing_kind_datasource"
seqs: [
{
schemas: [

View File

@ -1,7 +1,7 @@
{
"type": "datasource",
"name": "Missing slot impl",
"id": "missing-slot-datasource",
"name": "Missing kind impl",
"id": "missing-kind-datasource",
"backend": true,
"state": "alpha",
"info": {

View File

@ -8,11 +8,11 @@ import (
)
type PluginDecl struct {
Slot *kindsys.Slot
Lineage thema.Lineage
Imports []*ast.ImportSpec
PluginPath string
PluginMeta plugindef.PluginDef
SchemaInterface *kindsys.SchemaInterface
Lineage thema.Lineage
Imports []*ast.ImportSpec
PluginPath string
PluginMeta plugindef.PluginDef
}
func EmptyPluginDecl(path string, meta plugindef.PluginDef) *PluginDecl {
@ -24,5 +24,5 @@ func EmptyPluginDecl(path string, meta plugindef.PluginDef) *PluginDecl {
}
func (decl *PluginDecl) HasSchema() bool {
return decl.Lineage != nil && decl.Slot != nil
return decl.Lineage != nil && decl.SchemaInterface != nil
}

View File

@ -54,17 +54,17 @@ func (psr *declParser) Parse(root fs.FS) ([]*PluginDecl, error) {
}
for slotName, lin := range slots {
slot, err := kindsys.FindSlot(slotName)
slot, err := kindsys.FindSchemaInterface(slotName)
if err != nil {
log.Println(fmt.Errorf("parsing plugin failed for %s: %s", dir, err))
continue
}
decls = append(decls, &PluginDecl{
Slot: slot,
Lineage: lin,
Imports: p.CUEImports(),
PluginMeta: p.Meta(),
PluginPath: path,
SchemaInterface: &slot,
Lineage: lin,
Imports: p.CUEImports(),
PluginMeta: p.Meta(),
PluginPath: path,
})
}
}

View File

@ -43,7 +43,7 @@ var allowedImportsStr string
type slotandname struct {
name string
slot *kindsys.Slot
slot kindsys.SchemaInterface
}
var allslots []slotandname
@ -55,7 +55,7 @@ func init() {
}
allowedImportsStr = strings.Join(all, "\n")
for n, s := range kindsys.AllSlots(nil) {
for n, s := range kindsys.SchemaInterfaces(nil) {
allslots = append(allslots, slotandname{
name: n,
slot: s,
@ -93,7 +93,7 @@ func (t *Tree) SubPlugins() map[string]PluginInfo {
type TreeList []*Tree
// LineagesForSlot returns the set of plugin-defined lineages that implement a
// particular named Grafana slot (See ["github.com/grafana/grafana/pkg/framework/coremodel".Slot]).
// particular named Grafana slot (See ["github.com/grafana/grafana/pkg/framework/coremodel".SchemaInterface]).
func (tl TreeList) LineagesForSlot(slotname string) map[string]thema.Lineage {
m := make(map[string]thema.Lineage)
for _, tree := range tl {
@ -218,12 +218,14 @@ func ParsePluginFS(f fs.FS, rt *thema.Runtime) (*Tree, error) {
}
for _, s := range allslots {
iv := val.LookupPath(cue.ParsePath(s.slot.Name()))
lin, err := bindSlotLineage(iv, s.slot, r.meta, rt)
if lin != nil {
r.slotimpls[s.slot.Name()] = lin
}
if err != nil {
return nil, err
if iv.Exists() {
lin, err := bindSlotLineage(iv, s.slot, r.meta, rt)
if lin != nil {
r.slotimpls[s.slot.Name()] = lin
}
if err != nil {
return nil, err
}
}
}
}
@ -231,8 +233,10 @@ func ParsePluginFS(f fs.FS, rt *thema.Runtime) (*Tree, error) {
return tree, nil
}
func bindSlotLineage(v cue.Value, s *kindsys.Slot, meta plugindef.PluginDef, rt *thema.Runtime, opts ...thema.BindOption) (thema.Lineage, error) {
accept, required := s.ForPluginType(string(meta.Type))
func bindSlotLineage(v cue.Value, s kindsys.SchemaInterface, meta plugindef.PluginDef, rt *thema.Runtime, opts ...thema.BindOption) (thema.Lineage, error) {
// temporarily keep this around, there are IMMEDIATE plans to refactor
var required bool
accept := s.Should(string(meta.Type))
exists := v.Exists()
if !accept {

View File

@ -115,9 +115,7 @@ func TestParseTreeTestdata(t *testing.T) {
"wrong-slot-panel": {
err: ErrImplementedSlots,
},
"missing-slot-impl": {
err: ErrImplementedSlots,
},
"missing-kind-datasource": {},
"panel-conflicting-joinschema": {
err: ErrInvalidLineage,
skip: "TODO implement BindOption in thema, SatisfiesJoinSchema, then use it here",

View File

@ -85,7 +85,7 @@ func adaptToPipeline(j codejen.OneToOne[corecodegen.SchemaForGen]) codejen.OneTo
return corecodegen.SchemaForGen{
Name: pd.PluginMeta.Name,
Schema: pd.Lineage.Latest(),
IsGroup: pd.Slot.IsGroup(),
IsGroup: pd.SchemaInterface.IsGroup(),
}
})
}