diff --git a/base/commands/migration/estimate.go b/base/commands/migration/estimate.go new file mode 100644 index 00000000..8dde2888 --- /dev/null +++ b/base/commands/migration/estimate.go @@ -0,0 +1,56 @@ +//go:build std || migration + +package migration + +import ( + "context" + "fmt" + + "github.com/hazelcast/hazelcast-commandline-client/clc/paths" + "github.com/hazelcast/hazelcast-commandline-client/clc/ux/stage" + "github.com/hazelcast/hazelcast-commandline-client/internal/check" + "github.com/hazelcast/hazelcast-commandline-client/internal/plug" +) + +type EstimateCmd struct{} + +func (e EstimateCmd) Init(cc plug.InitContext) error { + cc.SetCommandUsage("estimate") + cc.SetCommandGroup("migration") + help := "Estimate migration" + cc.SetCommandHelp(help, help) + cc.AddStringArg(argDMTConfig, argTitleDMTConfig) + return nil +} + +func (e EstimateCmd) Exec(ctx context.Context, ec plug.ExecContext) error { + ec.PrintlnUnnecessary("") + ec.PrintlnUnnecessary(fmt.Sprintf(`%s + +Estimation usually ends within 15 seconds.`, banner)) + conf := ec.GetStringArg(argDMTConfig) + if !paths.Exists(conf) { + return fmt.Errorf("migration config does not exist: %s", conf) + } + mID := MakeMigrationID() + stages, err := NewEstimateStages(ec.Logger(), mID, conf) + if err != nil { + return err + } + sp := stage.NewFixedProvider(stages.Build(ctx, ec)...) + res, err := stage.Execute(ctx, ec, any(nil), sp) + if err != nil { + return err + } + resArr := res.([]string) + ec.PrintlnUnnecessary("") + ec.PrintlnUnnecessary(resArr[0]) + ec.PrintlnUnnecessary(resArr[1]) + ec.PrintlnUnnecessary("") + ec.PrintlnUnnecessary("OK Estimation completed successfully.") + return nil +} + +func init() { + check.Must(plug.Registry.RegisterCommand("estimate", &EstimateCmd{})) +} diff --git a/base/commands/migration/estimate_it_test.go b/base/commands/migration/estimate_it_test.go new file mode 100644 index 00000000..5779197d --- /dev/null +++ b/base/commands/migration/estimate_it_test.go @@ -0,0 +1,56 @@ +//go:build std || migration + +package migration_test + +import ( + "context" + "encoding/json" + "os" + "sync" + "testing" + "time" + + "github.com/hazelcast/hazelcast-commandline-client/base/commands/migration" + "github.com/hazelcast/hazelcast-commandline-client/internal/check" + "github.com/hazelcast/hazelcast-commandline-client/internal/it" + "github.com/hazelcast/hazelcast-go-client/serialization" +) + +func TestEstimate(t *testing.T) { + tcx := it.TestContext{T: t} + ctx := context.Background() + tcx.Tester(func(tcx it.TestContext) { + createMapping(ctx, tcx) + var wg sync.WaitGroup + wg.Add(1) + go func() { + defer wg.Done() + check.Must(tcx.CLC().Execute(ctx, "estimate", "testdata/dmt_config")) + }() + c := make(chan string) + go findEstimationID(ctx, tcx, c) + mID := <-c + go estimateRunner(ctx, tcx, mID) + wg.Wait() + tcx.AssertStdoutContains("OK Estimation completed successfully") + }) +} + +func estimateRunner(ctx context.Context, tcx it.TestContext, migrationID string) { + statusMap := check.MustValue(tcx.Client.GetMap(ctx, migration.StatusMapName)) + b := check.MustValue(os.ReadFile("testdata/estimate/estimate_completed.json")) + check.Must(statusMap.Set(ctx, migrationID, serialization.JSON(b))) +} + +func findEstimationID(ctx context.Context, tcx it.TestContext, c chan string) { + q := check.MustValue(tcx.Client.GetQueue(ctx, migration.EstimateQueueName)) + var b migration.ConfigBundle + for { + v := check.MustValue(q.PollWithTimeout(ctx, 100*time.Millisecond)) + if v != nil { + check.Must(json.Unmarshal(v.(serialization.JSON), &b)) + c <- b.MigrationID + break + } + } +} diff --git a/base/commands/migration/estimate_stages.go b/base/commands/migration/estimate_stages.go new file mode 100644 index 00000000..2c62a59f --- /dev/null +++ b/base/commands/migration/estimate_stages.go @@ -0,0 +1,179 @@ +//go:build std || migration + +package migration + +import ( + "context" + "errors" + "fmt" + "strconv" + "time" + + "github.com/hazelcast/hazelcast-commandline-client/clc/ux/stage" + "github.com/hazelcast/hazelcast-commandline-client/internal/log" + "github.com/hazelcast/hazelcast-commandline-client/internal/plug" + "github.com/hazelcast/hazelcast-go-client" + "github.com/hazelcast/hazelcast-go-client/serialization" +) + +type EstimateStages struct { + migrationID string + configDir string + ci *hazelcast.ClientInternal + estimateQueue *hazelcast.Queue + statusMap *hazelcast.Map + logger log.Logger +} + +func NewEstimateStages(logger log.Logger, migrationID, configDir string) (*EstimateStages, error) { + if migrationID == "" { + return nil, errors.New("migrationID is required") + } + return &EstimateStages{ + migrationID: migrationID, + configDir: configDir, + logger: logger, + }, nil +} + +func (es *EstimateStages) Build(ctx context.Context, ec plug.ExecContext) []stage.Stage[any] { + return []stage.Stage[any]{ + { + ProgressMsg: "Connecting to the migration cluster", + SuccessMsg: "Connected to the migration cluster", + FailureMsg: "Could not connect to the migration cluster", + Func: es.connectStage(ec), + }, + { + ProgressMsg: "Estimating the migration", + SuccessMsg: "Estimated the migration", + FailureMsg: "Could not estimate the migration", + Func: es.estimateStage(), + }, + } +} + +func (es *EstimateStages) connectStage(ec plug.ExecContext) func(context.Context, stage.Statuser[any]) (any, error) { + return func(ctx context.Context, s stage.Statuser[any]) (any, error) { + var err error + es.ci, err = ec.ClientInternal(ctx) + if err != nil { + return nil, err + } + es.estimateQueue, err = es.ci.Client().GetQueue(ctx, EstimateQueueName) + if err != nil { + return nil, fmt.Errorf("retrieving the estimate Queue: %w", err) + } + es.statusMap, err = es.ci.Client().GetMap(ctx, StatusMapName) + if err != nil { + return nil, fmt.Errorf("retrieving the status Map: %w", err) + } + return nil, nil + } +} + +func (es *EstimateStages) estimateStage() func(context.Context, stage.Statuser[any]) (any, error) { + return func(ctx context.Context, status stage.Statuser[any]) (any, error) { + cb, err := makeConfigBundle(es.configDir, es.migrationID) + if err != nil { + return nil, fmt.Errorf("making configuration bundle: %w", err) + } + if err = es.estimateQueue.Put(ctx, cb); err != nil { + return nil, fmt.Errorf("updating estimate Queue: %w", err) + } + childCtx, cancel := context.WithTimeout(ctx, 30*time.Second) + defer cancel() + if err = waitForEstimationToComplete(childCtx, es.ci, es.migrationID, status); err != nil { + return nil, fmt.Errorf("waiting for estimation to complete: %w", err) + } + return fetchEstimationResults(ctx, es.ci, es.migrationID) + } +} + +func waitForEstimationToComplete(ctx context.Context, ci *hazelcast.ClientInternal, migrationID string, stage stage.Statuser[any]) error { + duration := 16 * time.Second + interval := 1 * time.Second + for { + if duration > 0 { + stage.SetRemainingDuration(duration) + } else { + stage.SetText("Estimation took longer than expected.") + } + status, err := fetchMigrationStatus(ctx, ci, migrationID) + if err != nil { + if errors.Is(err, migrationStatusNotFoundErr) { + // migration status will not be available for a while, so we should wait for it + continue + } + return err + } + if Status(status) == StatusFailed { + errs, err := fetchMigrationErrors(ctx, ci, migrationID) + if err != nil { + return fmt.Errorf("estimation failed and dmt cannot fetch estimation errors: %w", err) + } + return errors.New(errs) + } + if Status(status) == StatusComplete { + return nil + } + time.Sleep(interval) + duration = duration - interval + } +} + +func fetchEstimationResults(ctx context.Context, ci *hazelcast.ClientInternal, migrationID string) ([]string, error) { + q := fmt.Sprintf(`SELECT JSON_QUERY(this, '$.estimatedTime'), JSON_QUERY(this, '$.estimatedSize') FROM %s WHERE __key='%s'`, StatusMapName, migrationID) + res, err := ci.Client().SQL().Execute(ctx, q) + if err != nil { + return nil, err + } + it, err := res.Iterator() + if err != nil { + return nil, err + } + if it.HasNext() { + // single iteration is enough that we are reading single result for a single migration + row, err := it.Next() + if err != nil { + return nil, err + } + estimatedTime, err := row.Get(0) + if err != nil { + return nil, err + } + estimatedSize, err := row.Get(1) + if err != nil { + return nil, err + } + et, err := msToSecs(estimatedTime.(serialization.JSON).String()) + if err != nil { + return nil, err + } + es, err := bytesToMegabytes(estimatedSize.(serialization.JSON).String()) + if err != nil { + return nil, err + } + return []string{fmt.Sprintf("Estimated Size: %s ", es), fmt.Sprintf("Estimated Time: %s", et)}, nil + } + return nil, errors.New("no rows found") +} + +func bytesToMegabytes(bytesStr string) (string, error) { + bytes, err := strconv.ParseFloat(bytesStr, 64) + if err != nil { + return "", err + } + mb := bytes / (1024.0 * 1024.0) + return fmt.Sprintf("%.2f MBs", mb), nil +} + +func msToSecs(ms string) (string, error) { + milliseconds, err := strconv.ParseInt(ms, 10, 64) + if err != nil { + return "", err + } + seconds := float64(milliseconds) / 1000.0 + secondsStr := fmt.Sprintf("%.1f sec", seconds) + return secondsStr, nil +} diff --git a/base/commands/migration/migration_stages.go b/base/commands/migration/migration_stages.go index 06d11923..9d176e6f 100644 --- a/base/commands/migration/migration_stages.go +++ b/base/commands/migration/migration_stages.go @@ -9,11 +9,13 @@ import ( "fmt" "os" "path/filepath" + "strconv" "strings" "time" "github.com/hazelcast/hazelcast-go-client" "github.com/hazelcast/hazelcast-go-client/serialization" + "github.com/hazelcast/hazelcast-go-client/sql" "github.com/hazelcast/hazelcast-commandline-client/clc/ux/stage" clcerrors "github.com/hazelcast/hazelcast-commandline-client/errors" @@ -26,6 +28,12 @@ var timeoutErr = fmt.Errorf("migration could not be completed: reached timeout w var migrationStatusNotFoundErr = fmt.Errorf("migration status not found") +var migrationReportNotFoundErr = "migration report cannot be found: %w" + +var noDataStructuresFoundErr = errors.New("no datastructures found to migrate") + +var progressMsg = "Migrating %s: %s" + func createMigrationStages(ctx context.Context, ec plug.ExecContext, ci *hazelcast.ClientInternal, migrationID string) ([]stage.Stage[any], error) { var stages []stage.Stage[any] dss, err := getDataStructuresToBeMigrated(ctx, ec, migrationID) @@ -35,7 +43,7 @@ func createMigrationStages(ctx context.Context, ec plug.ExecContext, ci *hazelca for i, d := range dss { i := i stages = append(stages, stage.Stage[any]{ - ProgressMsg: fmt.Sprintf("Migrating %s: %s", d.Type, d.Name), + ProgressMsg: fmt.Sprintf(progressMsg, d.Type, d.Name), SuccessMsg: fmt.Sprintf("Migrated %s: %s", d.Type, d.Name), FailureMsg: fmt.Sprintf("Failed migrating %s: %s", d.Type, d.Name), Func: func(ct context.Context, status stage.Statuser[any]) (any, error) { @@ -71,6 +79,16 @@ func createMigrationStages(ctx context.Context, ec plug.ExecContext, ci *hazelca case StatusCanceled, StatusCanceling: execErr = errors.New("migration canceled by the user") break statusReaderLoop + case StatusInProgress: + rt, cp, err := fetchOverallProgress(ctx, ci, migrationID) + if err != nil { + ec.Logger().Error(err) + status.SetText("Unable to calculate remaining duration and progress") + } else { + status.SetText(fmt.Sprintf(progressMsg, d.Type, d.Name)) + status.SetProgress(cp) + status.SetRemainingDuration(rt) + } } q := fmt.Sprintf(`SELECT JSON_QUERY(this, '$.migrations[%d]') FROM %s WHERE __key= '%s'`, i, StatusMapName, migrationID) res, err := ci.Client().SQL().Execute(ctx, q) @@ -99,7 +117,6 @@ func createMigrationStages(ctx context.Context, ec plug.ExecContext, ci *hazelca execErr = err break statusReaderLoop } - status.SetProgress(m.CompletionPercentage) switch m.Status { case StatusComplete: return nil, nil @@ -130,12 +147,19 @@ func getDataStructuresToBeMigrated(ctx context.Context, ec plug.ExecContext, mig if err != nil { return nil, err } + rr, err := r.Get(0) + if err != nil { + return nil, err + } + if rr == nil { + return nil, noDataStructuresFoundErr + } var status OverallMigrationStatus - if err = json.Unmarshal(r.(serialization.JSON), &status); err != nil { + if err = json.Unmarshal(rr.(serialization.JSON), &status); err != nil { return nil, err } if len(status.Migrations) == 0 { - return nil, errors.New("no datastructures found to migrate") + return nil, noDataStructuresFoundErr } for _, m := range status.Migrations { dss = append(dss, DataStructureInfo{ @@ -203,6 +227,7 @@ type OverallMigrationStatus struct { Errors []string `json:"errors"` Report string `json:"report"` CompletionPercentage float32 `json:"completionPercentage"` + RemainingTime float32 `json:"remainingTime"` Migrations []DataStructureMigrationStatus `json:"migrations"` } @@ -225,26 +250,84 @@ func fetchMigrationStatus(ctx context.Context, ci *hazelcast.ClientInternal, mig if err != nil { return "", migrationStatusNotFoundErr } - return strings.TrimSuffix(strings.TrimPrefix(string(r.(serialization.JSON)), `"`), `"`), nil + rr, err := r.Get(0) + if err != nil { + return "", migrationStatusNotFoundErr + } + if rr == nil { + return "", migrationStatusNotFoundErr + } + return strings.TrimSuffix(strings.TrimPrefix(string(rr.(serialization.JSON)), `"`), `"`), nil +} + +func fetchOverallProgress(ctx context.Context, ci *hazelcast.ClientInternal, migrationID string) (time.Duration, float32, error) { + q := fmt.Sprintf(`SELECT JSON_QUERY(this, '$.remainingTime'), JSON_QUERY(this, '$.completionPercentage') FROM %s WHERE __key='%s'`, StatusMapName, migrationID) + r, err := querySingleRow(ctx, ci, q) + if err != nil { + return 0, 0, err + } + if r == nil { + return 0, 0, errors.New("overall progress not found") + } + remainingTime, err := r.Get(0) + if err != nil { + return 0, 0, err + } + completionPercentage, err := r.Get(1) + if err != nil { + return 0, 0, err + } + if completionPercentage == nil { + return 0, 0, fmt.Errorf("completionPercentage is not available in %s", StatusMapName) + } + if remainingTime == nil { + return 0, 0, fmt.Errorf("remainingTime is not available in %s", StatusMapName) + } + rt, err := strconv.ParseInt(remainingTime.(serialization.JSON).String(), 10, 64) + if err != nil { + return 0, 0, err + } + cpStr := completionPercentage.(serialization.JSON).String() + cp, err := strconv.ParseFloat(cpStr, 32) + if err != nil { + return 0, 0, err + } + return time.Duration(rt) * time.Millisecond, float32(cp), nil } func fetchMigrationReport(ctx context.Context, ci *hazelcast.ClientInternal, migrationID string) (string, error) { q := fmt.Sprintf(`SELECT JSON_QUERY(this, '$.report') FROM %s WHERE __key='%s'`, StatusMapName, migrationID) r, err := querySingleRow(ctx, ci, q) if err != nil { - return "", fmt.Errorf("migration report cannot be found: %w", err) + return "", fmt.Errorf(migrationReportNotFoundErr, err) + } + if r == nil { + return "", errors.New("migration report not found") + } + rr, err := r.Get(0) + if err != nil { + return "", fmt.Errorf(migrationReportNotFoundErr, err) } - return strings.ReplaceAll(string(r.(serialization.JSON)), `\"`, ``), nil + var t string + json.Unmarshal(rr.(serialization.JSON), &t) + return t, nil } func fetchMigrationErrors(ctx context.Context, ci *hazelcast.ClientInternal, migrationID string) (string, error) { q := fmt.Sprintf(`SELECT JSON_QUERY(this, '$.errors' WITH WRAPPER) FROM %s WHERE __key='%s'`, StatusMapName, migrationID) - row, err := querySingleRow(ctx, ci, q) + r, err := querySingleRow(ctx, ci, q) + if err != nil { + return "", err + } + if r == nil { + return "", errors.New("could not fetch migration errors") + } + rr, err := r.Get(0) if err != nil { return "", err } var errs []string - err = json.Unmarshal(row.(serialization.JSON), &errs) + err = json.Unmarshal(rr.(serialization.JSON), &errs) if err != nil { return "", err } @@ -264,7 +347,7 @@ func finalizeMigration(ctx context.Context, ec plug.ExecContext, ci *hazelcast.C return nil } -func querySingleRow(ctx context.Context, ci *hazelcast.ClientInternal, query string) (any, error) { +func querySingleRow(ctx context.Context, ci *hazelcast.ClientInternal, query string) (sql.Row, error) { res, err := ci.Client().SQL().Execute(ctx, query) if err != nil { return nil, err @@ -279,11 +362,39 @@ func querySingleRow(ctx context.Context, ci *hazelcast.ClientInternal, query str if err != nil { return nil, err } - r, err := row.Get(0) - if err != nil { - return "", err - } - return r, nil + return row, nil } return nil, errors.New("no result found") } + +func maybePrintWarnings(ctx context.Context, ec plug.ExecContext, ci *hazelcast.ClientInternal, migrationID string) { + q := fmt.Sprintf(`SELECT JSON_QUERY(this, '$.warnings' WITH WRAPPER) FROM %s WHERE __key='%s'`, StatusMapName, migrationID) + r, err := querySingleRow(ctx, ci, q) + if err != nil { + ec.Logger().Error(err) + return + } + if r == nil { + ec.Logger().Info("could not find any warnings") + return + } + rr, err := r.Get(0) + if err != nil { + ec.Logger().Error(err) + return + } + if rr == nil { + return + } + var warnings []string + err = json.Unmarshal(rr.(serialization.JSON), &warnings) + if err != nil { + ec.Logger().Error(err) + return + } + if len(warnings) <= 5 { + ec.PrintlnUnnecessary("* " + strings.Join(warnings, "\n* ")) + } else { + ec.PrintlnUnnecessary(fmt.Sprintf("You have %d warnings that you can find in your migration report.", len(warnings))) + } +} diff --git a/base/commands/migration/migration_start.go b/base/commands/migration/migration_start.go index b9df75e0..c11ad5a0 100644 --- a/base/commands/migration/migration_start.go +++ b/base/commands/migration/migration_start.go @@ -4,8 +4,10 @@ package migration import ( "context" + "fmt" "github.com/hazelcast/hazelcast-commandline-client/clc" + "github.com/hazelcast/hazelcast-commandline-client/clc/paths" "github.com/hazelcast/hazelcast-commandline-client/clc/ux/stage" clcerrors "github.com/hazelcast/hazelcast-commandline-client/errors" "github.com/hazelcast/hazelcast-commandline-client/internal/check" @@ -41,6 +43,10 @@ func (StartCmd) Exec(ctx context.Context, ec plug.ExecContext) (err error) { Selected data structures in the source cluster will be migrated to the target cluster. `) + conf := ec.GetStringArg(argDMTConfig) + if !paths.Exists(conf) { + return fmt.Errorf("migration config does not exist: %s", conf) + } if !ec.Props().GetBool(clc.FlagAutoYes) { p := prompt.New(ec.Stdin(), ec.Stdout()) yes, err := p.YesNo("Proceed?") @@ -54,12 +60,13 @@ Selected data structures in the source cluster will be migrated to the target cl ec.PrintlnUnnecessary("") mID := MigrationIDGeneratorFunc() defer func() { + maybePrintWarnings(ctx, ec, ci, mID) finalizeErr := finalizeMigration(ctx, ec, ci, mID, ec.Props().GetString(flagOutputDir)) if err == nil { err = finalizeErr } }() - sts, err := NewStartStages(ec.Logger(), mID, ec.GetStringArg(argDMTConfig)) + sts, err := NewStartStages(ec.Logger(), mID, conf) if err != nil { return err } diff --git a/base/commands/migration/migration_status.go b/base/commands/migration/migration_status.go index aeba1993..40cf9e71 100644 --- a/base/commands/migration/migration_status.go +++ b/base/commands/migration/migration_status.go @@ -37,6 +37,7 @@ func (s StatusCmd) Exec(ctx context.Context, ec plug.ExecContext) (err error) { return err } defer func() { + maybePrintWarnings(ctx, ec, ci, mID.(string)) finalizeErr := finalizeMigration(ctx, ec, ci, mID.(string), ec.Props().GetString(flagOutputDir)) if err == nil { err = finalizeErr diff --git a/base/commands/migration/start_stages.go b/base/commands/migration/start_stages.go index b13e3dfa..4b8179bf 100644 --- a/base/commands/migration/start_stages.go +++ b/base/commands/migration/start_stages.go @@ -4,7 +4,6 @@ package migration import ( "context" - "encoding/json" "errors" "fmt" "time" @@ -13,7 +12,6 @@ import ( "github.com/hazelcast/hazelcast-commandline-client/internal/log" "github.com/hazelcast/hazelcast-commandline-client/internal/plug" "github.com/hazelcast/hazelcast-go-client" - "github.com/hazelcast/hazelcast-go-client/serialization" ) type StartStages struct { @@ -124,16 +122,3 @@ func waitForMigrationToStart(ctx context.Context, ci *hazelcast.ClientInternal, } } } - -func makeConfigBundle(configDir, migrationID string) (serialization.JSON, error) { - var cb ConfigBundle - cb.MigrationID = migrationID - if err := cb.Walk(configDir); err != nil { - return nil, err - } - b, err := json.Marshal(cb) - if err != nil { - return nil, err - } - return b, nil -} diff --git a/base/commands/migration/start_stages_it_test.go b/base/commands/migration/start_stages_it_test.go index 519804aa..72eb8bb6 100644 --- a/base/commands/migration/start_stages_it_test.go +++ b/base/commands/migration/start_stages_it_test.go @@ -69,7 +69,7 @@ func startMigrationTest(t *testing.T, expectedErr error, statusMapStateFiles []s var execErr error go tcx.WithReset(func() { defer wg.Done() - execErr = tcx.CLC().Execute(ctx, "start", "dmt-config", "--yes", "-o", outDir) + execErr = tcx.CLC().Execute(ctx, "start", "testdata/dmt_config", "--yes", "-o", outDir) }) c := make(chan string) go findMigrationID(ctx, tcx, c) diff --git a/base/commands/migration/testdata/estimate/estimate_completed.json b/base/commands/migration/testdata/estimate/estimate_completed.json new file mode 100644 index 00000000..e6ad6081 --- /dev/null +++ b/base/commands/migration/testdata/estimate/estimate_completed.json @@ -0,0 +1,5 @@ +{ + "status": "COMPLETED", + "estimatedTime": 1, + "estimatedSize": 1 +} \ No newline at end of file diff --git a/base/commands/migration/testdata/stages/migration_in_progress.json b/base/commands/migration/testdata/stages/migration_in_progress.json index 5f90ba74..6be6e6bb 100644 --- a/base/commands/migration/testdata/stages/migration_in_progress.json +++ b/base/commands/migration/testdata/stages/migration_in_progress.json @@ -5,6 +5,7 @@ "finishTimestamp": "2023-01-01T00:01:00Z", "type": "MIGRATION", "completionPercentage": 12.123, + "remainingTime": 1, "migrations": [ { "name": "imap5", diff --git a/base/commands/migration/utils.go b/base/commands/migration/utils.go index 43b9d8fc..1f1b3ec8 100644 --- a/base/commands/migration/utils.go +++ b/base/commands/migration/utils.go @@ -5,6 +5,7 @@ package migration import ( "bufio" "encoding/base64" + "encoding/json" "fmt" "io/fs" "os" @@ -12,6 +13,7 @@ import ( "sort" "strings" + "github.com/hazelcast/hazelcast-go-client/serialization" "github.com/hazelcast/hazelcast-go-client/types" "github.com/hazelcast/hazelcast-commandline-client/clc/paths" @@ -126,3 +128,16 @@ var MigrationIDGeneratorFunc = makeMigrationID func makeMigrationID() string { return types.NewUUID().String() } + +func makeConfigBundle(configDir, migrationID string) (serialization.JSON, error) { + var cb ConfigBundle + cb.MigrationID = migrationID + if err := cb.Walk(configDir); err != nil { + return nil, err + } + b, err := json.Marshal(cb) + if err != nil { + return nil, err + } + return b, nil +}