mirror of
https://github.com/grafana/grafana.git
synced 2025-02-25 18:55:37 -06:00
Schemas: Refactor plugin's metadata (#83696)
* Remove kinds verification for kind-registry * Read plugin's json information with json library instead of use thema binding * Remove grafanaplugin unification * Don't use kindsys for extract the slot name * Fix IsGroup * Remove all plugindef generation * Refactor schema interfaces * Pushed this change from a different branch by mistake... * Create small plugin definition structure adding additional information for plugins registration * Add some validation checks * Delete unused code * Fix imports lint
This commit is contained in:
parent
beea7d1c2b
commit
1181141b40
Makefileembed.go
pkg
api
plugins
services/pluginsintegration
loader
pipeline
pluginconfig
serviceregistration
public/app/plugins
1
Makefile
1
Makefile
@ -103,7 +103,6 @@ openapi3-gen: swagger-gen ## Generates OpenApi 3 specs from the Swagger 2 alread
|
||||
##@ Building
|
||||
gen-cue: ## Do all CUE/Thema code generation
|
||||
@echo "generate code from .cue files"
|
||||
go generate ./pkg/plugins/plugindef
|
||||
go generate ./kinds/gen.go
|
||||
go generate ./public/app/plugins/gen.go
|
||||
|
||||
|
2
embed.go
2
embed.go
@ -6,5 +6,5 @@ import (
|
||||
|
||||
// CueSchemaFS embeds all schema-related CUE files in the Grafana project.
|
||||
//
|
||||
//go:embed cue.mod/module.cue kinds/*.cue kinds/*/*.cue packages/grafana-schema/src/common/*.cue public/app/plugins/*/*/*.cue public/app/plugins/*/*/plugin.json pkg/plugins/*/*.cue
|
||||
//go:embed cue.mod/module.cue kinds/*.cue kinds/*/*.cue packages/grafana-schema/src/common/*.cue public/app/plugins/*/*/*.cue public/app/plugins/*/*/plugin.json
|
||||
var CueSchemaFS embed.FS
|
||||
|
@ -2,7 +2,7 @@ package dtos
|
||||
|
||||
import (
|
||||
"github.com/grafana/grafana/pkg/plugins"
|
||||
"github.com/grafana/grafana/pkg/plugins/plugindef"
|
||||
"github.com/grafana/grafana/pkg/plugins/pfs"
|
||||
"github.com/grafana/grafana/pkg/services/accesscontrol"
|
||||
)
|
||||
|
||||
@ -48,7 +48,7 @@ type PluginListItem struct {
|
||||
SignatureOrg string `json:"signatureOrg"`
|
||||
AccessControl accesscontrol.Metadata `json:"accessControl,omitempty"`
|
||||
AngularDetected bool `json:"angularDetected"`
|
||||
IAM *plugindef.IAM `json:"iam,omitempty"`
|
||||
IAM *pfs.IAM `json:"iam,omitempty"`
|
||||
}
|
||||
|
||||
type PluginList []PluginListItem
|
||||
|
@ -21,7 +21,7 @@ import (
|
||||
"github.com/grafana/grafana/pkg/api/dtos"
|
||||
"github.com/grafana/grafana/pkg/api/response"
|
||||
"github.com/grafana/grafana/pkg/plugins"
|
||||
"github.com/grafana/grafana/pkg/plugins/plugindef"
|
||||
"github.com/grafana/grafana/pkg/plugins/pfs"
|
||||
"github.com/grafana/grafana/pkg/plugins/repo"
|
||||
ac "github.com/grafana/grafana/pkg/services/accesscontrol"
|
||||
contextmodel "github.com/grafana/grafana/pkg/services/contexthandler/model"
|
||||
@ -552,7 +552,7 @@ func (hs *HTTPServer) hasPluginRequestedPermissions(c *contextmodel.ReqContext,
|
||||
}
|
||||
|
||||
// evalAllPermissions generates an evaluator with all permissions from the input slice
|
||||
func evalAllPermissions(ps []plugindef.Permission) ac.Evaluator {
|
||||
func evalAllPermissions(ps []pfs.Permission) ac.Evaluator {
|
||||
res := []ac.Evaluator{}
|
||||
for _, p := range ps {
|
||||
if p.Scope != nil {
|
||||
|
@ -27,7 +27,7 @@ import (
|
||||
"github.com/grafana/grafana/pkg/plugins/manager/fakes"
|
||||
"github.com/grafana/grafana/pkg/plugins/manager/filestore"
|
||||
"github.com/grafana/grafana/pkg/plugins/manager/registry"
|
||||
"github.com/grafana/grafana/pkg/plugins/plugindef"
|
||||
"github.com/grafana/grafana/pkg/plugins/pfs"
|
||||
"github.com/grafana/grafana/pkg/plugins/pluginscdn"
|
||||
ac "github.com/grafana/grafana/pkg/services/accesscontrol"
|
||||
"github.com/grafana/grafana/pkg/services/accesscontrol/acimpl"
|
||||
@ -658,8 +658,8 @@ func TestHTTPServer_hasPluginRequestedPermissions(t *testing.T) {
|
||||
pluginReg := pluginstore.Plugin{
|
||||
JSONData: plugins.JSONData{
|
||||
ID: "grafana-test-app",
|
||||
IAM: &plugindef.IAM{
|
||||
Permissions: []plugindef.Permission{{Action: ac.ActionUsersRead, Scope: newStr(ac.ScopeUsersAll)}, {Action: ac.ActionUsersCreate}},
|
||||
IAM: &pfs.IAM{
|
||||
Permissions: []pfs.Permission{{Action: ac.ActionUsersRead, Scope: newStr(ac.ScopeUsersAll)}, {Action: ac.ActionUsersCreate}},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
@ -3,7 +3,7 @@ package auth
|
||||
import (
|
||||
"context"
|
||||
|
||||
"github.com/grafana/grafana/pkg/plugins/plugindef"
|
||||
"github.com/grafana/grafana/pkg/plugins/pfs"
|
||||
)
|
||||
|
||||
type ExternalService struct {
|
||||
@ -14,6 +14,6 @@ type ExternalService struct {
|
||||
|
||||
type ExternalServiceRegistry interface {
|
||||
HasExternalService(ctx context.Context, pluginID string) (bool, error)
|
||||
RegisterExternalService(ctx context.Context, pluginID string, pType plugindef.Type, svc *plugindef.IAM) (*ExternalService, error)
|
||||
RegisterExternalService(ctx context.Context, pluginID string, pType pfs.Type, svc *pfs.IAM) (*ExternalService, error)
|
||||
RemoveExternalService(ctx context.Context, pluginID string) error
|
||||
}
|
||||
|
@ -35,10 +35,10 @@ func (j *pgoJenny) Generate(decl *pfs.PluginDecl) (*codejen.File, error) {
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
slotname := strings.ToLower(decl.SchemaInterface.Name())
|
||||
slotname := strings.ToLower(decl.SchemaInterface.Name)
|
||||
byt, err := gocode.GenerateTypesOpenAPI(decl.Lineage.Latest(), &gocode.TypeConfigOpenAPI{
|
||||
Config: &openapi.Config{
|
||||
Group: decl.SchemaInterface.IsGroup(),
|
||||
Group: decl.SchemaInterface.IsGroup,
|
||||
Config: &copenapi.Config{
|
||||
MaxCycleDepth: 10,
|
||||
},
|
||||
|
@ -42,8 +42,8 @@ func (j *pleJenny) Generate(decl *pfs.PluginDecl) (codejen.Files, error) {
|
||||
}
|
||||
|
||||
version := "export const pluginVersion = \"%s\";"
|
||||
if decl.PluginMeta.Info.Version != nil {
|
||||
version = fmt.Sprintf(version, *decl.PluginMeta.Info.Version)
|
||||
if decl.PluginMeta.Version != nil {
|
||||
version = fmt.Sprintf(version, *decl.PluginMeta.Version)
|
||||
} else {
|
||||
version = fmt.Sprintf(version, getGrafanaVersion())
|
||||
}
|
||||
|
@ -51,7 +51,7 @@ func (j *ptsJenny) Generate(decl *pfs.PluginDecl) (*codejen.File, error) {
|
||||
Data: string(jf.Data),
|
||||
})
|
||||
|
||||
path := filepath.Join(j.root, decl.PluginPath, fmt.Sprintf("%s.gen.ts", strings.ToLower(decl.SchemaInterface.Name())))
|
||||
path := filepath.Join(j.root, decl.PluginPath, fmt.Sprintf("%s.gen.ts", strings.ToLower(decl.SchemaInterface.Name)))
|
||||
data := []byte(tsf.String())
|
||||
data = data[:len(data)-1] // remove the additional line break added by the inner jenny
|
||||
|
||||
|
@ -13,7 +13,7 @@ import (
|
||||
"github.com/grafana/grafana/pkg/plugins/auth"
|
||||
"github.com/grafana/grafana/pkg/plugins/backendplugin"
|
||||
"github.com/grafana/grafana/pkg/plugins/log"
|
||||
"github.com/grafana/grafana/pkg/plugins/plugindef"
|
||||
"github.com/grafana/grafana/pkg/plugins/pfs"
|
||||
"github.com/grafana/grafana/pkg/plugins/repo"
|
||||
"github.com/grafana/grafana/pkg/plugins/storage"
|
||||
)
|
||||
@ -456,7 +456,7 @@ func (f *FakeAuthService) HasExternalService(ctx context.Context, pluginID strin
|
||||
return f.Result != nil, nil
|
||||
}
|
||||
|
||||
func (f *FakeAuthService) RegisterExternalService(ctx context.Context, pluginID string, pType plugindef.Type, svc *plugindef.IAM) (*auth.ExternalService, error) {
|
||||
func (f *FakeAuthService) RegisterExternalService(ctx context.Context, pluginID string, pType pfs.Type, svc *pfs.IAM) (*auth.ExternalService, error) {
|
||||
return f.Result, nil
|
||||
}
|
||||
|
||||
|
@ -4,20 +4,30 @@ import (
|
||||
"cuelang.org/go/cue/ast"
|
||||
"github.com/grafana/kindsys"
|
||||
"github.com/grafana/thema"
|
||||
|
||||
"github.com/grafana/grafana/pkg/plugins/plugindef"
|
||||
)
|
||||
|
||||
type PluginDecl struct {
|
||||
SchemaInterface *kindsys.SchemaInterface
|
||||
SchemaInterface *SchemaInterface
|
||||
Lineage thema.Lineage
|
||||
Imports []*ast.ImportSpec
|
||||
PluginPath string
|
||||
PluginMeta plugindef.PluginDef
|
||||
PluginMeta Metadata
|
||||
KindDecl kindsys.Def[kindsys.ComposableProperties]
|
||||
}
|
||||
|
||||
func EmptyPluginDecl(path string, meta plugindef.PluginDef) *PluginDecl {
|
||||
type SchemaInterface struct {
|
||||
Name string
|
||||
IsGroup bool
|
||||
}
|
||||
|
||||
type Metadata struct {
|
||||
Id string
|
||||
Name string
|
||||
Backend *bool
|
||||
Version *string
|
||||
}
|
||||
|
||||
func EmptyPluginDecl(path string, meta Metadata) *PluginDecl {
|
||||
return &PluginDecl{
|
||||
PluginPath: path,
|
||||
PluginMeta: meta,
|
||||
|
@ -6,7 +6,6 @@ import (
|
||||
"path/filepath"
|
||||
"sort"
|
||||
|
||||
"github.com/grafana/kindsys"
|
||||
"github.com/grafana/thema"
|
||||
)
|
||||
|
||||
@ -15,6 +14,18 @@ type declParser struct {
|
||||
skip map[string]bool
|
||||
}
|
||||
|
||||
// Extracted from kindsys repository
|
||||
var schemaInterfaces = map[string]*SchemaInterface{
|
||||
"PanelCfg": {
|
||||
Name: "PanelCfg",
|
||||
IsGroup: true,
|
||||
},
|
||||
"DataQuery": {
|
||||
Name: "DataQuery",
|
||||
IsGroup: false,
|
||||
},
|
||||
}
|
||||
|
||||
func NewDeclParser(rt *thema.Runtime, skip map[string]bool) *declParser {
|
||||
return &declParser{
|
||||
rt: rt,
|
||||
@ -50,12 +61,11 @@ func (psr *declParser) Parse(root fs.FS) ([]*PluginDecl, error) {
|
||||
}
|
||||
|
||||
for slotName, kind := range pp.ComposableKinds {
|
||||
slot, err := kindsys.FindSchemaInterface(slotName)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("parsing plugin failed for %s: %s", dir, err)
|
||||
}
|
||||
decls = append(decls, &PluginDecl{
|
||||
SchemaInterface: &slot,
|
||||
SchemaInterface: schemaInterfaces[slotName],
|
||||
Lineage: kind.Lineage(),
|
||||
Imports: pp.CUEImports,
|
||||
PluginMeta: pp.Properties,
|
||||
|
@ -11,16 +11,6 @@ var ErrNoRootFile = errors.New("no plugin.json at root of fs.fS")
|
||||
// ErrInvalidRootFile indicates that the root plugin.json file is invalid.
|
||||
var ErrInvalidRootFile = errors.New("plugin.json is invalid")
|
||||
|
||||
// ErrComposableNotExpected indicates that a plugin has a composable kind for a
|
||||
// schema interface that is not expected, given the type of the plugin. (For
|
||||
// example, a datasource plugin has a panelcfg composable kind)
|
||||
var ErrComposableNotExpected = errors.New("plugin type should not produce composable kind for schema interface")
|
||||
|
||||
// ErrExpectedComposable indicates that a plugin lacks a composable kind
|
||||
// implementation for a schema interface that is expected for that plugin's
|
||||
// type. (For example, a datasource plugin lacks a queries composable kind)
|
||||
var ErrExpectedComposable = errors.New("plugin type should produce composable kind for schema interface")
|
||||
|
||||
// ErrInvalidGrafanaPluginInstance indicates a plugin's set of .cue
|
||||
// grafanaplugin package files are invalid with respect to the GrafanaPlugin
|
||||
// spec.
|
||||
|
@ -1,31 +0,0 @@
|
||||
package pfs
|
||||
|
||||
import (
|
||||
"github.com/grafana/kindsys"
|
||||
)
|
||||
|
||||
// GrafanaPlugin specifies what plugins may declare in .cue files in a
|
||||
// `grafanaplugin` CUE package in the plugin root directory (adjacent to plugin.json).
|
||||
GrafanaPlugin: {
|
||||
// id and pascalName are injected from plugin.json. Plugin authors can write
|
||||
// values for them in .cue files, but the only valid values will be the ones
|
||||
// given in plugin.json.
|
||||
id: string
|
||||
pascalName: string
|
||||
|
||||
// A plugin defines its Composable kinds under this key.
|
||||
//
|
||||
// This struct is open for forwards compatibility - older versions of Grafana (or
|
||||
// dependent tooling) should not break if new versions introduce additional schema interfaces.
|
||||
composableKinds?: [Iface=string]: kindsys.Composable & {
|
||||
name: pascalName + Iface
|
||||
schemaInterface: Iface
|
||||
lineage: name: pascalName + Iface
|
||||
}
|
||||
|
||||
// A plugin defines its Custom kinds under this key.
|
||||
customKinds?: [Name=string]: kindsys.Custom & {
|
||||
name: Name
|
||||
}
|
||||
...
|
||||
}
|
@ -1,56 +1,29 @@
|
||||
package pfs
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"io/fs"
|
||||
"path/filepath"
|
||||
"sort"
|
||||
"strings"
|
||||
"sync"
|
||||
"testing/fstest"
|
||||
|
||||
"cuelang.org/go/cue"
|
||||
"cuelang.org/go/cue/build"
|
||||
"cuelang.org/go/cue/cuecontext"
|
||||
"cuelang.org/go/cue/errors"
|
||||
"cuelang.org/go/cue/parser"
|
||||
"cuelang.org/go/cue/token"
|
||||
"github.com/grafana/kindsys"
|
||||
"github.com/grafana/thema"
|
||||
"github.com/grafana/thema/load"
|
||||
"github.com/grafana/thema/vmux"
|
||||
"github.com/yalue/merged_fs"
|
||||
|
||||
"github.com/grafana/grafana/pkg/cuectx"
|
||||
"github.com/grafana/grafana/pkg/plugins/plugindef"
|
||||
)
|
||||
|
||||
// PackageName is the name of the CUE package that Grafana will load when
|
||||
// looking for a Grafana plugin's kind declarations.
|
||||
const PackageName = "grafanaplugin"
|
||||
|
||||
var onceGP sync.Once
|
||||
var defaultGP cue.Value
|
||||
|
||||
func doLoadGP(ctx *cue.Context) cue.Value {
|
||||
v, err := cuectx.BuildGrafanaInstance(ctx, filepath.Join("pkg", "plugins", "pfs"), "pfs", nil)
|
||||
if err != nil {
|
||||
// should be unreachable
|
||||
panic(err)
|
||||
}
|
||||
return v.LookupPath(cue.MakePath(cue.Str("GrafanaPlugin")))
|
||||
}
|
||||
|
||||
func loadGP(ctx *cue.Context) cue.Value {
|
||||
if ctx == nil || ctx == cuectx.GrafanaCUEContext() {
|
||||
onceGP.Do(func() {
|
||||
defaultGP = doLoadGP(ctx)
|
||||
})
|
||||
return defaultGP
|
||||
}
|
||||
return doLoadGP(ctx)
|
||||
}
|
||||
|
||||
// PermittedCUEImports returns the list of import paths that may be used in a
|
||||
// plugin's grafanaplugin cue package.
|
||||
var PermittedCUEImports = cuectx.PermittedCUEImports
|
||||
@ -109,35 +82,14 @@ func ParsePluginFS(fsys fs.FS, rt *thema.Runtime) (ParsedPlugin, error) {
|
||||
rt = cuectx.GrafanaThemaRuntime()
|
||||
}
|
||||
|
||||
lin, err := plugindef.Lineage(rt)
|
||||
metadata, err := getPluginMetadata(fsys)
|
||||
if err != nil {
|
||||
panic(fmt.Sprintf("plugindef lineage is invalid or broken, needs dev attention: %s", err))
|
||||
}
|
||||
ctx := rt.Context()
|
||||
|
||||
b, err := fs.ReadFile(fsys, "plugin.json")
|
||||
if err != nil {
|
||||
if errors.Is(err, fs.ErrNotExist) {
|
||||
return ParsedPlugin{}, ErrNoRootFile
|
||||
}
|
||||
return ParsedPlugin{}, fmt.Errorf("error reading plugin.json: %w", err)
|
||||
return ParsedPlugin{}, err
|
||||
}
|
||||
|
||||
pp := ParsedPlugin{
|
||||
ComposableKinds: make(map[string]kindsys.Composable),
|
||||
// CustomKinds: make(map[string]kindsys.Custom),
|
||||
}
|
||||
|
||||
// Pass the raw bytes into the muxer, get the populated PluginDef type out that we want.
|
||||
// TODO stop ignoring second return. (for now, lacunas are a WIP and can't occur until there's >1 schema in the plugindef lineage)
|
||||
pinst, _, err := vmux.NewTypedMux(lin.TypedSchema(), vmux.NewJSONCodec("plugin.json"))(b)
|
||||
if err != nil {
|
||||
return ParsedPlugin{}, errors.Wrap(errors.Promote(err, ""), ErrInvalidRootFile)
|
||||
}
|
||||
pp.Properties = *(pinst.ValueP())
|
||||
// FIXME remove this once it's being correctly populated coming out of lineage
|
||||
if pp.Properties.PascalName == "" {
|
||||
pp.Properties.PascalName = plugindef.DerivePascalName(pp.Properties)
|
||||
Properties: metadata,
|
||||
}
|
||||
|
||||
if cuefiles, err := fs.Glob(fsys, "*.cue"); err != nil {
|
||||
@ -146,8 +98,6 @@ func ParsePluginFS(fsys fs.FS, rt *thema.Runtime) (ParsedPlugin, error) {
|
||||
return pp, nil
|
||||
}
|
||||
|
||||
gpv := loadGP(rt.Context())
|
||||
|
||||
fsys, err = ensureCueMod(fsys, pp.Properties)
|
||||
if err != nil {
|
||||
return ParsedPlugin{}, fmt.Errorf("%s has invalid cue.mod: %w", pp.Properties.Id, err)
|
||||
@ -161,11 +111,6 @@ func ParsePluginFS(fsys fs.FS, rt *thema.Runtime) (ParsedPlugin, error) {
|
||||
return ParsedPlugin{}, errors.Wrap(errors.Newf(token.NoPos, "%s did not load", pp.Properties.Id), err)
|
||||
}
|
||||
|
||||
f, _ := parser.ParseFile("plugin.json", fmt.Sprintf(`{
|
||||
"id": %q,
|
||||
"pascalName": %q
|
||||
}`, pp.Properties.Id, pp.Properties.PascalName))
|
||||
|
||||
for _, f := range bi.Files {
|
||||
for _, im := range f.Imports {
|
||||
ip := strings.Trim(im.Path.Value, "\"")
|
||||
@ -187,16 +132,7 @@ func ParsePluginFS(fsys fs.FS, rt *thema.Runtime) (ParsedPlugin, error) {
|
||||
panic("Refactor required - upstream CUE implementation changed, bi.Files is no longer populated")
|
||||
}
|
||||
|
||||
// Inject the JSON directly into the build so it gets loaded together
|
||||
bi.BuildFiles = append(bi.BuildFiles, &build.File{
|
||||
Filename: "plugin.json",
|
||||
Encoding: build.JSON,
|
||||
Form: build.Data,
|
||||
Source: b,
|
||||
})
|
||||
bi.Files = append(bi.Files, f)
|
||||
|
||||
gpi := ctx.BuildInstance(bi).Unify(gpv)
|
||||
gpi := rt.Context().BuildInstance(bi)
|
||||
if gpi.Err() != nil {
|
||||
return ParsedPlugin{}, errors.Wrap(errors.Promote(ErrInvalidGrafanaPluginInstance, pp.Properties.Id), gpi.Err())
|
||||
}
|
||||
@ -207,6 +143,18 @@ func ParsePluginFS(fsys fs.FS, rt *thema.Runtime) (ParsedPlugin, error) {
|
||||
continue
|
||||
}
|
||||
|
||||
iv = iv.FillPath(cue.MakePath(cue.Str("schemaInterface")), si.Name())
|
||||
iv = iv.FillPath(cue.MakePath(cue.Str("name")), derivePascalName(pp.Properties.Id, pp.Properties.Name)+si.Name())
|
||||
lineageNamePath := iv.LookupPath(cue.MakePath(cue.Str("lineage"), cue.Str("name")))
|
||||
if !lineageNamePath.Exists() {
|
||||
iv = iv.FillPath(cue.MakePath(cue.Str("lineage"), cue.Str("name")), derivePascalName(pp.Properties.Id, pp.Properties.Name)+si.Name())
|
||||
}
|
||||
|
||||
validSchema := iv.LookupPath(cue.ParsePath("lineage.schemas[0].schema"))
|
||||
if !validSchema.Exists() {
|
||||
return ParsedPlugin{}, errors.Wrap(errors.Promote(ErrInvalidGrafanaPluginInstance, pp.Properties.Id), validSchema.Err())
|
||||
}
|
||||
|
||||
props, err := kindsys.ToKindProps[kindsys.ComposableProperties](iv)
|
||||
if err != nil {
|
||||
return ParsedPlugin{}, err
|
||||
@ -222,7 +170,6 @@ func ParsePluginFS(fsys fs.FS, rt *thema.Runtime) (ParsedPlugin, error) {
|
||||
pp.ComposableKinds[si.Name()] = compo
|
||||
}
|
||||
|
||||
// TODO custom kinds
|
||||
return pp, nil
|
||||
}
|
||||
|
||||
@ -237,7 +184,7 @@ func ParsePluginFS(fsys fs.FS, rt *thema.Runtime) (ParsedPlugin, error) {
|
||||
func LoadComposableKindDef(fsys fs.FS, rt *thema.Runtime, defpath string) (kindsys.Def[kindsys.ComposableProperties], error) {
|
||||
pp := ParsedPlugin{
|
||||
ComposableKinds: make(map[string]kindsys.Composable),
|
||||
Properties: plugindef.PluginDef{
|
||||
Properties: Metadata{
|
||||
Id: defpath,
|
||||
},
|
||||
}
|
||||
@ -269,13 +216,13 @@ func LoadComposableKindDef(fsys fs.FS, rt *thema.Runtime, defpath string) (kinds
|
||||
}, nil
|
||||
}
|
||||
|
||||
func ensureCueMod(fsys fs.FS, pdef plugindef.PluginDef) (fs.FS, error) {
|
||||
func ensureCueMod(fsys fs.FS, metadata Metadata) (fs.FS, error) {
|
||||
if modf, err := fs.ReadFile(fsys, "cue.mod/module.cue"); err != nil {
|
||||
if !errors.Is(err, fs.ErrNotExist) {
|
||||
return nil, err
|
||||
}
|
||||
return merged_fs.NewMergedFS(fsys, fstest.MapFS{
|
||||
"cue.mod/module.cue": &fstest.MapFile{Data: []byte(fmt.Sprintf(`module: "grafana.com/grafana/plugins/%s"`, pdef.Id))},
|
||||
"cue.mod/module.cue": &fstest.MapFile{Data: []byte(fmt.Sprintf(`module: "grafana.com/grafana/plugins/%s"`, metadata.Id))},
|
||||
}), nil
|
||||
} else if _, err := cuecontext.New().CompileBytes(modf).LookupPath(cue.MakePath(cue.Str("module"))).String(); err != nil {
|
||||
return nil, fmt.Errorf("error reading cue module name: %w", err)
|
||||
@ -283,3 +230,61 @@ func ensureCueMod(fsys fs.FS, pdef plugindef.PluginDef) (fs.FS, error) {
|
||||
|
||||
return fsys, nil
|
||||
}
|
||||
|
||||
func getPluginMetadata(fsys fs.FS) (Metadata, error) {
|
||||
b, err := fs.ReadFile(fsys, "plugin.json")
|
||||
if err != nil {
|
||||
if errors.Is(err, fs.ErrNotExist) {
|
||||
return Metadata{}, ErrNoRootFile
|
||||
}
|
||||
return Metadata{}, fmt.Errorf("error reading plugin.json: %w", err)
|
||||
}
|
||||
|
||||
var metadata PluginDef
|
||||
if err := json.Unmarshal(b, &metadata); err != nil {
|
||||
return Metadata{}, fmt.Errorf("error unmarshalling plugin.json: %s", err)
|
||||
}
|
||||
|
||||
if err := metadata.Validate(); err != nil {
|
||||
return Metadata{}, err
|
||||
}
|
||||
|
||||
return Metadata{
|
||||
Id: metadata.Id,
|
||||
Name: metadata.Name,
|
||||
Backend: metadata.Backend,
|
||||
Version: metadata.Info.Version,
|
||||
}, nil
|
||||
}
|
||||
|
||||
func derivePascalName(id string, name string) string {
|
||||
sani := func(s string) string {
|
||||
ret := strings.Title(strings.Map(func(r rune) rune {
|
||||
switch {
|
||||
case r >= 'a' && r <= 'z':
|
||||
return r
|
||||
case r >= 'A' && r <= 'Z':
|
||||
return r
|
||||
default:
|
||||
return -1
|
||||
}
|
||||
}, strings.Title(strings.Map(func(r rune) rune {
|
||||
switch r {
|
||||
case '-', '_':
|
||||
return ' '
|
||||
default:
|
||||
return r
|
||||
}
|
||||
}, s))))
|
||||
if len(ret) > 63 {
|
||||
return ret[:63]
|
||||
}
|
||||
return ret
|
||||
}
|
||||
|
||||
fromname := sani(name)
|
||||
if len(fromname) != 0 {
|
||||
return fromname
|
||||
}
|
||||
return sani(strings.Split(id, "-")[1])
|
||||
}
|
||||
|
@ -3,8 +3,6 @@ package pfs
|
||||
import (
|
||||
"cuelang.org/go/cue/ast"
|
||||
"github.com/grafana/kindsys"
|
||||
|
||||
"github.com/grafana/grafana/pkg/plugins/plugindef"
|
||||
)
|
||||
|
||||
// ParsedPlugin represents everything knowable about a single plugin from static
|
||||
@ -14,7 +12,7 @@ import (
|
||||
// struct returned from [ParsePluginFS].
|
||||
type ParsedPlugin struct {
|
||||
// Properties contains the plugin's definition, as declared in plugin.json.
|
||||
Properties plugindef.PluginDef
|
||||
Properties Metadata
|
||||
|
||||
// ComposableKinds is a map of all the composable kinds declared in this plugin.
|
||||
// Keys are the name of the [kindsys.SchemaInterface] implemented by the value.
|
||||
|
42
pkg/plugins/pfs/plugindef_types.go
Normal file
42
pkg/plugins/pfs/plugindef_types.go
Normal file
@ -0,0 +1,42 @@
|
||||
package pfs
|
||||
|
||||
type Type string
|
||||
|
||||
// Defines values for Type.
|
||||
const (
|
||||
TypeApp Type = "app"
|
||||
TypeDatasource Type = "datasource"
|
||||
TypePanel Type = "panel"
|
||||
TypeRenderer Type = "renderer"
|
||||
TypeSecretsmanager Type = "secretsmanager"
|
||||
)
|
||||
|
||||
type PluginDef struct {
|
||||
Id string
|
||||
Name string
|
||||
Backend *bool
|
||||
Type Type
|
||||
Info Info
|
||||
IAM IAM
|
||||
}
|
||||
|
||||
type Info struct {
|
||||
Version *string
|
||||
}
|
||||
|
||||
type IAM struct {
|
||||
Permissions []Permission `json:"permissions,omitempty"`
|
||||
}
|
||||
|
||||
type Permission struct {
|
||||
Action string `json:"action"`
|
||||
Scope *string `json:"scope,omitempty"`
|
||||
}
|
||||
|
||||
func (pd PluginDef) Validate() error {
|
||||
if pd.Id == "" || pd.Name == "" || pd.Type == "" {
|
||||
return ErrInvalidRootFile
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
@ -1,130 +0,0 @@
|
||||
//go:build ignore
|
||||
// +build ignore
|
||||
|
||||
package main
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"os"
|
||||
"path/filepath"
|
||||
|
||||
"cuelang.org/go/cue/cuecontext"
|
||||
"github.com/dave/dst"
|
||||
"github.com/grafana/codejen"
|
||||
"github.com/grafana/grafana/pkg/codegen"
|
||||
"github.com/grafana/grafana/pkg/cuectx"
|
||||
"github.com/grafana/thema"
|
||||
"github.com/grafana/thema/encoding/gocode"
|
||||
"github.com/grafana/thema/encoding/jsonschema"
|
||||
)
|
||||
|
||||
var dirPlugindef = filepath.Join("pkg", "plugins", "plugindef")
|
||||
|
||||
// main generator for plugindef. plugindef isn't a kind, so it has its own
|
||||
// one-off main generator.
|
||||
func main() {
|
||||
v := elsedie(cuectx.BuildGrafanaInstance(nil, dirPlugindef, "", nil))("could not load plugindef cue package")
|
||||
|
||||
lin := elsedie(thema.BindLineage(v, cuectx.GrafanaThemaRuntime()))("plugindef lineage is invalid")
|
||||
|
||||
jl := &codejen.JennyList[thema.Lineage]{}
|
||||
jl.AppendOneToOne(&jennytypego{}, &jennybindgo{})
|
||||
jl.AddPostprocessors(codegen.SlashHeaderMapper(filepath.Join(dirPlugindef, "gen.go")))
|
||||
|
||||
cwd, err := os.Getwd()
|
||||
if err != nil {
|
||||
fmt.Fprintf(os.Stderr, "could not get working directory: %s", err)
|
||||
os.Exit(1)
|
||||
}
|
||||
|
||||
groot := filepath.Clean(filepath.Join(cwd, "../../.."))
|
||||
|
||||
jfs := elsedie(jl.GenerateFS(lin))("plugindef jenny pipeline failed")
|
||||
if _, set := os.LookupEnv("CODEGEN_VERIFY"); set {
|
||||
if err := jfs.Verify(context.Background(), groot); err != nil {
|
||||
die(fmt.Errorf("generated code is out of sync with inputs:\n%s\nrun `make gen-cue` to regenerate", err))
|
||||
}
|
||||
} else if err := jfs.Write(context.Background(), groot); err != nil {
|
||||
die(fmt.Errorf("error while writing generated code to disk:\n%s", err))
|
||||
}
|
||||
}
|
||||
|
||||
// one-off jenny for plugindef go types
|
||||
type jennytypego struct{}
|
||||
|
||||
func (j *jennytypego) JennyName() string {
|
||||
return "PluginGoTypes"
|
||||
}
|
||||
|
||||
func (j *jennytypego) Generate(lin thema.Lineage) (*codejen.File, error) {
|
||||
f, err := codegen.GoTypesJenny{}.Generate(codegen.SchemaForGen{
|
||||
Name: "PluginDef",
|
||||
Schema: lin.Latest(),
|
||||
IsGroup: false,
|
||||
})
|
||||
if f != nil {
|
||||
f.RelativePath = filepath.Join(dirPlugindef, f.RelativePath)
|
||||
}
|
||||
return f, err
|
||||
}
|
||||
|
||||
// one-off jenny for plugindef go bindings
|
||||
type jennybindgo struct{}
|
||||
|
||||
func (j *jennybindgo) JennyName() string {
|
||||
return "PluginGoBindings"
|
||||
}
|
||||
|
||||
func (j *jennybindgo) Generate(lin thema.Lineage) (*codejen.File, error) {
|
||||
b, err := gocode.GenerateLineageBinding(lin, &gocode.BindingConfig{
|
||||
TitleName: "PluginDef",
|
||||
Assignee: dst.NewIdent("*PluginDef"),
|
||||
PrivateFactory: true,
|
||||
})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return codejen.NewFile(filepath.Join(dirPlugindef, "plugindef_bindings_gen.go"), b, j), nil
|
||||
}
|
||||
|
||||
// one-off jenny for plugindef json schema generator
|
||||
type jennyjschema struct{}
|
||||
|
||||
func (j *jennyjschema) JennyName() string {
|
||||
return "PluginJSONSchema"
|
||||
}
|
||||
|
||||
func (j *jennyjschema) Generate(lin thema.Lineage) (*codejen.File, error) {
|
||||
f, err := jsonschema.GenerateSchema(lin.Latest())
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
b, _ := cuecontext.New().BuildFile(f).MarshalJSON()
|
||||
nb := new(bytes.Buffer)
|
||||
die(json.Indent(nb, b, "", " "))
|
||||
return codejen.NewFile(filepath.FromSlash("docs/sources/developers/plugins/plugin.schema.json"), nb.Bytes(), j), nil
|
||||
}
|
||||
|
||||
func elsedie[T any](t T, err error) func(msg string) T {
|
||||
if err != nil {
|
||||
return func(msg string) T {
|
||||
fmt.Fprintf(os.Stderr, "%s: %s\n", msg, err)
|
||||
os.Exit(1)
|
||||
return t
|
||||
}
|
||||
}
|
||||
return func(msg string) T {
|
||||
return t
|
||||
}
|
||||
}
|
||||
|
||||
func die(err error) {
|
||||
if err != nil {
|
||||
fmt.Fprint(os.Stderr, err, "\n")
|
||||
os.Exit(1)
|
||||
}
|
||||
}
|
@ -1,43 +0,0 @@
|
||||
package plugindef
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
func TestDerivePascal(t *testing.T) {
|
||||
table := []struct {
|
||||
id, name, out string
|
||||
}{
|
||||
{
|
||||
name: "-- Grafana --",
|
||||
out: "Grafana",
|
||||
},
|
||||
{
|
||||
name: "A weird/Thing",
|
||||
out: "AWeirdThing",
|
||||
},
|
||||
{
|
||||
name: "/",
|
||||
out: "Empty",
|
||||
},
|
||||
{
|
||||
name: "some really Long thing WHY would38883 anyone do this i don't know but hey It seems like it this is just going on and",
|
||||
out: "SomeReallyLongThingWHYWouldAnyoneDoThisIDonTKnowButHeyItSeemsLi",
|
||||
},
|
||||
}
|
||||
|
||||
for _, row := range table {
|
||||
if row.id == "" {
|
||||
row.id = "default-empty-panel"
|
||||
}
|
||||
|
||||
pd := PluginDef{
|
||||
Id: row.id,
|
||||
Name: row.name,
|
||||
}
|
||||
|
||||
require.Equal(t, row.out, DerivePascalName(pd))
|
||||
}
|
||||
}
|
@ -1,428 +0,0 @@
|
||||
package plugindef
|
||||
|
||||
import (
|
||||
"regexp"
|
||||
"strings"
|
||||
|
||||
"github.com/grafana/thema"
|
||||
)
|
||||
|
||||
thema.#Lineage
|
||||
name: "plugindef"
|
||||
schemas: [{
|
||||
version: [0, 0]
|
||||
schema: {
|
||||
// Unique name of the plugin. If the plugin is published on
|
||||
// grafana.com, then the plugin `id` has to follow the naming
|
||||
// conventions.
|
||||
id: string & strings.MinRunes(1)
|
||||
id: =~"^([0-9a-z]+\\-([0-9a-z]+\\-)?(\(strings.Join([ for t in _types {t}], "|"))))|(alertGroups|alertlist|annolist|barchart|bargauge|candlestick|canvas|dashlist|debug|datagrid|gauge|geomap|gettingstarted|graph|heatmap|histogram|icon|live|logs|news|nodeGraph|piechart|pluginlist|stat|state-timeline|status-history|table|table-old|text|timeseries|trend|traces|welcome|xychart|alertmanager|cloudwatch|dashboard|elasticsearch|grafana|grafana-azure-monitor-datasource|stackdriver|graphite|influxdb|jaeger|loki|mixed|mssql|mysql|opentsdb|postgres|prometheus|stackdriver|tempo|grafana-testdata-datasource|zipkin|phlare|parca)$"
|
||||
|
||||
// An alias is useful when migrating from one plugin id to another (rebranding etc)
|
||||
// This should be used sparingly, and is currently only supported though a hardcoded checklist
|
||||
aliasIDs?: [...string]
|
||||
|
||||
// Human-readable name of the plugin that is shown to the user in
|
||||
// the UI.
|
||||
name: string
|
||||
|
||||
// The set of all plugin types. This hidden field exists solely
|
||||
// so that the set can be string-interpolated into other fields.
|
||||
_types: ["app", "datasource", "panel", "renderer", "secretsmanager"]
|
||||
|
||||
// type indicates which type of Grafana plugin this is, of the defined
|
||||
// set of Grafana plugin types.
|
||||
type: or(_types)
|
||||
|
||||
// IncludeType is a string identifier of a plugin include type, which is
|
||||
// a superset of plugin types.
|
||||
#IncludeType: type | "dashboard" | "page"
|
||||
|
||||
// Metadata about the plugin
|
||||
info: #Info
|
||||
|
||||
// Metadata about a Grafana plugin. Some fields are used on the plugins
|
||||
// page in Grafana and others on grafana.com, if the plugin is published.
|
||||
#Info: {
|
||||
// Information about the plugin author
|
||||
author?: {
|
||||
// Author's name
|
||||
name?: string
|
||||
|
||||
// Author's name
|
||||
email?: string
|
||||
|
||||
// Link to author's website
|
||||
url?: string
|
||||
}
|
||||
|
||||
// Build information
|
||||
build?: #BuildInfo
|
||||
|
||||
// Description of plugin. Used on the plugins page in Grafana and
|
||||
// for search on grafana.com.
|
||||
description?: string
|
||||
|
||||
// Array of plugin keywords. Used for search on grafana.com.
|
||||
keywords: [...string]
|
||||
// should be this, but CUE to openapi converter screws this up
|
||||
// by inserting a non-concrete default.
|
||||
// keywords: [string, ...string]
|
||||
|
||||
// An array of link objects to be displayed on this plugin's
|
||||
// project page in the form `{name: 'foo', url:
|
||||
// 'http://example.com'}`
|
||||
links?: [...{
|
||||
name?: string
|
||||
url?: string
|
||||
}]
|
||||
|
||||
// SVG images that are used as plugin icons
|
||||
logos?: {
|
||||
// Link to the "small" version of the plugin logo, which must be
|
||||
// an SVG image. "Large" and "small" logos can be the same image.
|
||||
small: string
|
||||
|
||||
// Link to the "large" version of the plugin logo, which must be
|
||||
// an SVG image. "Large" and "small" logos can be the same image.
|
||||
large: string
|
||||
}
|
||||
|
||||
// An array of screenshot objects in the form `{name: 'bar', path:
|
||||
// 'img/screenshot.png'}`
|
||||
screenshots?: [...{
|
||||
name?: string
|
||||
path?: string
|
||||
}]
|
||||
|
||||
// Date when this plugin was built
|
||||
updated?: =~"^(\\d{4}-\\d{2}-\\d{2}|\\%TODAY\\%)$"
|
||||
|
||||
// Project version of this commit, e.g. `6.7.x`
|
||||
version?: =~"^(0|[1-9]\\d*)\\.(0|[1-9]\\d*)\\.(0|[1-9]\\d*)|(\\%VERSION\\%)$"
|
||||
}
|
||||
|
||||
#BuildInfo: {
|
||||
// Time when the plugin was built, as a Unix timestamp
|
||||
time?: int64
|
||||
repo?: string
|
||||
|
||||
// Git branch the plugin was built from
|
||||
branch?: string
|
||||
|
||||
// Git hash of the commit the plugin was built from
|
||||
hash?: string
|
||||
number?: int64
|
||||
|
||||
// GitHub pull request the plugin was built from
|
||||
pr?: int32
|
||||
}
|
||||
|
||||
// Dependency information related to Grafana and other plugins
|
||||
dependencies: #Dependencies
|
||||
|
||||
#Dependencies: {
|
||||
// (Deprecated) Required Grafana version for this plugin, e.g.
|
||||
// `6.x.x 7.x.x` to denote plugin requires Grafana v6.x.x or
|
||||
// v7.x.x.
|
||||
grafanaVersion?: =~"^([0-9]+)(\\.[0-9x]+)?(\\.[0-9x])?$"
|
||||
|
||||
// Required Grafana version for this plugin. Validated using
|
||||
// https://github.com/npm/node-semver.
|
||||
grafanaDependency?: =~"^(<=|>=|<|>|=|~|\\^)?([0-9]+)(\\.[0-9x\\*]+)(\\.[0-9x\\*]+)?(\\s(<=|>=|<|=>)?([0-9]+)(\\.[0-9x]+)(\\.[0-9x]+))?(\\-[0-9]+)?$"
|
||||
|
||||
// An array of required plugins on which this plugin depends
|
||||
plugins?: [...#Dependency]
|
||||
}
|
||||
|
||||
// Dependency describes another plugin on which a plugin depends.
|
||||
// The id refers to the plugin package identifier, as given on
|
||||
// the grafana.com plugin marketplace.
|
||||
#Dependency: {
|
||||
id: =~"^[0-9a-z]+\\-([0-9a-z]+\\-)?(app|panel|datasource)$"
|
||||
type: "app" | "datasource" | "panel"
|
||||
name: string
|
||||
version: string
|
||||
...
|
||||
}
|
||||
|
||||
// Schema definition for the plugin.json file. Used primarily for schema validation.
|
||||
$schema?: string
|
||||
|
||||
// For data source plugins, if the plugin supports alerting. Requires `backend` to be set to `true`.
|
||||
alerting?: bool
|
||||
|
||||
// For data source plugins, if the plugin supports annotation
|
||||
// queries.
|
||||
annotations?: bool
|
||||
|
||||
// Set to true for app plugins that should be enabled and pinned to the navigation bar in all orgs.
|
||||
autoEnabled?: bool
|
||||
|
||||
// If the plugin has a backend component.
|
||||
backend?: bool
|
||||
|
||||
// [internal only] Indicates whether the plugin is developed and shipped as part
|
||||
// of Grafana. Also known as a 'core plugin'.
|
||||
builtIn: bool | *false
|
||||
|
||||
// Plugin category used on the Add data source page.
|
||||
category?: "tsdb" | "logging" | "cloud" | "tracing" | "profiling" | "sql" | "enterprise" | "iot" | "other"
|
||||
|
||||
// Grafana Enterprise specific features.
|
||||
enterpriseFeatures?: {
|
||||
// Enable/Disable health diagnostics errors. Requires Grafana
|
||||
// >=7.5.5.
|
||||
healthDiagnosticsErrors?: bool | *false
|
||||
...
|
||||
}
|
||||
|
||||
// The first part of the file name of the backend component
|
||||
// executable. There can be multiple executables built for
|
||||
// different operating system and architecture. Grafana will
|
||||
// check for executables named `<executable>_<$GOOS>_<lower case
|
||||
// $GOARCH><.exe for Windows>`, e.g. `plugin_linux_amd64`.
|
||||
// Combination of $GOOS and $GOARCH can be found here:
|
||||
// https://golang.org/doc/install/source#environment.
|
||||
executable?: string
|
||||
|
||||
// [internal only] Excludes the plugin from listings in Grafana's UI. Only
|
||||
// allowed for `builtIn` plugins.
|
||||
hideFromList: bool | *false
|
||||
|
||||
// Resources to include in plugin.
|
||||
includes?: [...#Include]
|
||||
|
||||
// A resource to be included in a plugin.
|
||||
#Include: {
|
||||
// Unique identifier of the included resource
|
||||
uid?: string
|
||||
type: #IncludeType
|
||||
name?: string
|
||||
|
||||
// (Legacy) The Angular component to use for a page.
|
||||
component?: string
|
||||
|
||||
// The minimum role a user must have to see this page in the navigation menu.
|
||||
role?: "Admin" | "Editor" | "Viewer"
|
||||
|
||||
// RBAC action the user must have to access the route
|
||||
action?: string
|
||||
|
||||
// Used for app plugins.
|
||||
path?: string
|
||||
|
||||
// Add the include to the navigation menu.
|
||||
addToNav?: bool
|
||||
|
||||
// Page or dashboard when user clicks the icon in the side menu.
|
||||
defaultNav?: bool
|
||||
|
||||
// Icon to use in the side menu. For information on available
|
||||
// icon, refer to [Icons
|
||||
// Overview](https://developers.grafana.com/ui/latest/index.html?path=/story/docs-overview-icon--icons-overview).
|
||||
icon?: string
|
||||
...
|
||||
}
|
||||
|
||||
// For data source plugins, if the plugin supports logs. It may be used to filter logs only features.
|
||||
logs?: bool
|
||||
|
||||
// For data source plugins, if the plugin supports metric queries.
|
||||
// Used to enable the plugin in the panel editor.
|
||||
metrics?: bool
|
||||
|
||||
// FIXME there appears to be a bug in thema that prevents this from working. Maybe it'd
|
||||
// help to refer to it with an alias, but thema can't support using current list syntax.
|
||||
// syntax (fixed by grafana/thema#82). Either way, for now, pascalName gets populated in Go.
|
||||
let sani = (strings.ToTitle(regexp.ReplaceAllLiteral("[^a-zA-Z]+", name, "")))
|
||||
|
||||
// [internal only] The PascalCase name for the plugin. Used for creating machine-friendly
|
||||
// identifiers, typically in code generation.
|
||||
//
|
||||
// If not provided, defaults to name, but title-cased and sanitized (only
|
||||
// alphabetical characters allowed).
|
||||
pascalName: string & =~"^([A-Z][a-zA-Z]{1,62})$" | *sani
|
||||
|
||||
// Initialize plugin on startup. By default, the plugin
|
||||
// initializes on first use.
|
||||
preload?: bool
|
||||
|
||||
// For data source plugins. There is a query options section in
|
||||
// the plugin's query editor and these options can be turned on
|
||||
// if needed.
|
||||
queryOptions?: {
|
||||
// For data source plugins. If the `max data points` option should
|
||||
// be shown in the query options section in the query editor.
|
||||
maxDataPoints?: bool
|
||||
|
||||
// For data source plugins. If the `min interval` option should be
|
||||
// shown in the query options section in the query editor.
|
||||
minInterval?: bool
|
||||
|
||||
// For data source plugins. If the `cache timeout` option should
|
||||
// be shown in the query options section in the query editor.
|
||||
cacheTimeout?: bool
|
||||
}
|
||||
|
||||
// Routes is a list of proxy routes, if any. For datasource plugins only.
|
||||
routes?: [...#Route]
|
||||
|
||||
// For panel plugins. Hides the query editor.
|
||||
skipDataQuery?: bool
|
||||
|
||||
// Marks a plugin as a pre-release.
|
||||
state?: #ReleaseState
|
||||
|
||||
// ReleaseState indicates release maturity state of a plugin.
|
||||
#ReleaseState: "alpha" | "beta" | "deprecated" | *"stable"
|
||||
|
||||
// For data source plugins, if the plugin supports streaming. Used in Explore to start live streaming.
|
||||
streaming?: bool
|
||||
|
||||
// For data source plugins, if the plugin supports tracing. Used for example to link logs (e.g. Loki logs) with tracing plugins.
|
||||
tracing?: bool
|
||||
|
||||
// Optional list of RBAC RoleRegistrations.
|
||||
// Describes and organizes the default permissions associated with any of the Grafana basic roles,
|
||||
// which characterizes what viewers, editors, admins, or grafana admins can do on the plugin.
|
||||
// The Admin basic role inherits its default permissions from the Editor basic role which in turn
|
||||
// inherits them from the Viewer basic role.
|
||||
roles?: [...#RoleRegistration]
|
||||
|
||||
// RoleRegistration describes an RBAC role and its assignments to basic roles.
|
||||
// It organizes related RBAC permissions on the plugin into a role and defines which basic roles
|
||||
// will get them by default.
|
||||
// Example: the role 'Schedules Reader' bundles permissions to view all schedules of the plugin
|
||||
// which will be granted to Admins by default.
|
||||
#RoleRegistration: {
|
||||
// RBAC role definition to bundle related RBAC permissions on the plugin.
|
||||
role: #Role
|
||||
|
||||
// Default assignment of the role to Grafana basic roles (Viewer, Editor, Admin, Grafana Admin)
|
||||
// The Admin basic role inherits its default permissions from the Editor basic role which in turn
|
||||
// inherits them from the Viewer basic role.
|
||||
grants: [...#BasicRole]
|
||||
}
|
||||
|
||||
// Role describes an RBAC role which allows grouping multiple related permissions on the plugin,
|
||||
// each of which has an action and an optional scope.
|
||||
// Example: the role 'Schedules Reader' bundles permissions to view all schedules of the plugin.
|
||||
#Role: {
|
||||
name: string
|
||||
name: =~"^([A-Z][0-9A-Za-z ]+)$"
|
||||
description: string
|
||||
permissions: [...#Permission]
|
||||
}
|
||||
|
||||
// Permission describes an RBAC permission on the plugin. A permission has an action and an optional
|
||||
// scope.
|
||||
// Example: action: 'test-app.schedules:read', scope: 'test-app.schedules:*'
|
||||
#Permission: {
|
||||
action: string
|
||||
scope?: string
|
||||
}
|
||||
|
||||
// BasicRole is a Grafana basic role, which can be 'Viewer', 'Editor', 'Admin' or 'Grafana Admin'.
|
||||
// With RBAC, the Admin basic role inherits its default permissions from the Editor basic role which
|
||||
// in turn inherits them from the Viewer basic role.
|
||||
#BasicRole: "Grafana Admin" | "Admin" | "Editor" | "Viewer"
|
||||
|
||||
// Header describes an HTTP header that is forwarded with a proxied request for
|
||||
// a plugin route.
|
||||
#Header: {
|
||||
name: string
|
||||
content: string
|
||||
}
|
||||
|
||||
// URLParam describes query string parameters for
|
||||
// a url in a plugin route
|
||||
#URLParam: {
|
||||
name: string
|
||||
content: string
|
||||
}
|
||||
|
||||
// A proxy route used in datasource plugins for plugin authentication
|
||||
// and adding headers to HTTP requests made by the plugin.
|
||||
// For more information, refer to [Authentication for data source
|
||||
// plugins](https://grafana.com/developers/plugin-tools/create-a-plugin/extend-a-plugin/add-authentication-for-data-source-plugins).
|
||||
#Route: {
|
||||
// For data source plugins. The route path that is replaced by the
|
||||
// route URL field when proxying the call.
|
||||
path?: string
|
||||
|
||||
// For data source plugins. Route method matches the HTTP verb
|
||||
// like GET or POST. Multiple methods can be provided as a
|
||||
// comma-separated list.
|
||||
method?: string
|
||||
|
||||
// For data source plugins. Route URL is where the request is
|
||||
// proxied to.
|
||||
url?: string
|
||||
|
||||
urlParams?: [...#URLParam]
|
||||
reqSignedIn?: bool
|
||||
reqRole?: string
|
||||
|
||||
// RBAC action the user must have to access the route. i.e. plugin-id.projects:read
|
||||
reqAction?: string
|
||||
|
||||
// For data source plugins. Route headers adds HTTP headers to the
|
||||
// proxied request.
|
||||
headers?: [...#Header]
|
||||
|
||||
// For data source plugins. Route headers set the body content and
|
||||
// length to the proxied request.
|
||||
body?: {
|
||||
...
|
||||
}
|
||||
|
||||
// For data source plugins. Token authentication section used with
|
||||
// an OAuth API.
|
||||
tokenAuth?: #TokenAuth
|
||||
|
||||
// For data source plugins. Token authentication section used with
|
||||
// an JWT OAuth API.
|
||||
jwtTokenAuth?: #JWTTokenAuth
|
||||
}
|
||||
|
||||
// TODO docs
|
||||
#TokenAuth: {
|
||||
// URL to fetch the authentication token.
|
||||
url?: string
|
||||
|
||||
// The list of scopes that your application should be granted
|
||||
// access to.
|
||||
scopes?: [...string]
|
||||
|
||||
// Parameters for the token authentication request.
|
||||
params: [string]: string
|
||||
}
|
||||
|
||||
// TODO docs
|
||||
// TODO should this really be separate from TokenAuth?
|
||||
#JWTTokenAuth: {
|
||||
// URL to fetch the JWT token.
|
||||
url: string
|
||||
|
||||
// The list of scopes that your application should be granted
|
||||
// access to.
|
||||
scopes: [...string]
|
||||
|
||||
// Parameters for the JWT token authentication request.
|
||||
params: [string]: string
|
||||
}
|
||||
|
||||
// Identity and Access Management information.
|
||||
// Allows the plugin to define the permissions it requires to have on Grafana.
|
||||
iam: #IAM
|
||||
|
||||
// IAM allows the plugin to get a service account with tailored permissions and a token
|
||||
// (or to use the client_credentials grant if the token provider is the OAuth2 Server)
|
||||
#IAM: {
|
||||
// Permissions are the permissions that the external service needs its associated service account to have.
|
||||
permissions?: [...#Permission]
|
||||
}
|
||||
}
|
||||
}]
|
||||
lenses: []
|
@ -1,73 +0,0 @@
|
||||
package plugindef
|
||||
|
||||
import (
|
||||
"strings"
|
||||
"sync"
|
||||
|
||||
"cuelang.org/go/cue/build"
|
||||
"github.com/grafana/grafana/pkg/cuectx"
|
||||
"github.com/grafana/thema"
|
||||
)
|
||||
|
||||
//go:generate go run gen.go
|
||||
|
||||
func loadInstanceForplugindef() (*build.Instance, error) {
|
||||
return cuectx.LoadGrafanaInstance("pkg/plugins/plugindef", "", nil)
|
||||
}
|
||||
|
||||
var linonce sync.Once
|
||||
var pdlin thema.ConvergentLineage[*PluginDef]
|
||||
var pdlinerr error
|
||||
|
||||
// Lineage returns the [thema.ConvergentLineage] for plugindef, the canonical
|
||||
// specification for Grafana plugin.json files.
|
||||
//
|
||||
// Unless a custom thema.Runtime is specifically needed, prefer calling this with
|
||||
// nil, as a cached lineage will be returned.
|
||||
func Lineage(rt *thema.Runtime, opts ...thema.BindOption) (thema.ConvergentLineage[*PluginDef], error) {
|
||||
if len(opts) == 0 && (rt == nil || rt == cuectx.GrafanaThemaRuntime()) {
|
||||
linonce.Do(func() {
|
||||
pdlin, pdlinerr = doLineage(rt)
|
||||
})
|
||||
return pdlin, pdlinerr
|
||||
}
|
||||
return doLineage(rt, opts...)
|
||||
}
|
||||
|
||||
// DerivePascalName derives a PascalCase name from a PluginDef.
|
||||
//
|
||||
// This function does not mutate the input PluginDef; as such, it ignores
|
||||
// whether there exists any value for PluginDef.PascalName.
|
||||
//
|
||||
// FIXME this should be removable once CUE logic for it works/unmarshals correctly.
|
||||
func DerivePascalName(pd PluginDef) string {
|
||||
sani := func(s string) string {
|
||||
ret := strings.Title(strings.Map(func(r rune) rune {
|
||||
switch {
|
||||
case r >= 'a' && r <= 'z':
|
||||
return r
|
||||
case r >= 'A' && r <= 'Z':
|
||||
return r
|
||||
default:
|
||||
return -1
|
||||
}
|
||||
}, strings.Title(strings.Map(func(r rune) rune {
|
||||
switch r {
|
||||
case '-', '_':
|
||||
return ' '
|
||||
default:
|
||||
return r
|
||||
}
|
||||
}, s))))
|
||||
if len(ret) > 63 {
|
||||
return ret[:63]
|
||||
}
|
||||
return ret
|
||||
}
|
||||
|
||||
fromname := sani(pd.Name)
|
||||
if len(fromname) != 0 {
|
||||
return fromname
|
||||
}
|
||||
return sani(strings.Split(pd.Id, "-")[1])
|
||||
}
|
@ -1,84 +0,0 @@
|
||||
// Code generated - EDITING IS FUTILE. DO NOT EDIT.
|
||||
//
|
||||
// Generated by:
|
||||
// pkg/plugins/plugindef/gen.go
|
||||
// Using jennies:
|
||||
// PluginGoBindings
|
||||
//
|
||||
// Run 'make gen-cue' from repository root to regenerate.
|
||||
|
||||
package plugindef
|
||||
|
||||
import (
|
||||
"cuelang.org/go/cue/build"
|
||||
"github.com/grafana/thema"
|
||||
)
|
||||
|
||||
// doLineage returns a [thema.ConvergentLineage] for the 'plugindef' Thema lineage.
|
||||
//
|
||||
// The lineage is the canonical specification of plugindef. It contains all
|
||||
// schema versions that have ever existed for plugindef, and the lenses that
|
||||
// allow valid instances of one schema in the lineage to be translated to
|
||||
// another schema in the lineage.
|
||||
//
|
||||
// As a [thema.ConvergentLineage], the returned lineage has one primary schema, 0.0,
|
||||
// which is [thema.AssignableTo] [*PluginDef], the lineage's parameterized type.
|
||||
//
|
||||
// This function will return an error if the [Thema invariants] are not met by
|
||||
// the underlying lineage declaration in CUE, or if [*PluginDef] is not
|
||||
// [thema.AssignableTo] the 0.0 schema.
|
||||
//
|
||||
// [Thema's general invariants]: https://github.com/grafana/thema/blob/main/docs/invariants.md
|
||||
func doLineage(rt *thema.Runtime, opts ...thema.BindOption) (thema.ConvergentLineage[*PluginDef], error) {
|
||||
lin, err := baseLineage(rt, opts...)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
sch := thema.SchemaP(lin, thema.SV(0, 0))
|
||||
typ := new(PluginDef)
|
||||
tsch, err := thema.BindType(sch, typ)
|
||||
if err != nil {
|
||||
// This will error out if the 0.0 schema isn't assignable to
|
||||
// *PluginDef. If Thema also generates that type, this should be unreachable,
|
||||
// barring a critical bug in Thema's Go generator.
|
||||
return nil, err
|
||||
}
|
||||
return tsch.ConvergentLineage(), nil
|
||||
}
|
||||
func baseLineage(rt *thema.Runtime, opts ...thema.BindOption) (thema.Lineage, error) {
|
||||
// First, we must get the bytes of the .cue file(s) in which the "plugindef" lineage
|
||||
// is declared, and load them into a
|
||||
// "cuelang.org/go/cue/build".Instance.
|
||||
//
|
||||
// For most Thema-based development workflows, these bytes should come from an embed.FS.
|
||||
// This ensures Go is always compiled with the current state of the .cue files.
|
||||
var inst *build.Instance
|
||||
var err error
|
||||
|
||||
// loadInstanceForplugindef must be manually implemented in another file in this
|
||||
// Go package.
|
||||
inst, err = loadInstanceForplugindef()
|
||||
if err != nil {
|
||||
// Errors at this point indicate a problem with basic loading of .cue file bytes,
|
||||
// which typically means the code generator was misconfigured and a path input
|
||||
// is incorrect.
|
||||
return nil, err
|
||||
}
|
||||
|
||||
raw := rt.Context().BuildInstance(inst)
|
||||
|
||||
// An error returned from thema.BindLineage indicates one of the following:
|
||||
// - The parsed path does not exist in the loaded CUE file (["github.com/grafana/thema/errors".ErrValueNotExist])
|
||||
// - The value at the parsed path exists, but does not appear to be a Thema
|
||||
// lineage (["github.com/grafana/thema/errors".ErrValueNotALineage])
|
||||
// - The value at the parsed path exists and is a lineage (["github.com/grafana/thema/errors".ErrInvalidLineage]),
|
||||
// but is invalid due to the violation of some general Thema invariant -
|
||||
// for example, declared schemas don't follow backwards compatibility rules,
|
||||
// lenses are incomplete.
|
||||
return thema.BindLineage(raw, rt, opts...)
|
||||
}
|
||||
|
||||
// type guards
|
||||
var _ thema.ConvergentLineageFactory[*PluginDef] = doLineage
|
||||
var _ thema.LineageFactory = baseLineage
|
@ -1,484 +0,0 @@
|
||||
// Code generated - EDITING IS FUTILE. DO NOT EDIT.
|
||||
//
|
||||
// Generated by:
|
||||
// pkg/plugins/plugindef/gen.go
|
||||
// Using jennies:
|
||||
// GoTypesJenny
|
||||
//
|
||||
// Run 'make gen-cue' from repository root to regenerate.
|
||||
|
||||
package plugindef
|
||||
|
||||
// Defines values for BasicRole.
|
||||
const (
|
||||
BasicRoleAdmin BasicRole = "Admin"
|
||||
BasicRoleEditor BasicRole = "Editor"
|
||||
BasicRoleGrafanaAdmin BasicRole = "Grafana Admin"
|
||||
BasicRoleViewer BasicRole = "Viewer"
|
||||
)
|
||||
|
||||
// Defines values for DependencyType.
|
||||
const (
|
||||
DependencyTypeApp DependencyType = "app"
|
||||
DependencyTypeDatasource DependencyType = "datasource"
|
||||
DependencyTypePanel DependencyType = "panel"
|
||||
)
|
||||
|
||||
// Defines values for IncludeRole.
|
||||
const (
|
||||
IncludeRoleAdmin IncludeRole = "Admin"
|
||||
IncludeRoleEditor IncludeRole = "Editor"
|
||||
IncludeRoleViewer IncludeRole = "Viewer"
|
||||
)
|
||||
|
||||
// Defines values for IncludeType.
|
||||
const (
|
||||
IncludeTypeApp IncludeType = "app"
|
||||
IncludeTypeDashboard IncludeType = "dashboard"
|
||||
IncludeTypeDatasource IncludeType = "datasource"
|
||||
IncludeTypePage IncludeType = "page"
|
||||
IncludeTypePanel IncludeType = "panel"
|
||||
IncludeTypeRenderer IncludeType = "renderer"
|
||||
IncludeTypeSecretsmanager IncludeType = "secretsmanager"
|
||||
)
|
||||
|
||||
// Defines values for Category.
|
||||
const (
|
||||
CategoryCloud Category = "cloud"
|
||||
CategoryEnterprise Category = "enterprise"
|
||||
CategoryIot Category = "iot"
|
||||
CategoryLogging Category = "logging"
|
||||
CategoryOther Category = "other"
|
||||
CategoryProfiling Category = "profiling"
|
||||
CategorySql Category = "sql"
|
||||
CategoryTracing Category = "tracing"
|
||||
CategoryTsdb Category = "tsdb"
|
||||
)
|
||||
|
||||
// Defines values for Type.
|
||||
const (
|
||||
TypeApp Type = "app"
|
||||
TypeDatasource Type = "datasource"
|
||||
TypePanel Type = "panel"
|
||||
TypeRenderer Type = "renderer"
|
||||
TypeSecretsmanager Type = "secretsmanager"
|
||||
)
|
||||
|
||||
// Defines values for ReleaseState.
|
||||
const (
|
||||
ReleaseStateAlpha ReleaseState = "alpha"
|
||||
ReleaseStateBeta ReleaseState = "beta"
|
||||
ReleaseStateDeprecated ReleaseState = "deprecated"
|
||||
ReleaseStateStable ReleaseState = "stable"
|
||||
)
|
||||
|
||||
// BasicRole is a Grafana basic role, which can be 'Viewer', 'Editor', 'Admin' or 'Grafana Admin'.
|
||||
// With RBAC, the Admin basic role inherits its default permissions from the Editor basic role which
|
||||
// in turn inherits them from the Viewer basic role.
|
||||
type BasicRole string
|
||||
|
||||
// BuildInfo defines model for BuildInfo.
|
||||
type BuildInfo struct {
|
||||
// Git branch the plugin was built from
|
||||
Branch *string `json:"branch,omitempty"`
|
||||
|
||||
// Git hash of the commit the plugin was built from
|
||||
Hash *string `json:"hash,omitempty"`
|
||||
Number *int64 `json:"number,omitempty"`
|
||||
|
||||
// GitHub pull request the plugin was built from
|
||||
Pr *int32 `json:"pr,omitempty"`
|
||||
Repo *string `json:"repo,omitempty"`
|
||||
|
||||
// Time when the plugin was built, as a Unix timestamp
|
||||
Time *int64 `json:"time,omitempty"`
|
||||
}
|
||||
|
||||
// Dependencies defines model for Dependencies.
|
||||
type Dependencies struct {
|
||||
// Required Grafana version for this plugin. Validated using
|
||||
// https://github.com/npm/node-semver.
|
||||
GrafanaDependency *string `json:"grafanaDependency,omitempty"`
|
||||
|
||||
// (Deprecated) Required Grafana version for this plugin, e.g.
|
||||
// `6.x.x 7.x.x` to denote plugin requires Grafana v6.x.x or
|
||||
// v7.x.x.
|
||||
GrafanaVersion *string `json:"grafanaVersion,omitempty"`
|
||||
|
||||
// An array of required plugins on which this plugin depends
|
||||
Plugins []Dependency `json:"plugins,omitempty"`
|
||||
}
|
||||
|
||||
// Dependency describes another plugin on which a plugin depends.
|
||||
// The id refers to the plugin package identifier, as given on
|
||||
// the grafana.com plugin marketplace.
|
||||
type Dependency struct {
|
||||
Id string `json:"id"`
|
||||
Name string `json:"name"`
|
||||
Type DependencyType `json:"type"`
|
||||
Version string `json:"version"`
|
||||
}
|
||||
|
||||
// DependencyType defines model for Dependency.Type.
|
||||
type DependencyType string
|
||||
|
||||
// Header describes an HTTP header that is forwarded with a proxied request for
|
||||
// a plugin route.
|
||||
type Header struct {
|
||||
Content string `json:"content"`
|
||||
Name string `json:"name"`
|
||||
}
|
||||
|
||||
// IAM allows the plugin to get a service account with tailored permissions and a token
|
||||
// (or to use the client_credentials grant if the token provider is the OAuth2 Server)
|
||||
type IAM struct {
|
||||
// Permissions are the permissions that the external service needs its associated service account to have.
|
||||
Permissions []Permission `json:"permissions,omitempty"`
|
||||
}
|
||||
|
||||
// A resource to be included in a plugin.
|
||||
type Include struct {
|
||||
// RBAC action the user must have to access the route
|
||||
Action *string `json:"action,omitempty"`
|
||||
|
||||
// Add the include to the navigation menu.
|
||||
AddToNav *bool `json:"addToNav,omitempty"`
|
||||
|
||||
// (Legacy) The Angular component to use for a page.
|
||||
Component *string `json:"component,omitempty"`
|
||||
|
||||
// Page or dashboard when user clicks the icon in the side menu.
|
||||
DefaultNav *bool `json:"defaultNav,omitempty"`
|
||||
|
||||
// Icon to use in the side menu. For information on available
|
||||
// icon, refer to [Icons
|
||||
// Overview](https://developers.grafana.com/ui/latest/index.html?path=/story/docs-overview-icon--icons-overview).
|
||||
Icon *string `json:"icon,omitempty"`
|
||||
Name *string `json:"name,omitempty"`
|
||||
|
||||
// Used for app plugins.
|
||||
Path *string `json:"path,omitempty"`
|
||||
|
||||
// The minimum role a user must have to see this page in the navigation menu.
|
||||
Role *IncludeRole `json:"role,omitempty"`
|
||||
|
||||
// IncludeType is a string identifier of a plugin include type, which is
|
||||
// a superset of plugin types.
|
||||
Type IncludeType `json:"type"`
|
||||
|
||||
// Unique identifier of the included resource
|
||||
Uid *string `json:"uid,omitempty"`
|
||||
}
|
||||
|
||||
// The minimum role a user must have to see this page in the navigation menu.
|
||||
type IncludeRole string
|
||||
|
||||
// IncludeType is a string identifier of a plugin include type, which is
|
||||
// a superset of plugin types.
|
||||
type IncludeType string
|
||||
|
||||
// Metadata about a Grafana plugin. Some fields are used on the plugins
|
||||
// page in Grafana and others on grafana.com, if the plugin is published.
|
||||
type Info struct {
|
||||
// Information about the plugin author
|
||||
Author *struct {
|
||||
// Author's name
|
||||
Email *string `json:"email,omitempty"`
|
||||
|
||||
// Author's name
|
||||
Name *string `json:"name,omitempty"`
|
||||
|
||||
// Link to author's website
|
||||
Url *string `json:"url,omitempty"`
|
||||
} `json:"author,omitempty"`
|
||||
Build *BuildInfo `json:"build,omitempty"`
|
||||
|
||||
// Description of plugin. Used on the plugins page in Grafana and
|
||||
// for search on grafana.com.
|
||||
Description *string `json:"description,omitempty"`
|
||||
|
||||
// Array of plugin keywords. Used for search on grafana.com.
|
||||
Keywords []string `json:"keywords"`
|
||||
|
||||
// An array of link objects to be displayed on this plugin's
|
||||
// project page in the form `{name: 'foo', url:
|
||||
// 'http://example.com'}`
|
||||
Links []struct {
|
||||
Name *string `json:"name,omitempty"`
|
||||
Url *string `json:"url,omitempty"`
|
||||
} `json:"links,omitempty"`
|
||||
|
||||
// SVG images that are used as plugin icons
|
||||
Logos *struct {
|
||||
// Link to the "large" version of the plugin logo, which must be
|
||||
// an SVG image. "Large" and "small" logos can be the same image.
|
||||
Large string `json:"large"`
|
||||
|
||||
// Link to the "small" version of the plugin logo, which must be
|
||||
// an SVG image. "Large" and "small" logos can be the same image.
|
||||
Small string `json:"small"`
|
||||
} `json:"logos,omitempty"`
|
||||
|
||||
// An array of screenshot objects in the form `{name: 'bar', path:
|
||||
// 'img/screenshot.png'}`
|
||||
Screenshots []struct {
|
||||
Name *string `json:"name,omitempty"`
|
||||
Path *string `json:"path,omitempty"`
|
||||
} `json:"screenshots,omitempty"`
|
||||
|
||||
// Date when this plugin was built
|
||||
Updated *string `json:"updated,omitempty"`
|
||||
|
||||
// Project version of this commit, e.g. `6.7.x`
|
||||
Version *string `json:"version,omitempty"`
|
||||
}
|
||||
|
||||
// TODO docs
|
||||
// TODO should this really be separate from TokenAuth?
|
||||
type JWTTokenAuth struct {
|
||||
// Parameters for the JWT token authentication request.
|
||||
Params map[string]string `json:"params"`
|
||||
|
||||
// The list of scopes that your application should be granted
|
||||
// access to.
|
||||
Scopes []string `json:"scopes"`
|
||||
|
||||
// URL to fetch the JWT token.
|
||||
Url string `json:"url"`
|
||||
}
|
||||
|
||||
// Permission describes an RBAC permission on the plugin. A permission has an action and an optional
|
||||
// scope.
|
||||
// Example: action: 'test-app.schedules:read', scope: 'test-app.schedules:*'
|
||||
type Permission struct {
|
||||
Action string `json:"action"`
|
||||
Scope *string `json:"scope,omitempty"`
|
||||
}
|
||||
|
||||
// PluginDef defines model for PluginDef.
|
||||
type PluginDef struct {
|
||||
// Schema definition for the plugin.json file. Used primarily for schema validation.
|
||||
Schema *string `json:"$schema,omitempty"`
|
||||
|
||||
// For data source plugins, if the plugin supports alerting. Requires `backend` to be set to `true`.
|
||||
Alerting *bool `json:"alerting,omitempty"`
|
||||
|
||||
// An alias is useful when migrating from one plugin id to another (rebranding etc)
|
||||
// This should be used sparingly, and is currently only supported though a hardcoded checklist
|
||||
AliasIDs []string `json:"aliasIDs,omitempty"`
|
||||
|
||||
// For data source plugins, if the plugin supports annotation
|
||||
// queries.
|
||||
Annotations *bool `json:"annotations,omitempty"`
|
||||
|
||||
// Set to true for app plugins that should be enabled and pinned to the navigation bar in all orgs.
|
||||
AutoEnabled *bool `json:"autoEnabled,omitempty"`
|
||||
|
||||
// If the plugin has a backend component.
|
||||
Backend *bool `json:"backend,omitempty"`
|
||||
|
||||
// [internal only] Indicates whether the plugin is developed and shipped as part
|
||||
// of Grafana. Also known as a 'core plugin'.
|
||||
BuiltIn bool `json:"builtIn"`
|
||||
|
||||
// Plugin category used on the Add data source page.
|
||||
Category *Category `json:"category,omitempty"`
|
||||
Dependencies Dependencies `json:"dependencies"`
|
||||
|
||||
// Grafana Enterprise specific features.
|
||||
EnterpriseFeatures *struct {
|
||||
// Enable/Disable health diagnostics errors. Requires Grafana
|
||||
// >=7.5.5.
|
||||
HealthDiagnosticsErrors *bool `json:"healthDiagnosticsErrors,omitempty"`
|
||||
} `json:"enterpriseFeatures,omitempty"`
|
||||
|
||||
// The first part of the file name of the backend component
|
||||
// executable. There can be multiple executables built for
|
||||
// different operating system and architecture. Grafana will
|
||||
// check for executables named `<executable>_<$GOOS>_<lower case
|
||||
// $GOARCH><.exe for Windows>`, e.g. `plugin_linux_amd64`.
|
||||
// Combination of $GOOS and $GOARCH can be found here:
|
||||
// https://golang.org/doc/install/source#environment.
|
||||
Executable *string `json:"executable,omitempty"`
|
||||
|
||||
// [internal only] Excludes the plugin from listings in Grafana's UI. Only
|
||||
// allowed for `builtIn` plugins.
|
||||
HideFromList bool `json:"hideFromList"`
|
||||
|
||||
// IAM allows the plugin to get a service account with tailored permissions and a token
|
||||
// (or to use the client_credentials grant if the token provider is the OAuth2 Server)
|
||||
Iam IAM `json:"iam"`
|
||||
|
||||
// Unique name of the plugin. If the plugin is published on
|
||||
// grafana.com, then the plugin `id` has to follow the naming
|
||||
// conventions.
|
||||
Id string `json:"id"`
|
||||
|
||||
// Resources to include in plugin.
|
||||
Includes []Include `json:"includes,omitempty"`
|
||||
|
||||
// Metadata about a Grafana plugin. Some fields are used on the plugins
|
||||
// page in Grafana and others on grafana.com, if the plugin is published.
|
||||
Info Info `json:"info"`
|
||||
|
||||
// For data source plugins, if the plugin supports logs. It may be used to filter logs only features.
|
||||
Logs *bool `json:"logs,omitempty"`
|
||||
|
||||
// For data source plugins, if the plugin supports metric queries.
|
||||
// Used to enable the plugin in the panel editor.
|
||||
Metrics *bool `json:"metrics,omitempty"`
|
||||
|
||||
// Human-readable name of the plugin that is shown to the user in
|
||||
// the UI.
|
||||
Name string `json:"name"`
|
||||
|
||||
// [internal only] The PascalCase name for the plugin. Used for creating machine-friendly
|
||||
// identifiers, typically in code generation.
|
||||
//
|
||||
// If not provided, defaults to name, but title-cased and sanitized (only
|
||||
// alphabetical characters allowed).
|
||||
PascalName string `json:"pascalName"`
|
||||
|
||||
// Initialize plugin on startup. By default, the plugin
|
||||
// initializes on first use.
|
||||
Preload *bool `json:"preload,omitempty"`
|
||||
|
||||
// For data source plugins. There is a query options section in
|
||||
// the plugin's query editor and these options can be turned on
|
||||
// if needed.
|
||||
QueryOptions *struct {
|
||||
// For data source plugins. If the `cache timeout` option should
|
||||
// be shown in the query options section in the query editor.
|
||||
CacheTimeout *bool `json:"cacheTimeout,omitempty"`
|
||||
|
||||
// For data source plugins. If the `max data points` option should
|
||||
// be shown in the query options section in the query editor.
|
||||
MaxDataPoints *bool `json:"maxDataPoints,omitempty"`
|
||||
|
||||
// For data source plugins. If the `min interval` option should be
|
||||
// shown in the query options section in the query editor.
|
||||
MinInterval *bool `json:"minInterval,omitempty"`
|
||||
} `json:"queryOptions,omitempty"`
|
||||
|
||||
// Optional list of RBAC RoleRegistrations.
|
||||
// Describes and organizes the default permissions associated with any of the Grafana basic roles,
|
||||
// which characterizes what viewers, editors, admins, or grafana admins can do on the plugin.
|
||||
// The Admin basic role inherits its default permissions from the Editor basic role which in turn
|
||||
// inherits them from the Viewer basic role.
|
||||
Roles []RoleRegistration `json:"roles,omitempty"`
|
||||
|
||||
// Routes is a list of proxy routes, if any. For datasource plugins only.
|
||||
Routes []Route `json:"routes,omitempty"`
|
||||
|
||||
// For panel plugins. Hides the query editor.
|
||||
SkipDataQuery *bool `json:"skipDataQuery,omitempty"`
|
||||
|
||||
// ReleaseState indicates release maturity state of a plugin.
|
||||
State *ReleaseState `json:"state,omitempty"`
|
||||
|
||||
// For data source plugins, if the plugin supports streaming. Used in Explore to start live streaming.
|
||||
Streaming *bool `json:"streaming,omitempty"`
|
||||
|
||||
// For data source plugins, if the plugin supports tracing. Used for example to link logs (e.g. Loki logs) with tracing plugins.
|
||||
Tracing *bool `json:"tracing,omitempty"`
|
||||
|
||||
// type indicates which type of Grafana plugin this is, of the defined
|
||||
// set of Grafana plugin types.
|
||||
Type Type `json:"type"`
|
||||
}
|
||||
|
||||
// Plugin category used on the Add data source page.
|
||||
type Category string
|
||||
|
||||
// Type type indicates which type of Grafana plugin this is, of the defined
|
||||
// set of Grafana plugin types.
|
||||
type Type string
|
||||
|
||||
// ReleaseState indicates release maturity state of a plugin.
|
||||
type ReleaseState string
|
||||
|
||||
// Role describes an RBAC role which allows grouping multiple related permissions on the plugin,
|
||||
// each of which has an action and an optional scope.
|
||||
// Example: the role 'Schedules Reader' bundles permissions to view all schedules of the plugin.
|
||||
type Role struct {
|
||||
Description string `json:"description"`
|
||||
Name string `json:"name"`
|
||||
Permissions []Permission `json:"permissions"`
|
||||
}
|
||||
|
||||
// RoleRegistration describes an RBAC role and its assignments to basic roles.
|
||||
// It organizes related RBAC permissions on the plugin into a role and defines which basic roles
|
||||
// will get them by default.
|
||||
// Example: the role 'Schedules Reader' bundles permissions to view all schedules of the plugin
|
||||
// which will be granted to Admins by default.
|
||||
type RoleRegistration struct {
|
||||
// Default assignment of the role to Grafana basic roles (Viewer, Editor, Admin, Grafana Admin)
|
||||
// The Admin basic role inherits its default permissions from the Editor basic role which in turn
|
||||
// inherits them from the Viewer basic role.
|
||||
Grants []BasicRole `json:"grants"`
|
||||
|
||||
// Role describes an RBAC role which allows grouping multiple related permissions on the plugin,
|
||||
// each of which has an action and an optional scope.
|
||||
// Example: the role 'Schedules Reader' bundles permissions to view all schedules of the plugin.
|
||||
Role Role `json:"role"`
|
||||
}
|
||||
|
||||
// A proxy route used in datasource plugins for plugin authentication
|
||||
// and adding headers to HTTP requests made by the plugin.
|
||||
// For more information, refer to [Authentication for data source
|
||||
// plugins](https://grafana.com/developers/plugin-tools/create-a-plugin/extend-a-plugin/add-authentication-for-data-source-plugins).
|
||||
type Route struct {
|
||||
// For data source plugins. Route headers set the body content and
|
||||
// length to the proxied request.
|
||||
Body map[string]any `json:"body,omitempty"`
|
||||
|
||||
// For data source plugins. Route headers adds HTTP headers to the
|
||||
// proxied request.
|
||||
Headers []Header `json:"headers,omitempty"`
|
||||
|
||||
// TODO docs
|
||||
// TODO should this really be separate from TokenAuth?
|
||||
JwtTokenAuth *JWTTokenAuth `json:"jwtTokenAuth,omitempty"`
|
||||
|
||||
// For data source plugins. Route method matches the HTTP verb
|
||||
// like GET or POST. Multiple methods can be provided as a
|
||||
// comma-separated list.
|
||||
Method *string `json:"method,omitempty"`
|
||||
|
||||
// For data source plugins. The route path that is replaced by the
|
||||
// route URL field when proxying the call.
|
||||
Path *string `json:"path,omitempty"`
|
||||
|
||||
// RBAC action the user must have to access the route. i.e. plugin-id.projects:read
|
||||
ReqAction *string `json:"reqAction,omitempty"`
|
||||
ReqRole *string `json:"reqRole,omitempty"`
|
||||
ReqSignedIn *bool `json:"reqSignedIn,omitempty"`
|
||||
|
||||
// TODO docs
|
||||
TokenAuth *TokenAuth `json:"tokenAuth,omitempty"`
|
||||
|
||||
// For data source plugins. Route URL is where the request is
|
||||
// proxied to.
|
||||
Url *string `json:"url,omitempty"`
|
||||
UrlParams []URLParam `json:"urlParams,omitempty"`
|
||||
}
|
||||
|
||||
// TODO docs
|
||||
type TokenAuth struct {
|
||||
// Parameters for the token authentication request.
|
||||
Params map[string]string `json:"params"`
|
||||
|
||||
// The list of scopes that your application should be granted
|
||||
// access to.
|
||||
Scopes []string `json:"scopes,omitempty"`
|
||||
|
||||
// URL to fetch the authentication token.
|
||||
Url *string `json:"url,omitempty"`
|
||||
}
|
||||
|
||||
// URLParam describes query string parameters for
|
||||
// a url in a plugin route
|
||||
type URLParam struct {
|
||||
Content string `json:"content"`
|
||||
Name string `json:"name"`
|
||||
}
|
@ -19,7 +19,7 @@ import (
|
||||
"github.com/grafana/grafana/pkg/plugins/backendplugin/pluginextensionv2"
|
||||
"github.com/grafana/grafana/pkg/plugins/backendplugin/secretsmanagerplugin"
|
||||
"github.com/grafana/grafana/pkg/plugins/log"
|
||||
"github.com/grafana/grafana/pkg/plugins/plugindef"
|
||||
"github.com/grafana/grafana/pkg/plugins/pfs"
|
||||
"github.com/grafana/grafana/pkg/services/org"
|
||||
"github.com/grafana/grafana/pkg/util"
|
||||
)
|
||||
@ -118,7 +118,7 @@ type JSONData struct {
|
||||
Executable string `json:"executable,omitempty"`
|
||||
|
||||
// App Service Auth Registration
|
||||
IAM *plugindef.IAM `json:"iam,omitempty"`
|
||||
IAM *pfs.IAM `json:"iam,omitempty"`
|
||||
}
|
||||
|
||||
func ReadPluginJSON(reader io.Reader) (JSONData, error) {
|
||||
|
@ -24,7 +24,7 @@ import (
|
||||
"github.com/grafana/grafana/pkg/plugins/manager/registry"
|
||||
"github.com/grafana/grafana/pkg/plugins/manager/signature"
|
||||
"github.com/grafana/grafana/pkg/plugins/manager/sources"
|
||||
"github.com/grafana/grafana/pkg/plugins/plugindef"
|
||||
"github.com/grafana/grafana/pkg/plugins/pfs"
|
||||
"github.com/grafana/grafana/pkg/plugins/pluginscdn"
|
||||
"github.com/grafana/grafana/pkg/services/featuremgmt"
|
||||
"github.com/grafana/grafana/pkg/services/org"
|
||||
@ -541,8 +541,8 @@ func TestLoader_Load_ExternalRegistration(t *testing.T) {
|
||||
GrafanaVersion: "*",
|
||||
Plugins: []plugins.Dependency{},
|
||||
},
|
||||
IAM: &plugindef.IAM{
|
||||
Permissions: []plugindef.Permission{
|
||||
IAM: &pfs.IAM{
|
||||
Permissions: []pfs.Permission{
|
||||
{
|
||||
Action: "read",
|
||||
Scope: stringPtr("datasource"),
|
||||
|
@ -14,7 +14,7 @@ import (
|
||||
"github.com/grafana/grafana/pkg/plugins/manager/pipeline/validation"
|
||||
"github.com/grafana/grafana/pkg/plugins/manager/registry"
|
||||
"github.com/grafana/grafana/pkg/plugins/manager/signature"
|
||||
"github.com/grafana/grafana/pkg/plugins/plugindef"
|
||||
"github.com/grafana/grafana/pkg/plugins/pfs"
|
||||
"github.com/grafana/grafana/pkg/services/featuremgmt"
|
||||
"github.com/grafana/grafana/pkg/services/pluginsintegration/pluginerrs"
|
||||
)
|
||||
@ -42,7 +42,7 @@ func newExternalServiceRegistration(cfg *config.PluginManagementCfg, serviceRegi
|
||||
// Register registers the external service with the external service registry, if the feature is enabled.
|
||||
func (r *ExternalServiceRegistration) Register(ctx context.Context, p *plugins.Plugin) (*plugins.Plugin, error) {
|
||||
if p.IAM != nil {
|
||||
s, err := r.externalServiceRegistry.RegisterExternalService(ctx, p.ID, plugindef.Type(p.Type), p.IAM)
|
||||
s, err := r.externalServiceRegistry.RegisterExternalService(ctx, p.ID, pfs.Type(p.Type), p.IAM)
|
||||
if err != nil {
|
||||
r.log.Error("Could not register an external service. Initialization skipped", "pluginId", p.ID, "error", err)
|
||||
return nil, err
|
||||
|
@ -17,7 +17,7 @@ import (
|
||||
"github.com/grafana/grafana/pkg/plugins/config"
|
||||
"github.com/grafana/grafana/pkg/plugins/envvars"
|
||||
"github.com/grafana/grafana/pkg/plugins/manager/fakes"
|
||||
"github.com/grafana/grafana/pkg/plugins/plugindef"
|
||||
"github.com/grafana/grafana/pkg/plugins/pfs"
|
||||
"github.com/grafana/grafana/pkg/services/featuremgmt"
|
||||
"github.com/grafana/grafana/pkg/setting"
|
||||
)
|
||||
@ -590,7 +590,7 @@ func TestPluginEnvVarsProvider_authEnvVars(t *testing.T) {
|
||||
p := &plugins.Plugin{
|
||||
JSONData: plugins.JSONData{
|
||||
ID: "test",
|
||||
IAM: &plugindef.IAM{},
|
||||
IAM: &pfs.IAM{},
|
||||
},
|
||||
ExternalService: &auth.ExternalService{
|
||||
ClientID: "clientID",
|
||||
|
@ -7,7 +7,7 @@ import (
|
||||
"github.com/grafana/grafana/pkg/plugins/auth"
|
||||
"github.com/grafana/grafana/pkg/plugins/config"
|
||||
"github.com/grafana/grafana/pkg/plugins/log"
|
||||
"github.com/grafana/grafana/pkg/plugins/plugindef"
|
||||
"github.com/grafana/grafana/pkg/plugins/pfs"
|
||||
"github.com/grafana/grafana/pkg/services/accesscontrol"
|
||||
"github.com/grafana/grafana/pkg/services/extsvcauth"
|
||||
"github.com/grafana/grafana/pkg/services/featuremgmt"
|
||||
@ -41,7 +41,7 @@ func (s *Service) HasExternalService(ctx context.Context, pluginID string) (bool
|
||||
}
|
||||
|
||||
// RegisterExternalService is a simplified wrapper around SaveExternalService for the plugin use case.
|
||||
func (s *Service) RegisterExternalService(ctx context.Context, pluginID string, pType plugindef.Type, svc *plugindef.IAM) (*auth.ExternalService, error) {
|
||||
func (s *Service) RegisterExternalService(ctx context.Context, pluginID string, pType pfs.Type, svc *pfs.IAM) (*auth.ExternalService, error) {
|
||||
if !s.featureEnabled {
|
||||
s.log.Warn("Skipping External Service Registration. The feature is behind a feature toggle and needs to be enabled.")
|
||||
return nil, nil
|
||||
@ -50,7 +50,7 @@ func (s *Service) RegisterExternalService(ctx context.Context, pluginID string,
|
||||
// Datasource plugins can only be enabled
|
||||
enabled := true
|
||||
// App plugins can be disabled
|
||||
if pType == plugindef.TypeApp {
|
||||
if pType == pfs.TypeApp {
|
||||
settings, err := s.settingsSvc.GetPluginSettingByPluginID(ctx, &pluginsettings.GetByPluginIDArgs{PluginID: pluginID})
|
||||
if err != nil && !errors.Is(err, pluginsettings.ErrPluginSettingNotFound) {
|
||||
return nil, err
|
||||
@ -86,7 +86,7 @@ func (s *Service) RegisterExternalService(ctx context.Context, pluginID string,
|
||||
PrivateKey: privateKey}, nil
|
||||
}
|
||||
|
||||
func toAccessControlPermissions(ps []plugindef.Permission) []accesscontrol.Permission {
|
||||
func toAccessControlPermissions(ps []pfs.Permission) []accesscontrol.Permission {
|
||||
res := make([]accesscontrol.Permission, 0, len(ps))
|
||||
for _, p := range ps {
|
||||
scope := ""
|
||||
|
@ -14,12 +14,11 @@ import (
|
||||
"strings"
|
||||
|
||||
"github.com/grafana/codejen"
|
||||
"github.com/grafana/kindsys"
|
||||
|
||||
corecodegen "github.com/grafana/grafana/pkg/codegen"
|
||||
"github.com/grafana/grafana/pkg/cuectx"
|
||||
"github.com/grafana/grafana/pkg/plugins/codegen"
|
||||
"github.com/grafana/grafana/pkg/plugins/pfs"
|
||||
"github.com/grafana/kindsys"
|
||||
"github.com/grafana/thema"
|
||||
)
|
||||
|
||||
@ -86,7 +85,7 @@ func adaptToPipeline(j codejen.OneToOne[corecodegen.SchemaForGen]) codejen.OneTo
|
||||
return corecodegen.SchemaForGen{
|
||||
Name: strings.ReplaceAll(pd.PluginMeta.Name, " ", ""),
|
||||
Schema: pd.Lineage.Latest(),
|
||||
IsGroup: pd.SchemaInterface.IsGroup(),
|
||||
IsGroup: pd.SchemaInterface.IsGroup,
|
||||
}
|
||||
})
|
||||
}
|
||||
|
Loading…
Reference in New Issue
Block a user