feat: Adds abra app move
All checks were successful
continuous-integration/drone/pr Build is passing

This commit is contained in:
2025-08-18 14:28:27 +02:00
parent 3fae036db2
commit 9849d47b64
10 changed files with 327 additions and 23 deletions

View File

@ -141,7 +141,7 @@ func CopyToContainer(cl *dockerClient.Client, containerID, srcPath, dstPath stri
Cmd: []string{"mkdir", "-p", dstPath},
Detach: false,
Tty: true,
}); err != nil {
}, true); err != nil {
return fmt.Errorf("create remote directory: %s", err)
}
case CopyModeFileToFile:
@ -180,7 +180,7 @@ func CopyToContainer(cl *dockerClient.Client, containerID, srcPath, dstPath stri
Cmd: []string{"mv", path.Join("/tmp", srcFile), movePath},
Detach: false,
Tty: true,
}); err != nil {
}, true); err != nil {
return fmt.Errorf("create remote directory: %s", err)
}
}

292
cli/app/move.go Normal file
View File

@ -0,0 +1,292 @@
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 <domain> <server> [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
}

View File

@ -85,7 +85,7 @@ var AppRunCommand = &cobra.Command{
log.Fatal(err)
}
if _, err := container.RunExec(dcli, cl, targetContainer.ID, &execCreateOpts); err != nil {
if _, err := container.RunExec(dcli, cl, targetContainer.ID, &execCreateOpts, true); err != nil {
log.Fatal(err)
}
},

View File

@ -196,7 +196,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)
}

View File

@ -66,10 +66,10 @@ func RunBackupCmdRemote(
return nil, err
}
out, err := container.RunExec(dcli, cl, containerID, &execBackupListOpts)
_, err = container.RunExec(dcli, cl, containerID, &execBackupListOpts, true)
if err != nil {
return nil, err
}
return out, nil
return nil, nil
}

View File

@ -64,7 +64,7 @@ func RunCmdRemote(
Tty: false,
}
if _, err := container.RunExec(dcli, cl, targetContainer.ID, &execCreateOpts); err != nil {
if _, err := container.RunExec(dcli, cl, targetContainer.ID, &execCreateOpts, true); err != nil {
log.Infof("%s does not exist for %s, use /bin/sh as fallback", shell, app.Name)
shell = "/bin/sh"
}
@ -91,7 +91,7 @@ func RunCmdRemote(
log.Debugf("not requesting a remote TTY")
}
if _, err := container.RunExec(dcli, cl, targetContainer.ID, &execCreateOpts); err != nil {
if _, err := container.RunExec(dcli, cl, targetContainer.ID, &execCreateOpts, true); err != nil {
return err
}

View File

@ -203,6 +203,7 @@ func Run(version, commit string) {
app.AppRestartCommand,
app.AppRestoreCommand,
app.AppRollbackCommand,
app.AppMoveCommand,
app.AppRunCommand,
app.AppSecretCommand,
app.AppServicesCommand,