mirror of https://github.com/grafana/grafana
extract kindsys to external library (#64562)
* extract kindsys * reinstate kindsys report This may end up living somewhere else (or not! who knows!), but the important part is that I don't get rid of it right now :) I hate the package layout (kindsysreport/codegen) for the main function and will take pretty much any alternative suggestion, but we can change also change it later. Note that the generated report.json is in a different location - anything using this (ops something) needs to be updated. * kindsysreport in codeownerspull/64833/head
parent
3f5acf346d
commit
8745d7ef1b
@ -0,0 +1,236 @@ |
||||
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/kindsys" |
||||
"github.com/grafana/thema" |
||||
"github.com/grafana/thema/load" |
||||
"github.com/yalue/merged_fs" |
||||
|
||||
"github.com/grafana/grafana" |
||||
) |
||||
|
||||
// LoadGrafanaInstancesWithThema loads CUE files containing a lineage
|
||||
// representing some Grafana core model schema. It is expected to be used when
|
||||
// implementing a thema.LineageFactory.
|
||||
//
|
||||
// This function primarily juggles paths to make CUE's loader happy. Provide the
|
||||
// path from the grafana root to the directory containing the lineage.cue. The
|
||||
// lineage.cue file must be the sole contents of the provided fs.FS.
|
||||
//
|
||||
// More details on underlying behavior can be found in the docs for github.com/grafana/thema/load.InstanceWithThema.
|
||||
//
|
||||
// TODO this approach is complicated and confusing, refactor to something understandable
|
||||
func LoadGrafanaInstancesWithThema(path string, cueFS fs.FS, rt *thema.Runtime, opts ...thema.BindOption) (thema.Lineage, error) { |
||||
prefix := filepath.FromSlash(path) |
||||
fs, err := prefixWithGrafanaCUE(prefix, cueFS) |
||||
if err != nil { |
||||
return nil, err |
||||
} |
||||
inst, err := load.InstanceWithThema(fs, prefix) |
||||
|
||||
// Need to trick loading by creating the embedded file and
|
||||
// making it look like a module in the root dir.
|
||||
if err != nil { |
||||
return nil, err |
||||
} |
||||
|
||||
val := rt.Context().BuildInstance(inst) |
||||
|
||||
lin, err := thema.BindLineage(val, rt, opts...) |
||||
if err != nil { |
||||
return nil, err |
||||
} |
||||
|
||||
return lin, nil |
||||
} |
||||
|
||||
// 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 [load.InstanceWithThema].
|
||||
func prefixWithGrafanaCUE(prefix string, inputfs fs.FS) (fs.FS, error) { |
||||
m, err := prefixFS(prefix, inputfs) |
||||
if err != nil { |
||||
return nil, err |
||||
} |
||||
return merged_fs.NewMergedFS(m, grafana.CueSchemaFS), nil |
||||
} |
||||
|
||||
// TODO such a waste, replace with stateless impl that just transforms paths on the fly
|
||||
func prefixFS(prefix string, fsys fs.FS) (fs.FS, error) { |
||||
m := make(fstest.MapFS) |
||||
|
||||
prefix = filepath.FromSlash(prefix) |
||||
err := fs.WalkDir(fsys, ".", func(path string, d fs.DirEntry, err error) error { |
||||
if err != nil { |
||||
return err |
||||
} |
||||
|
||||
if d.IsDir() { |
||||
return nil |
||||
} |
||||
|
||||
b, err := fs.ReadFile(fsys, filepath.ToSlash(path)) |
||||
if err != nil { |
||||
return err |
||||
} |
||||
// fstest can recognize only forward slashes.
|
||||
m[filepath.ToSlash(filepath.Join(prefix, path))] = &fstest.MapFile{Data: b} |
||||
return nil |
||||
}) |
||||
return m, err |
||||
} |
||||
|
||||
// LoadGrafanaInstance wraps [load.InstanceWithThema] to load a
|
||||
// [*build.Instance] corresponding to a particular path within the
|
||||
// github.com/grafana/grafana CUE module.
|
||||
//
|
||||
// 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 deprecated in favor of a more generic loader
|
||||
func LoadGrafanaInstance(relpath string, pkg string, overlay fs.FS) (*build.Instance, 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
|
||||
// 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
|
||||
relpath = filepath.ToSlash(relpath) |
||||
|
||||
var f fs.FS = grafana.CueSchemaFS |
||||
// merge the kindsys filesystem with ours for the kind categories
|
||||
f = merged_fs.NewMergedFS(kindsys.CueSchemaFS, f) |
||||
|
||||
var err error |
||||
if overlay != nil { |
||||
f, err = prefixWithGrafanaCUE(relpath, overlay) |
||||
if err != nil { |
||||
return nil, err |
||||
} |
||||
} |
||||
|
||||
if pkg != "" { |
||||
return load.InstanceWithThema(f, relpath, load.Package(pkg)) |
||||
} |
||||
return load.InstanceWithThema(f, relpath) |
||||
} |
||||
|
||||
// BuildGrafanaInstance wraps [LoadGrafanaInstance], additionally building
|
||||
// the returned [*build.Instance] into a [cue.Value].
|
||||
//
|
||||
// An error is returned if:
|
||||
// - The underlying call to [LoadGrafanaInstance] returns an error
|
||||
// - The built [cue.Value] has an error ([cue.Value.Err] returns non-nil)
|
||||
//
|
||||
// NOTE this function will be deprecated in favor of a more generic builder
|
||||
func BuildGrafanaInstance(ctx *cue.Context, relpath string, pkg string, overlay fs.FS) (cue.Value, error) { |
||||
bi, err := LoadGrafanaInstance(relpath, pkg, overlay) |
||||
if err != nil { |
||||
return cue.Value{}, err |
||||
} |
||||
|
||||
if ctx == nil { |
||||
ctx = GrafanaCUEContext() |
||||
} |
||||
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 |
||||
} |
||||
|
||||
// LoadInstanceWithGrafana loads a [*build.Instance] from .cue files
|
||||
// in the provided modFS as ["cuelang.org/go/cue/load".Instances], but
|
||||
// fulfilling any imports of CUE packages under:
|
||||
//
|
||||
// - github.com/grafana/grafana
|
||||
// - github.com/grafana/thema
|
||||
//
|
||||
// This function is modeled after [load.InstanceWithThema]. It has the same
|
||||
// signature and expectations for the modFS.
|
||||
//
|
||||
// Attempting to use this func to load files within the
|
||||
// github.com/grafana/grafana CUE module will result in an error. Use
|
||||
// [LoadGrafanaInstance] instead.
|
||||
//
|
||||
// NOTE This function will be deprecated in favor of a more generic loader
|
||||
func LoadInstanceWithGrafana(fsys fs.FS, dir string, opts ...load.Option) (*build.Instance, error) { |
||||
if modf, err := fs.ReadFile(fsys, "cue.mod/module.cue"); err != nil { |
||||
// delegate error handling
|
||||
return load.InstanceWithThema(fsys, dir, opts...) |
||||
} else if modname, err := cuecontext.New().CompileBytes(modf).LookupPath(cue.MakePath(cue.Str("module"))).String(); err != nil { |
||||
// delegate error handling
|
||||
return load.InstanceWithThema(fsys, dir, opts...) |
||||
} else if modname == "github.com/grafana/grafana" { |
||||
return nil, fmt.Errorf("use cuectx.LoadGrafanaInstance to load .cue files within github.com/grafana/grafana CUE module") |
||||
} |
||||
|
||||
// TODO wasteful, doing this every time - make that stateless prefixfs!
|
||||
depFS, err := prefixFS("cue.mod/pkg/github.com/grafana/grafana", grafana.CueSchemaFS) |
||||
if err != nil { |
||||
panic(err) |
||||
} |
||||
// TODO wasteful, doing this every time - make that stateless prefixfs!
|
||||
kindsysFS, err := prefixFS("cue.mod/pkg/github.com/grafana/kindsys", kindsys.CueSchemaFS) |
||||
if err != nil { |
||||
panic(err) |
||||
} |
||||
|
||||
allTheFs := merged_fs.MergeMultiple(kindsysFS, depFS, fsys) |
||||
|
||||
// FIXME remove grafana from cue.mod/pkg if it exists, otherwise external thing can inject files to be loaded
|
||||
return load.InstanceWithThema(allTheFs, dir, opts...) |
||||
} |
||||
|
||||
// LoadCoreKindDef loads and validates a core kind definition of the kind
|
||||
// category indicated by the type parameter. On success, it returns a [Def]
|
||||
// which contains the entire contents of the kind definition.
|
||||
//
|
||||
// defpath is the path to the directory containing the core kind definition,
|
||||
// relative to the root of the caller's repository. For example, under
|
||||
// dashboards are in "kinds/dashboard".
|
||||
func LoadCoreKindDef(defpath string, ctx *cue.Context, overlay fs.FS) (kindsys.Def[kindsys.CoreProperties], error) { |
||||
vk, err := BuildGrafanaInstance(ctx, defpath, "kind", overlay) |
||||
if err != nil { |
||||
return kindsys.Def[kindsys.CoreProperties]{}, err |
||||
} |
||||
|
||||
props, err := kindsys.ToKindProps[kindsys.CoreProperties](vk) |
||||
if err != nil { |
||||
return kindsys.Def[kindsys.CoreProperties]{}, err |
||||
} |
||||
|
||||
return kindsys.Def[kindsys.CoreProperties]{ |
||||
V: vk, |
||||
Properties: props, |
||||
}, nil |
||||
} |
@ -1,59 +0,0 @@ |
||||
# Kind System |
||||
|
||||
This package contains Grafana's kind system, which defines the rules that govern all Grafana kind definitions, 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 defined. |
||||
* **Go framework** - the Go package in this directory containing utilities for loading individual kind definitions, validating them against the CUE framework, and representing them consistently in Go. |
||||
* **Code generators** - written using the `github.com/grafana/codejen` framework, which applies the [single responsibility principle](https://en.wikipedia.org/wiki/Single-responsibility_principle) to code generation, allowing us to compose modular code generators. Each jenny - a modular generator with a single responsibility - is written as a `pkg/codegen/jenny_*.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 definitions** - the definitions of individual kinds. By kind category: |
||||
* **Core** - each child directory of `kinds`. |
||||
* **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 definitions. 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 |
@ -1,96 +0,0 @@ |
||||
package kindsys |
||||
|
||||
import ( |
||||
"github.com/grafana/thema" |
||||
) |
||||
|
||||
// genericComposable is a general representation of a parsed and validated
|
||||
// Composable kind.
|
||||
type genericComposable struct { |
||||
def Def[ComposableProperties] |
||||
lin thema.Lineage |
||||
} |
||||
|
||||
var _ Composable = genericComposable{} |
||||
|
||||
func (k genericComposable) Props() SomeKindProperties { |
||||
return k.def.Properties |
||||
} |
||||
|
||||
func (k genericComposable) Name() string { |
||||
return k.def.Properties.Name |
||||
} |
||||
|
||||
func (k genericComposable) MachineName() string { |
||||
return k.def.Properties.MachineName |
||||
} |
||||
|
||||
func (k genericComposable) Maturity() Maturity { |
||||
return k.def.Properties.Maturity |
||||
} |
||||
|
||||
func (k genericComposable) Def() Def[ComposableProperties] { |
||||
return k.def |
||||
} |
||||
|
||||
func (k genericComposable) Lineage() thema.Lineage { |
||||
return k.lin |
||||
} |
||||
|
||||
// TODO docs
|
||||
func BindComposable(rt *thema.Runtime, def Def[ComposableProperties], opts ...thema.BindOption) (Composable, error) { |
||||
lin, err := def.Some().BindKindLineage(rt, opts...) |
||||
if err != nil { |
||||
return nil, err |
||||
} |
||||
|
||||
return genericComposable{ |
||||
def: def, |
||||
lin: lin, |
||||
}, nil |
||||
} |
||||
|
||||
// genericCore is a general representation of a parsed and validated Core kind.
|
||||
type genericCore struct { |
||||
def Def[CoreProperties] |
||||
lin thema.Lineage |
||||
} |
||||
|
||||
var _ Core = genericCore{} |
||||
|
||||
func (k genericCore) Props() SomeKindProperties { |
||||
return k.def.Properties |
||||
} |
||||
|
||||
func (k genericCore) Name() string { |
||||
return k.def.Properties.Name |
||||
} |
||||
|
||||
func (k genericCore) MachineName() string { |
||||
return k.def.Properties.MachineName |
||||
} |
||||
|
||||
func (k genericCore) Maturity() Maturity { |
||||
return k.def.Properties.Maturity |
||||
} |
||||
|
||||
func (k genericCore) Def() Def[CoreProperties] { |
||||
return k.def |
||||
} |
||||
|
||||
func (k genericCore) Lineage() thema.Lineage { |
||||
return k.lin |
||||
} |
||||
|
||||
// TODO docs
|
||||
func BindCore(rt *thema.Runtime, def Def[CoreProperties], opts ...thema.BindOption) (Core, error) { |
||||
lin, err := def.Some().BindKindLineage(rt, opts...) |
||||
if err != nil { |
||||
return nil, err |
||||
} |
||||
|
||||
return genericCore{ |
||||
def: def, |
||||
lin: lin, |
||||
}, nil |
||||
} |
@ -1,38 +0,0 @@ |
||||
package kindsys |
||||
|
||||
// Canonically defined in pkg/kindsys/dataquery.cue FOR NOW to avoid having any external imports |
||||
// in kindsys. Code generation copies this file to the common schemas in packages/grafana-schema/src/common. |
||||
// |
||||
// NOTE make gen-cue must be run twice when updating this file |
||||
|
||||
// These are the common properties available to all queries in all datasources. |
||||
// Specific implementations will *extend* this interface, adding the required |
||||
// properties for the given context. |
||||
DataQuery: { |
||||
// A unique identifier for the query within the list of targets. |
||||
// In server side expressions, the refId is used as a variable name to identify results. |
||||
// By default, the UI will assign A->Z; however setting meaningful names may be useful. |
||||
refId: string |
||||
|
||||
// true if query is disabled (ie should not be returned to the dashboard) |
||||
// Note this does not always imply that the query should not be executed since |
||||
// the results from a hidden query may be used as the input to other queries (SSE etc) |
||||
hide?: bool |
||||
|
||||
// Specify the query flavor |
||||
// TODO make this required and give it a default |
||||
queryType?: string |
||||
|
||||
// For mixed data sources the selected datasource is on the query level. |
||||
// For non mixed scenarios this is undefined. |
||||
// TODO find a better way to do this ^ that's friendly to schema |
||||
// TODO this shouldn't be unknown but DataSourceRef | null |
||||
datasource?: _ |
||||
} @cuetsy(kind="interface") |
||||
|
||||
DataSourceRef: { |
||||
// The plugin type-id |
||||
type?: string |
||||
// Specific datasource instance |
||||
uid?: string |
||||
} @cuetsy(kind="interface") |
@ -1,18 +0,0 @@ |
||||
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
|
||||
// Kind. This is almost always a user error - they oops'd and provided the
|
||||
// wrong path, file, etc.
|
||||
ErrValueNotAKind = errors.New("not a kind") |
||||
|
||||
// ErrInvalidCUE indicates that the CUE representing the kind is invalid.
|
||||
ErrInvalidCUE = errors.New("CUE syntax error") |
||||
) |
@ -1,154 +0,0 @@ |
||||
package k8ssys |
||||
|
||||
import ( |
||||
"fmt" |
||||
"reflect" |
||||
|
||||
"github.com/grafana/grafana/pkg/kindsys" |
||||
apiextensionsv1 "k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/v1" |
||||
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" |
||||
"k8s.io/apimachinery/pkg/runtime" |
||||
k8schema "k8s.io/apimachinery/pkg/runtime/schema" |
||||
) |
||||
|
||||
type Kind struct { |
||||
GrafanaKind kindsys.Kind |
||||
Object runtime.Object // singular type
|
||||
ObjectList runtime.Object // list type
|
||||
Schema apiextensionsv1.CustomResourceDefinition |
||||
} |
||||
|
||||
// TODO this could probably be done in CUE/framework
|
||||
func (crd Kind) GVK() k8schema.GroupVersionKind { |
||||
// TODO custom structured
|
||||
props := crd.GrafanaKind.Props().(kindsys.CoreProperties) |
||||
gvk := k8schema.GroupVersionKind{ |
||||
Group: fmt.Sprintf("%s.core.grafana.com", props.MachineName), |
||||
Kind: props.Name, |
||||
} |
||||
if props.Maturity.Less(kindsys.MaturityStable) { |
||||
gvk.Version = "v0-0alpha1" |
||||
} else { |
||||
gvk.Version = fmt.Sprintf("v%d-%d", props.CurrentVersion[0], props.CurrentVersion[1]) |
||||
} |
||||
|
||||
return gvk |
||||
} |
||||
|
||||
// CustomResourceDefinitionList is the kubernetes-API-compliant representation of a list of CustomResourceDefinitions
|
||||
type CustomResourceDefinitionList struct { |
||||
ListBase[CustomResourceDefinition] |
||||
} |
||||
|
||||
// CustomResourceDefinition is the kubernetes-API-compliant representation of a Custom Resource Definition
|
||||
type CustomResourceDefinition struct { |
||||
metav1.TypeMeta `json:",inline" yaml:",inline"` |
||||
metav1.ObjectMeta `json:"metadata,omitempty" yaml:"metadata,omitempty"` |
||||
Spec CustomResourceDefinitionSpec `json:"spec"` |
||||
} |
||||
|
||||
// DeepCopyObject implements runtime.Object.
|
||||
func (crd *CustomResourceDefinition) DeepCopyObject() runtime.Object { |
||||
return DeepCopyObject(crd) |
||||
} |
||||
|
||||
// CustomResourceDefinitionSpec is the body or spec of a kubernetes Custom Resource Definition
|
||||
type CustomResourceDefinitionSpec struct { |
||||
Group string `json:"group" yaml:"group"` |
||||
Versions []CustomResourceDefinitionSpecVersion `json:"versions" yaml:"versions"` |
||||
Names CustomResourceDefinitionSpecNames `json:"names" yaml:"names"` |
||||
Scope string `json:"scope" yaml:"scope"` |
||||
} |
||||
|
||||
// CustomResourceDefinitionSpecVersion is the representation of a specific version of a CRD, as part of the overall spec
|
||||
type CustomResourceDefinitionSpecVersion struct { |
||||
Name string `json:"name" yaml:"name"` |
||||
Served bool `json:"served" yaml:"served"` |
||||
Storage bool `json:"storage" yaml:"storage"` |
||||
Schema map[string]any `json:"schema" yaml:"schema"` |
||||
Subresources map[string]any `json:"subresources,omitempty" yaml:"subresources,omitempty"` |
||||
} |
||||
|
||||
// CustomResourceDefinitionSpecNames is the struct representing the names (kind and plural) of a kubernetes CRD
|
||||
type CustomResourceDefinitionSpecNames struct { |
||||
Kind string `json:"kind" yaml:"kind"` |
||||
Plural string `json:"plural" yaml:"plural"` |
||||
} |
||||
|
||||
// Base is a struct which describes a basic CRD, and implements runtime.Object.
|
||||
// SpecType should be the struct that represents the spec in the definition.
|
||||
// It cannot be used on its own, as the name of the CRD in kubernetes must exactly match the name of struct.
|
||||
// Instead, this struct can be used as a component of a new named struct, for examples:
|
||||
//
|
||||
// type MyCustomResource struct {
|
||||
// crd.Base[MyCustomResourceSpec]
|
||||
// }
|
||||
type Base[SpecType any] struct { |
||||
metav1.TypeMeta `json:",inline"` |
||||
metav1.ObjectMeta `json:"metadata,omitempty"` |
||||
Spec SpecType `json:"spec"` |
||||
} |
||||
|
||||
// DeepCopyObject is implemented for Base so it will implement runtime.Object.
|
||||
// DeepCopyObject here just calls crd.DeepCopyObject on itself.
|
||||
func (b *Base[T]) DeepCopyObject() runtime.Object { |
||||
return DeepCopyObject(b) |
||||
} |
||||
|
||||
// ListBase is a struct which describes a list of CRDs, and implements runtime.Object.
|
||||
// ItemType should be the CRD type being listed (NOT the model).
|
||||
// It cannot be used on its own, as the struct name must exactly match `<name of kubernetes CRD>List`.
|
||||
// Instead, this struct can be used as a component of a new named struct, for examples:
|
||||
//
|
||||
// type MyCustomResourceList struct {
|
||||
// crd.Base[MyCustomResource]
|
||||
// }
|
||||
type ListBase[ItemType any] struct { |
||||
metav1.TypeMeta `json:",inline"` |
||||
metav1.ListMeta `json:"metadata"` |
||||
Items []ItemType `json:"items"` |
||||
} |
||||
|
||||
// DeepCopyObject is implemented for Base so it will implement runtime.Object.
|
||||
// DeepCopyObject here just calls crd.DeepCopyObject on itself.
|
||||
func (b *ListBase[T]) DeepCopyObject() runtime.Object { |
||||
return DeepCopyObject(b) |
||||
} |
||||
|
||||
// BaseStatus extends Base by including a Status subresource.
|
||||
// This should be used if your kubernetes CRD includes the status subresource and you want to be able to view/modify it.
|
||||
// Usage is identical to Base
|
||||
type BaseStatus[SpecType, StatusType any] struct { |
||||
metav1.TypeMeta `json:",inline"` |
||||
metav1.ObjectMeta `json:"metadata,omitempty"` |
||||
Spec SpecType `json:"spec"` |
||||
Status StatusType `json:"status"` |
||||
} |
||||
|
||||
// DeepCopyObject is implemented for Base so it will implement runtime.Object.
|
||||
// DeepCopyObject here just calls crd.DeepCopyObject on itself.
|
||||
func (b *BaseStatus[T, S]) DeepCopyObject() runtime.Object { |
||||
return DeepCopyObject(b) |
||||
} |
||||
|
||||
// DeepCopyObject is an implementation of the receiver method required for implementing runtime.Object.
|
||||
// It should be used in your own runtime.Object implementations if you do not wish to implement custom behavior.
|
||||
// Example:
|
||||
//
|
||||
// func (c *CustomObject) DeepCopyObject() runtime.Object {
|
||||
// return crd.DeepCopyObject(c)
|
||||
// }
|
||||
func DeepCopyObject(in any) runtime.Object { |
||||
val := reflect.ValueOf(in).Elem() |
||||
|
||||
cpy := reflect.New(val.Type()) |
||||
cpy.Elem().Set(val) |
||||
|
||||
// Using the <obj>, <ok> for the type conversion ensures that it doesn't panic if it can't be converted
|
||||
if obj, ok := cpy.Interface().(runtime.Object); ok { |
||||
return obj |
||||
} |
||||
|
||||
// TODO: better return than nil?
|
||||
return nil |
||||
} |
@ -1,92 +0,0 @@ |
||||
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) |
||||
} |
||||
|
||||
func (m Maturity) String() string { |
||||
return string(m) |
||||
} |
||||
|
||||
// Kind describes a Grafana kind object: a Go representation of the definition of
|
||||
// one of Grafana's categories of kinds.
|
||||
type Kind interface { |
||||
// Props returns a [kindsys.SomeKindProps], representing the properties
|
||||
// of the kind as declared in the .cue source. The underlying type is
|
||||
// determined by the category of kind.
|
||||
//
|
||||
// This method is largely for convenience, as all actual kind categories are
|
||||
// expected to implement one of the other interfaces, each of which contain
|
||||
// a Def() method through which these same properties are accessible.
|
||||
Props() SomeKindProperties |
||||
|
||||
// TODO docs
|
||||
Lineage() thema.Lineage |
||||
|
||||
// TODO remove, unnecessary with Props()
|
||||
Name() string |
||||
|
||||
// TODO remove, unnecessary with Props()
|
||||
MachineName() string |
||||
|
||||
// TODO remove, unnecessary with Props()
|
||||
Maturity() Maturity // TODO unclear if we want maturity for raw kinds
|
||||
} |
||||
|
||||
type Core interface { |
||||
Kind |
||||
|
||||
// TODO docs
|
||||
Def() Def[CoreProperties] |
||||
} |
||||
|
||||
type Custom interface { |
||||
Kind |
||||
|
||||
// TODO docs
|
||||
Def() Def[CustomProperties] |
||||
} |
||||
|
||||
type Composable interface { |
||||
Kind |
||||
|
||||
// TODO docs
|
||||
Def() Def[ComposableProperties] |
||||
} |
@ -1,31 +0,0 @@ |
||||
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 defined 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: string |
||||
// TODO is it worth doing something like below, given that we have to keep this set open for forward compatibility? |
||||
// schemaInterface: or([ for k, _ in schemaInterfaces {k}, 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. |
||||
// FIXME cuetsy currently gets confused by all the unifications - maybe openapi too. Do something like the following after thema separates joinSchema/constraint expression |
||||
// lineage: { joinSchema: schif.interface } |
||||
// lineage: { joinSchema: (schif.interface | *{}) } |
||||
|
||||
lineageIsGroup: schif.group |
||||
} |
@ -1,15 +0,0 @@ |
||||
package kindsys |
||||
|
||||
// Custom specifies the kind category for plugin-defined arbitrary types. |
||||
// Custom kinds have the same purpose as Core kinds, differing only in |
||||
// that they are defined by external plugins rather than in Grafana core. As such, |
||||
// this specification is kept closely aligned with the Core kind. |
||||
// |
||||
// 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={ |
||||
_sharedKind |
||||
|
||||
lineage: { name: S.machineName } |
||||
lineageIsGroup: false |
||||
} |
@ -1,127 +0,0 @@ |
||||
package kindsys |
||||
|
||||
import ( |
||||
"strings" |
||||
|
||||
"github.com/grafana/thema" |
||||
) |
||||
|
||||
// A Kind is a specification for a type of object that Grafana knows |
||||
// how to work with. Each kind definition contains a schema, and some |
||||
// declarative metadata and constraints. |
||||
// |
||||
// An instance of a kind is called a resource. Resources are a sequence of |
||||
// bytes - for example, a JSON file or HTTP request body - that conforms |
||||
// to the schemas and other constraints defined in a Kind. |
||||
// |
||||
// 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. |
||||
// |
||||
// Grafana's kinds are similar to Kubernetes CustomResourceDefinitions. |
||||
// 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 |
||||
|
||||
// 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 value of this field is set by the kindsys framework. It cannot be changed |
||||
// in the definition 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 |
||||
|
||||
// lineage is the Thema lineage containing all the schemas that have existed for this kind. |
||||
lineage: thema.#Lineage |
||||
|
||||
// currentVersion is computed to be the syntactic version number of the latest |
||||
// schema in lineage. |
||||
currentVersion: thema.#SyntacticVersion & (thema.#LatestVersion & {lin: lineage}).out |
||||
|
||||
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" |
||||
} |
||||
|
||||
// properties shared by all kinds that represent a complete object from root (i.e., not composable) |
||||
_rootKind: { |
||||
// description is a brief narrative description of the nature and purpose of the kind. |
||||
// The contents of this field is shown to end users. Prefer clear, concise wording |
||||
// with minimal jargon. |
||||
description: nonEmptyString |
||||
} |
||||
|
||||
// Maturity indicates the how far a given kind definition is in its initial |
||||
// journey. Mature kinds still evolve, but with guarantees about compatibility. |
||||
Maturity: "merged" | "experimental" | "stable" | "mature" |
||||
|
||||
// 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({ |
||||
_sharedKind |
||||
_rootKind |
||||
|
||||
lineage: { name: S.machineName } |
||||
lineageIsGroup: false |
||||
|
||||
// crd contains properties specific to converting this kind to a Kubernetes CRD. |
||||
crd: { |
||||
// group is used as the CRD group name in the GVK. |
||||
group: "\(S.machineName).core.grafana.com" |
||||
|
||||
// scope determines whether resources of this kind exist globally ("Cluster") or |
||||
// within Kubernetes namespaces. |
||||
scope: "Cluster" | *"Namespaced" |
||||
|
||||
// dummySchema determines whether a dummy OpenAPI schema - where the schema is |
||||
// simply an empty, open object - should be generated for the kind. |
||||
// |
||||
// It is a goal that this option eventually be force dto false. Only set to |
||||
// true when Grafana's code generators produce OpenAPI that is rejected by |
||||
// Kubernetes' CRD validation. |
||||
dummySchema: bool | *false |
||||
|
||||
// deepCopy determines whether a generic implementation of copying should be |
||||
// generated, or a passthrough call to a Go function. |
||||
// deepCopy: *"generic" | "passthrough" |
||||
} |
||||
}) |
||||
|
||||
nonEmptyString: string & strings.MinRunes(1) |
@ -1,213 +0,0 @@ |
||||
package kindsys |
||||
|
||||
import ( |
||||
"fmt" |
||||
"io/fs" |
||||
"path/filepath" |
||||
"sync" |
||||
|
||||
"cuelang.org/go/cue" |
||||
"cuelang.org/go/cue/errors" |
||||
"github.com/grafana/grafana/pkg/cuectx" |
||||
"github.com/grafana/thema" |
||||
) |
||||
|
||||
// CoreDefParentPath is the path, relative to the repository root, where
|
||||
// each child directory is expected to contain .cue files defining one
|
||||
// Core kind.
|
||||
var CoreDefParentPath = "kinds" |
||||
|
||||
// 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) |
||||
} |
||||
}) |
||||
} |
||||
|
||||
func doLoadFrameworkCUE(ctx *cue.Context) (cue.Value, error) { |
||||
v, err := cuectx.BuildGrafanaInstance(ctx, filepath.Join("pkg", "kindsys"), "kindsys", nil) |
||||
if err != nil { |
||||
return v, err |
||||
} |
||||
|
||||
if err = v.Validate(cue.Concrete(false), cue.All()); err != nil { |
||||
return cue.Value{}, fmt.Errorf("kindsys 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 define all the frameworky CUE bits in a single package. Other Go types
|
||||
// make the constructs in the returned cue.Value easy to use.
|
||||
//
|
||||
// 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
|
||||
// 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 |
||||
} |
||||
|
||||
// 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 ToKindProps[T KindProperties](v cue.Value) (T, error) { |
||||
props := new(T) |
||||
if !v.Exists() { |
||||
return *props, ErrValueNotExist |
||||
} |
||||
|
||||
fw := CUEFramework(v.Context()) |
||||
var kdef cue.Value |
||||
|
||||
anyprops := any(*props).(SomeKindProperties) |
||||
switch anyprops.(type) { |
||||
case CoreProperties: |
||||
kdef = fw.LookupPath(cue.MakePath(cue.Str("Core"))) |
||||
case CustomProperties: |
||||
kdef = fw.LookupPath(cue.MakePath(cue.Str("Custom"))) |
||||
case ComposableProperties: |
||||
kdef = fw.LookupPath(cue.MakePath(cue.Str("Composable"))) |
||||
default: |
||||
// unreachable so long as all the possibilities in KindProperties have switch branches
|
||||
panic("unreachable") |
||||
} |
||||
|
||||
item := v.Unify(kdef) |
||||
if item.Err() != nil { |
||||
return *props, errors.Wrap(errors.Promote(ErrValueNotAKind, ""), item.Err()) |
||||
} |
||||
|
||||
if err := item.Decode(props); err != nil { |
||||
// Should only be reachable if CUE and Go framework types have diverged
|
||||
panic(errors.Details(err, nil)) |
||||
} |
||||
|
||||
return *props, nil |
||||
} |
||||
|
||||
// SomeDef represents a single kind definition, having been loaded and
|
||||
// validated by a func such as [LoadCoreKindDef].
|
||||
//
|
||||
// The underlying type of the Properties field indicates the category of kind.
|
||||
type SomeDef struct { |
||||
// V is the cue.Value containing the entire Kind definition.
|
||||
V cue.Value |
||||
// Properties contains the kind's declarative non-schema properties.
|
||||
Properties SomeKindProperties |
||||
} |
||||
|
||||
// BindKindLineage binds the lineage for the kind definition.
|
||||
//
|
||||
// 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 (def SomeDef) BindKindLineage(rt *thema.Runtime, opts ...thema.BindOption) (thema.Lineage, error) { |
||||
if rt == nil { |
||||
rt = cuectx.GrafanaThemaRuntime() |
||||
} |
||||
return thema.BindLineage(def.V.LookupPath(cue.MakePath(cue.Str("lineage"))), rt, opts...) |
||||
} |
||||
|
||||
// IsCore indicates whether the represented kind is a core kind.
|
||||
func (def SomeDef) IsCore() bool { |
||||
_, is := def.Properties.(CoreProperties) |
||||
return is |
||||
} |
||||
|
||||
// IsCustom indicates whether the represented kind is a custom kind.
|
||||
func (def SomeDef) IsCustom() bool { |
||||
_, is := def.Properties.(CustomProperties) |
||||
return is |
||||
} |
||||
|
||||
// IsComposable indicates whether the represented kind is a composable kind.
|
||||
func (def SomeDef) IsComposable() bool { |
||||
_, is := def.Properties.(ComposableProperties) |
||||
return is |
||||
} |
||||
|
||||
// Def represents a single kind definition, having been loaded and validated by
|
||||
// a func such as [LoadCoreKindDef].
|
||||
//
|
||||
// Its type parameter indicates the category of kind.
|
||||
//
|
||||
// Thema lineages in the contained definition have not yet necessarily been
|
||||
// validated.
|
||||
type Def[T KindProperties] struct { |
||||
// V is the cue.Value containing the entire Kind definition.
|
||||
V cue.Value |
||||
// Properties contains the kind's declarative non-schema properties.
|
||||
Properties T |
||||
} |
||||
|
||||
// Some converts the typed Def to the equivalent typeless SomeDef.
|
||||
func (def Def[T]) Some() SomeDef { |
||||
return SomeDef{ |
||||
V: def.V, |
||||
Properties: any(def.Properties).(SomeKindProperties), |
||||
} |
||||
} |
||||
|
||||
// LoadCoreKindDef loads and validates a core kind definition of the kind category
|
||||
// indicated by the type parameter. On success, it returns a [Def] which
|
||||
// contains the entire contents of the kind definition.
|
||||
//
|
||||
// declpath is the path to the directory containing the core kind definition,
|
||||
// relative to the grafana/grafana root. For example, dashboards are in
|
||||
// "kinds/dashboard".
|
||||
//
|
||||
// The .cue file bytes containing the core kind definition 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 LoadCoreKindDef(defpath string, ctx *cue.Context, overlay fs.FS) (Def[CoreProperties], error) { |
||||
none := Def[CoreProperties]{} |
||||
vk, err := cuectx.BuildGrafanaInstance(ctx, defpath, "kind", overlay) |
||||
if err != nil { |
||||
return none, err |
||||
} |
||||
|
||||
props, err := ToKindProps[CoreProperties](vk) |
||||
if err != nil { |
||||
return none, err |
||||
} |
||||
|
||||
return Def[CoreProperties]{ |
||||
V: vk, |
||||
Properties: props, |
||||
}, nil |
||||
} |
@ -1,80 +0,0 @@ |
||||
package kindsys |
||||
|
||||
import "github.com/grafana/thema" |
||||
|
||||
// CommonProperties contains the metadata common to all categories of kinds.
|
||||
type CommonProperties 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"` |
||||
Description string `json:"description,omitempty"` |
||||
} |
||||
|
||||
// CoreProperties represents the static properties in the definition of a
|
||||
// Core kind that are representable with basic Go types. This
|
||||
// excludes Thema schemas.
|
||||
//
|
||||
// When .cue file(s) containing a Core definition is loaded through the standard
|
||||
// [LoadCoreKindDef], func, it is fully validated and populated according to all
|
||||
// rules specified in CUE for Core kinds.
|
||||
type CoreProperties struct { |
||||
CommonProperties |
||||
CurrentVersion thema.SyntacticVersion `json:"currentVersion"` |
||||
CRD struct { |
||||
Group string `json:"group"` |
||||
Scope string `json:"scope"` |
||||
DummySchema bool `json:"dummySchema"` |
||||
} `json:"crd"` |
||||
} |
||||
|
||||
func (m CoreProperties) _private() {} |
||||
func (m CoreProperties) Common() CommonProperties { |
||||
return m.CommonProperties |
||||
} |
||||
|
||||
// CustomProperties represents the static properties in the definition of a
|
||||
// Custom kind that are representable with basic Go types. This
|
||||
// excludes Thema schemas.
|
||||
type CustomProperties struct { |
||||
CommonProperties |
||||
CurrentVersion thema.SyntacticVersion `json:"currentVersion"` |
||||
} |
||||
|
||||
func (m CustomProperties) _private() {} |
||||
func (m CustomProperties) Common() CommonProperties { |
||||
return m.CommonProperties |
||||
} |
||||
|
||||
// ComposableProperties represents the static properties in the definition of a
|
||||
// Composable kind that are representable with basic Go types. This
|
||||
// excludes Thema schemas.
|
||||
type ComposableProperties struct { |
||||
CommonProperties |
||||
CurrentVersion thema.SyntacticVersion `json:"currentVersion"` |
||||
SchemaInterface string `json:"schemaInterface"` |
||||
} |
||||
|
||||
func (m ComposableProperties) _private() {} |
||||
func (m ComposableProperties) Common() CommonProperties { |
||||
return m.CommonProperties |
||||
} |
||||
|
||||
// SomeKindProperties is an interface type to abstract over the different kind
|
||||
// property struct types: [CoreProperties], [CustomProperties],
|
||||
// [ComposableProperties].
|
||||
//
|
||||
// It is the traditional interface counterpart to the generic type constraint
|
||||
// KindProperties.
|
||||
type SomeKindProperties interface { |
||||
_private() |
||||
Common() CommonProperties |
||||
} |
||||
|
||||
// KindProperties is a type parameter that comprises the base possible set of
|
||||
// kind metadata configurations.
|
||||
type KindProperties interface { |
||||
CoreProperties | CustomProperties | ComposableProperties |
||||
} |
@ -1,147 +0,0 @@ |
||||
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 |
||||
} |
||||
|
||||
// alias the exported type because DataQuery is shadowed by the schema interface |
||||
// name where we need to use the type |
||||
let dq = DataQuery |
||||
|
||||
// The canonical list of all Grafana schema interfaces. |
||||
schemaInterfaces: [N=string]: SchemaInterface & { name: N } |
||||
schemaInterfaces: { |
||||
PanelCfg: { |
||||
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 |
||||
} |
||||
|
||||
// The DataQuery schema interface specifies how (datasource) plugins are expected to define |
||||
// the shape of their queries. |
||||
// |
||||
// It is expected that plugins may support multiple logically distinct query types within |
||||
// their single DataQuery composable kind. Implementations are generally free to model |
||||
// this as they please, with understanding that Grafana systems will look to the queryType |
||||
// field as a discriminator - each distinct value will be assumed, where possible, to |
||||
// identify a distinct type of query supported by the plugin. |
||||
DataQuery: { |
||||
interface: { |
||||
dq |
||||
} |
||||
|
||||
pluginTypes: ["datasource"] |
||||
group: false |
||||
} |
||||
|
||||
DataSourceCfg: { |
||||
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 |
||||
} |
||||
} |
@ -1,121 +0,0 @@ |
||||
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 |
||||
} |
||||
|
||||
// Contract returns the cue.Value representing the meta-schema that is the
|
||||
// contract between core/custom kinds that consume schemas that are instances
|
||||
// of the SchemaInterface contract, and composable kinds that produce such schemas.
|
||||
func (s SchemaInterface) Contract() 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 |
||||
} |
@ -1,24 +0,0 @@ |
||||
package kindsys |
||||
|
||||
// Ptr returns a pointer to a value of an arbitrary type.
|
||||
//
|
||||
// This function is provided to compensate for Grafana's Go code generation that
|
||||
// represents optional fields using pointers.
|
||||
//
|
||||
// Pointers are the only technically [correct, non-ambiguous] way of
|
||||
// representing an optional field in Go's type system. However, Go does not
|
||||
// allow taking the address of certain primitive types inline. That is,
|
||||
// this is invalid Go code:
|
||||
//
|
||||
// var str *string
|
||||
// str = &"colorless green ideas sleep furiously"
|
||||
//
|
||||
// This func allows making such declarations in a single line:
|
||||
//
|
||||
// var str *string
|
||||
// str = kindsys.Ptr("colorless green ideas sleep furiously")
|
||||
//
|
||||
// [correct, non-ambiguous]: https://github.com/grafana/grok/issues/1
|
||||
func Ptr[T any](v T) *T { |
||||
return &v |
||||
} |
Loading…
Reference in new issue