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:
Selene 2024-03-07 11:09:19 +01:00 committed by GitHub
parent beea7d1c2b
commit 1181141b40
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
29 changed files with 181 additions and 1401 deletions

View File

@ -103,7 +103,6 @@ openapi3-gen: swagger-gen ## Generates OpenApi 3 specs from the Swagger 2 alread
##@ Building ##@ Building
gen-cue: ## Do all CUE/Thema code generation gen-cue: ## Do all CUE/Thema code generation
@echo "generate code from .cue files" @echo "generate code from .cue files"
go generate ./pkg/plugins/plugindef
go generate ./kinds/gen.go go generate ./kinds/gen.go
go generate ./public/app/plugins/gen.go go generate ./public/app/plugins/gen.go

View File

@ -6,5 +6,5 @@ import (
// CueSchemaFS embeds all schema-related CUE files in the Grafana project. // 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 var CueSchemaFS embed.FS

View File

@ -2,7 +2,7 @@ package dtos
import ( import (
"github.com/grafana/grafana/pkg/plugins" "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" "github.com/grafana/grafana/pkg/services/accesscontrol"
) )
@ -48,7 +48,7 @@ type PluginListItem struct {
SignatureOrg string `json:"signatureOrg"` SignatureOrg string `json:"signatureOrg"`
AccessControl accesscontrol.Metadata `json:"accessControl,omitempty"` AccessControl accesscontrol.Metadata `json:"accessControl,omitempty"`
AngularDetected bool `json:"angularDetected"` AngularDetected bool `json:"angularDetected"`
IAM *plugindef.IAM `json:"iam,omitempty"` IAM *pfs.IAM `json:"iam,omitempty"`
} }
type PluginList []PluginListItem type PluginList []PluginListItem

View File

@ -21,7 +21,7 @@ import (
"github.com/grafana/grafana/pkg/api/dtos" "github.com/grafana/grafana/pkg/api/dtos"
"github.com/grafana/grafana/pkg/api/response" "github.com/grafana/grafana/pkg/api/response"
"github.com/grafana/grafana/pkg/plugins" "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" "github.com/grafana/grafana/pkg/plugins/repo"
ac "github.com/grafana/grafana/pkg/services/accesscontrol" ac "github.com/grafana/grafana/pkg/services/accesscontrol"
contextmodel "github.com/grafana/grafana/pkg/services/contexthandler/model" 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 // 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{} res := []ac.Evaluator{}
for _, p := range ps { for _, p := range ps {
if p.Scope != nil { if p.Scope != nil {

View File

@ -27,7 +27,7 @@ import (
"github.com/grafana/grafana/pkg/plugins/manager/fakes" "github.com/grafana/grafana/pkg/plugins/manager/fakes"
"github.com/grafana/grafana/pkg/plugins/manager/filestore" "github.com/grafana/grafana/pkg/plugins/manager/filestore"
"github.com/grafana/grafana/pkg/plugins/manager/registry" "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" "github.com/grafana/grafana/pkg/plugins/pluginscdn"
ac "github.com/grafana/grafana/pkg/services/accesscontrol" ac "github.com/grafana/grafana/pkg/services/accesscontrol"
"github.com/grafana/grafana/pkg/services/accesscontrol/acimpl" "github.com/grafana/grafana/pkg/services/accesscontrol/acimpl"
@ -658,8 +658,8 @@ func TestHTTPServer_hasPluginRequestedPermissions(t *testing.T) {
pluginReg := pluginstore.Plugin{ pluginReg := pluginstore.Plugin{
JSONData: plugins.JSONData{ JSONData: plugins.JSONData{
ID: "grafana-test-app", ID: "grafana-test-app",
IAM: &plugindef.IAM{ IAM: &pfs.IAM{
Permissions: []plugindef.Permission{{Action: ac.ActionUsersRead, Scope: newStr(ac.ScopeUsersAll)}, {Action: ac.ActionUsersCreate}}, Permissions: []pfs.Permission{{Action: ac.ActionUsersRead, Scope: newStr(ac.ScopeUsersAll)}, {Action: ac.ActionUsersCreate}},
}, },
}, },
} }

View File

@ -3,7 +3,7 @@ package auth
import ( import (
"context" "context"
"github.com/grafana/grafana/pkg/plugins/plugindef" "github.com/grafana/grafana/pkg/plugins/pfs"
) )
type ExternalService struct { type ExternalService struct {
@ -14,6 +14,6 @@ type ExternalService struct {
type ExternalServiceRegistry interface { type ExternalServiceRegistry interface {
HasExternalService(ctx context.Context, pluginID string) (bool, error) 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 RemoveExternalService(ctx context.Context, pluginID string) error
} }

View File

@ -35,10 +35,10 @@ func (j *pgoJenny) Generate(decl *pfs.PluginDecl) (*codejen.File, error) {
return nil, nil return nil, nil
} }
slotname := strings.ToLower(decl.SchemaInterface.Name()) slotname := strings.ToLower(decl.SchemaInterface.Name)
byt, err := gocode.GenerateTypesOpenAPI(decl.Lineage.Latest(), &gocode.TypeConfigOpenAPI{ byt, err := gocode.GenerateTypesOpenAPI(decl.Lineage.Latest(), &gocode.TypeConfigOpenAPI{
Config: &openapi.Config{ Config: &openapi.Config{
Group: decl.SchemaInterface.IsGroup(), Group: decl.SchemaInterface.IsGroup,
Config: &copenapi.Config{ Config: &copenapi.Config{
MaxCycleDepth: 10, MaxCycleDepth: 10,
}, },

View File

@ -42,8 +42,8 @@ func (j *pleJenny) Generate(decl *pfs.PluginDecl) (codejen.Files, error) {
} }
version := "export const pluginVersion = \"%s\";" version := "export const pluginVersion = \"%s\";"
if decl.PluginMeta.Info.Version != nil { if decl.PluginMeta.Version != nil {
version = fmt.Sprintf(version, *decl.PluginMeta.Info.Version) version = fmt.Sprintf(version, *decl.PluginMeta.Version)
} else { } else {
version = fmt.Sprintf(version, getGrafanaVersion()) version = fmt.Sprintf(version, getGrafanaVersion())
} }

View File

@ -51,7 +51,7 @@ func (j *ptsJenny) Generate(decl *pfs.PluginDecl) (*codejen.File, error) {
Data: string(jf.Data), 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 := []byte(tsf.String())
data = data[:len(data)-1] // remove the additional line break added by the inner jenny data = data[:len(data)-1] // remove the additional line break added by the inner jenny

View File

@ -13,7 +13,7 @@ import (
"github.com/grafana/grafana/pkg/plugins/auth" "github.com/grafana/grafana/pkg/plugins/auth"
"github.com/grafana/grafana/pkg/plugins/backendplugin" "github.com/grafana/grafana/pkg/plugins/backendplugin"
"github.com/grafana/grafana/pkg/plugins/log" "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/repo"
"github.com/grafana/grafana/pkg/plugins/storage" "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 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 return f.Result, nil
} }

View File

@ -4,20 +4,30 @@ import (
"cuelang.org/go/cue/ast" "cuelang.org/go/cue/ast"
"github.com/grafana/kindsys" "github.com/grafana/kindsys"
"github.com/grafana/thema" "github.com/grafana/thema"
"github.com/grafana/grafana/pkg/plugins/plugindef"
) )
type PluginDecl struct { type PluginDecl struct {
SchemaInterface *kindsys.SchemaInterface SchemaInterface *SchemaInterface
Lineage thema.Lineage Lineage thema.Lineage
Imports []*ast.ImportSpec Imports []*ast.ImportSpec
PluginPath string PluginPath string
PluginMeta plugindef.PluginDef PluginMeta Metadata
KindDecl kindsys.Def[kindsys.ComposableProperties] 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{ return &PluginDecl{
PluginPath: path, PluginPath: path,
PluginMeta: meta, PluginMeta: meta,

View File

@ -6,7 +6,6 @@ import (
"path/filepath" "path/filepath"
"sort" "sort"
"github.com/grafana/kindsys"
"github.com/grafana/thema" "github.com/grafana/thema"
) )
@ -15,6 +14,18 @@ type declParser struct {
skip map[string]bool 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 { func NewDeclParser(rt *thema.Runtime, skip map[string]bool) *declParser {
return &declParser{ return &declParser{
rt: rt, rt: rt,
@ -50,12 +61,11 @@ func (psr *declParser) Parse(root fs.FS) ([]*PluginDecl, error) {
} }
for slotName, kind := range pp.ComposableKinds { for slotName, kind := range pp.ComposableKinds {
slot, err := kindsys.FindSchemaInterface(slotName)
if err != nil { if err != nil {
return nil, fmt.Errorf("parsing plugin failed for %s: %s", dir, err) return nil, fmt.Errorf("parsing plugin failed for %s: %s", dir, err)
} }
decls = append(decls, &PluginDecl{ decls = append(decls, &PluginDecl{
SchemaInterface: &slot, SchemaInterface: schemaInterfaces[slotName],
Lineage: kind.Lineage(), Lineage: kind.Lineage(),
Imports: pp.CUEImports, Imports: pp.CUEImports,
PluginMeta: pp.Properties, PluginMeta: pp.Properties,

View File

@ -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. // ErrInvalidRootFile indicates that the root plugin.json file is invalid.
var ErrInvalidRootFile = errors.New("plugin.json 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 // ErrInvalidGrafanaPluginInstance indicates a plugin's set of .cue
// grafanaplugin package files are invalid with respect to the GrafanaPlugin // grafanaplugin package files are invalid with respect to the GrafanaPlugin
// spec. // spec.

View File

@ -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
}
...
}

View File

@ -1,56 +1,29 @@
package pfs package pfs
import ( import (
"encoding/json"
"fmt" "fmt"
"io/fs" "io/fs"
"path/filepath"
"sort" "sort"
"strings" "strings"
"sync"
"testing/fstest" "testing/fstest"
"cuelang.org/go/cue" "cuelang.org/go/cue"
"cuelang.org/go/cue/build"
"cuelang.org/go/cue/cuecontext" "cuelang.org/go/cue/cuecontext"
"cuelang.org/go/cue/errors" "cuelang.org/go/cue/errors"
"cuelang.org/go/cue/parser"
"cuelang.org/go/cue/token" "cuelang.org/go/cue/token"
"github.com/grafana/kindsys" "github.com/grafana/kindsys"
"github.com/grafana/thema" "github.com/grafana/thema"
"github.com/grafana/thema/load" "github.com/grafana/thema/load"
"github.com/grafana/thema/vmux"
"github.com/yalue/merged_fs" "github.com/yalue/merged_fs"
"github.com/grafana/grafana/pkg/cuectx" "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 // PackageName is the name of the CUE package that Grafana will load when
// looking for a Grafana plugin's kind declarations. // looking for a Grafana plugin's kind declarations.
const PackageName = "grafanaplugin" 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 // PermittedCUEImports returns the list of import paths that may be used in a
// plugin's grafanaplugin cue package. // plugin's grafanaplugin cue package.
var PermittedCUEImports = cuectx.PermittedCUEImports var PermittedCUEImports = cuectx.PermittedCUEImports
@ -109,35 +82,14 @@ func ParsePluginFS(fsys fs.FS, rt *thema.Runtime) (ParsedPlugin, error) {
rt = cuectx.GrafanaThemaRuntime() rt = cuectx.GrafanaThemaRuntime()
} }
lin, err := plugindef.Lineage(rt) metadata, err := getPluginMetadata(fsys)
if err != nil { if err != nil {
panic(fmt.Sprintf("plugindef lineage is invalid or broken, needs dev attention: %s", err)) return ParsedPlugin{}, 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)
} }
pp := ParsedPlugin{ pp := ParsedPlugin{
ComposableKinds: make(map[string]kindsys.Composable), ComposableKinds: make(map[string]kindsys.Composable),
// CustomKinds: make(map[string]kindsys.Custom), Properties: metadata,
}
// 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)
} }
if cuefiles, err := fs.Glob(fsys, "*.cue"); err != nil { 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 return pp, nil
} }
gpv := loadGP(rt.Context())
fsys, err = ensureCueMod(fsys, pp.Properties) fsys, err = ensureCueMod(fsys, pp.Properties)
if err != nil { if err != nil {
return ParsedPlugin{}, fmt.Errorf("%s has invalid cue.mod: %w", pp.Properties.Id, err) 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) 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 _, f := range bi.Files {
for _, im := range f.Imports { for _, im := range f.Imports {
ip := strings.Trim(im.Path.Value, "\"") 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") 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 gpi := rt.Context().BuildInstance(bi)
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)
if gpi.Err() != nil { if gpi.Err() != nil {
return ParsedPlugin{}, errors.Wrap(errors.Promote(ErrInvalidGrafanaPluginInstance, pp.Properties.Id), gpi.Err()) 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 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) props, err := kindsys.ToKindProps[kindsys.ComposableProperties](iv)
if err != nil { if err != nil {
return ParsedPlugin{}, err return ParsedPlugin{}, err
@ -222,7 +170,6 @@ func ParsePluginFS(fsys fs.FS, rt *thema.Runtime) (ParsedPlugin, error) {
pp.ComposableKinds[si.Name()] = compo pp.ComposableKinds[si.Name()] = compo
} }
// TODO custom kinds
return pp, nil 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) { func LoadComposableKindDef(fsys fs.FS, rt *thema.Runtime, defpath string) (kindsys.Def[kindsys.ComposableProperties], error) {
pp := ParsedPlugin{ pp := ParsedPlugin{
ComposableKinds: make(map[string]kindsys.Composable), ComposableKinds: make(map[string]kindsys.Composable),
Properties: plugindef.PluginDef{ Properties: Metadata{
Id: defpath, Id: defpath,
}, },
} }
@ -269,13 +216,13 @@ func LoadComposableKindDef(fsys fs.FS, rt *thema.Runtime, defpath string) (kinds
}, nil }, 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 modf, err := fs.ReadFile(fsys, "cue.mod/module.cue"); err != nil {
if !errors.Is(err, fs.ErrNotExist) { if !errors.Is(err, fs.ErrNotExist) {
return nil, err return nil, err
} }
return merged_fs.NewMergedFS(fsys, fstest.MapFS{ 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 }), nil
} else if _, err := cuecontext.New().CompileBytes(modf).LookupPath(cue.MakePath(cue.Str("module"))).String(); err != 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) 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 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])
}

View File

@ -3,8 +3,6 @@ package pfs
import ( import (
"cuelang.org/go/cue/ast" "cuelang.org/go/cue/ast"
"github.com/grafana/kindsys" "github.com/grafana/kindsys"
"github.com/grafana/grafana/pkg/plugins/plugindef"
) )
// ParsedPlugin represents everything knowable about a single plugin from static // ParsedPlugin represents everything knowable about a single plugin from static
@ -14,7 +12,7 @@ import (
// struct returned from [ParsePluginFS]. // struct returned from [ParsePluginFS].
type ParsedPlugin struct { type ParsedPlugin struct {
// Properties contains the plugin's definition, as declared in plugin.json. // 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. // 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. // Keys are the name of the [kindsys.SchemaInterface] implemented by the value.

View 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
}

View File

@ -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)
}
}

View File

@ -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))
}
}

View File

@ -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: []

View File

@ -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])
}

View File

@ -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

View File

@ -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"`
}

View File

@ -19,7 +19,7 @@ import (
"github.com/grafana/grafana/pkg/plugins/backendplugin/pluginextensionv2" "github.com/grafana/grafana/pkg/plugins/backendplugin/pluginextensionv2"
"github.com/grafana/grafana/pkg/plugins/backendplugin/secretsmanagerplugin" "github.com/grafana/grafana/pkg/plugins/backendplugin/secretsmanagerplugin"
"github.com/grafana/grafana/pkg/plugins/log" "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/services/org"
"github.com/grafana/grafana/pkg/util" "github.com/grafana/grafana/pkg/util"
) )
@ -118,7 +118,7 @@ type JSONData struct {
Executable string `json:"executable,omitempty"` Executable string `json:"executable,omitempty"`
// App Service Auth Registration // App Service Auth Registration
IAM *plugindef.IAM `json:"iam,omitempty"` IAM *pfs.IAM `json:"iam,omitempty"`
} }
func ReadPluginJSON(reader io.Reader) (JSONData, error) { func ReadPluginJSON(reader io.Reader) (JSONData, error) {

View File

@ -24,7 +24,7 @@ import (
"github.com/grafana/grafana/pkg/plugins/manager/registry" "github.com/grafana/grafana/pkg/plugins/manager/registry"
"github.com/grafana/grafana/pkg/plugins/manager/signature" "github.com/grafana/grafana/pkg/plugins/manager/signature"
"github.com/grafana/grafana/pkg/plugins/manager/sources" "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/plugins/pluginscdn"
"github.com/grafana/grafana/pkg/services/featuremgmt" "github.com/grafana/grafana/pkg/services/featuremgmt"
"github.com/grafana/grafana/pkg/services/org" "github.com/grafana/grafana/pkg/services/org"
@ -541,8 +541,8 @@ func TestLoader_Load_ExternalRegistration(t *testing.T) {
GrafanaVersion: "*", GrafanaVersion: "*",
Plugins: []plugins.Dependency{}, Plugins: []plugins.Dependency{},
}, },
IAM: &plugindef.IAM{ IAM: &pfs.IAM{
Permissions: []plugindef.Permission{ Permissions: []pfs.Permission{
{ {
Action: "read", Action: "read",
Scope: stringPtr("datasource"), Scope: stringPtr("datasource"),

View File

@ -14,7 +14,7 @@ import (
"github.com/grafana/grafana/pkg/plugins/manager/pipeline/validation" "github.com/grafana/grafana/pkg/plugins/manager/pipeline/validation"
"github.com/grafana/grafana/pkg/plugins/manager/registry" "github.com/grafana/grafana/pkg/plugins/manager/registry"
"github.com/grafana/grafana/pkg/plugins/manager/signature" "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/featuremgmt"
"github.com/grafana/grafana/pkg/services/pluginsintegration/pluginerrs" "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. // 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) { func (r *ExternalServiceRegistration) Register(ctx context.Context, p *plugins.Plugin) (*plugins.Plugin, error) {
if p.IAM != nil { 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 { if err != nil {
r.log.Error("Could not register an external service. Initialization skipped", "pluginId", p.ID, "error", err) r.log.Error("Could not register an external service. Initialization skipped", "pluginId", p.ID, "error", err)
return nil, err return nil, err

View File

@ -17,7 +17,7 @@ import (
"github.com/grafana/grafana/pkg/plugins/config" "github.com/grafana/grafana/pkg/plugins/config"
"github.com/grafana/grafana/pkg/plugins/envvars" "github.com/grafana/grafana/pkg/plugins/envvars"
"github.com/grafana/grafana/pkg/plugins/manager/fakes" "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/services/featuremgmt"
"github.com/grafana/grafana/pkg/setting" "github.com/grafana/grafana/pkg/setting"
) )
@ -590,7 +590,7 @@ func TestPluginEnvVarsProvider_authEnvVars(t *testing.T) {
p := &plugins.Plugin{ p := &plugins.Plugin{
JSONData: plugins.JSONData{ JSONData: plugins.JSONData{
ID: "test", ID: "test",
IAM: &plugindef.IAM{}, IAM: &pfs.IAM{},
}, },
ExternalService: &auth.ExternalService{ ExternalService: &auth.ExternalService{
ClientID: "clientID", ClientID: "clientID",

View File

@ -7,7 +7,7 @@ import (
"github.com/grafana/grafana/pkg/plugins/auth" "github.com/grafana/grafana/pkg/plugins/auth"
"github.com/grafana/grafana/pkg/plugins/config" "github.com/grafana/grafana/pkg/plugins/config"
"github.com/grafana/grafana/pkg/plugins/log" "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/accesscontrol"
"github.com/grafana/grafana/pkg/services/extsvcauth" "github.com/grafana/grafana/pkg/services/extsvcauth"
"github.com/grafana/grafana/pkg/services/featuremgmt" "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. // 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 { if !s.featureEnabled {
s.log.Warn("Skipping External Service Registration. The feature is behind a feature toggle and needs to be enabled.") s.log.Warn("Skipping External Service Registration. The feature is behind a feature toggle and needs to be enabled.")
return nil, nil return nil, nil
@ -50,7 +50,7 @@ func (s *Service) RegisterExternalService(ctx context.Context, pluginID string,
// Datasource plugins can only be enabled // Datasource plugins can only be enabled
enabled := true enabled := true
// App plugins can be disabled // App plugins can be disabled
if pType == plugindef.TypeApp { if pType == pfs.TypeApp {
settings, err := s.settingsSvc.GetPluginSettingByPluginID(ctx, &pluginsettings.GetByPluginIDArgs{PluginID: pluginID}) settings, err := s.settingsSvc.GetPluginSettingByPluginID(ctx, &pluginsettings.GetByPluginIDArgs{PluginID: pluginID})
if err != nil && !errors.Is(err, pluginsettings.ErrPluginSettingNotFound) { if err != nil && !errors.Is(err, pluginsettings.ErrPluginSettingNotFound) {
return nil, err return nil, err
@ -86,7 +86,7 @@ func (s *Service) RegisterExternalService(ctx context.Context, pluginID string,
PrivateKey: privateKey}, nil PrivateKey: privateKey}, nil
} }
func toAccessControlPermissions(ps []plugindef.Permission) []accesscontrol.Permission { func toAccessControlPermissions(ps []pfs.Permission) []accesscontrol.Permission {
res := make([]accesscontrol.Permission, 0, len(ps)) res := make([]accesscontrol.Permission, 0, len(ps))
for _, p := range ps { for _, p := range ps {
scope := "" scope := ""

View File

@ -14,12 +14,11 @@ import (
"strings" "strings"
"github.com/grafana/codejen" "github.com/grafana/codejen"
"github.com/grafana/kindsys"
corecodegen "github.com/grafana/grafana/pkg/codegen" corecodegen "github.com/grafana/grafana/pkg/codegen"
"github.com/grafana/grafana/pkg/cuectx" "github.com/grafana/grafana/pkg/cuectx"
"github.com/grafana/grafana/pkg/plugins/codegen" "github.com/grafana/grafana/pkg/plugins/codegen"
"github.com/grafana/grafana/pkg/plugins/pfs" "github.com/grafana/grafana/pkg/plugins/pfs"
"github.com/grafana/kindsys"
"github.com/grafana/thema" "github.com/grafana/thema"
) )
@ -86,7 +85,7 @@ func adaptToPipeline(j codejen.OneToOne[corecodegen.SchemaForGen]) codejen.OneTo
return corecodegen.SchemaForGen{ return corecodegen.SchemaForGen{
Name: strings.ReplaceAll(pd.PluginMeta.Name, " ", ""), Name: strings.ReplaceAll(pd.PluginMeta.Name, " ", ""),
Schema: pd.Lineage.Latest(), Schema: pd.Lineage.Latest(),
IsGroup: pd.SchemaInterface.IsGroup(), IsGroup: pd.SchemaInterface.IsGroup,
} }
}) })
} }