Add apiVersion to plugin models (#87510)

ui/sticky-page-header
Andres Martinez Gotor 1 year ago committed by GitHub
parent 2f11cf84e8
commit d8904f3ca4
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
  1. 5
      docs/sources/developers/plugins/plugin.schema.json
  2. 1
      pkg/api/datasources.go
  3. 2
      pkg/api/dtos/datasource.go
  4. 2
      pkg/api/dtos/plugins.go
  5. 2
      pkg/api/plugins.go
  6. 35
      pkg/plugins/manager/pipeline/validation/steps.go
  7. 75
      pkg/plugins/manager/pipeline/validation/steps_test.go
  8. 3
      pkg/plugins/plugins.go
  9. 1
      pkg/services/datasources/errors.go
  10. 6
      pkg/services/datasources/models.go
  11. 20
      pkg/services/datasources/service/datasource.go
  12. 19
      pkg/services/datasources/service/datasource_test.go
  13. 3
      pkg/services/datasources/service/store.go
  14. 20
      pkg/services/datasources/service/store_test.go
  15. 1
      pkg/services/pluginsintegration/adapters/adapters.go
  16. 1
      pkg/services/pluginsintegration/pipeline/pipeline.go
  17. 3
      pkg/services/pluginsintegration/plugincontext/plugincontext.go
  18. 12
      pkg/services/pluginsintegration/plugincontext/plugincontext_test.go
  19. 4
      pkg/services/sqlstore/migrations/datasource_mig.go
  20. 3
      pkg/tests/api/plugins/data/expectedListResp.json
  21. 1
      public/app/plugins/datasource/grafana-testdata-datasource/plugin.json

@ -584,6 +584,11 @@
}
}
}
},
"apiVersion": {
"type": "string",
"description": "[internal only] The API version for the plugin. Used for Datasource API servers. This metadata is temporary and will be removed in the future.",
"pattern": "^v([\\d]+)(?:(alpha|beta)([\\d]+))?$"
}
}
}

@ -840,6 +840,7 @@ func (hs *HTTPServer) convertModelToDtos(ctx context.Context, ds *datasources.Da
SecureJsonFields: map[string]bool{},
Version: ds.Version,
ReadOnly: ds.ReadOnly,
APIVersion: ds.APIVersion,
}
if hs.pluginStore != nil {

@ -28,6 +28,8 @@ type DataSource struct {
Version int `json:"version"`
ReadOnly bool `json:"readOnly"`
AccessControl accesscontrol.Metadata `json:"accessControl,omitempty"`
// swagger:ignore
APIVersion string `json:"apiVersion"`
}
type DataSourceListItemDTO struct {

@ -28,6 +28,7 @@ type PluginSetting struct {
SignatureType plugins.SignatureType `json:"signatureType"`
SignatureOrg string `json:"signatureOrg"`
AngularDetected bool `json:"angularDetected"`
APIVersion string `json:"apiVersion"`
}
type PluginListItem struct {
@ -49,6 +50,7 @@ type PluginListItem struct {
AccessControl accesscontrol.Metadata `json:"accessControl,omitempty"`
AngularDetected bool `json:"angularDetected"`
IAM *pfs.IAM `json:"iam,omitempty"`
APIVersion string `json:"apiVersion"`
}
type PluginList []PluginListItem

@ -142,6 +142,7 @@ func (hs *HTTPServer) GetPluginList(c *contextmodel.ReqContext) response.Respons
SignatureOrg: pluginDef.SignatureOrg,
AccessControl: pluginsMetadata[pluginDef.ID],
AngularDetected: pluginDef.Angular.Detected,
APIVersion: pluginDef.APIVersion,
}
if hs.Features.IsEnabled(c.Req.Context(), featuremgmt.FlagExternalServiceAccounts) {
@ -208,6 +209,7 @@ func (hs *HTTPServer) GetPluginSettingByID(c *contextmodel.ReqContext) response.
SignatureOrg: plugin.SignatureOrg,
SecureJsonFields: map[string]bool{},
AngularDetected: plugin.Angular.Detected,
APIVersion: plugin.APIVersion,
}
if plugin.IsApp() {

@ -3,6 +3,8 @@ package validation
import (
"context"
"errors"
"fmt"
"regexp"
"slices"
"time"
@ -115,3 +117,36 @@ func (a *AngularDetector) Validate(ctx context.Context, p *plugins.Plugin) error
p.Angular.HideDeprecation = slices.Contains(a.cfg.HideAngularDeprecation, p.ID)
return nil
}
// APIVersionValidation implements a ValidateFunc for validating plugin API versions.
type APIVersionValidation struct {
}
// APIVersionValidationStep returns a new ValidateFunc for validating plugin signatures.
func APIVersionValidationStep() ValidateFunc {
sv := &APIVersionValidation{}
return sv.Validate
}
// Validate validates the plugin signature. If a signature error is encountered, the error is recorded with the
// pluginerrs.ErrorTracker.
func (v *APIVersionValidation) Validate(ctx context.Context, p *plugins.Plugin) error {
if p.APIVersion != "" {
if !p.Backend {
return fmt.Errorf("plugin %s has an API version but is not a backend plugin", p.ID)
}
// Eventually, all backend plugins will be supported
if p.Type != plugins.TypeDataSource {
return fmt.Errorf("plugin %s has an API version but is not a datasource plugin", p.ID)
}
m, err := regexp.MatchString(`^v([\d]+)(?:(alpha|beta)([\d]+))?$`, p.APIVersion)
if err != nil {
return fmt.Errorf("failed to verify apiVersion %s: %v", p.APIVersion, err)
}
if !m {
return fmt.Errorf("plugin %s has an invalid API version %s", p.ID, p.APIVersion)
}
}
return nil
}

@ -0,0 +1,75 @@
package validation
import (
"context"
"testing"
"github.com/grafana/grafana/pkg/plugins"
"github.com/stretchr/testify/require"
)
func TestAPIVersionValidation(t *testing.T) {
s := APIVersionValidationStep()
tests := []struct {
name string
plugin *plugins.Plugin
err bool
}{
{
name: "valid plugin",
plugin: &plugins.Plugin{
JSONData: plugins.JSONData{
Backend: true,
Type: plugins.TypeDataSource,
APIVersion: "v0alpha1",
},
},
err: false,
},
{
name: "invalid plugin - not backend",
plugin: &plugins.Plugin{
JSONData: plugins.JSONData{
Backend: false,
Type: plugins.TypeDataSource,
APIVersion: "v0alpha1",
},
},
err: true,
},
{
name: "invalid plugin - not datasource",
plugin: &plugins.Plugin{
JSONData: plugins.JSONData{
Backend: true,
Type: plugins.TypeApp,
APIVersion: "v0alpha1",
},
},
err: true,
},
{
name: "invalid plugin - invalid API version",
plugin: &plugins.Plugin{
JSONData: plugins.JSONData{
Backend: true,
Type: plugins.TypeDataSource,
APIVersion: "invalid",
},
},
err: true,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
err := s(context.Background(), tt.plugin)
if tt.err {
require.Error(t, err)
} else {
require.NoError(t, err)
}
})
}
}

@ -119,6 +119,9 @@ type JSONData struct {
// App Service Auth Registration
IAM *pfs.IAM `json:"iam,omitempty"`
// API Version: Temporary field while plugins don't expose a OpenAPI schema
APIVersion string `json:"apiVersion,omitempty"`
}
func ReadPluginJSON(reader io.Reader) (JSONData, error) {

@ -17,4 +17,5 @@ var (
ErrDatasourceIsReadOnly = errors.New("data source is readonly, can only be updated from configuration")
ErrDataSourceNameInvalid = errutil.ValidationFailed("datasource.nameInvalid", errutil.WithPublicMessage("Invalid datasource name."))
ErrDataSourceURLInvalid = errutil.ValidationFailed("datasource.urlInvalid", errutil.WithPublicMessage("Invalid datasource url."))
ErrDataSourceAPIVersionInvalid = errutil.ValidationFailed("datasource.apiVersionInvalid", errutil.WithPublicMessage("Invalid datasource apiVersion."))
)

@ -63,6 +63,8 @@ type DataSource struct {
ReadOnly bool `json:"readOnly"`
UID string `json:"uid" xorm:"uid"`
// swagger:ignore
APIVersion string `json:"apiVersion" xorm:"api_version"`
// swagger:ignore
IsPrunable bool `xorm:"is_prunable"`
Created time.Time `json:"created,omitempty"`
@ -164,6 +166,8 @@ type AddDataSourceCommand struct {
SecureJsonData map[string]string `json:"secureJsonData"`
UID string `json:"uid"`
// swagger:ignore
APIVersion string `json:"apiVersion"`
// swagger:ignore
IsPrunable bool
OrgID int64 `json:"-"`
@ -190,6 +194,8 @@ type UpdateDataSourceCommand struct {
Version int `json:"version"`
UID string `json:"uid"`
// swagger:ignore
APIVersion string `json:"apiVersion"`
// swagger:ignore
IsPrunable bool
OrgID int64 `json:"-"`

@ -205,7 +205,7 @@ func (s *Service) AddDataSource(ctx context.Context, cmd *datasources.AddDataSou
cmd.Name = getAvailableName(cmd.Type, dataSources)
}
if err := validateFields(cmd.Name, cmd.URL); err != nil {
if err := s.validateFields(ctx, cmd.Name, cmd.URL, cmd.Type, cmd.APIVersion); err != nil {
return nil, err
}
@ -287,7 +287,7 @@ func (s *Service) DeleteDataSource(ctx context.Context, cmd *datasources.DeleteD
func (s *Service) UpdateDataSource(ctx context.Context, cmd *datasources.UpdateDataSourceCommand) (*datasources.DataSource, error) {
var dataSource *datasources.DataSource
if err := validateFields(cmd.Name, cmd.URL); err != nil {
if err := s.validateFields(ctx, cmd.Name, cmd.URL, cmd.Type, cmd.APIVersion); err != nil {
return dataSource, err
}
@ -716,7 +716,7 @@ func (s *Service) fillWithSecureJSONData(ctx context.Context, cmd *datasources.U
return nil
}
func validateFields(name, url string) error {
func (s *Service) validateFields(ctx context.Context, name, url, pluginID, apiVersion string) error {
if len(name) > maxDatasourceNameLen {
return datasources.ErrDataSourceNameInvalid.Errorf("max length is %d", maxDatasourceNameLen)
}
@ -725,6 +725,20 @@ func validateFields(name, url string) error {
return datasources.ErrDataSourceURLInvalid.Errorf("max length is %d", maxDatasourceUrlLen)
}
if apiVersion == "" {
return nil
}
p, found := s.pluginStore.Plugin(context.Background(), pluginID)
if !found {
// Plugin not installed, ignore apiVersion check
return nil
}
if p.APIVersion != "" && p.APIVersion != apiVersion {
return datasources.ErrDataSourceAPIVersionInvalid.Errorf("expected %s, got %s", p.APIVersion, apiVersion)
}
return nil
}

@ -19,6 +19,7 @@ import (
"github.com/grafana/grafana/pkg/infra/db"
"github.com/grafana/grafana/pkg/infra/httpclient"
"github.com/grafana/grafana/pkg/infra/log"
"github.com/grafana/grafana/pkg/plugins"
"github.com/grafana/grafana/pkg/services/accesscontrol"
"github.com/grafana/grafana/pkg/services/accesscontrol/actest"
acmock "github.com/grafana/grafana/pkg/services/accesscontrol/mock"
@ -63,7 +64,14 @@ func TestService_AddDataSource(t *testing.T) {
secretsStore := secretskvs.NewSQLSecretsKVStore(sqlStore, secretsService, log.New("test.logger"))
quotaService := quotatest.New(false, nil)
mockPermission := acmock.NewMockedPermissionsService()
dsService, err := ProvideService(sqlStore, secretsService, secretsStore, cfg, featuremgmt.WithFeatures(), actest.FakeAccessControl{}, mockPermission, quotaService, &pluginstore.FakePluginStore{})
dsService, err := ProvideService(sqlStore, secretsService, secretsStore, cfg, featuremgmt.WithFeatures(), actest.FakeAccessControl{}, mockPermission, quotaService, &pluginstore.FakePluginStore{
PluginList: []pluginstore.Plugin{{
JSONData: plugins.JSONData{
Name: "test",
APIVersion: "v0alpha1",
},
}},
})
require.NoError(t, err)
cmd := &datasources.AddDataSourceCommand{
@ -81,6 +89,15 @@ func TestService_AddDataSource(t *testing.T) {
_, err = dsService.AddDataSource(context.Background(), cmd)
require.EqualError(t, err, "[datasource.urlInvalid] max length is 255")
cmd = &datasources.AddDataSourceCommand{
OrgID: 1,
Name: "test",
APIVersion: "v0alpha2",
}
_, err = dsService.AddDataSource(context.Background(), cmd)
require.EqualError(t, err, "[datasource.apiVersionInvalid] expected v0alpha1, got v0alpha2")
})
}

@ -284,6 +284,7 @@ func (ss *SqlStore) AddDataSource(ctx context.Context, cmd *datasources.AddDataS
ReadOnly: cmd.ReadOnly,
UID: cmd.UID,
IsPrunable: cmd.IsPrunable,
APIVersion: cmd.APIVersion,
}
if _, err := sess.Insert(ds); err != nil {
@ -361,6 +362,7 @@ func (ss *SqlStore) UpdateDataSource(ctx context.Context, cmd *datasources.Updat
Version: cmd.Version + 1,
UID: cmd.UID,
IsPrunable: cmd.IsPrunable,
APIVersion: cmd.APIVersion,
}
sess.UseBool("is_default")
@ -378,6 +380,7 @@ func (ss *SqlStore) UpdateDataSource(ctx context.Context, cmd *datasources.Updat
// Make sure secure json data is zeroed out if empty. We do this as we want to migrate secrets from
// secure json data to the unified secrets table.
sess.MustCols("secure_json_data")
sess.MustCols("api_version")
var updateSession *xorm.Session
if cmd.Version != 0 {

@ -56,13 +56,14 @@ func TestIntegrationDataAccess(t *testing.T) {
db := db.InitTestDB(t)
ss := SqlStore{db: db}
_, err := ss.AddDataSource(context.Background(), &datasources.AddDataSourceCommand{
OrgID: 10,
Name: "laban",
Type: datasources.DS_GRAPHITE,
Access: datasources.DS_ACCESS_DIRECT,
URL: "http://test",
Database: "site",
ReadOnly: true,
OrgID: 10,
Name: "laban",
Type: datasources.DS_GRAPHITE,
Access: datasources.DS_ACCESS_DIRECT,
URL: "http://test",
Database: "site",
ReadOnly: true,
APIVersion: "v0alpha1",
})
require.NoError(t, err)
@ -76,6 +77,7 @@ func TestIntegrationDataAccess(t *testing.T) {
require.EqualValues(t, 10, ds.OrgID)
require.Equal(t, "site", ds.Database)
require.True(t, ds.ReadOnly)
require.Equal(t, "v0alpha1", ds.APIVersion)
})
t.Run("generates uid if not specified", func(t *testing.T) {
@ -146,9 +148,11 @@ func TestIntegrationDataAccess(t *testing.T) {
cmd := defaultUpdateDatasourceCommand
cmd.ID = ds.ID
cmd.Version = ds.Version
cmd.APIVersion = "v0alpha1"
ss := SqlStore{db: db}
_, err := ss.UpdateDataSource(context.Background(), &cmd)
ds, err := ss.UpdateDataSource(context.Background(), &cmd)
require.NoError(t, err)
require.Equal(t, "v0alpha1", ds.APIVersion)
})
t.Run("does not overwrite UID if not specified", func(t *testing.T) {

@ -40,6 +40,7 @@ func ModelToInstanceSettings(ds *datasources.DataSource, decryptFn func(ds *data
JSONData: jsonDataBytes,
DecryptedSecureJSONData: decrypted,
Updated: ds.Updated,
APIVersion: ds.APIVersion,
}, err
}

@ -54,6 +54,7 @@ func ProvideValidationStage(cfg *config.PluginManagementCfg, sv signature.Valida
SignatureValidationStep(sv),
validation.ModuleJSValidationStep(),
validation.AngularDetectionStep(cfg, ai),
validation.APIVersionValidationStep(),
},
})
}

@ -68,6 +68,7 @@ func (p *Provider) Get(ctx context.Context, pluginID string, user identity.Reque
pCtx := backend.PluginContext{
PluginID: plugin.ID,
PluginVersion: plugin.Info.Version,
APIVersion: plugin.APIVersion,
}
if user != nil && !user.IsNil() {
pCtx.OrgID = user.GetOrgID()
@ -107,6 +108,7 @@ func (p *Provider) GetWithDataSource(ctx context.Context, pluginID string, user
pCtx := backend.PluginContext{
PluginID: plugin.ID,
PluginVersion: plugin.Info.Version,
APIVersion: plugin.APIVersion,
}
if user != nil && !user.IsNil() {
pCtx.OrgID = user.GetOrgID()
@ -159,6 +161,7 @@ func (p *Provider) PluginContextForDataSource(ctx context.Context, datasourceSet
pCtx := backend.PluginContext{
PluginID: plugin.ID,
PluginVersion: plugin.Info.Version,
APIVersion: plugin.APIVersion,
}
if user != nil && !user.IsNil() {
pCtx.OrgID = user.GetOrgID()

@ -26,15 +26,17 @@ import (
func TestGet(t *testing.T) {
const (
pluginID = "plugin-id"
alias = "alias"
pluginID = "plugin-id"
alias = "alias"
apiVersion = "v0alpha1"
)
preg := registry.NewInMemory()
require.NoError(t, preg.Add(context.Background(), &plugins.Plugin{
JSONData: plugins.JSONData{
ID: pluginID,
AliasIDs: []string{alias},
ID: pluginID,
AliasIDs: []string{alias},
APIVersion: apiVersion,
},
}))
@ -59,6 +61,7 @@ func TestGet(t *testing.T) {
pCtx, err := pcp.Get(context.Background(), tc.input, identity, identity.OrgID)
require.NoError(t, err)
require.Equal(t, pluginID, pCtx.PluginID)
require.Equal(t, apiVersion, pCtx.APIVersion)
require.NotNil(t, pCtx.GrafanaConfig)
})
@ -72,6 +75,7 @@ func TestGet(t *testing.T) {
})
require.NoError(t, err)
require.Equal(t, pluginID, pCtx.PluginID)
require.Equal(t, apiVersion, pCtx.APIVersion)
require.NotNil(t, pCtx.GrafanaConfig)
})
})

@ -138,4 +138,8 @@ func addDataSourceMigration(mg *Migrator) {
mg.AddMigration("Add is_prunable column", NewAddColumnMigration(tableV2, &Column{
Name: "is_prunable", Type: DB_Bool, Nullable: true, Default: "0",
}))
mg.AddMigration("Add api_version column", NewAddColumnMigration(tableV2, &Column{
Name: "api_version", Type: DB_Varchar, Nullable: true, Length: 20,
}))
}

@ -1668,7 +1668,8 @@
"signature": "internal",
"signatureType": "",
"signatureOrg": "",
"angularDetected": false
"angularDetected": false,
"apiVersion": "v0alpha1"
},
{
"name": "Text",

@ -10,6 +10,7 @@
"alerting": true,
"annotations": true,
"backend": true,
"apiVersion": "v0alpha1",
"queryOptions": {
"minInterval": true,

Loading…
Cancel
Save