Plugins: Migrate plugincontext, adapters and pluginsettings to pkg/services/pluginsintegration package (#64154)

* migrate plugincontext, adapter and pluginsettings

* add to CODEOWNERS

* fix imports

* fix CODEOWNERS

* take pluginsettings

* migrate wire stuff
This commit is contained in:
Will Browne
2023-03-07 11:22:30 -05:00
committed by GitHub
parent 13650f3dc0
commit 94f39e69a3
36 changed files with 48 additions and 53 deletions
@@ -0,0 +1,75 @@
package pluginsettings
import (
"context"
"time"
)
type FakePluginSettings struct {
Service
Plugins map[string]*DTO
}
// GetPluginSettings returns all Plugin Settings for the provided Org
func (ps *FakePluginSettings) GetPluginSettings(_ context.Context, _ *GetArgs) ([]*InfoDTO, error) {
res := []*InfoDTO{}
for _, dto := range ps.Plugins {
res = append(res, &InfoDTO{
PluginID: dto.PluginID,
OrgID: dto.OrgID,
Enabled: dto.Enabled,
Pinned: dto.Pinned,
PluginVersion: dto.PluginVersion,
})
}
return res, nil
}
// GetPluginSettingByPluginID returns a Plugin Settings by Plugin ID
func (ps *FakePluginSettings) GetPluginSettingByPluginID(ctx context.Context, args *GetByPluginIDArgs) (*DTO, error) {
if res, ok := ps.Plugins[args.PluginID]; ok {
return res, nil
}
return nil, ErrPluginSettingNotFound
}
// UpdatePluginSetting updates a Plugin Setting
func (ps *FakePluginSettings) UpdatePluginSetting(ctx context.Context, args *UpdateArgs) error {
var secureData map[string][]byte
if args.SecureJSONData != nil {
secureData := map[string][]byte{}
for k, v := range args.SecureJSONData {
secureData[k] = ([]byte)(v)
}
}
// save
ps.Plugins[args.PluginID] = &DTO{
ID: int64(len(ps.Plugins)),
OrgID: args.OrgID,
PluginID: args.PluginID,
PluginVersion: args.PluginVersion,
JSONData: args.JSONData,
SecureJSONData: secureData,
Enabled: args.Enabled,
Pinned: args.Pinned,
Updated: time.Now(),
}
return nil
}
// UpdatePluginSettingPluginVersion updates a Plugin Setting's plugin version
func (ps *FakePluginSettings) UpdatePluginSettingPluginVersion(ctx context.Context, args *UpdatePluginVersionArgs) error {
if res, ok := ps.Plugins[args.PluginID]; ok {
res.PluginVersion = args.PluginVersion
return nil
}
return ErrPluginSettingNotFound
}
// DecryptedValues decrypts the encrypted secureJSONData of the provided plugin setting and
// returns the decrypted values.
func (ps *FakePluginSettings) DecryptedValues(dto *DTO) map[string]string {
// TODO: Implement
return nil
}
@@ -0,0 +1,116 @@
package pluginsettings
import (
"errors"
"time"
)
var (
ErrPluginSettingNotFound = errors.New("plugin setting not found")
)
type DTO struct {
ID int64
OrgID int64
PluginID string
PluginVersion string
JSONData map[string]interface{}
SecureJSONData map[string][]byte
Enabled bool
Pinned bool
Updated time.Time
}
type InfoDTO struct {
PluginID string
OrgID int64
Enabled bool
Pinned bool
PluginVersion string
}
type UpdateArgs struct {
Enabled bool
Pinned bool
JSONData map[string]interface{}
SecureJSONData map[string]string
PluginVersion string
PluginID string
OrgID int64
EncryptedSecureJSONData map[string][]byte
}
type UpdatePluginVersionArgs struct {
PluginVersion string
PluginID string
OrgID int64
}
type GetArgs struct {
OrgID int64
}
type GetByPluginIDArgs struct {
PluginID string
OrgID int64
}
type PluginSetting struct {
Id int64
PluginId string
OrgId int64
Enabled bool
Pinned bool
JsonData map[string]interface{}
SecureJsonData map[string][]byte
PluginVersion string
Created time.Time
Updated time.Time
}
type PluginSettingInfo struct {
PluginID string `xorm:"plugin_id"`
OrgID int64 `xorm:"org_id"`
Enabled bool `xorm:"enabled"`
Pinned bool `xorm:"pinned"`
PluginVersion string `xorm:"plugin_version"`
}
// ----------------------
// COMMANDS
// Also acts as api DTO
type UpdatePluginSettingCmd struct {
Enabled bool `json:"enabled"`
Pinned bool `json:"pinned"`
JsonData map[string]interface{} `json:"jsonData"`
SecureJsonData map[string]string `json:"secureJsonData"`
PluginVersion string `json:"version"`
PluginId string `json:"-"`
OrgId int64 `json:"-"`
EncryptedSecureJsonData map[string][]byte `json:"-"`
}
// specific command, will only update version
type UpdatePluginSettingVersionCmd struct {
PluginVersion string
PluginId string `json:"-"`
OrgId int64 `json:"-"`
}
// ---------------------
// QUERIES
type GetPluginSettingByIdQuery struct {
PluginId string
OrgId int64
Result *PluginSetting
}
type PluginStateChangedEvent struct {
PluginId string
OrgId int64
Enabled bool
}
@@ -0,0 +1,19 @@
package pluginsettings
import (
"context"
)
type Service interface {
// GetPluginSettings returns all Plugin Settings for the provided Org
GetPluginSettings(ctx context.Context, args *GetArgs) ([]*InfoDTO, error)
// GetPluginSettingByPluginID returns a Plugin Settings by Plugin ID
GetPluginSettingByPluginID(ctx context.Context, args *GetByPluginIDArgs) (*DTO, error)
// UpdatePluginSetting updates a Plugin Setting
UpdatePluginSetting(ctx context.Context, args *UpdateArgs) error
// UpdatePluginSettingPluginVersion updates a Plugin Setting's plugin version
UpdatePluginSettingPluginVersion(ctx context.Context, args *UpdatePluginVersionArgs) error
// DecryptedValues decrypts the encrypted secureJSONData of the provided plugin setting and
// returns the decrypted values.
DecryptedValues(ps *DTO) map[string]string
}
@@ -0,0 +1,234 @@
package service
import (
"context"
"sync"
"time"
"github.com/grafana/grafana/pkg/infra/db"
"github.com/grafana/grafana/pkg/infra/log"
"github.com/grafana/grafana/pkg/services/pluginsintegration/pluginsettings"
"github.com/grafana/grafana/pkg/services/secrets"
)
func ProvideService(db db.DB, secretsService secrets.Service) *Service {
s := &Service{
db: db,
decryptionCache: secureJSONDecryptionCache{
cache: make(map[int64]cachedDecryptedJSON),
},
secretsService: secretsService,
logger: log.New("pluginsettings"),
}
return s
}
type Service struct {
db db.DB
decryptionCache secureJSONDecryptionCache
secretsService secrets.Service
logger log.Logger
}
type cachedDecryptedJSON struct {
updated time.Time
json map[string]string
}
type secureJSONDecryptionCache struct {
cache map[int64]cachedDecryptedJSON
sync.Mutex
}
func (s *Service) GetPluginSettings(ctx context.Context, args *pluginsettings.GetArgs) ([]*pluginsettings.InfoDTO, error) {
ps, err := s.getPluginSettingsInfo(ctx, args.OrgID)
if err != nil {
return nil, err
}
result := make([]*pluginsettings.InfoDTO, 0, len(ps))
for _, p := range ps {
result = append(result, &pluginsettings.InfoDTO{
OrgID: p.OrgID,
PluginID: p.PluginID,
PluginVersion: p.PluginVersion,
Enabled: p.Enabled,
Pinned: p.Pinned,
})
}
return result, nil
}
func (s *Service) GetPluginSettingByPluginID(ctx context.Context, args *pluginsettings.GetByPluginIDArgs) (*pluginsettings.DTO, error) {
query := &pluginsettings.GetPluginSettingByIdQuery{
OrgId: args.OrgID,
PluginId: args.PluginID,
}
err := s.getPluginSettingById(ctx, query)
if err != nil {
return nil, err
}
return &pluginsettings.DTO{
ID: query.Result.Id,
OrgID: query.Result.OrgId,
PluginID: query.Result.PluginId,
PluginVersion: query.Result.PluginVersion,
JSONData: query.Result.JsonData,
SecureJSONData: query.Result.SecureJsonData,
Enabled: query.Result.Enabled,
Pinned: query.Result.Pinned,
Updated: query.Result.Updated,
}, nil
}
func (s *Service) UpdatePluginSetting(ctx context.Context, args *pluginsettings.UpdateArgs) error {
encryptedSecureJsonData, err := s.secretsService.EncryptJsonData(ctx, args.SecureJSONData, secrets.WithoutScope())
if err != nil {
return err
}
return s.updatePluginSetting(ctx, &pluginsettings.UpdatePluginSettingCmd{
Enabled: args.Enabled,
Pinned: args.Pinned,
JsonData: args.JSONData,
SecureJsonData: args.SecureJSONData,
PluginVersion: args.PluginVersion,
PluginId: args.PluginID,
OrgId: args.OrgID,
EncryptedSecureJsonData: encryptedSecureJsonData,
})
}
func (s *Service) UpdatePluginSettingPluginVersion(ctx context.Context, args *pluginsettings.UpdatePluginVersionArgs) error {
return s.updatePluginSettingVersion(ctx, &pluginsettings.UpdatePluginSettingVersionCmd{
PluginVersion: args.PluginVersion,
PluginId: args.PluginID,
OrgId: args.OrgID,
})
}
func (s *Service) DecryptedValues(ps *pluginsettings.DTO) map[string]string {
s.decryptionCache.Lock()
defer s.decryptionCache.Unlock()
if item, present := s.decryptionCache.cache[ps.ID]; present && ps.Updated.Equal(item.updated) {
return item.json
}
json, err := s.secretsService.DecryptJsonData(context.Background(), ps.SecureJSONData)
if err != nil {
s.logger.Error("Failed to decrypt secure json data", "error", err)
return map[string]string{}
}
s.decryptionCache.cache[ps.ID] = cachedDecryptedJSON{
updated: ps.Updated,
json: json,
}
return json
}
func (s *Service) getPluginSettingsInfo(ctx context.Context, orgID int64) ([]*pluginsettings.PluginSettingInfo, error) {
sql := `SELECT org_id, plugin_id, enabled, pinned, plugin_version FROM plugin_setting `
params := make([]interface{}, 0)
if orgID != 0 {
sql += "WHERE org_id=?"
params = append(params, orgID)
}
var rslt []*pluginsettings.PluginSettingInfo
err := s.db.WithDbSession(ctx, func(sess *db.Session) error {
return sess.SQL(sql, params...).Find(&rslt)
})
if err != nil {
return nil, err
}
return rslt, nil
}
func (s *Service) getPluginSettingById(ctx context.Context, query *pluginsettings.GetPluginSettingByIdQuery) error {
return s.db.WithDbSession(ctx, func(sess *db.Session) error {
pluginSetting := pluginsettings.PluginSetting{OrgId: query.OrgId, PluginId: query.PluginId}
has, err := sess.Get(&pluginSetting)
if err != nil {
return err
} else if !has {
return pluginsettings.ErrPluginSettingNotFound
}
query.Result = &pluginSetting
return nil
})
}
func (s *Service) updatePluginSetting(ctx context.Context, cmd *pluginsettings.UpdatePluginSettingCmd) error {
return s.db.WithTransactionalDbSession(ctx, func(sess *db.Session) error {
var pluginSetting pluginsettings.PluginSetting
exists, err := sess.Where("org_id=? and plugin_id=?", cmd.OrgId, cmd.PluginId).Get(&pluginSetting)
if err != nil {
return err
}
sess.UseBool("enabled")
sess.UseBool("pinned")
if !exists {
pluginSetting = pluginsettings.PluginSetting{
PluginId: cmd.PluginId,
OrgId: cmd.OrgId,
Enabled: cmd.Enabled,
Pinned: cmd.Pinned,
JsonData: cmd.JsonData,
PluginVersion: cmd.PluginVersion,
SecureJsonData: cmd.EncryptedSecureJsonData,
Created: time.Now(),
Updated: time.Now(),
}
// add state change event on commit success
sess.PublishAfterCommit(&pluginsettings.PluginStateChangedEvent{
PluginId: cmd.PluginId,
OrgId: cmd.OrgId,
Enabled: cmd.Enabled,
})
_, err = sess.Insert(&pluginSetting)
return err
}
for key, encryptedData := range cmd.EncryptedSecureJsonData {
pluginSetting.SecureJsonData[key] = encryptedData
}
// add state change event on commit success
if pluginSetting.Enabled != cmd.Enabled {
sess.PublishAfterCommit(&pluginsettings.PluginStateChangedEvent{
PluginId: cmd.PluginId,
OrgId: cmd.OrgId,
Enabled: cmd.Enabled,
})
}
pluginSetting.Updated = time.Now()
pluginSetting.Enabled = cmd.Enabled
pluginSetting.JsonData = cmd.JsonData
pluginSetting.Pinned = cmd.Pinned
pluginSetting.PluginVersion = cmd.PluginVersion
_, err = sess.ID(pluginSetting.Id).Update(&pluginSetting)
return err
})
}
func (s *Service) updatePluginSettingVersion(ctx context.Context, cmd *pluginsettings.UpdatePluginSettingVersionCmd) error {
return s.db.WithTransactionalDbSession(ctx, func(sess *db.Session) error {
_, err := sess.Exec("UPDATE plugin_setting SET plugin_version=? WHERE org_id=? AND plugin_id=?", cmd.PluginVersion, cmd.OrgId, cmd.PluginId)
return err
})
}
@@ -0,0 +1,272 @@
package service
import (
"context"
"testing"
"time"
"github.com/stretchr/testify/require"
"github.com/grafana/grafana/pkg/infra/db"
"github.com/grafana/grafana/pkg/services/pluginsintegration/pluginsettings"
"github.com/grafana/grafana/pkg/services/secrets"
"github.com/grafana/grafana/pkg/services/secrets/fakes"
secretsManager "github.com/grafana/grafana/pkg/services/secrets/manager"
)
func TestService_DecryptedValuesCache(t *testing.T) {
t.Run("When plugin settings hasn't been updated, encrypted JSON should be fetched from cache", func(t *testing.T) {
ctx := context.Background()
secretsService := secretsManager.SetupTestService(t, fakes.NewFakeSecretsStore())
psService := ProvideService(nil, secretsService)
encryptedJsonData, err := secretsService.EncryptJsonData(
ctx,
map[string]string{
"password": "password",
}, secrets.WithoutScope())
require.NoError(t, err)
ps := pluginsettings.DTO{
ID: 1,
JSONData: map[string]interface{}{},
SecureJSONData: encryptedJsonData,
}
// Populate cache
password, ok := psService.DecryptedValues(&ps)["password"]
require.Equal(t, "password", password)
require.True(t, ok)
encryptedJsonData, err = secretsService.EncryptJsonData(
ctx,
map[string]string{
"password": "",
}, secrets.WithoutScope())
require.NoError(t, err)
ps.SecureJSONData = encryptedJsonData
password, ok = psService.DecryptedValues(&ps)["password"]
require.Equal(t, "password", password)
require.True(t, ok)
})
t.Run("When plugin settings is updated, encrypted JSON should not be fetched from cache", func(t *testing.T) {
ctx := context.Background()
secretsService := secretsManager.SetupTestService(t, fakes.NewFakeSecretsStore())
psService := ProvideService(nil, secretsService)
encryptedJsonData, err := secretsService.EncryptJsonData(
ctx,
map[string]string{
"password": "password",
}, secrets.WithoutScope())
require.NoError(t, err)
ps := pluginsettings.DTO{
ID: 1,
JSONData: map[string]interface{}{},
SecureJSONData: encryptedJsonData,
}
// Populate cache
password, ok := psService.DecryptedValues(&ps)["password"]
require.Equal(t, "password", password)
require.True(t, ok)
encryptedJsonData, err = secretsService.EncryptJsonData(
ctx,
map[string]string{
"password": "",
}, secrets.WithoutScope())
require.NoError(t, err)
ps.SecureJSONData = encryptedJsonData
ps.Updated = time.Now()
password, ok = psService.DecryptedValues(&ps)["password"]
require.Empty(t, password)
require.True(t, ok)
})
}
func TestIntegrationPluginSettings(t *testing.T) {
if testing.Short() {
t.Skip("skipping integration test")
}
store := db.InitTestDB(t)
secretsService := secretsManager.SetupTestService(t, fakes.NewFakeSecretsStore())
psService := ProvideService(store, secretsService)
t.Run("Existing plugin settings", func(t *testing.T) {
secureJsonData, err := secretsService.EncryptJsonData(context.Background(), map[string]string{"secureKey": "secureValue"}, secrets.WithoutScope())
require.NoError(t, err)
existing := pluginsettings.PluginSetting{
OrgId: 1,
PluginId: "existing",
Enabled: false,
Pinned: false,
JsonData: map[string]interface{}{
"key": "value",
},
SecureJsonData: secureJsonData,
PluginVersion: "1.0.0",
Created: time.Now(),
Updated: time.Now(),
}
err = store.WithTransactionalDbSession(context.Background(), func(sess *db.Session) error {
affectedRows, innerErr := sess.Insert(&existing)
require.Equal(t, int64(1), affectedRows)
return innerErr
})
require.NoError(t, err)
require.Greater(t, existing.Id, int64(0))
t.Run("GetPluginSettings with orgID=0 should return all existing plugin settings", func(t *testing.T) {
pluginSettings, err := psService.GetPluginSettings(context.Background(), &pluginsettings.GetArgs{OrgID: 0})
require.NoError(t, err)
require.Len(t, pluginSettings, 1)
ps := pluginSettings[0]
require.Equal(t, existing.OrgId, ps.OrgID)
require.Equal(t, existing.PluginId, ps.PluginID)
require.False(t, ps.Enabled)
})
t.Run("GetPluginSettings with orgID=1 should return all existing plugin settings", func(t *testing.T) {
pluginSettings, err := psService.GetPluginSettings(context.Background(), &pluginsettings.GetArgs{OrgID: 1})
require.NoError(t, err)
require.Len(t, pluginSettings, 1)
ps := pluginSettings[0]
require.Equal(t, existing.OrgId, ps.OrgID)
require.Equal(t, existing.PluginId, ps.PluginID)
require.False(t, ps.Enabled)
})
t.Run("GetPluginSettingById should return existing plugin settings", func(t *testing.T) {
query := &pluginsettings.GetByPluginIDArgs{
OrgID: existing.OrgId,
PluginID: existing.PluginId,
}
ps, err := psService.GetPluginSettingByPluginID(context.Background(), query)
require.NoError(t, err)
require.NotNil(t, ps)
require.Equal(t, existing.OrgId, ps.OrgID)
require.Equal(t, existing.PluginId, ps.PluginID)
require.False(t, ps.Enabled)
require.NotNil(t, ps.JSONData)
require.Equal(t, existing.JsonData, ps.JSONData)
require.NotNil(t, ps.SecureJSONData)
require.Equal(t, existing.SecureJsonData, ps.SecureJSONData)
})
t.Run("UpdatePluginSetting should update existing plugin settings and publish PluginStateChangedEvent", func(t *testing.T) {
var pluginStateChangedEvent *pluginsettings.PluginStateChangedEvent
store.Bus().AddEventListener(func(_ context.Context, evt *pluginsettings.PluginStateChangedEvent) error {
pluginStateChangedEvent = evt
return nil
})
cmd := &pluginsettings.UpdateArgs{
OrgID: existing.OrgId,
PluginID: existing.PluginId,
Enabled: true,
PluginVersion: "1.0.1",
JSONData: map[string]interface{}{
"key2": "value2",
},
SecureJSONData: map[string]string{
"secureKey": "secureValue",
"secureKey2": "secureValue2",
},
Pinned: true,
}
err := psService.UpdatePluginSetting(context.Background(), cmd)
require.NoError(t, err)
require.NotNil(t, pluginStateChangedEvent)
require.Equal(t, existing.OrgId, pluginStateChangedEvent.OrgId)
require.Equal(t, existing.PluginId, pluginStateChangedEvent.PluginId)
require.True(t, pluginStateChangedEvent.Enabled)
err = psService.UpdatePluginSettingPluginVersion(context.Background(), &pluginsettings.UpdatePluginVersionArgs{
OrgID: cmd.OrgID,
PluginID: cmd.PluginID,
PluginVersion: "1.0.2",
})
require.NoError(t, err)
t.Run("GetPluginSettingById should return updated plugin settings", func(t *testing.T) {
query := &pluginsettings.GetByPluginIDArgs{
OrgID: existing.OrgId,
PluginID: existing.PluginId,
}
ps, err := psService.GetPluginSettingByPluginID(context.Background(), query)
require.NoError(t, err)
require.NotNil(t, ps)
require.Equal(t, existing.OrgId, ps.OrgID)
require.Equal(t, existing.PluginId, ps.PluginID)
require.True(t, ps.Enabled)
require.NotNil(t, ps.JSONData)
require.Equal(t, cmd.JSONData, ps.JSONData)
require.NotNil(t, ps.SecureJSONData)
require.Equal(t, cmd.SecureJSONData, psService.DecryptedValues(ps))
require.Equal(t, "1.0.2", ps.PluginVersion)
require.True(t, ps.Pinned)
})
})
})
t.Run("Non-existing plugin settings", func(t *testing.T) {
t.Run("UpdatePluginSetting should insert plugin settings and publish PluginStateChangedEvent", func(t *testing.T) {
var pluginStateChangedEvent *pluginsettings.PluginStateChangedEvent
store.Bus().AddEventListener(func(_ context.Context, evt *pluginsettings.PluginStateChangedEvent) error {
pluginStateChangedEvent = evt
return nil
})
cmd := &pluginsettings.UpdateArgs{
PluginID: "test",
Enabled: true,
OrgID: 1,
PluginVersion: "1.0.0",
JSONData: map[string]interface{}{
"key": "value",
},
SecureJSONData: map[string]string{
"secureKey": "secureValue",
},
}
err := psService.UpdatePluginSetting(context.Background(), cmd)
require.NoError(t, err)
require.NotNil(t, pluginStateChangedEvent)
require.Equal(t, cmd.OrgID, pluginStateChangedEvent.OrgId)
require.Equal(t, cmd.PluginID, pluginStateChangedEvent.PluginId)
require.True(t, pluginStateChangedEvent.Enabled)
t.Run("GetPluginSettingById should return inserted plugin settings", func(t *testing.T) {
query := &pluginsettings.GetByPluginIDArgs{
OrgID: cmd.OrgID,
PluginID: cmd.PluginID,
}
ps, err := psService.GetPluginSettingByPluginID(context.Background(), query)
require.NoError(t, err)
require.NotNil(t, ps)
require.Equal(t, cmd.OrgID, ps.OrgID)
require.Equal(t, cmd.PluginID, ps.PluginID)
require.True(t, ps.Enabled)
require.NotNil(t, ps.JSONData)
require.Equal(t, cmd.JSONData, ps.JSONData)
require.NotNil(t, ps.SecureJSONData)
require.Equal(t, cmd.PluginVersion, ps.PluginVersion)
require.False(t, ps.Pinned)
})
})
})
}