Cloud migrations: store snapshots in the database (#108551)

* Cloud migrations: store snapshots in the database

* update github.com/grafana/grafana-cloud-migration-snapshot to v1.9.0

* make update-workspace

* use new field name in test

* return error after call to fmt.Errorf

* create methods for readability / fix session deletiong not deleting snapshots

* remove debugging changes

* update sample.ini

* update tests to include OrgID in ListSnapshotsQuery

* lint

* lint

* Update pkg/services/cloudmigration/cloudmigrationimpl/snapshot_mgmt.go

Co-authored-by: Matheus Macabu <macabu@users.noreply.github.com>

* remove TODO

* Update pkg/services/cloudmigration/cloudmigrationimpl/snapshot_mgmt.go

Co-authored-by: Matheus Macabu <macabu@users.noreply.github.com>

* remove one of the debug logs

---------

Co-authored-by: Matheus Macabu <macabu@users.noreply.github.com>
This commit is contained in:
Bruno
2025-07-25 11:41:21 -03:00
committed by GitHub
co-authored by Matheus Macabu
parent 47cf7ea8b6
commit b1592b5e36
17 changed files with 470 additions and 107 deletions
@@ -496,23 +496,24 @@ func (s *Service) CreateSnapshot(ctx context.Context, signedInUser *user.SignedI
// save snapshot to the db
snapshot := cloudmigration.CloudMigrationSnapshot{
UID: util.GenerateShortUID(),
SessionUID: cmd.SessionUID,
Status: cloudmigration.SnapshotStatusCreating,
EncryptionKey: initResp.EncryptionKey,
GMSSnapshotUID: initResp.SnapshotID,
LocalDir: filepath.Join(s.cfg.CloudMigration.SnapshotFolder, "grafana", "snapshots", initResp.SnapshotID),
UID: util.GenerateShortUID(),
SessionUID: cmd.SessionUID,
Status: cloudmigration.SnapshotStatusCreating,
GMSPublicKey: initResp.GMSPublicKey,
GMSSnapshotUID: initResp.SnapshotID,
Metadata: initResp.Metadata,
EncryptionAlgo: initResp.Algo,
LocalDir: filepath.Join(s.cfg.CloudMigration.SnapshotFolder, "grafana", "snapshots", initResp.SnapshotID),
ResourceStorageType: s.cfg.CloudMigration.ResourceStorageType,
}
uid, err := s.store.CreateSnapshot(ctx, snapshot)
if err != nil {
if err := s.store.CreateSnapshot(ctx, snapshot); err != nil {
return nil, fmt.Errorf("saving snapshot: %w", err)
}
snapshot.UID = uid
// Update status to "creating" to ensure the frontend polls from now on
if err := s.updateSnapshotWithRetries(ctx, cloudmigration.UpdateSnapshotCmd{
UID: uid,
UID: snapshot.UID,
SessionID: cmd.SessionUID,
Status: cloudmigration.SnapshotStatusCreating,
}); err != nil {
@@ -538,6 +539,7 @@ func (s *Service) CreateSnapshot(ctx context.Context, signedInUser *user.SignedI
s.report(asyncCtx, session, gmsclient.EventStartBuildingSnapshot, 0, nil, signedInUser.UserUID)
start := time.Now()
err := s.buildSnapshot(asyncCtx, signedInUser, initResp.MaxItemsPerPartition, initResp.Metadata, snapshot, cmd.ResourceTypes)
if err != nil {
asyncSpan.SetStatus(codes.Error, "error building snapshot")
@@ -896,10 +898,12 @@ func (s *Service) deleteLocalFiles(snapshots []cloudmigration.CloudMigrationSnap
var err error
for _, snapshot := range snapshots {
err = os.RemoveAll(snapshot.LocalDir)
if err != nil {
// in this case we only log the error, don't return it to continue with the process
s.log.Error("deleting migration snapshot files", "err", err)
if snapshot.LocalDir != "" {
err = os.RemoveAll(snapshot.LocalDir)
if err != nil {
// in this case we only log the error, don't return it to continue with the process
s.log.Error("deleting migration snapshot files", "err", err)
}
}
}
return err
@@ -103,14 +103,15 @@ func Test_GetSnapshotStatusFromGMS(t *testing.T) {
})
require.NoError(t, err)
uid, err := s.store.CreateSnapshot(ctx, cloudmigration.CloudMigrationSnapshot{
UID: "test uid",
uid := "test uid"
err = s.store.CreateSnapshot(ctx, cloudmigration.CloudMigrationSnapshot{
UID: uid,
SessionUID: sess.UID,
Status: cloudmigration.SnapshotStatusCreating,
GMSSnapshotUID: "gms uid",
})
require.NoError(t, err)
assert.Equal(t, "test uid", uid)
// Make sure status is coming from the db only
snapshot, err := s.GetSnapshot(ctx, cloudmigration.GetSnapshotsQuery{
@@ -381,8 +382,9 @@ func Test_OnlyQueriesStatusFromGMSWhenRequired(t *testing.T) {
})
require.NoError(t, err)
uid, err := s.store.CreateSnapshot(context.Background(), cloudmigration.CloudMigrationSnapshot{
UID: uuid.NewString(),
uid := uuid.NewString()
err = s.store.CreateSnapshot(context.Background(), cloudmigration.CloudMigrationSnapshot{
UID: uid,
SessionUID: sess.UID,
Status: cloudmigration.SnapshotStatusCreating,
GMSSnapshotUID: "gms uid",
@@ -1,6 +1,7 @@
package cloudmigrationimpl
import (
"bytes"
"context"
cryptoRand "crypto/rand"
"encoding/json"
@@ -562,7 +563,7 @@ func (s *Service) buildSnapshot(
// Use GMS public key + the grafana generated private key to encrypt snapshot files.
snapshotWriter, err := snapshot.NewSnapshotWriter(contracts.AssymetricKeys{
Public: snapshotMeta.EncryptionKey,
Public: snapshotMeta.GMSPublicKey,
Private: privateKey[:],
},
crypto.NewNacl(),
@@ -605,6 +606,56 @@ func (s *Service) buildSnapshot(
}
}
switch s.cfg.CloudMigration.ResourceStorageType {
case cloudmigration.ResourceStorageTypeDb:
if err := s.buildSnapshotWithDBStorage(ctx, snapshotMeta.UID, snapshotWriter, resourcesGroupedByType, maxItemsPerPartition); err != nil {
return fmt.Errorf("building snapshot with database storage: %w", err)
}
s.log.Debug(fmt.Sprintf("buildSnapshot: wrote data partitions with database storage in %d ms", time.Since(start).Milliseconds()))
case cloudmigration.ResourceStorageTypeFs:
if err := s.buildSnapshotWithFSStorage(publicKey[:], metadata, snapshotWriter, resourcesGroupedByType, maxItemsPerPartition); err != nil {
return fmt.Errorf("building snapshot with file system storage: %w", err)
}
s.log.Debug(fmt.Sprintf("buildSnapshot: wrote data partitions with file system storage in %d ms", time.Since(start).Milliseconds()))
default:
return fmt.Errorf("unknown resource storage type, check your configuration and try again: %q", s.cfg.CloudMigration.ResourceStorageType)
}
// update snapshot status to pending upload with retries
if err := s.updateSnapshotWithRetries(ctx, cloudmigration.UpdateSnapshotCmd{
UID: snapshotMeta.UID,
SessionID: snapshotMeta.SessionUID,
Status: cloudmigration.SnapshotStatusPendingUpload,
LocalResourcesToCreate: localSnapshotResource,
PublicKey: publicKey[:],
}); err != nil {
return err
}
return nil
}
func (s *Service) buildSnapshotWithDBStorage(ctx context.Context, snapshotUID string, snapshotWriter *snapshot.SnapshotWriter, resourcesGroupedByType map[cloudmigration.MigrateDataType][]snapshot.MigrateDataRequestItemDTO, maxItemsPerPartition uint32) error {
for _, resourceType := range currentMigrationTypes {
i := 0
for chunk := range slices.Chunk(resourcesGroupedByType[resourceType], int(maxItemsPerPartition)) {
encoded, err := snapshotWriter.EncodePartition(chunk)
if err != nil {
return fmt.Errorf("encoding snapshot partition: %w", err)
}
if err := s.store.StorePartition(ctx, snapshotUID, string(resourceType), i, encoded); err != nil {
return fmt.Errorf("storing partition into database: %w", err)
}
i += 1
}
}
return nil
}
func (s *Service) buildSnapshotWithFSStorage(publicKey, metadata []byte, snapshotWriter *snapshot.SnapshotWriter, resourcesGroupedByType map[cloudmigration.MigrateDataType][]snapshot.MigrateDataRequestItemDTO, maxItemsPerPartition uint32) error {
for _, resourceType := range currentMigrationTypes {
for chunk := range slices.Chunk(resourcesGroupedByType[resourceType], int(maxItemsPerPartition)) {
if err := snapshotWriter.Write(string(resourceType), chunk); err != nil {
@@ -613,8 +664,6 @@ func (s *Service) buildSnapshot(
}
}
s.log.Debug(fmt.Sprintf("buildSnapshot: wrote data files in %d ms", time.Since(start).Milliseconds()))
// Add the grafana generated public key to the index file so gms can use it to decrypt the snapshot files later.
// This works because the snapshot files are being encrypted with
// the grafana generated private key + the gms public key.
@@ -625,18 +674,6 @@ func (s *Service) buildSnapshot(
return fmt.Errorf("finishing writing snapshot files and generating index file: %w", err)
}
s.log.Debug(fmt.Sprintf("buildSnapshot: finished snapshot in %d ms", time.Since(start).Milliseconds()))
// update snapshot status to pending upload with retries
if err := s.updateSnapshotWithRetries(ctx, cloudmigration.UpdateSnapshotCmd{
UID: snapshotMeta.UID,
SessionID: snapshotMeta.SessionUID,
Status: cloudmigration.SnapshotStatusPendingUpload,
LocalResourcesToCreate: localSnapshotResource,
}); err != nil {
return err
}
return nil
}
@@ -654,13 +691,94 @@ func (s *Service) uploadSnapshot(ctx context.Context, session *cloudmigration.Cl
s.log.Debug(fmt.Sprintf("uploadSnapshot: method completed in %d ms", time.Since(start).Milliseconds()))
}()
switch s.cfg.CloudMigration.ResourceStorageType {
case cloudmigration.ResourceStorageTypeDb:
if err := s.uploadSnapshotWithDBStorage(ctx, session, snapshotMeta, uploadUrl); err != nil {
return fmt.Errorf("uploading snapshot with database storage: %w", err)
}
case cloudmigration.ResourceStorageTypeFs:
if err := s.uploadSnapshotWithFSStorage(ctx, session, snapshotMeta, uploadUrl); err != nil {
return fmt.Errorf("uploading snapshot with file system storage: %w", err)
}
default:
return fmt.Errorf("unknown resource storage type, check your configuration and try again: %q", s.cfg.CloudMigration.ResourceStorageType)
}
s.log.Info("successfully uploaded snapshot", "snapshotUid", snapshotMeta.UID, "cloud_snapshotUid", snapshotMeta.GMSSnapshotUID)
// update snapshot status to processing with retries
if err := s.updateSnapshotWithRetries(ctx, cloudmigration.UpdateSnapshotCmd{
UID: snapshotMeta.UID,
SessionID: snapshotMeta.SessionUID,
Status: cloudmigration.SnapshotStatusProcessing,
}); err != nil {
return err
}
return nil
}
func (s *Service) uploadSnapshotWithDBStorage(ctx context.Context, session *cloudmigration.CloudMigrationSession, snapshotMeta *cloudmigration.CloudMigrationSnapshot, uploadUrl string) error {
index, err := s.store.GetIndex(ctx, session.OrgID, snapshotMeta.SessionUID, snapshotMeta.UID)
if err != nil {
return fmt.Errorf("fetching index from database: %w", err)
}
snapshotIndex := snapshot.Index{
Version: 1,
EncryptionAlgo: index.EncryptionAlgo,
PublicKey: index.PublicKey,
Metadata: index.Metadata,
Items: make(map[string][]string),
}
var partitionToFileName = func(resourceType string, partitionNumber int) string {
return fmt.Sprintf("%+v_%+v", resourceType, partitionNumber)
}
for resourceType, partitionsNumbers := range index.Items {
for _, partitionNumber := range partitionsNumbers {
fileName := partitionToFileName(resourceType, partitionNumber)
snapshotIndex.Items[resourceType] = append(snapshotIndex.Items[resourceType], fileName)
key := fmt.Sprintf("%d/snapshots/%s/%+v", session.StackID, snapshotMeta.GMSSnapshotUID, fileName)
partition, err := s.store.GetPartition(ctx, snapshotMeta.UID, resourceType, partitionNumber)
if err != nil {
return fmt.Errorf("fetching partition from database: %w", err)
}
if err = s.objectStorage.PresignedURLUpload(ctx, uploadUrl, key, bytes.NewReader(partition.Data)); err != nil {
return fmt.Errorf("uploading file using presigned url: %w", err)
}
}
}
key := fmt.Sprintf("%d/snapshots/%s/%s", session.StackID, snapshotMeta.GMSSnapshotUID, "index.json")
buffer, err := snapshot.EncodeIndex(snapshotIndex)
if err != nil {
return fmt.Errorf("encoding snapshot index for upload: %w", err)
}
if err = s.objectStorage.PresignedURLUpload(ctx, uploadUrl, key, bytes.NewReader(buffer)); err != nil {
return fmt.Errorf("uploading index file using presigned url: %w", err)
}
return nil
}
func (s *Service) uploadSnapshotWithFSStorage(ctx context.Context, session *cloudmigration.CloudMigrationSession, snapshotMeta *cloudmigration.CloudMigrationSnapshot, uploadUrl string) error {
indexFilePath := filepath.Join(snapshotMeta.LocalDir, "index.json")
start := time.Now()
// LocalDir can be set in the configuration, therefore the file path can be set to any path.
// nolint:gosec
indexFile, err := os.Open(indexFilePath)
if err != nil {
// TODO: Clean this notice once we've fixed the HA bug
return fmt.Errorf("opening index files: %w. If you are running Grafana in a highly-available setup, try scaling down to one replica to avoid a known bug: https://github.com/grafana/grafana/issues/107264", err)
return fmt.Errorf("opening index files: %w. If you are running Grafana in a highly-available setup, try setting cloud_migration.resource_storage_type to 'db' or scaling down to one replica", err)
}
defer func() {
if closeErr := indexFile.Close(); closeErr != nil {
@@ -679,8 +797,6 @@ func (s *Service) uploadSnapshot(ctx context.Context, session *cloudmigration.Cl
}
readIndexSpan.End()
s.log.Debug(fmt.Sprintf("uploadSnapshot: read index file in %d ms", time.Since(start).Milliseconds()))
uploadCtx, uploadSpan := s.tracer.Start(ctx, "CloudMigrationService.uploadSnapshot.uploadDataFiles")
// Upload the data files.
for _, fileNames := range index.Items {
@@ -724,16 +840,6 @@ func (s *Service) uploadSnapshot(ctx context.Context, session *cloudmigration.Cl
uploadSpan.End()
s.log.Debug(fmt.Sprintf("uploadSnapshot: uploaded index file in %d ms", time.Since(start).Milliseconds()))
s.log.Info("successfully uploaded snapshot", "snapshotUid", snapshotMeta.UID, "cloud_snapshotUid", snapshotMeta.GMSSnapshotUID)
// update snapshot status to processing with retries
if err := s.updateSnapshotWithRetries(ctx, cloudmigration.UpdateSnapshotCmd{
UID: snapshotMeta.UID,
SessionID: snapshotMeta.SessionUID,
Status: cloudmigration.SnapshotStatusProcessing,
}); err != nil {
return err
}
return nil
}
@@ -12,8 +12,11 @@ type store interface {
GetCloudMigrationSessionList(ctx context.Context, orgID int64) ([]*cloudmigration.CloudMigrationSession, error)
DeleteMigrationSessionByUID(ctx context.Context, orgID int64, uid string) (*cloudmigration.CloudMigrationSession, []cloudmigration.CloudMigrationSnapshot, error)
CreateSnapshot(ctx context.Context, snapshot cloudmigration.CloudMigrationSnapshot) (string, error)
CreateSnapshot(ctx context.Context, snapshot cloudmigration.CloudMigrationSnapshot) error
UpdateSnapshot(ctx context.Context, snapshot cloudmigration.UpdateSnapshotCmd) error
GetIndex(ctx context.Context, orgID int64, sessionUID string, snapshotUID string) (cloudmigration.CloudMigrationSnapshotIndex, error)
GetPartition(ctx context.Context, snapshotUID string, resourceType string, partitionNumber int) (cloudmigration.CloudMigrationSnapshotPartition, error)
StorePartition(ctx context.Context, snapshotUID string, resourceType string, partitionNumber int, data []byte) error
GetSnapshotByUID(ctx context.Context, orgID int64, sessUid, id string, params cloudmigration.SnapshotResultQueryParams) (*cloudmigration.CloudMigrationSnapshot, error)
GetSnapshotList(ctx context.Context, query cloudmigration.ListSnapshotsQuery) ([]cloudmigration.CloudMigrationSnapshot, error)
}
@@ -121,6 +121,7 @@ func (ss *sqlStore) DeleteMigrationSessionByUID(ctx context.Context, orgID int64
SessionUID: uid,
Page: 1,
Limit: GetAllSnapshots,
OrgID: orgID,
}
snapshots, err := ss.GetSnapshotList(ctx, q)
if err != nil {
@@ -129,12 +130,13 @@ func (ss *sqlStore) DeleteMigrationSessionByUID(ctx context.Context, orgID int64
err = ss.db.InTransaction(ctx, func(ctx context.Context) error {
for _, snapshot := range snapshots {
err := ss.deleteSnapshotResources(ctx, snapshot.UID)
if err != nil {
if err := ss.deleteSnapshotResources(ctx, snapshot.UID); err != nil {
return fmt.Errorf("deleting snapshot resource from db: %w", err)
}
err = ss.deleteSnapshot(ctx, snapshot.UID)
if err != nil {
if err := ss.deleteSnapshotPartitions(ctx, snapshot.UID); err != nil {
return fmt.Errorf("deleting snapshot partitions: %w", err)
}
if err := ss.deleteSnapshot(ctx, snapshot.UID); err != nil {
return fmt.Errorf("deleting snapshot from db: %w", err)
}
}
@@ -166,33 +168,33 @@ func (ss *sqlStore) DeleteMigrationSessionByUID(ctx context.Context, orgID int64
return &c, snapshots, nil
}
func (ss *sqlStore) CreateSnapshot(ctx context.Context, snapshot cloudmigration.CloudMigrationSnapshot) (string, error) {
func (ss *sqlStore) CreateSnapshot(ctx context.Context, snapshot cloudmigration.CloudMigrationSnapshot) error {
if snapshot.SessionUID == "" {
return "", fmt.Errorf("sessionUID is required")
return fmt.Errorf("sessionUID is required")
}
if snapshot.UID == "" {
snapshot.UID = util.GenerateShortUID()
return fmt.Errorf("snapshot uid is required")
}
if err := ss.secretsStore.Set(ctx, secretskv.AllOrganizations, snapshot.UID, secretType, string(snapshot.EncryptionKey)); err != nil {
return "", err
if err := ss.secretsStore.Set(ctx, secretskv.AllOrganizations, snapshot.UID, secretType, string(snapshot.GMSPublicKey)); err != nil {
return err
}
err := ss.db.WithDbSession(ctx, func(sess *sqlstore.DBSession) error {
snapshot.Created = time.Now()
snapshot.Updated = time.Now()
_, err := sess.Insert(&snapshot)
_, err := sess.InsertOne(&snapshot)
if err != nil {
return err
}
return nil
})
if err != nil {
return "", err
return err
}
return snapshot.UID, nil
return nil
}
// UpdateSnapshot takes a command containing a snapshot uid and any updates to apply to the snapshot.
@@ -232,19 +234,133 @@ func (ss *sqlStore) UpdateSnapshot(ctx context.Context, update cloudmigration.Up
return err
}
}
if update.PublicKey != nil {
if err := ss.db.WithDbSession(ctx, func(sess *sqlstore.DBSession) error {
rawSQL := "UPDATE cloud_migration_snapshot SET public_key=? WHERE session_uid=? AND uid=?"
if _, err := sess.Exec(rawSQL, update.PublicKey, update.SessionID, update.UID); err != nil {
return fmt.Errorf("updating snapshot public key for uid %s: %w", update.UID, err)
}
return nil
}); err != nil {
return err
}
}
return nil
}
func (ss *sqlStore) deleteSnapshot(ctx context.Context, snapshotUid string) error {
return ss.db.WithDbSession(ctx, func(sess *sqlstore.DBSession) error {
_, err := sess.Delete(cloudmigration.CloudMigrationSnapshot{
UID: snapshotUid,
func (ss *sqlStore) StorePartition(ctx context.Context, snapshotUID string, resourceType string, partitionNumber int, data []byte) error {
return ss.db.InTransaction(ctx, func(ctx context.Context) error {
return ss.db.WithDbSession(ctx, func(sess *sqlstore.DBSession) error {
_, err := sess.Insert(cloudmigration.CloudMigrationSnapshotPartition{
SnapshotUID: snapshotUID,
ResourceType: resourceType,
PartitionNumber: partitionNumber,
Data: data,
})
if err != nil {
return fmt.Errorf("inserting snapshot partition into database: %w", err)
}
return nil
})
return err
})
}
func (ss *sqlStore) GetIndex(ctx context.Context, orgID int64, sessionUID string, snapshotUID string) (cloudmigration.CloudMigrationSnapshotIndex, error) {
var snap *cloudmigration.CloudMigrationSnapshot
partitions := make([]cloudmigration.CloudMigrationSnapshotPartition, 0)
if err := ss.db.InTransaction(ctx, func(ctx context.Context) error {
return ss.db.WithDbSession(ctx, func(sess *sqlstore.DBSession) error {
s, err := ss.getSnapshotByUID(ctx, orgID, sessionUID, snapshotUID)
if err != nil {
return fmt.Errorf("fetching snapshot from database: %w", err)
}
snap = s
if err := sess.OrderBy("cloud_migration_snapshot_partition.resource_type,cloud_migration_snapshot_partition.partition_number ASC").Find(&partitions, &cloudmigration.CloudMigrationSnapshotPartition{SnapshotUID: snapshotUID}); err != nil {
return fmt.Errorf("fetching partition from database: %w", err)
}
if secret, found, err := ss.secretsStore.Get(ctx, secretskv.AllOrganizations, snap.UID, secretType); err != nil {
return err
} else if !found {
return fmt.Errorf("encryption key not found for snapshot with UID %s", snap.UID)
} else {
snap.GMSPublicKey = []byte(secret)
}
return nil
})
}); err != nil {
return cloudmigration.CloudMigrationSnapshotIndex{}, err
}
partitionsByResourceType := make(map[string][]int)
for _, partition := range partitions {
partitionsByResourceType[partition.ResourceType] = append(partitionsByResourceType[partition.ResourceType], partition.PartitionNumber)
}
return cloudmigration.CloudMigrationSnapshotIndex{
EncryptionAlgo: snap.EncryptionAlgo,
PublicKey: snap.PublicKey,
Metadata: snap.Metadata,
Items: partitionsByResourceType,
}, nil
}
func (ss *sqlStore) GetPartition(ctx context.Context, snapshotUID string, resourceType string, partitionNumber int) (cloudmigration.CloudMigrationSnapshotPartition, error) {
var partition cloudmigration.CloudMigrationSnapshotPartition
err := ss.db.InTransaction(ctx, func(ctx context.Context) error {
return ss.db.WithDbSession(ctx, func(sess *sqlstore.DBSession) error {
if _, err := sess.Where("snapshot_uid = ? AND resource_type = ? AND partition_number = ?", snapshotUID, resourceType, partitionNumber).Get(&partition); err != nil {
return fmt.Errorf("fetching partition from database: %w", err)
}
return nil
})
})
return partition, err
}
func (ss *sqlStore) deleteSnapshot(ctx context.Context, snapshotUid string) error {
return ss.db.InTransaction(ctx, func(ctx context.Context) error {
return ss.db.WithDbSession(ctx, func(sess *sqlstore.DBSession) error {
if _, err := sess.Delete(cloudmigration.CloudMigrationSnapshot{
UID: snapshotUid,
}); err != nil {
return fmt.Errorf("deleting snapshot: %w", err)
}
return nil
})
})
}
func (ss *sqlStore) getSnapshotByUID(ctx context.Context, orgID int64, sessionUID string, snapshotUID string) (*cloudmigration.CloudMigrationSnapshot, error) {
session, err := ss.GetMigrationSessionByUID(ctx, orgID, sessionUID)
if err != nil || session == nil {
return nil, err
}
// now we get the snapshot
var snapshot cloudmigration.CloudMigrationSnapshot
err = ss.db.WithDbSession(ctx, func(sess *db.Session) error {
exist, err := sess.Where("session_uid=? AND uid=?", sessionUID, snapshotUID).Get(&snapshot)
if err != nil {
return err
}
if !exist {
return cloudmigration.ErrSnapshotNotFound
}
return nil
})
if err != nil {
return nil, err
}
return &snapshot, nil
}
func (ss *sqlStore) GetSnapshotByUID(ctx context.Context, orgID int64, sessionUid, uid string, params cloudmigration.SnapshotResultQueryParams) (*cloudmigration.CloudMigrationSnapshot, error) {
// first we check if the session exists, using orgId and sessionUid
session, err := ss.GetMigrationSessionByUID(ctx, orgID, sessionUid)
@@ -273,7 +389,7 @@ func (ss *sqlStore) GetSnapshotByUID(ctx context.Context, orgID int64, sessionUi
} else if !found {
return &snapshot, fmt.Errorf("encryption key not found for snapshot with UID %s", snapshot.UID)
} else {
snapshot.EncryptionKey = []byte(secret)
snapshot.GMSPublicKey = []byte(secret)
}
resources, err := ss.getSnapshotResources(ctx, uid, params)
@@ -291,6 +407,12 @@ func (ss *sqlStore) GetSnapshotByUID(ctx context.Context, orgID int64, sessionUi
// GetSnapshotList returns snapshots without resources included. Use GetSnapshotByUID to get individual snapshot results.
// passing GetAllSnapshots will return all the elements regardless of the page
func (ss *sqlStore) GetSnapshotList(ctx context.Context, query cloudmigration.ListSnapshotsQuery) ([]cloudmigration.CloudMigrationSnapshot, error) {
if query.OrgID == 0 {
return nil, fmt.Errorf("org id is required")
}
if query.SessionUID == "" {
return nil, fmt.Errorf("session uid is required")
}
var snapshots = make([]cloudmigration.CloudMigrationSnapshot, 0)
err := ss.db.WithDbSession(ctx, func(sess *db.Session) error {
sess.Join("INNER", "cloud_migration_session",
@@ -310,13 +432,14 @@ func (ss *sqlStore) GetSnapshotList(ctx context.Context, query cloudmigration.Li
if err != nil {
return nil, err
}
for i, snapshot := range snapshots {
if secret, found, err := ss.secretsStore.Get(ctx, secretskv.AllOrganizations, snapshot.UID, secretType); err != nil {
return nil, err
} else if !found {
return nil, fmt.Errorf("encryption key not found for snapshot with UID %s", snapshot.UID)
} else {
snapshot.EncryptionKey = []byte(secret)
snapshot.GMSPublicKey = []byte(secret)
}
if stats, err := ss.getSnapshotResourceStats(ctx, snapshot.UID); err != nil {
@@ -531,6 +654,17 @@ func (ss *sqlStore) deleteSnapshotResources(ctx context.Context, snapshotUid str
})
}
func (ss *sqlStore) deleteSnapshotPartitions(ctx context.Context, snapshotUid string) error {
return ss.db.WithDbSession(ctx, func(sess *sqlstore.DBSession) error {
if _, err := sess.Delete(cloudmigration.CloudMigrationSnapshotPartition{
SnapshotUID: snapshotUid,
}); err != nil {
return fmt.Errorf("deleting snapshot partitions: %w", err)
}
return nil
})
}
func (ss *sqlStore) encryptToken(ctx context.Context, cm *cloudmigration.CloudMigrationSession) error {
s, err := ss.secretsService.Encrypt(ctx, []byte(cm.AuthToken), secrets.WithoutScope())
if err != nil {
@@ -1,13 +1,18 @@
package cloudmigrationimpl
import (
"bytes"
"context"
cryptoRand "crypto/rand"
"encoding/base64"
"fmt"
"strconv"
"testing"
"github.com/google/uuid"
snapshot "github.com/grafana/grafana-cloud-migration-snapshot/src"
"github.com/grafana/grafana-cloud-migration-snapshot/src/contracts"
"github.com/grafana/grafana-cloud-migration-snapshot/src/infra/crypto"
"github.com/grafana/grafana/pkg/infra/db"
"github.com/grafana/grafana/pkg/services/cloudmigration"
fakeSecrets "github.com/grafana/grafana/pkg/services/secrets/fakes"
@@ -15,6 +20,7 @@ import (
"github.com/grafana/grafana/pkg/services/sqlstore"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"golang.org/x/crypto/nacl/box"
)
func Test_GetAllCloudMigrationSessions(t *testing.T) {
@@ -112,17 +118,18 @@ func Test_SnapshotManagement(t *testing.T) {
require.NoError(t, err)
// create a snapshot
uid := uuid.NewString()
cmr := cloudmigration.CloudMigrationSnapshot{
UID: uid,
SessionUID: session.UID,
Status: cloudmigration.SnapshotStatusCreating,
}
snapshotUid, err := s.CreateSnapshot(ctx, cmr)
err = s.CreateSnapshot(ctx, cmr)
require.NoError(t, err)
require.NotEmpty(t, snapshotUid)
//retrieve it from the db
snapshot, err := s.GetSnapshotByUID(ctx, 1, session.UID, snapshotUid, cloudmigration.SnapshotResultQueryParams{
snapshot, err := s.GetSnapshotByUID(ctx, 1, session.UID, uid, cloudmigration.SnapshotResultQueryParams{
ResultPage: 1,
ResultLimit: 100,
SortColumn: cloudmigration.SortColumnID,
@@ -132,11 +139,11 @@ func Test_SnapshotManagement(t *testing.T) {
require.Equal(t, cloudmigration.SnapshotStatusCreating, snapshot.Status)
// update its status
err = s.UpdateSnapshot(ctx, cloudmigration.UpdateSnapshotCmd{UID: snapshotUid, Status: cloudmigration.SnapshotStatusCreating, SessionID: session.UID})
err = s.UpdateSnapshot(ctx, cloudmigration.UpdateSnapshotCmd{UID: uid, Status: cloudmigration.SnapshotStatusCreating, SessionID: session.UID})
require.NoError(t, err)
//retrieve it again
snapshot, err = s.GetSnapshotByUID(ctx, 1, session.UID, snapshotUid, cloudmigration.SnapshotResultQueryParams{
snapshot, err = s.GetSnapshotByUID(ctx, 1, session.UID, uid, cloudmigration.SnapshotResultQueryParams{
ResultPage: 1,
ResultLimit: 100,
SortColumn: cloudmigration.SortColumnID,
@@ -152,11 +159,11 @@ func Test_SnapshotManagement(t *testing.T) {
require.Equal(t, *snapshot, snapshots[0])
// delete snapshot
err = s.deleteSnapshot(ctx, snapshotUid)
err = s.deleteSnapshot(ctx, uid)
require.NoError(t, err)
// now we expect not to find the snapshot
snapshot, err = s.GetSnapshotByUID(ctx, 1, session.UID, snapshotUid, cloudmigration.SnapshotResultQueryParams{
snapshot, err = s.GetSnapshotByUID(ctx, 1, session.UID, uid, cloudmigration.SnapshotResultQueryParams{
ResultPage: 1,
ResultLimit: 100,
SortColumn: cloudmigration.SortColumnID,
@@ -174,12 +181,13 @@ func Test_SnapshotManagement(t *testing.T) {
require.NoError(t, err)
// create a snapshot
snapshotUid, err := s.CreateSnapshot(ctx, cloudmigration.CloudMigrationSnapshot{
uid := uuid.NewString()
err = s.CreateSnapshot(ctx, cloudmigration.CloudMigrationSnapshot{
UID: uid,
SessionUID: session.UID,
Status: cloudmigration.SnapshotStatusCreating,
})
require.NoError(t, err)
require.NotEmpty(t, snapshotUid)
// Generate 50,001 test resources in order to test both update conditions (reached the batch limit or reached the end)
const numResources = 50001
@@ -196,7 +204,7 @@ func Test_SnapshotManagement(t *testing.T) {
// Update the snapshot with the resources to create
err = s.UpdateSnapshot(ctx, cloudmigration.UpdateSnapshotCmd{
UID: snapshotUid,
UID: uid,
Status: cloudmigration.SnapshotStatusPendingUpload,
SessionID: session.UID,
LocalResourcesToCreate: resources,
@@ -204,7 +212,7 @@ func Test_SnapshotManagement(t *testing.T) {
require.NoError(t, err)
// Get the Snapshot and ensure it's in the right state
snapshot, err := s.GetSnapshotByUID(ctx, 1, session.UID, snapshotUid, cloudmigration.SnapshotResultQueryParams{
snapshot, err := s.GetSnapshotByUID(ctx, 1, session.UID, uid, cloudmigration.SnapshotResultQueryParams{
ResultPage: 1,
ResultLimit: numResources,
SortColumn: cloudmigration.SortColumnID,
@@ -226,7 +234,7 @@ func Test_SnapshotManagement(t *testing.T) {
// Update the snapshot with the resources to update
err = s.UpdateSnapshot(ctx, cloudmigration.UpdateSnapshotCmd{
UID: snapshotUid,
UID: uid,
Status: cloudmigration.SnapshotStatusFinished,
SessionID: session.UID,
CloudResourcesToUpdate: snapshot.Resources,
@@ -234,7 +242,7 @@ func Test_SnapshotManagement(t *testing.T) {
require.NoError(t, err)
// Get the Snapshot and ensure it's in the right state
snapshot, err = s.GetSnapshotByUID(ctx, 1, session.UID, snapshotUid, cloudmigration.SnapshotResultQueryParams{
snapshot, err = s.GetSnapshotByUID(ctx, 1, session.UID, uid, cloudmigration.SnapshotResultQueryParams{
ResultPage: 1,
ResultLimit: numResources,
SortColumn: cloudmigration.SortColumnID,
@@ -637,7 +645,7 @@ func TestGetSnapshotList(t *testing.T) {
})
t.Run("return no snapshots if limit is set to 0", func(t *testing.T) {
snapshots, err := s.GetSnapshotList(ctx, cloudmigration.ListSnapshotsQuery{SessionUID: sessionUID, Page: 1, Limit: 0})
snapshots, err := s.GetSnapshotList(ctx, cloudmigration.ListSnapshotsQuery{SessionUID: sessionUID, OrgID: 1, Page: 1, Limit: 0})
require.NoError(t, err)
assert.Empty(t, snapshots)
})
@@ -669,7 +677,7 @@ func TestGetSnapshotList(t *testing.T) {
})
t.Run("only the snapshots that belong to a specific session are returned", func(t *testing.T) {
snapshots, err := s.GetSnapshotList(ctx, cloudmigration.ListSnapshotsQuery{SessionUID: "session-uid-that-doesnt-exist", Page: 1, Limit: 100})
snapshots, err := s.GetSnapshotList(ctx, cloudmigration.ListSnapshotsQuery{SessionUID: "session-uid-that-doesnt-exist", OrgID: 1, Page: 1, Limit: 100})
require.NoError(t, err)
assert.Empty(t, snapshots)
})
@@ -680,7 +688,7 @@ func TestGetSnapshotList(t *testing.T) {
require.NoError(t, err)
// Fetch the snapshots that belong to the deleted session.
snapshots, err := s.GetSnapshotList(ctx, cloudmigration.ListSnapshotsQuery{SessionUID: sessionUID, Page: 1, Limit: 100})
snapshots, err := s.GetSnapshotList(ctx, cloudmigration.ListSnapshotsQuery{SessionUID: sessionUID, OrgID: 1, Page: 1, Limit: 100})
require.NoError(t, err)
// No snapshots should be returned because the session that
@@ -801,3 +809,42 @@ func setUpTest(t *testing.T) (*sqlstore.SQLStore, *sqlStore) {
func encodeToken(t string) string {
return base64.StdEncoding.EncodeToString([]byte(t))
}
func TestEncodeDecode(t *testing.T) {
gmsPublicKey, gmsPrivateKey, err := box.GenerateKey(cryptoRand.Reader)
require.NoError(t, err)
grafanaPublicKey, grafanaPrivateKey, err := box.GenerateKey(cryptoRand.Reader)
require.NoError(t, err)
snapshotWriter, err := snapshot.NewSnapshotWriter(contracts.AssymetricKeys{
Public: gmsPublicKey[:],
Private: grafanaPrivateKey[:],
},
crypto.NewNacl(),
"",
)
require.NoError(t, err)
chunk := []snapshot.MigrateDataRequestItemDTO{{
Type: snapshot.AlertRuleGroupType,
RefID: "foo",
Name: "name",
Data: map[string]any{"a": "b"},
}}
encoded, err := snapshotWriter.EncodePartition(chunk)
require.NoError(t, err)
require.NoError(t, snapshotWriter.Write("RESOURCE_TYPE", chunk))
reader := snapshot.NewSnapshotReader(contracts.AssymetricKeys{
Public: grafanaPublicKey[:],
Private: gmsPrivateKey[:],
},
crypto.NewNacl())
partition, err := reader.ReadFile(bytes.NewReader(encoded))
require.NoError(t, err)
require.Equal(t, chunk, partition.Items)
}