CloudMigrations: Bulk update local resources (#96002)

* wip

* make tests pass

* get all tests passing

* fixes

* some small cleanup

* fix test

* convert delimiter keys to struct keys

* dont execute empty sql statement

* remove printlns

* fix unit test

* a bit more cleanup

* whoops
This commit is contained in:
Michael Mandrus
2025-01-10 14:42:18 -05:00
committed by GitHub
parent 99a0eb825d
commit c9d22f06c3
9 changed files with 139 additions and 63 deletions
@@ -4,6 +4,7 @@ import (
"context"
"encoding/base64"
"fmt"
"strings"
"time"
"github.com/grafana/grafana/pkg/infra/db"
@@ -94,6 +95,7 @@ func (ss *sqlStore) GetCloudMigrationSessionList(ctx context.Context, orgID int6
return migrations, nil
}
// DeleteMigrationSessionByUID deletes the migration session, and all the related snapshot and resources the work is done in a transaction.
func (ss *sqlStore) DeleteMigrationSessionByUID(ctx context.Context, orgID int64, uid string) (*cloudmigration.CloudMigrationSession, []cloudmigration.CloudMigrationSnapshot, error) {
var c cloudmigration.CloudMigrationSession
err := ss.db.WithDbSession(ctx, func(sess *db.Session) error {
@@ -127,7 +129,7 @@ func (ss *sqlStore) DeleteMigrationSessionByUID(ctx context.Context, orgID int64
if err != nil {
return fmt.Errorf("deleting snapshot resource from db: %w", err)
}
err = ss.deleteSnapshot(ctx, orgID, snapshot.UID)
err = ss.deleteSnapshot(ctx, snapshot.UID)
if err != nil {
return fmt.Errorf("deleting snapshot from db: %w", err)
}
@@ -211,19 +213,26 @@ func (ss *sqlStore) UpdateSnapshot(ctx context.Context, update cloudmigration.Up
}
}
// Update resources if set
if len(update.Resources) > 0 {
if err := ss.createUpdateSnapshotResources(ctx, update.UID, update.Resources); err != nil {
// If local resources are set, it means we have to create them for the first time
if len(update.LocalResourcesToCreate) > 0 {
if err := ss.CreateSnapshotResources(ctx, update.UID, update.LocalResourcesToCreate); err != nil {
return err
}
}
// If cloud resources are set, it means we have to update our resource local state
if len(update.CloudResourcesToUpdate) > 0 {
if err := ss.UpdateSnapshotResources(ctx, update.UID, update.CloudResourcesToUpdate); err != nil {
return err
}
}
return nil
})
return err
}
func (ss *sqlStore) deleteSnapshot(ctx context.Context, orgID int64, snapshotUid string) error {
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,
@@ -316,30 +325,90 @@ func (ss *sqlStore) GetSnapshotList(ctx context.Context, query cloudmigration.Li
return snapshots, nil
}
// CreateUpdateSnapshotResources either updates a migration resource for a snapshot, or creates it if it does not exist
// If the uid is not known, it uses snapshot_uid + resource_uid as a lookup
func (ss *sqlStore) createUpdateSnapshotResources(ctx context.Context, snapshotUid string, resources []cloudmigration.CloudMigrationResource) error {
// CreateSnapshotResources initializes the local state of a resources belonging to a snapshot
func (ss *sqlStore) CreateSnapshotResources(ctx context.Context, snapshotUid string, resources []cloudmigration.CloudMigrationResource) error {
for i := 0; i < len(resources); i++ {
resources[i].UID = util.GenerateShortUID()
// ensure snapshot_uids are consistent so that we can use in conjunction with refID for lookup later
resources[i].SnapshotUID = snapshotUid
}
err := ss.db.WithDbSession(ctx, func(sess *sqlstore.DBSession) error {
_, err := sess.Insert(resources)
if err != nil {
return err
}
return nil
})
if err != nil {
return fmt.Errorf("creating resources: %w", err)
}
return nil
}
// UpdateSnapshotResources updates a migration resource for a snapshot, using snapshot_uid + resource_uid as a lookup
// It does preprocessing on the results in order to minimize the sql queries executed.
func (ss *sqlStore) UpdateSnapshotResources(ctx context.Context, snapshotUid string, resources []cloudmigration.CloudMigrationResource) error {
// refIds of resources that migrated successfully in order to update in bulk
okIds := make([]any, 0, len(resources))
// group any failed resources by errCode and errStr
type errId struct {
errCode cloudmigration.ResourceErrorCode
errStr string
}
errorIds := make(map[errId][]any)
for _, r := range resources {
if r.Status == cloudmigration.ItemStatusOK {
okIds = append(okIds, r.RefID)
} else if r.Status == cloudmigration.ItemStatusError {
key := errId{errCode: r.ErrorCode, errStr: r.Error}
if ids, ok := errorIds[key]; ok {
errorIds[key] = append(ids, r.RefID)
} else {
errorIds[key] = []any{r.RefID}
}
}
}
type statement struct {
sql string
args []any
}
// Prepare a sql statement for all of the OK statuses
var okUpdateStatement *statement
if len(okIds) > 0 {
okUpdateStatement = &statement{
sql: fmt.Sprintf("UPDATE cloud_migration_resource SET status=? WHERE snapshot_uid=? AND resource_uid IN (?%s)", strings.Repeat(", ?", len(okIds)-1)),
args: append([]any{cloudmigration.ItemStatusOK, snapshotUid}, okIds...),
}
}
// Prepare however many sql statements are necessary for the error statuses
errorStatements := make([]statement, 0, len(errorIds))
for k, ids := range errorIds {
errorStatements = append(errorStatements, statement{
sql: fmt.Sprintf("UPDATE cloud_migration_resource SET status=?, error_code=?, error_string=? WHERE snapshot_uid=? AND resource_uid IN (?%s)", strings.Repeat(", ?", len(ids)-1)),
args: append([]any{cloudmigration.ItemStatusError, k.errCode, k.errStr, snapshotUid}, ids...),
})
}
// Execute the minimum number of required statements!
return ss.db.InTransaction(ctx, func(ctx context.Context) error {
sql := "UPDATE cloud_migration_resource SET status=?, error_string=?, error_code=? WHERE uid=? OR (snapshot_uid=? AND resource_uid=?)"
err := ss.db.WithDbSession(ctx, func(sess *sqlstore.DBSession) error {
for _, r := range resources {
// try an update first
result, err := sess.Exec(sql, r.Status, r.Error, r.ErrorCode, r.UID, snapshotUid, r.RefID)
if err != nil {
if okUpdateStatement != nil {
if _, err := sess.Exec(append([]any{okUpdateStatement.sql}, okUpdateStatement.args...)...); err != nil {
return err
}
// if this had no effect, assign a uid and insert instead
n, err := result.RowsAffected()
if err != nil {
}
for _, q := range errorStatements {
if _, err := sess.Exec(append([]any{q.sql}, q.args...)...); err != nil {
return err
} else if n == 0 {
r.UID = util.GenerateShortUID()
// ensure snapshot_uids are consistent so that we can use them to query when uid isn't known
r.SnapshotUID = snapshotUid
_, err := sess.Insert(r)
if err != nil {
return err
}
}
}
return nil
@@ -364,7 +433,7 @@ func (ss *sqlStore) getSnapshotResources(ctx context.Context, snapshotUid string
err := ss.db.WithDbSession(ctx, func(sess *db.Session) error {
offset := (page - 1) * limit
sess.Limit(limit, offset)
return sess.Find(&resources, &cloudmigration.CloudMigrationResource{
return sess.OrderBy("id ASC").Find(&resources, &cloudmigration.CloudMigrationResource{
SnapshotUID: snapshotUid,
})
})