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/upstream/container" "coopcloud.tech/abra/pkg/upstream/convert" "coopcloud.tech/abra/pkg/upstream/stack" "github.com/docker/cli/cli/command" "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 } dcli, err := command.NewDockerCli() if err != nil { return err } secretsToStore := map[string]string{} volumes := map[string]containertypes.MountPoint{} for _, s := range services { log.Info("service", s.Spec.Name) // stackAndServiceName := fmt.Sprintf("^%s_%s", app.StackName(), s.Spec.Name) 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 } } execCreateOpts := containertypes.ExecOptions{ AttachStderr: false, AttachStdin: false, AttachStdout: true, Cmd: []string{"ls", "/run/secrets"}, Detach: false, Tty: false, } out, err := container.RunExec(dcli, cl, targetContainer.ID, &execCreateOpts, false) if err != nil { log.Error(out) continue } for _, secret := range strings.Split(strings.TrimSpace(out), "\n") { if _, ok := secretsToStore[secret]; ok { continue } log.Debugf("extracting secret %s", secret) execCreateOpts := containertypes.ExecOptions{ AttachStderr: false, AttachStdin: false, AttachStdout: true, Cmd: []string{"cat", fmt.Sprintf("/run/secrets/%s", secret)}, Detach: false, Tty: false, } out, err := container.RunExec(dcli, cl, targetContainer.ID, &execCreateOpts, false) if err != nil { return err } secretsToStore[secret] = out } } composeFiles, err := app.Recipe.GetComposeFiles(app.Env) 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) } 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 } filters, err := app.Filters(false, false) if err != nil { log.Fatal(err) } secretList, err := cl.SecretList(context.Background(), types.SecretListOptions{Filters: filters}) for _, s := range secretList { sname := strings.Split(strings.TrimPrefix(s.Spec.Name, app.StackName()+"_"), "_") secretName := strings.Join(sname[:len(sname)-1], "_") data := secretsToStore[secretName] fmt.Println(s.Spec.Name) fmt.Println(secretName) fmt.Println(data) 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) } } fmt.Println(strings.ReplaceAll(app.Path, app.Server, args[1])) 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 }