// Copyright 2022 Clastix Labs // SPDX-License-Identifier: Apache-2.0 package migrate import ( "context" "fmt" "strings" "time" "github.com/spf13/cobra" "k8s.io/apimachinery/pkg/runtime" "k8s.io/apimachinery/pkg/types" ctrl "sigs.k8s.io/controller-runtime" ctrlclient "sigs.k8s.io/controller-runtime/pkg/client" kamajiv1alpha1 "github.com/clastix/kamaji/api/v1alpha1" "github.com/clastix/kamaji/internal/datastore" ) func NewCmd(scheme *runtime.Scheme) *cobra.Command { // CLI flags var ( tenantControlPlane string targetDataStore string cleanupPriorMigration bool timeout time.Duration ) cmd := &cobra.Command{ Use: "migrate", Short: "Migrate the data of a TenantControlPlane to another compatible DataStore", SilenceUsage: true, RunE: func(*cobra.Command, []string) error { ctx, cancelFn := context.WithTimeout(context.Background(), timeout) defer cancelFn() log := ctrl.Log log.Info("generating the controller-runtime client") client, err := ctrlclient.New(ctrl.GetConfigOrDie(), ctrlclient.Options{ Scheme: scheme, }) if err != nil { return err } parts := strings.Split(tenantControlPlane, string(types.Separator)) if len(parts) != 2 { return fmt.Errorf("non well-formed namespaced name for the tenant control plane, expected /NAME, fot %s", tenantControlPlane) } log.Info("retrieving the TenantControlPlane") tcp := &kamajiv1alpha1.TenantControlPlane{} if err = client.Get(ctx, types.NamespacedName{Namespace: parts[0], Name: parts[1]}, tcp); err != nil { return err } log.Info("retrieving the TenantControlPlane used DataStore") originDs := &kamajiv1alpha1.DataStore{} if err = client.Get(ctx, types.NamespacedName{Name: tcp.Status.Storage.DataStoreName}, originDs); err != nil { return err } log.Info("retrieving the target DataStore") targetDs := &kamajiv1alpha1.DataStore{} if err = client.Get(ctx, types.NamespacedName{Name: targetDataStore}, targetDs); err != nil { return err } if tcp.Status.Storage.Driver != string(targetDs.Spec.Driver) { return fmt.Errorf("migration between DataStore with different driver is not supported") } if tcp.Status.Storage.DataStoreName == targetDs.GetName() { return fmt.Errorf("cannot migrate to the same DataStore") } log.Info("generating the origin storage connection") originConnection, err := datastore.NewStorageConnection(ctx, client, *originDs) if err != nil { return err } defer originConnection.Close() log.Info("generating the target storage connection") targetConnection, err := datastore.NewStorageConnection(ctx, client, *targetDs) if err != nil { return err } defer targetConnection.Close() if cleanupPriorMigration { log.Info("Checking if target DataStore should be clean-up prior migration") if exists, _ := targetConnection.DBExists(ctx, tcp.Status.Storage.Setup.Schema); exists { log.Info("A colliding schema on target DataStore is present, cleaning up") if dErr := targetConnection.DeleteDB(ctx, tcp.Status.Storage.Setup.Schema); dErr != nil { return fmt.Errorf("error cleaning up prior migration: %s", dErr.Error()) } log.Info("Cleaning up prior migration has been completed") } } // Start migrating from the old Datastore to the new one log.Info("migration from origin to target started") if err = originConnection.Migrate(ctx, *tcp, targetConnection); err != nil { return fmt.Errorf("unable to migrate data from %s to %s: %w", originDs.GetName(), targetDs.GetName(), err) } log.Info("migration completed") return nil }, } cmd.Flags().StringVar(&tenantControlPlane, "tenant-control-plane", "", "Namespaced-name of the TenantControlPlane that must be migrated (e.g.: default/test)") cmd.Flags().StringVar(&targetDataStore, "target-datastore", "", "Name of the Datastore to which the TenantControlPlane will be migrated") cmd.Flags().BoolVar(&cleanupPriorMigration, "cleanup-prior-migration", false, "When set to true, migration job will drop existing data in the target DataStore: useful to avoid stale data when migrating back and forth between DataStores.") cmd.Flags().DurationVar(&timeout, "timeout", 5*time.Minute, "Amount of time for the context timeout") _ = cmd.MarkFlagRequired("tenant-control-plane") _ = cmd.MarkFlagRequired("target-datastore") return cmd }