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:
Michael Mandrus
2022-08-24 16:24:50 -04:00
committed by GitHub
parent c8f2cd2599
commit 277ea836b6
15 changed files with 405 additions and 59 deletions

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

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

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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