Add apiVersion to plugin models (#87510)

This commit is contained in:
Andres Martinez Gotor
2024-05-14 13:58:27 +02:00
committed by GitHub
parent 2f11cf84e8
commit d8904f3ca4
21 changed files with 202 additions and 17 deletions
@@ -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]+))?$"
}
}
}
+1
View File
@@ -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 {
+2
View File
@@ -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 {
+2
View File
@@ -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
+2
View File
@@ -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)
}
})
}
}
+3
View File
@@ -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) {
+1
View File
@@ -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."))
)
+6
View File
@@ -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:"-"`
+17 -3
View File
@@ -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 {
+12 -8
View File
@@ -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,