mirror of
https://github.com/grafana/grafana.git
synced 2025-09-27 13:03:53 +08:00
Secrets: Implement migration of secrets from plugin back to unified secrets (#53561)
* initial cut at migration from plugin * create new migration from plugin * only migrate to or from, not both * remove cfg check from plugin migration itself * update comments, clean up secret after migration * add better error handling * hook up REST API with migrations * Minor fixes * fix wire injection issue * modify migrator to access plugin calls directly. create unit tests * change pre-migration checks in admin api * stop plugin after migrating from it * fix compile issues after merge * add comment about migration * fix linting issue * bleh, fix unit test * fix another unit test * update plugin error fatal flag after a migration from the plugin * add extra logging to migration * make linter happy Co-authored-by: Leandro Deveikis <leandro.deveikis@gmail.com>
This commit is contained in:
@ -5,6 +5,7 @@ import (
|
|||||||
|
|
||||||
"github.com/grafana/grafana/pkg/api/response"
|
"github.com/grafana/grafana/pkg/api/response"
|
||||||
"github.com/grafana/grafana/pkg/models"
|
"github.com/grafana/grafana/pkg/models"
|
||||||
|
skv "github.com/grafana/grafana/pkg/services/secrets/kvstore"
|
||||||
)
|
)
|
||||||
|
|
||||||
func (hs *HTTPServer) AdminRotateDataEncryptionKeys(c *models.ReqContext) response.Response {
|
func (hs *HTTPServer) AdminRotateDataEncryptionKeys(c *models.ReqContext) response.Response {
|
||||||
@ -48,3 +49,33 @@ func (hs *HTTPServer) AdminRollbackSecrets(c *models.ReqContext) response.Respon
|
|||||||
|
|
||||||
return response.Respond(http.StatusOK, "Secrets rolled back successfully")
|
return response.Respond(http.StatusOK, "Secrets rolled back successfully")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// To migrate to the plugin, it must be installed and configured
|
||||||
|
// so as not to lose access to migrated secrets
|
||||||
|
func (hs *HTTPServer) MigrateSecretsToPlugin(c *models.ReqContext) response.Response {
|
||||||
|
if skv.EvaluateRemoteSecretsPlugin(hs.secretsPluginManager, hs.Cfg) != nil {
|
||||||
|
hs.log.Warn("Received secrets plugin migration request while plugin is not available")
|
||||||
|
return response.Respond(http.StatusBadRequest, "Secrets plugin is not available")
|
||||||
|
}
|
||||||
|
err := hs.secretsPluginMigrator.TriggerPluginMigration(c.Req.Context(), true)
|
||||||
|
if err != nil {
|
||||||
|
hs.log.Error("Failed to trigger secret migration to plugin", "error", err.Error())
|
||||||
|
return response.Respond(http.StatusInternalServerError, "Secret migration to plugin failed")
|
||||||
|
}
|
||||||
|
return response.Respond(http.StatusOK, "Secret migration to plugin triggered successfully")
|
||||||
|
}
|
||||||
|
|
||||||
|
// To migrate from the plugin, it must be installed only
|
||||||
|
// as it is possible the user disabled it and then wants to migrate
|
||||||
|
func (hs *HTTPServer) MigrateSecretsFromPlugin(c *models.ReqContext) response.Response {
|
||||||
|
if hs.secretsPluginManager.SecretsManager() == nil {
|
||||||
|
hs.log.Warn("Received secrets plugin migration request while plugin is not installed")
|
||||||
|
return response.Respond(http.StatusBadRequest, "Secrets plugin is not installed")
|
||||||
|
}
|
||||||
|
err := hs.secretsPluginMigrator.TriggerPluginMigration(c.Req.Context(), false)
|
||||||
|
if err != nil {
|
||||||
|
hs.log.Error("Failed to trigger secret migration from plugin", "error", err.Error())
|
||||||
|
return response.Respond(http.StatusInternalServerError, "Secret migration from plugin failed")
|
||||||
|
}
|
||||||
|
return response.Respond(http.StatusOK, "Secret migration from plugin triggered successfully")
|
||||||
|
}
|
||||||
|
@ -599,6 +599,8 @@ func (hs *HTTPServer) registerRoutes() {
|
|||||||
adminRoute.Post("/encryption/reencrypt-data-keys", reqGrafanaAdmin, routing.Wrap(hs.AdminReEncryptEncryptionKeys))
|
adminRoute.Post("/encryption/reencrypt-data-keys", reqGrafanaAdmin, routing.Wrap(hs.AdminReEncryptEncryptionKeys))
|
||||||
adminRoute.Post("/encryption/reencrypt-secrets", reqGrafanaAdmin, routing.Wrap(hs.AdminReEncryptSecrets))
|
adminRoute.Post("/encryption/reencrypt-secrets", reqGrafanaAdmin, routing.Wrap(hs.AdminReEncryptSecrets))
|
||||||
adminRoute.Post("/encryption/rollback-secrets", reqGrafanaAdmin, routing.Wrap(hs.AdminRollbackSecrets))
|
adminRoute.Post("/encryption/rollback-secrets", reqGrafanaAdmin, routing.Wrap(hs.AdminRollbackSecrets))
|
||||||
|
adminRoute.Post("/encryption/migrate-secrets/to-plugin", reqGrafanaAdmin, routing.Wrap(hs.MigrateSecretsToPlugin))
|
||||||
|
adminRoute.Post("/encryption/migrate-secrets/from-plugin", reqGrafanaAdmin, routing.Wrap(hs.MigrateSecretsFromPlugin))
|
||||||
|
|
||||||
adminRoute.Post("/provisioning/dashboards/reload", authorize(reqGrafanaAdmin, ac.EvalPermission(ActionProvisioningReload, ScopeProvisionersDashboards)), routing.Wrap(hs.AdminProvisioningReloadDashboards))
|
adminRoute.Post("/provisioning/dashboards/reload", authorize(reqGrafanaAdmin, ac.EvalPermission(ActionProvisioningReload, ScopeProvisionersDashboards)), routing.Wrap(hs.AdminProvisioningReloadDashboards))
|
||||||
adminRoute.Post("/provisioning/plugins/reload", authorize(reqGrafanaAdmin, ac.EvalPermission(ActionProvisioningReload, ScopeProvisionersPlugins)), routing.Wrap(hs.AdminProvisioningReloadPlugins))
|
adminRoute.Post("/provisioning/plugins/reload", authorize(reqGrafanaAdmin, ac.EvalPermission(ActionProvisioningReload, ScopeProvisionersPlugins)), routing.Wrap(hs.AdminProvisioningReloadPlugins))
|
||||||
|
@ -76,6 +76,7 @@ import (
|
|||||||
"github.com/grafana/grafana/pkg/services/search"
|
"github.com/grafana/grafana/pkg/services/search"
|
||||||
"github.com/grafana/grafana/pkg/services/searchusers"
|
"github.com/grafana/grafana/pkg/services/searchusers"
|
||||||
"github.com/grafana/grafana/pkg/services/secrets"
|
"github.com/grafana/grafana/pkg/services/secrets"
|
||||||
|
spm "github.com/grafana/grafana/pkg/services/secrets/kvstore/migrations"
|
||||||
"github.com/grafana/grafana/pkg/services/serviceaccounts"
|
"github.com/grafana/grafana/pkg/services/serviceaccounts"
|
||||||
"github.com/grafana/grafana/pkg/services/shorturls"
|
"github.com/grafana/grafana/pkg/services/shorturls"
|
||||||
"github.com/grafana/grafana/pkg/services/sqlstore"
|
"github.com/grafana/grafana/pkg/services/sqlstore"
|
||||||
@ -178,6 +179,7 @@ type HTTPServer struct {
|
|||||||
apiKeyService apikey.Service
|
apiKeyService apikey.Service
|
||||||
kvStore kvstore.KVStore
|
kvStore kvstore.KVStore
|
||||||
secretsMigrator secrets.Migrator
|
secretsMigrator secrets.Migrator
|
||||||
|
secretsPluginMigrator *spm.SecretMigrationServiceImpl
|
||||||
userService user.Service
|
userService user.Service
|
||||||
tempUserService tempUser.Service
|
tempUserService tempUser.Service
|
||||||
loginAttemptService loginAttempt.Service
|
loginAttemptService loginAttempt.Service
|
||||||
@ -217,7 +219,8 @@ func ProvideHTTPServer(opts ServerOptions, cfg *setting.Cfg, routeRegister routi
|
|||||||
teamsPermissionsService accesscontrol.TeamPermissionsService, folderPermissionsService accesscontrol.FolderPermissionsService,
|
teamsPermissionsService accesscontrol.TeamPermissionsService, folderPermissionsService accesscontrol.FolderPermissionsService,
|
||||||
dashboardPermissionsService accesscontrol.DashboardPermissionsService, dashboardVersionService dashver.Service,
|
dashboardPermissionsService accesscontrol.DashboardPermissionsService, dashboardVersionService dashver.Service,
|
||||||
starService star.Service, csrfService csrf.Service, coremodels *registry.Base,
|
starService star.Service, csrfService csrf.Service, coremodels *registry.Base,
|
||||||
playlistService playlist.Service, apiKeyService apikey.Service, kvStore kvstore.KVStore, secretsMigrator secrets.Migrator, secretsPluginManager plugins.SecretsPluginManager,
|
playlistService playlist.Service, apiKeyService apikey.Service, kvStore kvstore.KVStore,
|
||||||
|
secretsMigrator secrets.Migrator, secretsPluginManager plugins.SecretsPluginManager, secretsPluginMigrator *spm.SecretMigrationServiceImpl,
|
||||||
publicDashboardsApi *publicdashboardsApi.Api, userService user.Service, tempUserService tempUser.Service, loginAttemptService loginAttempt.Service, orgService org.Service,
|
publicDashboardsApi *publicdashboardsApi.Api, userService user.Service, tempUserService tempUser.Service, loginAttemptService loginAttempt.Service, orgService org.Service,
|
||||||
accesscontrolService accesscontrol.Service,
|
accesscontrolService accesscontrol.Service,
|
||||||
) (*HTTPServer, error) {
|
) (*HTTPServer, error) {
|
||||||
@ -307,6 +310,7 @@ func ProvideHTTPServer(opts ServerOptions, cfg *setting.Cfg, routeRegister routi
|
|||||||
kvStore: kvStore,
|
kvStore: kvStore,
|
||||||
PublicDashboardsApi: publicDashboardsApi,
|
PublicDashboardsApi: publicDashboardsApi,
|
||||||
secretsMigrator: secretsMigrator,
|
secretsMigrator: secretsMigrator,
|
||||||
|
secretsPluginMigrator: secretsPluginMigrator,
|
||||||
userService: userService,
|
userService: userService,
|
||||||
tempUserService: tempUserService,
|
tempUserService: tempUserService,
|
||||||
loginAttemptService: loginAttemptService,
|
loginAttemptService: loginAttemptService,
|
||||||
|
@ -307,7 +307,7 @@ var wireSet = wire.NewSet(
|
|||||||
userimpl.ProvideService,
|
userimpl.ProvideService,
|
||||||
orgimpl.ProvideService,
|
orgimpl.ProvideService,
|
||||||
datasourceservice.ProvideDataSourceMigrationService,
|
datasourceservice.ProvideDataSourceMigrationService,
|
||||||
secretsStore.ProvidePluginSecretMigrationService,
|
secretsStore.ProvideMigrateToPluginService,
|
||||||
secretsMigrations.ProvideSecretMigrationService,
|
secretsMigrations.ProvideSecretMigrationService,
|
||||||
wire.Bind(new(secretsMigrations.SecretMigrationService), new(*secretsMigrations.SecretMigrationServiceImpl)),
|
wire.Bind(new(secretsMigrations.SecretMigrationService), new(*secretsMigrations.SecretMigrationServiceImpl)),
|
||||||
userauthimpl.ProvideService,
|
userauthimpl.ProvideService,
|
||||||
|
@ -147,7 +147,7 @@ func (sl *ServerLockService) acquireForRelease(ctx context.Context, actionName s
|
|||||||
if len(lockRows) > 0 {
|
if len(lockRows) > 0 {
|
||||||
result := lockRows[0]
|
result := lockRows[0]
|
||||||
if sl.isLockWithinInterval(result, maxInterval) {
|
if sl.isLockWithinInterval(result, maxInterval) {
|
||||||
return errors.New("there is already a lock for this operation")
|
return errors.New("there is already a lock for this actionName: " + actionName)
|
||||||
} else {
|
} else {
|
||||||
// lock has timeouted, so we update the timestamp
|
// lock has timeouted, so we update the timestamp
|
||||||
result.LastExecution = time.Now().Unix()
|
result.LastExecution = time.Now().Unix()
|
||||||
@ -157,7 +157,7 @@ func (sl *ServerLockService) acquireForRelease(ctx context.Context, actionName s
|
|||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
if affected != 1 {
|
if affected != 1 {
|
||||||
sl.log.Error("Expected rows affected to be 1 if there was no error.", "rowAffected", affected)
|
sl.log.Error("Expected rows affected to be 1 if there was no error.", "actionName", actionName, "rowAffected", affected)
|
||||||
}
|
}
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
@ -175,7 +175,7 @@ func (sl *ServerLockService) acquireForRelease(ctx context.Context, actionName s
|
|||||||
|
|
||||||
if affected != 1 {
|
if affected != 1 {
|
||||||
// this means that there was no error but there is something not working correctly
|
// this means that there was no error but there is something not working correctly
|
||||||
sl.log.Error("Expected rows affected to be 1 if there was no error.", "rowAffected", affected)
|
sl.log.Error("Expected rows affected to be 1 if there was no error.", "actionName", actionName, "rowAffected", affected)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return nil
|
return nil
|
||||||
@ -195,7 +195,7 @@ func (sl *ServerLockService) releaseLock(ctx context.Context, actionName string)
|
|||||||
}
|
}
|
||||||
affected, err := res.RowsAffected()
|
affected, err := res.RowsAffected()
|
||||||
if affected != 1 {
|
if affected != 1 {
|
||||||
sl.log.Debug("Error releasing lock ", "affected", affected)
|
sl.log.Debug("Error releasing lock ", "actionName", actionName, "affected", affected)
|
||||||
}
|
}
|
||||||
return err
|
return err
|
||||||
})
|
})
|
||||||
|
@ -88,7 +88,7 @@ func TestLockAndRelease(t *testing.T) {
|
|||||||
|
|
||||||
err2 := sl.acquireForRelease(context.Background(), operationUID, duration)
|
err2 := sl.acquireForRelease(context.Background(), operationUID, duration)
|
||||||
require.Error(t, err2, "We should expect an error when trying to get the second lock")
|
require.Error(t, err2, "We should expect an error when trying to get the second lock")
|
||||||
require.Equal(t, "there is already a lock for this operation", err2.Error())
|
require.Equal(t, "there is already a lock for this actionName: "+operationUID, err2.Error())
|
||||||
|
|
||||||
err3 := sl.releaseLock(context.Background(), operationUID)
|
err3 := sl.releaseLock(context.Background(), operationUID)
|
||||||
require.NoError(t, err3)
|
require.NoError(t, err3)
|
||||||
|
@ -319,7 +319,8 @@ var wireBasicSet = wire.NewSet(
|
|||||||
tempuserimpl.ProvideService,
|
tempuserimpl.ProvideService,
|
||||||
loginattemptimpl.ProvideService,
|
loginattemptimpl.ProvideService,
|
||||||
datasourceservice.ProvideDataSourceMigrationService,
|
datasourceservice.ProvideDataSourceMigrationService,
|
||||||
secretsStore.ProvidePluginSecretMigrationService,
|
secretsStore.ProvideMigrateToPluginService,
|
||||||
|
secretsStore.ProvideMigrateFromPluginService,
|
||||||
secretsMigrations.ProvideSecretMigrationService,
|
secretsMigrations.ProvideSecretMigrationService,
|
||||||
wire.Bind(new(secretsMigrations.SecretMigrationService), new(*secretsMigrations.SecretMigrationServiceImpl)),
|
wire.Bind(new(secretsMigrations.SecretMigrationService), new(*secretsMigrations.SecretMigrationServiceImpl)),
|
||||||
userauthimpl.ProvideService,
|
userauthimpl.ProvideService,
|
||||||
|
116
pkg/services/secrets/kvstore/migrate_from_plugin.go
Normal file
116
pkg/services/secrets/kvstore/migrate_from_plugin.go
Normal file
@ -0,0 +1,116 @@
|
|||||||
|
package kvstore
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"fmt"
|
||||||
|
"sync"
|
||||||
|
|
||||||
|
"github.com/grafana/grafana/pkg/infra/kvstore"
|
||||||
|
"github.com/grafana/grafana/pkg/infra/log"
|
||||||
|
"github.com/grafana/grafana/pkg/plugins"
|
||||||
|
"github.com/grafana/grafana/pkg/plugins/backendplugin/secretsmanagerplugin"
|
||||||
|
"github.com/grafana/grafana/pkg/services/secrets"
|
||||||
|
"github.com/grafana/grafana/pkg/services/sqlstore"
|
||||||
|
"github.com/grafana/grafana/pkg/setting"
|
||||||
|
)
|
||||||
|
|
||||||
|
// MigrateFromPluginService This migrator will handle migration of the configured plugin secrets back to Grafana unified secrets
|
||||||
|
type MigrateFromPluginService struct {
|
||||||
|
cfg *setting.Cfg
|
||||||
|
logger log.Logger
|
||||||
|
sqlStore sqlstore.Store
|
||||||
|
secretsService secrets.Service
|
||||||
|
manager plugins.SecretsPluginManager
|
||||||
|
kvstore kvstore.KVStore
|
||||||
|
}
|
||||||
|
|
||||||
|
func ProvideMigrateFromPluginService(
|
||||||
|
cfg *setting.Cfg,
|
||||||
|
sqlStore sqlstore.Store,
|
||||||
|
secretsService secrets.Service,
|
||||||
|
manager plugins.SecretsPluginManager,
|
||||||
|
kvstore kvstore.KVStore,
|
||||||
|
|
||||||
|
) *MigrateFromPluginService {
|
||||||
|
return &MigrateFromPluginService{
|
||||||
|
cfg: cfg,
|
||||||
|
logger: log.New("sec-plugin-mig"),
|
||||||
|
sqlStore: sqlStore,
|
||||||
|
secretsService: secretsService,
|
||||||
|
manager: manager,
|
||||||
|
kvstore: kvstore,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *MigrateFromPluginService) Migrate(ctx context.Context) error {
|
||||||
|
s.logger.Debug("starting migration of plugin secrets to unified secrets")
|
||||||
|
// access the plugin directly
|
||||||
|
plugin, err := startAndReturnPlugin(s.manager, context.Background())
|
||||||
|
if err != nil {
|
||||||
|
s.logger.Error("Error retrieiving plugin", "error", err.Error())
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
// Get full list of secrets from the plugin
|
||||||
|
res, err := plugin.GetAllSecrets(ctx, &secretsmanagerplugin.GetAllSecretsRequest{})
|
||||||
|
if err != nil {
|
||||||
|
s.logger.Error("Failed to retrieve all secrets from plugin")
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
totalSecrets := len(res.Items)
|
||||||
|
s.logger.Debug("retrieved all secrets from plugin", "num secrets", totalSecrets)
|
||||||
|
// create a secret sql store manually
|
||||||
|
secretsSql := &secretsKVStoreSQL{
|
||||||
|
sqlStore: s.sqlStore,
|
||||||
|
secretsService: s.secretsService,
|
||||||
|
log: s.logger,
|
||||||
|
decryptionCache: decryptionCache{
|
||||||
|
cache: make(map[int64]cachedDecrypted),
|
||||||
|
},
|
||||||
|
}
|
||||||
|
for i, item := range res.Items {
|
||||||
|
s.logger.Debug(fmt.Sprintf("Migrating secret %d of %d", i+1, totalSecrets), "current", i+1, "secretCount", totalSecrets)
|
||||||
|
// Add to sql store
|
||||||
|
err = secretsSql.Set(ctx, item.Key.OrgId, item.Key.Namespace, item.Key.Type, item.Value)
|
||||||
|
if err != nil {
|
||||||
|
s.logger.Error("Error adding secret to unified secrets", "orgId", item.Key.OrgId,
|
||||||
|
"namespace", item.Key.Namespace, "type", item.Key.Type)
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
for i, item := range res.Items {
|
||||||
|
s.logger.Debug(fmt.Sprintf("Cleaning secret %d of %d", i+1, totalSecrets), "current", i+1, "secretCount", totalSecrets)
|
||||||
|
// Delete from the plugin
|
||||||
|
_, err := plugin.DeleteSecret(ctx, &secretsmanagerplugin.DeleteSecretRequest{
|
||||||
|
KeyDescriptor: &secretsmanagerplugin.Key{
|
||||||
|
OrgId: item.Key.OrgId,
|
||||||
|
Namespace: item.Key.Namespace,
|
||||||
|
Type: item.Key.Type,
|
||||||
|
}})
|
||||||
|
if err != nil {
|
||||||
|
s.logger.Error("Error deleting secret from plugin after migration", "orgId", item.Key.OrgId,
|
||||||
|
"namespace", item.Key.Namespace, "type", item.Key.Type)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
}
|
||||||
|
s.logger.Debug("Completed migration of secrets from plugin")
|
||||||
|
|
||||||
|
// The plugin is no longer needed at the moment
|
||||||
|
err = setPluginStartupErrorFatal(ctx, GetNamespacedKVStore(s.kvstore), false)
|
||||||
|
if err != nil {
|
||||||
|
s.logger.Error("Failed to remove plugin error fatal flag", "error", err.Error())
|
||||||
|
}
|
||||||
|
// Reset the fatal flag setter in case another secret is created on the plugin
|
||||||
|
fatalFlagOnce = sync.Once{}
|
||||||
|
|
||||||
|
s.logger.Debug("Shutting down secrets plugin now that migration is complete")
|
||||||
|
// if `use_plugin` wasn't set, stop the plugin after migration
|
||||||
|
if !s.cfg.SectionWithEnvOverrides("secrets").Key("use_plugin").MustBool(false) {
|
||||||
|
err := s.manager.SecretsManager().Stop(ctx)
|
||||||
|
if err != nil {
|
||||||
|
// Log a warning but don't throw an error
|
||||||
|
s.logger.Error("Error stopping secrets plugin after migration", "error", err.Error())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
97
pkg/services/secrets/kvstore/migrate_from_plugin_test.go
Normal file
97
pkg/services/secrets/kvstore/migrate_from_plugin_test.go
Normal file
@ -0,0 +1,97 @@
|
|||||||
|
package kvstore
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"github.com/grafana/grafana/pkg/infra/kvstore"
|
||||||
|
"github.com/grafana/grafana/pkg/infra/log"
|
||||||
|
"github.com/grafana/grafana/pkg/plugins/backendplugin/secretsmanagerplugin"
|
||||||
|
"github.com/grafana/grafana/pkg/services/secrets/fakes"
|
||||||
|
secretsManager "github.com/grafana/grafana/pkg/services/secrets/manager"
|
||||||
|
"github.com/grafana/grafana/pkg/services/sqlstore"
|
||||||
|
"github.com/grafana/grafana/pkg/setting"
|
||||||
|
"github.com/stretchr/testify/require"
|
||||||
|
)
|
||||||
|
|
||||||
|
// This tests will create a mock sql database and an inmemory
|
||||||
|
// implementation of the secret manager to simulate the plugin.
|
||||||
|
func TestPluginSecretMigrationService_MigrateFromPlugin(t *testing.T) {
|
||||||
|
ctx := context.Background()
|
||||||
|
|
||||||
|
t.Run("migrate secrets from secrets plugin to Grafana", func(t *testing.T) {
|
||||||
|
// --- SETUP
|
||||||
|
migratorService, plugin, sqlStore := setupTestMigrateFromPluginService(t)
|
||||||
|
|
||||||
|
addSecretToPluginStore(t, plugin, ctx, 1, "secret-1", "bogus", "value-1")
|
||||||
|
addSecretToPluginStore(t, plugin, ctx, 1, "secret-2", "bogus", "value-2")
|
||||||
|
|
||||||
|
// --- EXECUTION
|
||||||
|
err := migratorService.Migrate(ctx)
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
// --- VALIDATIONS
|
||||||
|
validatePluginSecretsWereDeleted(t, plugin, ctx)
|
||||||
|
|
||||||
|
validateSecretWasStoredInSql(t, sqlStore, ctx, 1, "secret-1", "bogus", "value-1")
|
||||||
|
validateSecretWasStoredInSql(t, sqlStore, ctx, 1, "secret-2", "bogus", "value-2")
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// Set up services used in migration
|
||||||
|
func setupTestMigrateFromPluginService(t *testing.T) (*MigrateFromPluginService, secretsmanagerplugin.SecretsManagerPlugin, *secretsKVStoreSQL) {
|
||||||
|
t.Helper()
|
||||||
|
|
||||||
|
// this is to init the sql secret store inside the migration
|
||||||
|
sqlStore := sqlstore.InitTestDB(t)
|
||||||
|
secretsService := secretsManager.SetupTestService(t, fakes.NewFakeSecretsStore())
|
||||||
|
manager := NewFakeSecretsPluginManager(t, false)
|
||||||
|
migratorService := ProvideMigrateFromPluginService(
|
||||||
|
setting.NewCfg(),
|
||||||
|
sqlStore,
|
||||||
|
secretsService,
|
||||||
|
manager,
|
||||||
|
kvstore.ProvideService(sqlStore),
|
||||||
|
)
|
||||||
|
|
||||||
|
secretsSql := &secretsKVStoreSQL{
|
||||||
|
sqlStore: sqlStore,
|
||||||
|
secretsService: secretsService,
|
||||||
|
log: log.New("test.logger"),
|
||||||
|
decryptionCache: decryptionCache{
|
||||||
|
cache: make(map[int64]cachedDecrypted),
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
return migratorService, manager.SecretsManager().SecretsManager, secretsSql
|
||||||
|
}
|
||||||
|
|
||||||
|
func addSecretToPluginStore(t *testing.T, plugin secretsmanagerplugin.SecretsManagerPlugin, ctx context.Context, orgId int64, namespace string, typ string, value string) {
|
||||||
|
t.Helper()
|
||||||
|
_, err := plugin.SetSecret(ctx, &secretsmanagerplugin.SetSecretRequest{
|
||||||
|
KeyDescriptor: &secretsmanagerplugin.Key{
|
||||||
|
OrgId: orgId,
|
||||||
|
Namespace: namespace,
|
||||||
|
Type: typ,
|
||||||
|
},
|
||||||
|
Value: value,
|
||||||
|
})
|
||||||
|
require.NoError(t, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// validates that secrets on the plugin were deleted
|
||||||
|
func validatePluginSecretsWereDeleted(t *testing.T, plugin secretsmanagerplugin.SecretsManagerPlugin, ctx context.Context) {
|
||||||
|
t.Helper()
|
||||||
|
res, err := plugin.GetAllSecrets(ctx, &secretsmanagerplugin.GetAllSecretsRequest{})
|
||||||
|
require.NoError(t, err)
|
||||||
|
require.Equal(t, 0, len(res.Items))
|
||||||
|
}
|
||||||
|
|
||||||
|
// validates that secrets are in sql
|
||||||
|
func validateSecretWasStoredInSql(t *testing.T, sqlStore *secretsKVStoreSQL, ctx context.Context, orgId int64, namespace string, typ string, expectedValue string) {
|
||||||
|
t.Helper()
|
||||||
|
res, exists, err := sqlStore.Get(ctx, orgId, namespace, typ)
|
||||||
|
require.NoError(t, err)
|
||||||
|
require.True(t, exists)
|
||||||
|
require.Equal(t, expectedValue, res)
|
||||||
|
}
|
@ -13,9 +13,9 @@ import (
|
|||||||
"github.com/grafana/grafana/pkg/setting"
|
"github.com/grafana/grafana/pkg/setting"
|
||||||
)
|
)
|
||||||
|
|
||||||
// PluginSecretMigrationService This migrator will handle migration of datasource secrets (aka Unified secrets)
|
// MigrateToPluginService This migrator will handle migration of datasource secrets (aka Unified secrets)
|
||||||
// into the plugin secrets configured
|
// into the plugin secrets configured
|
||||||
type PluginSecretMigrationService struct {
|
type MigrateToPluginService struct {
|
||||||
secretsStore SecretsKVStore
|
secretsStore SecretsKVStore
|
||||||
cfg *setting.Cfg
|
cfg *setting.Cfg
|
||||||
logger log.Logger
|
logger log.Logger
|
||||||
@ -25,15 +25,15 @@ type PluginSecretMigrationService struct {
|
|||||||
manager plugins.SecretsPluginManager
|
manager plugins.SecretsPluginManager
|
||||||
}
|
}
|
||||||
|
|
||||||
func ProvidePluginSecretMigrationService(
|
func ProvideMigrateToPluginService(
|
||||||
secretsStore SecretsKVStore,
|
secretsStore SecretsKVStore,
|
||||||
cfg *setting.Cfg,
|
cfg *setting.Cfg,
|
||||||
sqlStore sqlstore.Store,
|
sqlStore sqlstore.Store,
|
||||||
secretsService secrets.Service,
|
secretsService secrets.Service,
|
||||||
kvstore kvstore.KVStore,
|
kvstore kvstore.KVStore,
|
||||||
manager plugins.SecretsPluginManager,
|
manager plugins.SecretsPluginManager,
|
||||||
) *PluginSecretMigrationService {
|
) *MigrateToPluginService {
|
||||||
return &PluginSecretMigrationService{
|
return &MigrateToPluginService{
|
||||||
secretsStore: secretsStore,
|
secretsStore: secretsStore,
|
||||||
cfg: cfg,
|
cfg: cfg,
|
||||||
logger: log.New("secret.migration.plugin"),
|
logger: log.New("secret.migration.plugin"),
|
||||||
@ -44,8 +44,7 @@ func ProvidePluginSecretMigrationService(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *PluginSecretMigrationService) Migrate(ctx context.Context) error {
|
func (s *MigrateToPluginService) Migrate(ctx context.Context) error {
|
||||||
// Check if we should migrate to plugin - default false
|
|
||||||
if err := EvaluateRemoteSecretsPlugin(s.manager, s.cfg); err == nil {
|
if err := EvaluateRemoteSecretsPlugin(s.manager, s.cfg); err == nil {
|
||||||
s.logger.Debug("starting migration of unified secrets to the plugin")
|
s.logger.Debug("starting migration of unified secrets to the plugin")
|
||||||
// we need to get the fallback store since in this scenario the secrets store would be the plugin.
|
// we need to get the fallback store since in this scenario the secrets store would be the plugin.
|
@ -9,7 +9,6 @@ import (
|
|||||||
"github.com/grafana/grafana/pkg/services/secrets/fakes"
|
"github.com/grafana/grafana/pkg/services/secrets/fakes"
|
||||||
secretsManager "github.com/grafana/grafana/pkg/services/secrets/manager"
|
secretsManager "github.com/grafana/grafana/pkg/services/secrets/manager"
|
||||||
"github.com/grafana/grafana/pkg/services/sqlstore"
|
"github.com/grafana/grafana/pkg/services/sqlstore"
|
||||||
|
|
||||||
"github.com/grafana/grafana/pkg/setting"
|
"github.com/grafana/grafana/pkg/setting"
|
||||||
"github.com/stretchr/testify/require"
|
"github.com/stretchr/testify/require"
|
||||||
"gopkg.in/ini.v1"
|
"gopkg.in/ini.v1"
|
||||||
@ -17,12 +16,12 @@ import (
|
|||||||
|
|
||||||
// This tests will create a mock sql database and an inmemory
|
// This tests will create a mock sql database and an inmemory
|
||||||
// implementation of the secret manager to simulate the plugin.
|
// implementation of the secret manager to simulate the plugin.
|
||||||
func TestPluginSecretMigrationService_Migrate(t *testing.T) {
|
func TestPluginSecretMigrationService_MigrateToPlugin(t *testing.T) {
|
||||||
ctx := context.Background()
|
ctx := context.Background()
|
||||||
|
|
||||||
t.Run("migration run ok - 2 secrets migrated", func(t *testing.T) {
|
t.Run("migration run ok - 2 secrets migrated", func(t *testing.T) {
|
||||||
// --- SETUP
|
// --- SETUP
|
||||||
migratorService, secretsStore, sqlSecretStore := setupTestMigratorService(t)
|
migratorService, secretsStore, sqlSecretStore := setupTestMigrateToPluginService(t)
|
||||||
var orgId int64 = 1
|
var orgId int64 = 1
|
||||||
namespace1, namespace2 := "namespace-test", "namespace-test2"
|
namespace1, namespace2 := "namespace-test", "namespace-test2"
|
||||||
typ := "type-test"
|
typ := "type-test"
|
||||||
@ -36,41 +35,43 @@ func TestPluginSecretMigrationService_Migrate(t *testing.T) {
|
|||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
|
|
||||||
// --- VALIDATIONS
|
// --- VALIDATIONS
|
||||||
validateSecretWasDeleted(t, sqlSecretStore, ctx, orgId, namespace1, typ)
|
validateSqlSecretWasDeleted(t, sqlSecretStore, ctx, orgId, namespace1, typ)
|
||||||
validateSecretWasDeleted(t, sqlSecretStore, ctx, orgId, namespace2, typ)
|
validateSqlSecretWasDeleted(t, sqlSecretStore, ctx, orgId, namespace2, typ)
|
||||||
|
|
||||||
validateSecretWasStoreInPlugin(t, secretsStore, ctx, orgId, namespace1, typ)
|
validateSecretWasStoredInPlugin(t, secretsStore, ctx, orgId, namespace1, typ)
|
||||||
validateSecretWasStoreInPlugin(t, secretsStore, ctx, orgId, namespace1, typ)
|
validateSecretWasStoredInPlugin(t, secretsStore, ctx, orgId, namespace1, typ)
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
func addSecretToSqlStore(t *testing.T, sqlSecretStore *secretsKVStoreSQL, ctx context.Context, orgId int64, namespace1 string, typ string, value string) {
|
func addSecretToSqlStore(t *testing.T, sqlSecretStore *secretsKVStoreSQL, ctx context.Context, orgId int64, namespace1 string, typ string, value string) {
|
||||||
|
t.Helper()
|
||||||
err := sqlSecretStore.Set(ctx, orgId, namespace1, typ, value)
|
err := sqlSecretStore.Set(ctx, orgId, namespace1, typ, value)
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
}
|
}
|
||||||
|
|
||||||
// validates that secrets on the sql store were deleted.
|
// validates that secrets on the sql store were deleted.
|
||||||
func validateSecretWasDeleted(t *testing.T, sqlSecretStore *secretsKVStoreSQL, ctx context.Context, orgId int64, namespace1 string, typ string) {
|
func validateSqlSecretWasDeleted(t *testing.T, sqlSecretStore *secretsKVStoreSQL, ctx context.Context, orgId int64, namespace1 string, typ string) {
|
||||||
|
t.Helper()
|
||||||
res, err := sqlSecretStore.Keys(ctx, orgId, namespace1, typ)
|
res, err := sqlSecretStore.Keys(ctx, orgId, namespace1, typ)
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
require.Equal(t, 0, len(res))
|
require.Equal(t, 0, len(res))
|
||||||
}
|
}
|
||||||
|
|
||||||
// validates that secrets should be on the plugin
|
// validates that secrets should be on the plugin
|
||||||
func validateSecretWasStoreInPlugin(t *testing.T, secretsStore SecretsKVStore, ctx context.Context, orgId int64, namespace1 string, typ string) {
|
func validateSecretWasStoredInPlugin(t *testing.T, secretsStore SecretsKVStore, ctx context.Context, orgId int64, namespace1 string, typ string) {
|
||||||
|
t.Helper()
|
||||||
resPlugin, err := secretsStore.Keys(ctx, orgId, namespace1, typ)
|
resPlugin, err := secretsStore.Keys(ctx, orgId, namespace1, typ)
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
require.Equal(t, 1, len(resPlugin))
|
require.Equal(t, 1, len(resPlugin))
|
||||||
}
|
}
|
||||||
|
|
||||||
//
|
// Set up services used in migration
|
||||||
func setupTestMigratorService(t *testing.T) (*PluginSecretMigrationService, SecretsKVStore, *secretsKVStoreSQL) {
|
func setupTestMigrateToPluginService(t *testing.T) (*MigrateToPluginService, SecretsKVStore, *secretsKVStoreSQL) {
|
||||||
t.Helper()
|
t.Helper()
|
||||||
|
|
||||||
rawCfg := `
|
rawCfg := `
|
||||||
[secrets]
|
[secrets]
|
||||||
use_plugin = true
|
use_plugin = true
|
||||||
migrate_to_plugin = true
|
|
||||||
`
|
`
|
||||||
raw, err := ini.Load([]byte(rawCfg))
|
raw, err := ini.Load([]byte(rawCfg))
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
@ -82,7 +83,7 @@ func setupTestMigratorService(t *testing.T) (*PluginSecretMigrationService, Secr
|
|||||||
sqlStore := sqlstore.InitTestDB(t)
|
sqlStore := sqlstore.InitTestDB(t)
|
||||||
secretsService := secretsManager.SetupTestService(t, fakes.NewFakeSecretsStore())
|
secretsService := secretsManager.SetupTestService(t, fakes.NewFakeSecretsStore())
|
||||||
manager := NewFakeSecretsPluginManager(t, false)
|
manager := NewFakeSecretsPluginManager(t, false)
|
||||||
migratorService := ProvidePluginSecretMigrationService(
|
migratorService := ProvideMigrateToPluginService(
|
||||||
secretsStoreForPlugin,
|
secretsStoreForPlugin,
|
||||||
cfg,
|
cfg,
|
||||||
sqlStore,
|
sqlStore,
|
@ -9,41 +9,58 @@ import (
|
|||||||
"github.com/grafana/grafana/pkg/infra/serverlock"
|
"github.com/grafana/grafana/pkg/infra/serverlock"
|
||||||
datasources "github.com/grafana/grafana/pkg/services/datasources/service"
|
datasources "github.com/grafana/grafana/pkg/services/datasources/service"
|
||||||
"github.com/grafana/grafana/pkg/services/secrets/kvstore"
|
"github.com/grafana/grafana/pkg/services/secrets/kvstore"
|
||||||
|
"github.com/grafana/grafana/pkg/setting"
|
||||||
)
|
)
|
||||||
|
|
||||||
var logger = log.New("secret.migration")
|
var logger = log.New("secret.migration")
|
||||||
|
|
||||||
|
const actionName = "secret migration task "
|
||||||
|
|
||||||
// SecretMigrationService is used to migrate legacy secrets to new unified secrets.
|
// SecretMigrationService is used to migrate legacy secrets to new unified secrets.
|
||||||
type SecretMigrationService interface {
|
type SecretMigrationService interface {
|
||||||
Migrate(ctx context.Context) error
|
Migrate(ctx context.Context) error
|
||||||
}
|
}
|
||||||
|
|
||||||
type SecretMigrationServiceImpl struct {
|
type SecretMigrationServiceImpl struct {
|
||||||
Services []SecretMigrationService
|
services []SecretMigrationService
|
||||||
ServerLockService *serverlock.ServerLockService
|
ServerLockService *serverlock.ServerLockService
|
||||||
|
migrateToPluginService *kvstore.MigrateToPluginService
|
||||||
|
migrateFromPluginService *kvstore.MigrateFromPluginService
|
||||||
}
|
}
|
||||||
|
|
||||||
func ProvideSecretMigrationService(
|
func ProvideSecretMigrationService(
|
||||||
|
cfg *setting.Cfg,
|
||||||
serverLockService *serverlock.ServerLockService,
|
serverLockService *serverlock.ServerLockService,
|
||||||
dataSourceSecretMigrationService *datasources.DataSourceSecretMigrationService,
|
dataSourceSecretMigrationService *datasources.DataSourceSecretMigrationService,
|
||||||
pluginSecretMigrationService *kvstore.PluginSecretMigrationService,
|
migrateToPluginService *kvstore.MigrateToPluginService,
|
||||||
|
migrateFromPluginService *kvstore.MigrateFromPluginService,
|
||||||
) *SecretMigrationServiceImpl {
|
) *SecretMigrationServiceImpl {
|
||||||
services := make([]SecretMigrationService, 0)
|
services := make([]SecretMigrationService, 0)
|
||||||
services = append(services, dataSourceSecretMigrationService)
|
services = append(services, dataSourceSecretMigrationService)
|
||||||
// pluginMigrationService should always be the last one
|
// Plugin migration should always be last; should either migrate to or from, not both
|
||||||
services = append(services, pluginSecretMigrationService)
|
// This is because the migrateTo checks for use_plugin = true, in which case we should always
|
||||||
|
// migrate by default to ensure users don't lose access to secrets. If migration has
|
||||||
|
// already occurred, the migrateTo function will be called but it won't do anything
|
||||||
|
if cfg.SectionWithEnvOverrides("secrets").Key("migrate_from_plugin").MustBool(false) {
|
||||||
|
services = append(services, migrateFromPluginService)
|
||||||
|
} else {
|
||||||
|
services = append(services, migrateToPluginService)
|
||||||
|
}
|
||||||
|
|
||||||
return &SecretMigrationServiceImpl{
|
return &SecretMigrationServiceImpl{
|
||||||
ServerLockService: serverLockService,
|
ServerLockService: serverLockService,
|
||||||
Services: services,
|
services: services,
|
||||||
|
migrateToPluginService: migrateToPluginService,
|
||||||
|
migrateFromPluginService: migrateFromPluginService,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Migrate Run migration services. This will block until all services have exited.
|
// Migrate Run migration services. This will block until all services have exited.
|
||||||
|
// This should only be called once at startup
|
||||||
func (s *SecretMigrationServiceImpl) Migrate(ctx context.Context) error {
|
func (s *SecretMigrationServiceImpl) Migrate(ctx context.Context) error {
|
||||||
// Start migration services.
|
// Start migration services.
|
||||||
return s.ServerLockService.LockAndExecute(ctx, "migrate secrets to unified secrets", time.Minute*10, func(context.Context) {
|
return s.ServerLockService.LockExecuteAndRelease(ctx, actionName, time.Minute*10, func(context.Context) {
|
||||||
for _, service := range s.Services {
|
for _, service := range s.services {
|
||||||
serviceName := reflect.TypeOf(service).String()
|
serviceName := reflect.TypeOf(service).String()
|
||||||
logger.Debug("Starting secret migration service", "service", serviceName)
|
logger.Debug("Starting secret migration service", "service", serviceName)
|
||||||
err := service.Migrate(ctx)
|
err := service.Migrate(ctx)
|
||||||
@ -54,3 +71,23 @@ func (s *SecretMigrationServiceImpl) Migrate(ctx context.Context) error {
|
|||||||
}
|
}
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// TriggerPluginMigration Kick off a migration to or from the plugin. This will block until all services have exited.
|
||||||
|
func (s *SecretMigrationServiceImpl) TriggerPluginMigration(ctx context.Context, toPlugin bool) error {
|
||||||
|
// Don't migrate if there is already one happening
|
||||||
|
return s.ServerLockService.LockExecuteAndRelease(ctx, actionName, time.Minute*10, func(context.Context) {
|
||||||
|
var err error
|
||||||
|
if toPlugin {
|
||||||
|
err = s.migrateToPluginService.Migrate(ctx)
|
||||||
|
} else {
|
||||||
|
err = s.migrateFromPluginService.Migrate(ctx)
|
||||||
|
}
|
||||||
|
if err != nil {
|
||||||
|
direction := "from_plugin"
|
||||||
|
if toPlugin {
|
||||||
|
direction = "to_plugin"
|
||||||
|
}
|
||||||
|
logger.Error("Failed to migrate plugin secrets", "direction", direction, "error", err.Error())
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
@ -3,10 +3,13 @@ package kvstore
|
|||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
"errors"
|
"errors"
|
||||||
|
"fmt"
|
||||||
"sync"
|
"sync"
|
||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
"github.com/grafana/grafana/pkg/infra/kvstore"
|
"github.com/grafana/grafana/pkg/infra/kvstore"
|
||||||
|
"github.com/grafana/grafana/pkg/plugins"
|
||||||
|
"github.com/grafana/grafana/pkg/plugins/backendplugin/secretsmanagerplugin"
|
||||||
"github.com/grafana/grafana/pkg/services/secrets/fakes"
|
"github.com/grafana/grafana/pkg/services/secrets/fakes"
|
||||||
secretsManager "github.com/grafana/grafana/pkg/services/secrets/manager"
|
secretsManager "github.com/grafana/grafana/pkg/services/secrets/manager"
|
||||||
"github.com/grafana/grafana/pkg/services/sqlstore"
|
"github.com/grafana/grafana/pkg/services/sqlstore"
|
||||||
@ -19,7 +22,8 @@ import (
|
|||||||
// Set fatal flag to true, then simulate a plugin start failure
|
// Set fatal flag to true, then simulate a plugin start failure
|
||||||
// Should result in an error from the secret store provider
|
// Should result in an error from the secret store provider
|
||||||
func TestFatalPluginErr_PluginFailsToStartWithFatalFlagSet(t *testing.T) {
|
func TestFatalPluginErr_PluginFailsToStartWithFatalFlagSet(t *testing.T) {
|
||||||
svc, _, _, err := setupFatalCrashTest(t, true, true, false)
|
svc, mgr, _, _, err := setupFatalCrashTest(t, true, true, false)
|
||||||
|
_ = fmt.Sprint(mgr) // this is here to satisfy the linter
|
||||||
require.Error(t, err)
|
require.Error(t, err)
|
||||||
require.Nil(t, svc)
|
require.Nil(t, svc)
|
||||||
}
|
}
|
||||||
@ -27,7 +31,8 @@ func TestFatalPluginErr_PluginFailsToStartWithFatalFlagSet(t *testing.T) {
|
|||||||
// Set fatal flag to false, then simulate a plugin start failure
|
// Set fatal flag to false, then simulate a plugin start failure
|
||||||
// Should result in the secret store provider returning the sql impl
|
// Should result in the secret store provider returning the sql impl
|
||||||
func TestFatalPluginErr_PluginFailsToStartWithFatalFlagNotSet(t *testing.T) {
|
func TestFatalPluginErr_PluginFailsToStartWithFatalFlagNotSet(t *testing.T) {
|
||||||
svc, _, _, err := setupFatalCrashTest(t, true, false, false)
|
svc, mgr, _, _, err := setupFatalCrashTest(t, true, false, false)
|
||||||
|
_ = fmt.Sprint(mgr) // this is here to satisfy the linter
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
require.IsType(t, &CachedKVStore{}, svc)
|
require.IsType(t, &CachedKVStore{}, svc)
|
||||||
cachedKv, _ := svc.(*CachedKVStore)
|
cachedKv, _ := svc.(*CachedKVStore)
|
||||||
@ -37,7 +42,7 @@ func TestFatalPluginErr_PluginFailsToStartWithFatalFlagNotSet(t *testing.T) {
|
|||||||
// With fatal flag not set, store a secret in the plugin while backwards compatibility is disabled
|
// With fatal flag not set, store a secret in the plugin while backwards compatibility is disabled
|
||||||
// Should result in the fatal flag going from unset -> set to true
|
// Should result in the fatal flag going from unset -> set to true
|
||||||
func TestFatalPluginErr_FatalFlagGetsSetWithBackwardsCompatDisabled(t *testing.T) {
|
func TestFatalPluginErr_FatalFlagGetsSetWithBackwardsCompatDisabled(t *testing.T) {
|
||||||
svc, kvstore, _, err := setupFatalCrashTest(t, false, false, true)
|
svc, _, kvstore, _, err := setupFatalCrashTest(t, false, false, true)
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
require.NotNil(t, svc)
|
require.NotNil(t, svc)
|
||||||
err = svc.Set(context.Background(), 0, "datasource", "postgres", "my secret")
|
err = svc.Set(context.Background(), 0, "datasource", "postgres", "my secret")
|
||||||
@ -50,10 +55,21 @@ func TestFatalPluginErr_FatalFlagGetsSetWithBackwardsCompatDisabled(t *testing.T
|
|||||||
// With fatal flag set, retrieve a secret from the plugin while backwards compatibility is enabled
|
// With fatal flag set, retrieve a secret from the plugin while backwards compatibility is enabled
|
||||||
// Should result in the fatal flag going from set to true -> unset
|
// Should result in the fatal flag going from set to true -> unset
|
||||||
func TestFatalPluginErr_FatalFlagGetsUnSetWithBackwardsCompatEnabled(t *testing.T) {
|
func TestFatalPluginErr_FatalFlagGetsUnSetWithBackwardsCompatEnabled(t *testing.T) {
|
||||||
svc, kvstore, _, err := setupFatalCrashTest(t, false, true, false)
|
svc, mgr, kvstore, _, err := setupFatalCrashTest(t, false, true, false)
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
require.NotNil(t, svc)
|
require.NotNil(t, svc)
|
||||||
val, exists, err := svc.Get(context.Background(), 0, "datasource", "postgres")
|
// setup - store secret and manually bypassing the remote plugin impl
|
||||||
|
_, err = mgr.SecretsManager().SecretsManager.SetSecret(context.Background(), &secretsmanagerplugin.SetSecretRequest{
|
||||||
|
KeyDescriptor: &secretsmanagerplugin.Key{
|
||||||
|
OrgId: 0,
|
||||||
|
Namespace: "postgres",
|
||||||
|
Type: "datasource",
|
||||||
|
},
|
||||||
|
Value: "bogus",
|
||||||
|
})
|
||||||
|
require.NoError(t, err)
|
||||||
|
// retrieve the secret and check values
|
||||||
|
val, exists, err := svc.Get(context.Background(), 0, "postgres", "datasource")
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
require.NotNil(t, val)
|
require.NotNil(t, val)
|
||||||
require.True(t, exists)
|
require.True(t, exists)
|
||||||
@ -65,7 +81,7 @@ func TestFatalPluginErr_FatalFlagGetsUnSetWithBackwardsCompatEnabled(t *testing.
|
|||||||
// With fatal flag unset, do a migration with backwards compatibility disabled. When unified secrets are deleted, return an error on the first deletion
|
// With fatal flag unset, do a migration with backwards compatibility disabled. When unified secrets are deleted, return an error on the first deletion
|
||||||
// Should result in the fatal flag remaining unset
|
// Should result in the fatal flag remaining unset
|
||||||
func TestFatalPluginErr_MigrationTestWithErrorDeletingUnifiedSecrets(t *testing.T) {
|
func TestFatalPluginErr_MigrationTestWithErrorDeletingUnifiedSecrets(t *testing.T) {
|
||||||
svc, kvstore, _, err := setupFatalCrashTest(t, false, false, true)
|
svc, _, kvstore, _, err := setupFatalCrashTest(t, false, false, true)
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
|
|
||||||
migration := setupTestMigratorServiceWithDeletionError(t, svc, &mockstore.SQLStoreMock{
|
migration := setupTestMigratorServiceWithDeletionError(t, svc, &mockstore.SQLStoreMock{
|
||||||
@ -83,7 +99,7 @@ func setupFatalCrashTest(
|
|||||||
shouldFailOnStart bool,
|
shouldFailOnStart bool,
|
||||||
isPluginErrorFatal bool,
|
isPluginErrorFatal bool,
|
||||||
isBackwardsCompatDisabled bool,
|
isBackwardsCompatDisabled bool,
|
||||||
) (SecretsKVStore, kvstore.KVStore, *sqlstore.SQLStore, error) {
|
) (SecretsKVStore, plugins.SecretsPluginManager, kvstore.KVStore, *sqlstore.SQLStore, error) {
|
||||||
t.Helper()
|
t.Helper()
|
||||||
fatalFlagOnce = sync.Once{}
|
fatalFlagOnce = sync.Once{}
|
||||||
startupOnce = sync.Once{}
|
startupOnce = sync.Once{}
|
||||||
@ -100,7 +116,7 @@ func setupFatalCrashTest(
|
|||||||
t.Cleanup(func() {
|
t.Cleanup(func() {
|
||||||
fatalFlagOnce = sync.Once{}
|
fatalFlagOnce = sync.Once{}
|
||||||
})
|
})
|
||||||
return svc, kvstore, sqlStore, err
|
return svc, manager, kvstore, sqlStore, err
|
||||||
}
|
}
|
||||||
|
|
||||||
func setupTestMigratorServiceWithDeletionError(
|
func setupTestMigratorServiceWithDeletionError(
|
||||||
@ -108,14 +124,14 @@ func setupTestMigratorServiceWithDeletionError(
|
|||||||
secretskv SecretsKVStore,
|
secretskv SecretsKVStore,
|
||||||
sqlStore sqlstore.Store,
|
sqlStore sqlstore.Store,
|
||||||
kvstore kvstore.KVStore,
|
kvstore kvstore.KVStore,
|
||||||
) *PluginSecretMigrationService {
|
) *MigrateToPluginService {
|
||||||
t.Helper()
|
t.Helper()
|
||||||
fatalFlagOnce = sync.Once{}
|
fatalFlagOnce = sync.Once{}
|
||||||
startupOnce = sync.Once{}
|
startupOnce = sync.Once{}
|
||||||
cfg := setupTestConfig(t)
|
cfg := setupTestConfig(t)
|
||||||
secretsService := secretsManager.SetupTestService(t, fakes.NewFakeSecretsStore())
|
secretsService := secretsManager.SetupTestService(t, fakes.NewFakeSecretsStore())
|
||||||
manager := NewFakeSecretsPluginManager(t, false)
|
manager := NewFakeSecretsPluginManager(t, false)
|
||||||
migratorService := ProvidePluginSecretMigrationService(
|
migratorService := ProvideMigrateToPluginService(
|
||||||
secretskv,
|
secretskv,
|
||||||
cfg,
|
cfg,
|
||||||
sqlStore,
|
sqlStore,
|
||||||
@ -142,7 +158,6 @@ func setupTestConfig(t *testing.T) *setting.Cfg {
|
|||||||
rawCfg := `
|
rawCfg := `
|
||||||
[secrets]
|
[secrets]
|
||||||
use_plugin = true
|
use_plugin = true
|
||||||
migrate_to_plugin = true
|
|
||||||
`
|
`
|
||||||
raw, err := ini.Load([]byte(rawCfg))
|
raw, err := ini.Load([]byte(rawCfg))
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
|
@ -189,6 +189,7 @@ func updateFatalFlag(ctx context.Context, skv secretsKVStorePlugin) {
|
|||||||
// Rather than updating the flag in several places, it is cleaner to just do this check once
|
// Rather than updating the flag in several places, it is cleaner to just do this check once
|
||||||
// Very early on. Once backwards compatibility to legacy secrets is gone in Grafana 10, this can go away as well
|
// Very early on. Once backwards compatibility to legacy secrets is gone in Grafana 10, this can go away as well
|
||||||
fatalFlagOnce.Do(func() {
|
fatalFlagOnce.Do(func() {
|
||||||
|
skv.log.Debug("Updating plugin startup error fatal flag")
|
||||||
var err error
|
var err error
|
||||||
if isFatal, _ := isPluginStartupErrorFatal(ctx, skv.kvstore); !isFatal && skv.backwardsCompatibilityDisabled {
|
if isFatal, _ := isPluginStartupErrorFatal(ctx, skv.kvstore); !isFatal && skv.backwardsCompatibilityDisabled {
|
||||||
err = setPluginStartupErrorFatal(ctx, skv.kvstore, true)
|
err = setPluginStartupErrorFatal(ctx, skv.kvstore, true)
|
||||||
|
@ -65,10 +65,13 @@ func (f *FakeSecretsKVStore) Del(ctx context.Context, orgId int64, namespace str
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// List all keys with an optional filter. If default values are provided, filter is not applied.
|
||||||
func (f *FakeSecretsKVStore) Keys(ctx context.Context, orgId int64, namespace string, typ string) ([]Key, error) {
|
func (f *FakeSecretsKVStore) Keys(ctx context.Context, orgId int64, namespace string, typ string) ([]Key, error) {
|
||||||
res := make([]Key, 0)
|
res := make([]Key, 0)
|
||||||
for k := range f.store {
|
for k := range f.store {
|
||||||
if k.OrgId == orgId && k.Namespace == namespace && k.Type == typ {
|
if orgId == AllOrganizations && namespace == "" && typ == "" {
|
||||||
|
res = append(res, k)
|
||||||
|
} else if k.OrgId == orgId && k.Namespace == namespace && k.Type == typ {
|
||||||
res = append(res, k)
|
res = append(res, k)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -114,6 +117,14 @@ func buildKey(orgId int64, namespace string, typ string) Key {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func internalToProtoKey(k Key) *secretsmanagerplugin.Key {
|
||||||
|
return &secretsmanagerplugin.Key{
|
||||||
|
OrgId: k.OrgId,
|
||||||
|
Namespace: k.Namespace,
|
||||||
|
Type: k.Type,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Fake feature toggle - only need to check the backwards compatibility disabled flag
|
// Fake feature toggle - only need to check the backwards compatibility disabled flag
|
||||||
type fakeFeatureToggles struct {
|
type fakeFeatureToggles struct {
|
||||||
returnValue bool
|
returnValue bool
|
||||||
@ -131,40 +142,60 @@ func (f fakeFeatureToggles) IsEnabled(feature string) bool {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Fake grpc secrets plugin impl
|
// Fake grpc secrets plugin impl
|
||||||
type fakeGRPCSecretsPlugin struct{}
|
type fakeGRPCSecretsPlugin struct {
|
||||||
|
kv map[Key]string
|
||||||
|
}
|
||||||
|
|
||||||
func (c *fakeGRPCSecretsPlugin) GetSecret(ctx context.Context, in *secretsmanagerplugin.GetSecretRequest, opts ...grpc.CallOption) (*secretsmanagerplugin.GetSecretResponse, error) {
|
func (c *fakeGRPCSecretsPlugin) GetSecret(ctx context.Context, in *secretsmanagerplugin.GetSecretRequest, opts ...grpc.CallOption) (*secretsmanagerplugin.GetSecretResponse, error) {
|
||||||
|
val, ok := c.kv[buildKey(in.KeyDescriptor.OrgId, in.KeyDescriptor.Namespace, in.KeyDescriptor.Type)]
|
||||||
return &secretsmanagerplugin.GetSecretResponse{
|
return &secretsmanagerplugin.GetSecretResponse{
|
||||||
DecryptedValue: "bogus",
|
DecryptedValue: val,
|
||||||
Exists: true,
|
Exists: ok,
|
||||||
}, nil
|
}, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (c *fakeGRPCSecretsPlugin) SetSecret(ctx context.Context, in *secretsmanagerplugin.SetSecretRequest, opts ...grpc.CallOption) (*secretsmanagerplugin.SetSecretResponse, error) {
|
func (c *fakeGRPCSecretsPlugin) SetSecret(ctx context.Context, in *secretsmanagerplugin.SetSecretRequest, opts ...grpc.CallOption) (*secretsmanagerplugin.SetSecretResponse, error) {
|
||||||
|
c.kv[buildKey(in.KeyDescriptor.OrgId, in.KeyDescriptor.Namespace, in.KeyDescriptor.Type)] = in.Value
|
||||||
return &secretsmanagerplugin.SetSecretResponse{}, nil
|
return &secretsmanagerplugin.SetSecretResponse{}, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (c *fakeGRPCSecretsPlugin) DeleteSecret(ctx context.Context, in *secretsmanagerplugin.DeleteSecretRequest, opts ...grpc.CallOption) (*secretsmanagerplugin.DeleteSecretResponse, error) {
|
func (c *fakeGRPCSecretsPlugin) DeleteSecret(ctx context.Context, in *secretsmanagerplugin.DeleteSecretRequest, opts ...grpc.CallOption) (*secretsmanagerplugin.DeleteSecretResponse, error) {
|
||||||
|
delete(c.kv, buildKey(in.KeyDescriptor.OrgId, in.KeyDescriptor.Namespace, in.KeyDescriptor.Type))
|
||||||
return &secretsmanagerplugin.DeleteSecretResponse{}, nil
|
return &secretsmanagerplugin.DeleteSecretResponse{}, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (c *fakeGRPCSecretsPlugin) ListSecrets(ctx context.Context, in *secretsmanagerplugin.ListSecretsRequest, opts ...grpc.CallOption) (*secretsmanagerplugin.ListSecretsResponse, error) {
|
func (c *fakeGRPCSecretsPlugin) ListSecrets(ctx context.Context, in *secretsmanagerplugin.ListSecretsRequest, opts ...grpc.CallOption) (*secretsmanagerplugin.ListSecretsResponse, error) {
|
||||||
|
res := make([]*secretsmanagerplugin.Key, 0)
|
||||||
|
for k := range c.kv {
|
||||||
|
if in.KeyDescriptor.OrgId == AllOrganizations && in.KeyDescriptor.Namespace == "" && in.KeyDescriptor.Type == "" {
|
||||||
|
res = append(res, internalToProtoKey(k))
|
||||||
|
} else if k.OrgId == in.KeyDescriptor.OrgId && k.Namespace == in.KeyDescriptor.Namespace && k.Type == in.KeyDescriptor.Type {
|
||||||
|
res = append(res, internalToProtoKey(k))
|
||||||
|
}
|
||||||
|
}
|
||||||
return &secretsmanagerplugin.ListSecretsResponse{
|
return &secretsmanagerplugin.ListSecretsResponse{
|
||||||
Keys: make([]*secretsmanagerplugin.Key, 0),
|
Keys: res,
|
||||||
}, nil
|
}, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (c *fakeGRPCSecretsPlugin) RenameSecret(ctx context.Context, in *secretsmanagerplugin.RenameSecretRequest, opts ...grpc.CallOption) (*secretsmanagerplugin.RenameSecretResponse, error) {
|
func (c *fakeGRPCSecretsPlugin) RenameSecret(ctx context.Context, in *secretsmanagerplugin.RenameSecretRequest, opts ...grpc.CallOption) (*secretsmanagerplugin.RenameSecretResponse, error) {
|
||||||
|
oldKey := buildKey(in.KeyDescriptor.OrgId, in.KeyDescriptor.Namespace, in.KeyDescriptor.Type)
|
||||||
|
val := c.kv[oldKey]
|
||||||
|
delete(c.kv, oldKey)
|
||||||
|
c.kv[buildKey(in.KeyDescriptor.OrgId, in.NewNamespace, in.KeyDescriptor.Type)] = val
|
||||||
return &secretsmanagerplugin.RenameSecretResponse{}, nil
|
return &secretsmanagerplugin.RenameSecretResponse{}, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (c *fakeGRPCSecretsPlugin) GetAllSecrets(ctx context.Context, in *secretsmanagerplugin.GetAllSecretsRequest, opts ...grpc.CallOption) (*secretsmanagerplugin.GetAllSecretsResponse, error) {
|
func (c *fakeGRPCSecretsPlugin) GetAllSecrets(ctx context.Context, in *secretsmanagerplugin.GetAllSecretsRequest, opts ...grpc.CallOption) (*secretsmanagerplugin.GetAllSecretsResponse, error) {
|
||||||
|
items := make([]*secretsmanagerplugin.Item, 0)
|
||||||
|
for k, v := range c.kv {
|
||||||
|
items = append(items, &secretsmanagerplugin.Item{
|
||||||
|
Key: internalToProtoKey(k),
|
||||||
|
Value: v,
|
||||||
|
})
|
||||||
|
}
|
||||||
return &secretsmanagerplugin.GetAllSecretsResponse{
|
return &secretsmanagerplugin.GetAllSecretsResponse{
|
||||||
Items: []*secretsmanagerplugin.Item{
|
Items: items,
|
||||||
{
|
|
||||||
Value: "bogus",
|
|
||||||
},
|
|
||||||
},
|
|
||||||
}, nil
|
}, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -174,15 +205,22 @@ var _ secretsmanagerplugin.SecretsManagerPlugin = &fakeGRPCSecretsPlugin{}
|
|||||||
// Fake plugin manager
|
// Fake plugin manager
|
||||||
type fakePluginManager struct {
|
type fakePluginManager struct {
|
||||||
shouldFailOnStart bool
|
shouldFailOnStart bool
|
||||||
|
plugin *plugins.Plugin
|
||||||
}
|
}
|
||||||
|
|
||||||
func (mg *fakePluginManager) SecretsManager() *plugins.Plugin {
|
func (mg *fakePluginManager) SecretsManager() *plugins.Plugin {
|
||||||
|
if mg.plugin != nil {
|
||||||
|
return mg.plugin
|
||||||
|
}
|
||||||
p := &plugins.Plugin{
|
p := &plugins.Plugin{
|
||||||
SecretsManager: &fakeGRPCSecretsPlugin{},
|
SecretsManager: &fakeGRPCSecretsPlugin{
|
||||||
|
kv: make(map[Key]string),
|
||||||
|
},
|
||||||
}
|
}
|
||||||
p.RegisterClient(&fakePluginClient{
|
p.RegisterClient(&fakePluginClient{
|
||||||
shouldFailOnStart: mg.shouldFailOnStart,
|
shouldFailOnStart: mg.shouldFailOnStart,
|
||||||
})
|
})
|
||||||
|
mg.plugin = p
|
||||||
return p
|
return p
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -205,3 +243,7 @@ func (pc *fakePluginClient) Start(_ context.Context) error {
|
|||||||
}
|
}
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (pc *fakePluginClient) Stop(_ context.Context) error {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
Reference in New Issue
Block a user