From d2c8aa56593fb321cd735387d1fd085cdf466e6a Mon Sep 17 00:00:00 2001 From: p4u1 Date: Mon, 18 Aug 2025 14:28:27 +0200 Subject: [PATCH] feat(app): Adds abra app move command --- cli/app/move.go | 305 +++++++++++++++++++++++++++++++++++++++++++ cli/app/secret.go | 2 +- cli/run.go | 1 + pkg/client/secret.go | 2 +- pkg/secret/secret.go | 4 +- 5 files changed, 310 insertions(+), 4 deletions(-) create mode 100644 cli/app/move.go diff --git a/cli/app/move.go b/cli/app/move.go new file mode 100644 index 00000000..051116a0 --- /dev/null +++ b/cli/app/move.go @@ -0,0 +1,305 @@ +package app + +import ( + "context" + "fmt" + "os" + "os/exec" + "strings" + + "coopcloud.tech/abra/cli/internal" + appPkg "coopcloud.tech/abra/pkg/app" + "coopcloud.tech/abra/pkg/autocomplete" + "coopcloud.tech/abra/pkg/client" + containerPkg "coopcloud.tech/abra/pkg/container" + "coopcloud.tech/abra/pkg/log" + "coopcloud.tech/abra/pkg/secret" + "coopcloud.tech/abra/pkg/upstream/convert" + "coopcloud.tech/abra/pkg/upstream/stack" + "github.com/docker/docker/api/types" + containertypes "github.com/docker/docker/api/types/container" + "github.com/docker/docker/api/types/filters" + "github.com/docker/docker/api/types/mount" + "github.com/spf13/cobra" +) + +var AppMoveCommand = &cobra.Command{ + Use: "move [flags]", + Aliases: []string{"d"}, + Short: "Moves an app to a different server", + Long: `Deploy an app. + +This command supports chaos operations. Use "--chaos/-C" to deploy your recipe +checkout as-is. Recipe commit hashes are also supported as values for +"[version]". Please note, "upgrade"/"rollback" do not support chaos operations.`, + Example: ` # standard deployment + abra app deploy 1312.net + + # chaos deployment + abra app deploy 1312.net --chaos + + # deploy specific version + abra app deploy 1312.net 2.0.0+1.2.3 + + # deploy a specific git hash + abra app deploy 1312.net 886db76d`, + Args: cobra.RangeArgs(1, 2), + ValidArgsFunction: func( + cmd *cobra.Command, + args []string, + toComplete string, + ) ([]string, cobra.ShellCompDirective) { + switch l := len(args); l { + case 0: + return autocomplete.AppNameComplete() + case 1: + return autocomplete.ServerNameComplete() + default: + return nil, cobra.ShellCompDirectiveDefault + } + }, + RunE: func(cmd *cobra.Command, args []string) error { + app := internal.ValidateApp(args) + + if err := app.Recipe.Ensure(internal.GetEnsureContext()); err != nil { + return err + } + + cl, err := client.New(app.Server) + if err != nil { + return err + } + + filter := filters.NewArgs() + filter.Add("label", fmt.Sprintf("%s=%s", convert.LabelNamespace, app.StackName())) + services, err := cl.ServiceList(context.Background(), types.ServiceListOptions{Filters: filter}) + if err != nil { + return err + } + + secretsToStore := map[string]string{} + volumes := map[string]containertypes.MountPoint{} + + composeFiles, err := app.Recipe.GetComposeFiles(app.Env) + if err != nil { + log.Fatal(err) + } + + filtersSecret, err := app.Filters(false, false) + if err != nil { + log.Fatal(err) + } + + secretList, err := cl.SecretList(context.Background(), types.SecretListOptions{Filters: filtersSecret}) + if err != nil { + log.Fatal(err) + } + + secretConfigs, err := secret.ReadSecretsConfig(app.Path, composeFiles, app.StackName()) + if err != nil { + log.Fatal(err) + } + + opts := stack.Deploy{Composefiles: composeFiles, Namespace: app.StackName()} + compose, err := appPkg.GetAppComposeConfig(app.Name, opts, app.Env) + if err != nil { + log.Fatal(err) + } + for _, s := range services { + log.Info("service", s.Spec.Name) + + secretNames := map[string]string{} + for _, serviceCompose := range compose.Services { + if app.StackName()+"_"+serviceCompose.Name != s.Spec.Name { + continue + } + + for _, secret := range serviceCompose.Secrets { + for _, s := range secretList { + if s.Spec.Name == app.StackName()+"_"+secret.Source+"_"+secretConfigs[secret.Source].Version { + secretNames[secret.Source] = s.ID + break + } + } + } + } + + f := filters.NewArgs() + f.Add("name", s.Spec.Name) + targetContainer, err := containerPkg.GetContainer(context.Background(), cl, f, true) + if err != nil { + log.Error(err) + continue + } + for _, m := range targetContainer.Mounts { + if m.Type == mount.TypeVolume { + volumes[m.Name] = m + } + } + + for secretName, secretID := range secretNames { + if _, ok := secretsToStore[secretName]; ok { + continue + } + log.Debugf("extracting secret %s", secretName) + + out, err := exec.Command("ssh", app.Server, "-tt", fmt.Sprintf("sudo cat /var/lib/docker/containers/%s/mounts/secrets/%s", targetContainer.ID, secretID)).Output() + if err != nil { + fmt.Println(string(out)) + fmt.Println(err) + continue + } + secretsToStore[secretName] = string(out) + } + + } + + if internal.Dry { + fmt.Println(secretsToStore) + fmt.Println(volumes) + return nil + } + stack.WaitTimeout, err = appPkg.GetTimeoutFromLabel(compose, app.StackName()) + if err != nil { + log.Fatal(err) + } + rmOpts := stack.Remove{ + Namespaces: []string{app.StackName()}, + Detach: false, + } + if err := stack.RunRemove(context.Background(), cl, rmOpts); err != nil { + return err + } + + cl2, err := client.New(args[1]) + if err != nil { + return err + } + for _, s := range secretList { + sname := strings.Split(strings.TrimPrefix(s.Spec.Name, app.StackName()+"_"), "_") + secretName := strings.Join(sname[:len(sname)-1], "_") + data := secretsToStore[secretName] + if err := client.StoreSecret(cl2, s.Spec.Name, data); err != nil { + log.Info(err) + } + } + + for _, volume := range volumes { + fileName := fmt.Sprintf("%s.tar.gz", volume.Name) + + log.Infof("moving volume: %s", volume.Name) + log.Debug("creating %s", fileName) + cmd := exec.Command("ssh", app.Server, "-tt", fmt.Sprintf("sudo tar --same-owner -czhpf %s -C /var/lib/docker/volumes %s", fileName, volume.Name)) + if out, err := cmd.CombinedOutput(); err != nil { + fmt.Println(string(out)) + fmt.Println(err) + } + log.Debug("copying %s to local machine", fileName) + cmd = exec.Command("scp", fmt.Sprintf("%s:%s", app.Server, fileName), fileName) + if out, err := cmd.CombinedOutput(); err != nil { + fmt.Println(string(out)) + fmt.Println(err) + } + log.Debug("copying %s to %s", fileName, args[1]) + cmd = exec.Command("scp", fileName, fmt.Sprintf("%s:%s", args[1], fileName)) + if out, err := cmd.CombinedOutput(); err != nil { + fmt.Println(string(out)) + fmt.Println(err) + } + log.Debug("extracting %s on %s", fileName, args[1]) + cmd = exec.Command("ssh", args[1], "-tt", fmt.Sprintf("sudo tar --same-owner -xzpf %s -C /var/lib/docker/volumes", fileName)) + if out, err := cmd.CombinedOutput(); err != nil { + fmt.Println(string(out)) + fmt.Println(err) + } + cmd = exec.Command("ssh", args[1], "-tt", fmt.Sprintf("sudo rm %s", fileName)) + if out, err := cmd.CombinedOutput(); err != nil { + fmt.Println(string(out)) + fmt.Println(err) + } + cmd = exec.Command("ssh", app.Server, "-tt", fmt.Sprintf("sudo rm %s", fileName)) + if out, err := cmd.CombinedOutput(); err != nil { + fmt.Println(string(out)) + fmt.Println(err) + } + } + + if err := copyFile(app.Path, strings.ReplaceAll(app.Path, app.Server, args[1])); err != nil { + return err + } + if err := os.Remove(app.Path); err != nil { + return err + } + + // AppDeployCommand.Run(cmd, args) + + // app = internal.ValidateApp(args) + // cl, err := client.New(app.Server) + // if err != nil { + // return err + // } + // + // composeFiles, err := app.Recipe.GetComposeFiles(app.Env) + // if err != nil { + // log.Fatal(err) + // } + // stackName := app.StackName() + // deployOpts := stack.Deploy{ + // Composefiles: composeFiles, + // Namespace: stackName, + // Prune: false, + // ResolveImage: stack.ResolveImageAlways, + // Detach: false, + // } + // compose, err := appPkg.GetAppComposeConfig(app.Name, deployOpts, app.Env) + // if err != nil { + // return err + // } + // serviceNames, err := appPkg.GetAppServiceNames(app.Name) + // if err != nil { + // log.Fatal(err) + // } + // + // f, err := app.Filters(true, false, serviceNames...) + // if err != nil { + // log.Fatal(err) + // } + // if err := stack.RunDeploy( + // cl, + // deployOpts, + // compose, + // app.Name, + // app.Server, + // internal.DontWaitConverge, + // f, + // ); err != nil { + // log.Fatal(err) + // } + + return nil + }, +} + +func copyFile(src string, dst string) error { + // Read all content of src to data, may cause OOM for a large file. + data, err := os.ReadFile(src) + if err != nil { + return err + } + // Write data to dst + err = os.WriteFile(dst, data, 0o644) + if err != nil { + return err + } + return nil +} + +func init() { + AppMoveCommand.Flags().BoolVarP( + &internal.Dry, + "dry-run", + "r", + false, + "report changes that would be made", + ) +} diff --git a/cli/app/secret.go b/cli/app/secret.go index 6b5da2a2..bb4088f1 100644 --- a/cli/app/secret.go +++ b/cli/app/secret.go @@ -224,7 +224,7 @@ environment. Typically, you can let Abra generate them for you on app creation } secretName := fmt.Sprintf("%s_%s_%s", app.StackName(), name, version) - if err := client.StoreSecret(cl, secretName, data, app.Server); err != nil { + if err := client.StoreSecret(cl, secretName, data); err != nil { log.Fatal(err) } diff --git a/cli/run.go b/cli/run.go index 13dd8aad..006e1276 100644 --- a/cli/run.go +++ b/cli/run.go @@ -204,6 +204,7 @@ func Run(version, commit string) { app.AppRestartCommand, app.AppRestoreCommand, app.AppRollbackCommand, + app.AppMoveCommand, app.AppRunCommand, app.AppSecretCommand, app.AppServicesCommand, diff --git a/pkg/client/secret.go b/pkg/client/secret.go index f8419425..1d095467 100644 --- a/pkg/client/secret.go +++ b/pkg/client/secret.go @@ -7,7 +7,7 @@ import ( "github.com/docker/docker/client" ) -func StoreSecret(cl *client.Client, secretName, secretValue, server string) error { +func StoreSecret(cl *client.Client, secretName, secretValue string) error { ann := swarm.Annotations{Name: secretName} spec := swarm.SecretSpec{Annotations: ann, Data: []byte(secretValue)} diff --git a/pkg/secret/secret.go b/pkg/secret/secret.go index f8366c7b..7c30195a 100644 --- a/pkg/secret/secret.go +++ b/pkg/secret/secret.go @@ -216,7 +216,7 @@ func GenerateSecrets(cl *dockerClient.Client, secrets map[string]Secret, server return } - if err := client.StoreSecret(cl, secret.RemoteName, password, server); err != nil { + if err := client.StoreSecret(cl, secret.RemoteName, password); err != nil { if strings.Contains(err.Error(), "AlreadyExists") { log.Warnf("%s already exists", secret.RemoteName) ch <- nil @@ -236,7 +236,7 @@ func GenerateSecrets(cl *dockerClient.Client, secrets map[string]Secret, server return } - if err := client.StoreSecret(cl, secret.RemoteName, passphrase, server); err != nil { + if err := client.StoreSecret(cl, secret.RemoteName, passphrase); err != nil { if strings.Contains(err.Error(), "AlreadyExists") { log.Warnf("%s already exists", secret.RemoteName) ch <- nil