From 61849a358ca3e02447893823691f3a50a0619a7e Mon Sep 17 00:00:00 2001 From: p4u1 Date: Mon, 18 Aug 2025 14:28:27 +0200 Subject: [PATCH 1/4] feat(app): Adds abra app move command --- cli/app/move.go | 313 +++++++++++++++++++++++++++++++++++ cli/app/secret.go | 2 +- cli/internal/deploy.go | 53 ++++++ cli/run.go | 1 + pkg/client/secret.go | 2 +- pkg/secret/secret.go | 4 +- pkg/upstream/stack/remove.go | 2 +- 7 files changed, 372 insertions(+), 5 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..4c7ccd9e --- /dev/null +++ b/cli/app/move.go @@ -0,0 +1,313 @@ +package app + +import ( + "context" + "fmt" + "os" + "os/exec" + "strings" + + "coopcloud.tech/abra/cli/internal" + "coopcloud.tech/abra/pkg/app" + 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/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/docker/docker/api/types/swarm" + "github.com/docker/docker/api/types/volume" + dockerclient "github.com/docker/docker/client" + "github.com/spf13/cobra" +) + +var AppMoveCommand = &cobra.Command{ + Use: "move [flags]", + Short: "Moves an app to a different server", + Long: `Move an app to a differnt server. + +This will copy secrets and volumes from the old server to the new one. It will also undeploy the app from old server but not deploy it on the new. You will have to do that your self, after the move finished. + +Use "--dry-run/-r" to see which secrets and volumes will be moved.`, + Example: ` # moving an app + abra app move nextcloud.example.com myserver.com`, + 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 + } + }, + Run: func(cmd *cobra.Command, args []string) { + app := internal.ValidateApp(args) + if len(args) <= 1 { + log.Fatal("no server provided") + } + newServer := args[1] + + if err := app.Recipe.Ensure(internal.GetEnsureContext()); err != nil { + log.Fatal(err) + } + + cl, err := client.New(app.Server) + if err != nil { + log.Fatal(err) + } + + resources, err := getAppResources(cl, app) + if err != nil { + log.Fatal(err) + } + + internal.MoveOverview(app, newServer, resources.SecretNames(), resources.VolumeNames()) + if err := internal.PromptProcced(); err != nil { + return + } + + // NOTE: wait timeout will be removed, until it actually is just set it to a high value. + stack.WaitTimeout = 500 + rmOpts := stack.Remove{ + Namespaces: []string{app.StackName()}, + Detach: false, + } + if err := stack.RunRemove(context.Background(), cl, rmOpts); err != nil { + log.Fatal(err) + } + + cl2, err := client.New(newServer) + if err != nil { + log.Fatal(err) + } + + for _, s := range resources.SecretList { + sname := strings.Split(strings.TrimPrefix(s.Spec.Name, app.StackName()+"_"), "_") + secretName := strings.Join(sname[:len(sname)-1], "_") + data := resources.Secrets[secretName] + if err := client.StoreSecret(cl2, s.Spec.Name, data); err != nil { + log.Infof("creating secret: %s", s.Spec.Name) + log.Errorf("failed to store secret on new server: %s", err) + } + } + + for _, v := range resources.Volumes { + log.Infof("moving volume: %s", v.Name) + + // Need to create the volume before copying the data, because when + // docker creates a new volume it set the folder permissions to + // root, which might be wrong. This ensures we always have the + // correct folder permissions inside the volume. + log.Debug("creating volume: %s", v.Name) + _, err := cl2.VolumeCreate(context.Background(), volume.CreateOptions{ + Name: v.Name, + Driver: v.Driver, + }) + if err != nil { + log.Errorf("failed to create volume: %s", err) + } + + fileName := fmt.Sprintf("%s.tar.gz", v.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, v.Name)) + if out, err := cmd.CombinedOutput(); err != nil { + log.Errorf("failed to tar volume: %s", err) + fmt.Println(string(out)) + } + 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 { + log.Errorf("failed to copy tar to local machine: %s", err) + fmt.Println(string(out)) + } + log.Debug("copying %s to %s", fileName, newServer) + cmd = exec.Command("scp", fileName, fmt.Sprintf("%s:%s", newServer, fileName)) + if out, err := cmd.CombinedOutput(); err != nil { + log.Errorf("failed to copy tar to new server: %s", err) + fmt.Println(string(out)) + } + log.Debug("extracting %s on %s", fileName, newServer) + cmd = exec.Command("ssh", newServer, "-tt", fmt.Sprintf("sudo tar --same-owner -xzpf %s -C /var/lib/docker/volumes", fileName)) + if out, err := cmd.CombinedOutput(); err != nil { + log.Errorf("failed to extract tar: %s", err) + fmt.Println(string(out)) + } + + // Remove tar files + cmd = exec.Command("ssh", newServer, "-tt", fmt.Sprintf("sudo rm %s", fileName)) + if out, err := cmd.CombinedOutput(); err != nil { + log.Errorf("failed to remove tar from new server: %s", err) + fmt.Println(string(out)) + } + cmd = exec.Command("ssh", app.Server, "-tt", fmt.Sprintf("sudo rm %s", fileName)) + if out, err := cmd.CombinedOutput(); err != nil { + log.Errorf("failed to remove tar from old server: %s", err) + fmt.Println(string(out)) + } + cmd = exec.Command("rm", fileName) + if out, err := cmd.CombinedOutput(); err != nil { + log.Errorf("failed to remove tar on local machine: %s", err) + fmt.Println(string(out)) + } + } + + log.Debug("moving app config to new server") + if err := copyFile(app.Path, strings.ReplaceAll(app.Path, app.Server, newServer)); err != nil { + log.Fatal(err) + } + if err := os.Remove(app.Path); err != nil { + log.Fatal(err) + } + + fmt.Println("% was succefully moved to %s", app.Name, newServer) + fmt.Println("Run the following command to deploy the app", app.Name, newServer) + fmt.Println(" abra app deploy --no-domain-checks", app.Domain) + fmt.Println() + fmt.Println("And don't forget to update you DNS record. And don't panic, as it might take a bit for the dust to settle. Traefik for example might fail to obtain the lets encrypt certificate for a while.", app.Domain) + fmt.Println() + fmt.Println("If anything goes wrong, you can always move the app config file to the original server and deploy it there again. There was no data removed on the old server") + return + }, +} + +type AppResources struct { + Secrets map[string]string + SecretList []swarm.Secret + Volumes map[string]containertypes.MountPoint +} + +func (a *AppResources) SecretNames() []string { + secrets := []string{} + for name := range a.Secrets { + secrets = append(secrets, name) + } + return secrets +} + +func (a *AppResources) VolumeNames() []string { + volumes := []string{} + for name := range a.Volumes { + volumes = append(volumes, name) + } + return volumes +} + +func getAppResources(cl *dockerclient.Client, app app.App) (*AppResources, error) { + filter, err := app.Filters(false, false) + if err != nil { + return nil, err + } + + services, err := cl.ServiceList(context.Background(), types.ServiceListOptions{Filters: filter}) + if err != nil { + return nil, err + } + + composeFiles, err := app.Recipe.GetComposeFiles(app.Env) + if err != nil { + return nil, err + } + + secretList, err := cl.SecretList(context.Background(), types.SecretListOptions{Filters: filter}) + 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) + } + + resources := &AppResources{ + Secrets: make(map[string]string), + SecretList: secretList, + Volumes: make(map[string]containertypes.MountPoint), + } + for _, s := range services { + 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 { + resources.Volumes[m.Name] = m + } + } + + for secretName, secretID := range secretNames { + if _, ok := resources.Secrets[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 + } + resources.Secrets[secretName] = string(out) + } + } + return resources, 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 45dbcbaa..b3f5c0f7 100644 --- a/cli/app/secret.go +++ b/cli/app/secret.go @@ -247,7 +247,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/internal/deploy.go b/cli/internal/deploy.go index 37406128..44349288 100644 --- a/cli/internal/deploy.go +++ b/cli/internal/deploy.go @@ -142,6 +142,59 @@ func getDeployType(currentVersion, newVersion string) string { return i18n.G("DOWNGRADE") } +// MoveOverview shows a overview before moving an app to a different server +func MoveOverview( + app appPkg.App, + newServer string, + secrets []string, + volumes []string, +) { + server := app.Server + if app.Server == "default" { + server = "local" + } + + domain := app.Domain + if domain == "" { + domain = config.NO_DOMAIN_DEFAULT + } + + rows := [][]string{ + {"DOMAIN", domain}, + {"RECIPE", app.Recipe.Name}, + {"OLD SERVER", server}, + {"New SERVER", newServer}, + {"SECRETS", strings.Join(secrets, "\n")}, + {"VOLUMES", strings.Join(volumes, "\n")}, + } + + overview := formatter.CreateOverview("MOVE OVERVIEW", rows) + + fmt.Println(overview) +} + +func PromptProcced() error { + if NoInput { + return nil + } + + if Dry { + return fmt.Errorf("dry run") + } + + response := false + prompt := &survey.Confirm{Message: "proceed?"} + if err := survey.AskOne(prompt, &response); err != nil { + return err + } + + if !response { + return errors.New("cancelled") + } + + return nil +} + // PostCmds parses a string of commands and executes them inside of the respective services // the commands string must have the following format: // " | |... " diff --git a/cli/run.go b/cli/run.go index aafc86e4..a2540f9e 100644 --- a/cli/run.go +++ b/cli/run.go @@ -258,6 +258,7 @@ Config: 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 2f9c9529..ffec2915 100644 --- a/pkg/secret/secret.go +++ b/pkg/secret/secret.go @@ -218,7 +218,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(i18n.G("%s already exists", secret.RemoteName)) ch <- nil @@ -238,7 +238,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(i18n.G("%s already exists", secret.RemoteName)) ch <- nil diff --git a/pkg/upstream/stack/remove.go b/pkg/upstream/stack/remove.go index 35f0052d..e4011580 100644 --- a/pkg/upstream/stack/remove.go +++ b/pkg/upstream/stack/remove.go @@ -84,7 +84,7 @@ func RunRemove(ctx context.Context, client *apiclient.Client, opts Remove) error continue } - log.Info(i18n.G("polling undeploy status")) + log.Debug("polling undeploy status") timeout, err := waitOnTasks(ctx, client, namespace) if timeout { errs = append(errs, err.Error()) -- 2.49.0 From 824f314472142545be7874270b81624eb0adfad1 Mon Sep 17 00:00:00 2001 From: decentral1se Date: Mon, 1 Sep 2025 07:36:13 +0200 Subject: [PATCH 2/4] refactor: `app move` review pass --- .gitignore | 1 + cli/app/move.go | 201 +++++++++++++++++++++-------------- cli/internal/deploy.go | 32 ++++-- pkg/config/abra.go | 2 + pkg/upstream/stack/remove.go | 2 +- 5 files changed, 145 insertions(+), 93 deletions(-) diff --git a/.gitignore b/.gitignore index 1985cb7e..16e0eb69 100644 --- a/.gitignore +++ b/.gitignore @@ -1,3 +1,4 @@ +*.tar.gz *fmtcoverage.html .e2e.env .envrc diff --git a/cli/app/move.go b/cli/app/move.go index 4c7ccd9e..97b8d6ab 100644 --- a/cli/app/move.go +++ b/cli/app/move.go @@ -2,6 +2,7 @@ package app import ( "context" + "errors" "fmt" "os" "os/exec" @@ -12,7 +13,9 @@ import ( appPkg "coopcloud.tech/abra/pkg/app" "coopcloud.tech/abra/pkg/autocomplete" "coopcloud.tech/abra/pkg/client" + "coopcloud.tech/abra/pkg/config" containerPkg "coopcloud.tech/abra/pkg/container" + "coopcloud.tech/abra/pkg/i18n" "coopcloud.tech/abra/pkg/log" "coopcloud.tech/abra/pkg/secret" "coopcloud.tech/abra/pkg/upstream/stack" @@ -26,16 +29,36 @@ import ( "github.com/spf13/cobra" ) +// translators: `abra app move` aliases. use a comma separated list of aliases +// with no spaces in between +var appMoveAliases = i18n.G("m") + var AppMoveCommand = &cobra.Command{ - Use: "move [flags]", - Short: "Moves an app to a different server", - Long: `Move an app to a differnt server. + // translators: `app move` command + Use: i18n.G("move [flags]"), + Aliases: strings.Split(appMoveAliases, ","), + // translators: Short description for `app move` command + Short: i18n.G("Moves an app to a different server"), + Long: i18n.G(`Move an app to a differnt server. -This will copy secrets and volumes from the old server to the new one. It will also undeploy the app from old server but not deploy it on the new. You will have to do that your self, after the move finished. +This command will migrate an app config and copy secrets and volumes from the +old server to the new one. The app MUST be deployed on the old server before +doing the move. The app will be undeployed from the current server but not +deployed on the new server. -Use "--dry-run/-r" to see which secrets and volumes will be moved.`, - Example: ` # moving an app - abra app move nextcloud.example.com myserver.com`, +This command requires the "cat" command to be present on the app containers +when retrieving secrets. Some "distroless" images will not support this. Not +all apps are therefore moveable. Rsync is required on your local machine for +transferring volumes. + +Do not forget to update your DNS records. Don't panic, it might take a while +for the dust to settle after you move an app. If anything goes wrong, you can +always move the app config file to the original server and deploy it there +again. No data is removed from the old server. + +Use "--dry-run/-r" to see which secrets and volumes will be moved.`), + Example: i18n.G(` # move an app + abra app move nextcloud.1312.net myserver.com`), Args: cobra.RangeArgs(1, 2), ValidArgsFunction: func( cmd *cobra.Command, @@ -53,41 +76,50 @@ Use "--dry-run/-r" to see which secrets and volumes will be moved.`, }, Run: func(cmd *cobra.Command, args []string) { app := internal.ValidateApp(args) + if len(args) <= 1 { - log.Fatal("no server provided") + log.Fatal(i18n.G("no server provided?")) } - newServer := args[1] + newServer := internal.ValidateServer([]string{args[1]}) if err := app.Recipe.Ensure(internal.GetEnsureContext()); err != nil { log.Fatal(err) } - cl, err := client.New(app.Server) + currentServerClient, err := client.New(app.Server) if err != nil { log.Fatal(err) } - resources, err := getAppResources(cl, app) + deployMeta, err := stack.IsDeployed(context.Background(), currentServerClient, app.StackName()) if err != nil { log.Fatal(err) } + if !deployMeta.IsDeployed { + log.Fatal(i18n.G("%s must first be deployed on %s before moving", app.Name, app.Server)) + } + + resources, err := getAppResources(currentServerClient, app) + if err != nil { + log.Fatal(i18n.G("unable to retrieve %s resources on %s: %s", app.Name, app.Server, err)) + } + internal.MoveOverview(app, newServer, resources.SecretNames(), resources.VolumeNames()) if err := internal.PromptProcced(); err != nil { - return + log.Fatal(i18n.G("bailing out: %s", err)) } - // NOTE: wait timeout will be removed, until it actually is just set it to a high value. - stack.WaitTimeout = 500 + log.Info(i18n.G("undeploying %s on %s", app.Name, app.Server)) rmOpts := stack.Remove{ Namespaces: []string{app.StackName()}, Detach: false, } - if err := stack.RunRemove(context.Background(), cl, rmOpts); err != nil { - log.Fatal(err) + if err := stack.RunRemove(context.Background(), currentServerClient, rmOpts); err != nil { + log.Fatal(i18n.G("failed to remove app from %s: %s", err, app.Server)) } - cl2, err := client.New(newServer) + newServerClient, err := client.New(newServer) if err != nil { log.Fatal(err) } @@ -96,88 +128,87 @@ Use "--dry-run/-r" to see which secrets and volumes will be moved.`, sname := strings.Split(strings.TrimPrefix(s.Spec.Name, app.StackName()+"_"), "_") secretName := strings.Join(sname[:len(sname)-1], "_") data := resources.Secrets[secretName] - if err := client.StoreSecret(cl2, s.Spec.Name, data); err != nil { - log.Infof("creating secret: %s", s.Spec.Name) - log.Errorf("failed to store secret on new server: %s", err) + if err := client.StoreSecret(newServerClient, s.Spec.Name, data); err != nil { + log.Fatal(i18n.G("failed to store secret on %s: %s", err, newServer)) } + log.Info(i18n.G("created secret on %s: %s", s.Spec.Name, newServer)) } for _, v := range resources.Volumes { - log.Infof("moving volume: %s", v.Name) + log.Info(i18n.G("moving volume %s from %s to %s", v.Name, app.Server, newServer)) - // Need to create the volume before copying the data, because when - // docker creates a new volume it set the folder permissions to - // root, which might be wrong. This ensures we always have the - // correct folder permissions inside the volume. - log.Debug("creating volume: %s", v.Name) - _, err := cl2.VolumeCreate(context.Background(), volume.CreateOptions{ + // NOTE(p4u1): Need to create the volume before copying the data, because + // when docker creates a new volume it set the folder permissions to + // root, which might be wrong. This ensures we always have the correct + // folder permissions inside the volume. + log.Debug(i18n.G("creating volume %s on %s", v.Name, newServer)) + _, err := newServerClient.VolumeCreate(context.Background(), volume.CreateOptions{ Name: v.Name, Driver: v.Driver, }) if err != nil { - log.Errorf("failed to create volume: %s", err) + log.Fatal(i18n.G("failed to create volume %s on %s: %s", v.Name, newServer, err)) } - fileName := fmt.Sprintf("%s.tar.gz", v.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, v.Name)) + outgoingFilename := fmt.Sprintf("%s_outgoing.tar.gz", v.Name) + log.Debug(i18n.G("creating %s on %s", outgoingFilename, app.Server)) + tarCmd := fmt.Sprintf("sudo tar --same-owner -czhpf %s -C /var/lib/docker/volumes %s", outgoingFilename, v.Name) + cmd := exec.Command("ssh", app.Server, "-tt", tarCmd) if out, err := cmd.CombinedOutput(); err != nil { - log.Errorf("failed to tar volume: %s", err) - fmt.Println(string(out)) + log.Fatal(i18n.G("%s failed on %s: output:%s err:%s", tarCmd, app.Server, string(out), err)) } - log.Debug("copying %s to local machine", fileName) - cmd = exec.Command("scp", fmt.Sprintf("%s:%s", app.Server, fileName), fileName) + + log.Debug(i18n.G("rsyncing %s from %s to local machine", outgoingFilename, app.Server)) + cmd = exec.Command("rsync", "-a", "-v", fmt.Sprintf("%s:%s", app.Server, outgoingFilename), outgoingFilename) if out, err := cmd.CombinedOutput(); err != nil { - log.Errorf("failed to copy tar to local machine: %s", err) - fmt.Println(string(out)) + log.Fatal(i18n.G("failed to copy %s from %s to local machine: output:%s err:%s", outgoingFilename, app.Server, string(out), err)) } - log.Debug("copying %s to %s", fileName, newServer) - cmd = exec.Command("scp", fileName, fmt.Sprintf("%s:%s", newServer, fileName)) + + incomingFilename := fmt.Sprintf("%s_incoming.tar.gz", v.Name) + log.Debug(i18n.G("rsyncing %s (renaming to %s) to %s from local machine", outgoingFilename, incomingFilename, newServer)) + cmd = exec.Command("rsync", "-a", "-v", outgoingFilename, fmt.Sprintf("%s:%s", newServer, incomingFilename)) if out, err := cmd.CombinedOutput(); err != nil { - log.Errorf("failed to copy tar to new server: %s", err) - fmt.Println(string(out)) + log.Fatal(i18n.G("failed to copy %s from local machine to %s: output:%s err:%s", outgoingFilename, newServer, string(out), err)) } - log.Debug("extracting %s on %s", fileName, newServer) - cmd = exec.Command("ssh", newServer, "-tt", fmt.Sprintf("sudo tar --same-owner -xzpf %s -C /var/lib/docker/volumes", fileName)) + + log.Debug(i18n.G("extracting %s on %s", incomingFilename, newServer)) + tarExtractCmd := fmt.Sprintf("sudo tar --same-owner -xzpf %s -C /var/lib/docker/volumes", outgoingFilename) + cmd = exec.Command("ssh", newServer, "-tt", tarExtractCmd) if out, err := cmd.CombinedOutput(); err != nil { - log.Errorf("failed to extract tar: %s", err) - fmt.Println(string(out)) + log.Fatal(i18n.G("%s failed to extract %s on %s: output:%s err:%s", tarExtractCmd, outgoingFilename, newServer, string(out), err)) } // Remove tar files - cmd = exec.Command("ssh", newServer, "-tt", fmt.Sprintf("sudo rm %s", fileName)) + log.Debug(i18n.G("removing %s from %s", incomingFilename, newServer)) + cmd = exec.Command("ssh", newServer, "-tt", fmt.Sprintf("sudo rm %s", incomingFilename)) if out, err := cmd.CombinedOutput(); err != nil { - log.Errorf("failed to remove tar from new server: %s", err) - fmt.Println(string(out)) + log.Fatal(i18n.G("failed to remove %s from %s: output:%s err:%s", incomingFilename, newServer, string(out), err)) } - cmd = exec.Command("ssh", app.Server, "-tt", fmt.Sprintf("sudo rm %s", fileName)) + + log.Debug(i18n.G("removing %s from %s", outgoingFilename, app.Server)) + cmd = exec.Command("ssh", app.Server, "-tt", fmt.Sprintf("sudo rm %s", outgoingFilename)) if out, err := cmd.CombinedOutput(); err != nil { - log.Errorf("failed to remove tar from old server: %s", err) - fmt.Println(string(out)) + log.Fatal(i18n.G("failed to remove %s from %s: output:%s err:%s", outgoingFilename, app.Server, string(out), err)) } - cmd = exec.Command("rm", fileName) + + log.Debug(i18n.G("removing %s from local machine", outgoingFilename)) + cmd = exec.Command("rm", outgoingFilename) if out, err := cmd.CombinedOutput(); err != nil { - log.Errorf("failed to remove tar on local machine: %s", err) - fmt.Println(string(out)) + log.Fatal(i18n.G("failed to remove %s on local machine: output:%s err:%s", outgoingFilename, string(out), err)) } } - log.Debug("moving app config to new server") - if err := copyFile(app.Path, strings.ReplaceAll(app.Path, app.Server, newServer)); err != nil { - log.Fatal(err) + newServerPath := fmt.Sprintf("%s/servers/%s/%s.env", config.ABRA_DIR, newServer, app.Name) + log.Info(i18n.G("migrating app config from %s to %s", app.Server, newServerPath)) + if err := copyFile(app.Path, newServerPath); err != nil { + log.Fatal(i18n.G("failed to migrate app config: %s", err)) } + if err := os.Remove(app.Path); err != nil { - log.Fatal(err) + log.Fatal(i18n.G("unable to remove %s: %s", app.Path, err)) } - fmt.Println("% was succefully moved to %s", app.Name, newServer) - fmt.Println("Run the following command to deploy the app", app.Name, newServer) - fmt.Println(" abra app deploy --no-domain-checks", app.Domain) - fmt.Println() - fmt.Println("And don't forget to update you DNS record. And don't panic, as it might take a bit for the dust to settle. Traefik for example might fail to obtain the lets encrypt certificate for a while.", app.Domain) - fmt.Println() - fmt.Println("If anything goes wrong, you can always move the app config file to the original server and deploy it there again. There was no data removed on the old server") - return + log.Info(i18n.G("%s was successfully moved from %s to %s 🎉", app.Name, app.Server, newServer)) }, } @@ -221,18 +252,18 @@ func getAppResources(cl *dockerclient.Client, app app.App) (*AppResources, error secretList, err := cl.SecretList(context.Background(), types.SecretListOptions{Filters: filter}) if err != nil { - log.Fatal(err) + return nil, err } secretConfigs, err := secret.ReadSecretsConfig(app.Path, composeFiles, app.StackName()) if err != nil { - log.Fatal(err) + return nil, err } opts := stack.Deploy{Composefiles: composeFiles, Namespace: app.StackName()} compose, err := appPkg.GetAppComposeConfig(app.Name, opts, app.Env) if err != nil { - log.Fatal(err) + return nil, err } resources := &AppResources{ @@ -240,16 +271,20 @@ func getAppResources(cl *dockerclient.Client, app app.App) (*AppResources, error SecretList: secretList, Volumes: make(map[string]containertypes.MountPoint), } + for _, s := range services { secretNames := map[string]string{} for _, serviceCompose := range compose.Services { - if app.StackName()+"_"+serviceCompose.Name != s.Spec.Name { + stackService := fmt.Sprintf("%s_%s", app.StackName(), serviceCompose.Name) + if stackService != s.Spec.Name { + log.Debug(i18n.G("skipping %s as it does not match %s", stackService, 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 { + stackSecret := fmt.Sprintf("%s_%s_%s", app.StackName(), secret.Source, secretConfigs[secret.Source].Version) + if s.Spec.Name == stackSecret { secretNames[secret.Source] = s.ID break } @@ -261,9 +296,9 @@ func getAppResources(cl *dockerclient.Client, app app.App) (*AppResources, error f.Add("name", s.Spec.Name) targetContainer, err := containerPkg.GetContainer(context.Background(), cl, f, true) if err != nil { - log.Error(err) - continue + return nil, errors.New(i18n.G("unable to get container matching %s: %s", s.Spec.Name, err)) } + for _, m := range targetContainer.Mounts { if m.Type == mount.TypeVolume { resources.Volumes[m.Name] = m @@ -274,17 +309,19 @@ func getAppResources(cl *dockerclient.Client, app app.App) (*AppResources, error if _, ok := resources.Secrets[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() + log.Debug(i18n.G("extracting secret %s on %s", secretName, app.Server)) + + cmd := fmt.Sprintf("sudo cat /var/lib/docker/containers/%s/mounts/secrets/%s", targetContainer.ID, secretID) + out, err := exec.Command("ssh", app.Server, "-tt", cmd).Output() if err != nil { - fmt.Println(string(out)) - fmt.Println(err) - continue + return nil, errors.New(i18n.G("%s failed on %s: output:%s err:%s", cmd, app.Server, string(out), err)) } + resources.Secrets[secretName] = string(out) } } + return resources, nil } @@ -294,20 +331,22 @@ func copyFile(src string, dst string) error { 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", + i18n.G("dry-run"), + i18n.G("r"), false, - "report changes that would be made", + i18n.G("report changes that would be made"), ) } diff --git a/cli/internal/deploy.go b/cli/internal/deploy.go index 44349288..4f816804 100644 --- a/cli/internal/deploy.go +++ b/cli/internal/deploy.go @@ -159,16 +159,26 @@ func MoveOverview( domain = config.NO_DOMAIN_DEFAULT } - rows := [][]string{ - {"DOMAIN", domain}, - {"RECIPE", app.Recipe.Name}, - {"OLD SERVER", server}, - {"New SERVER", newServer}, - {"SECRETS", strings.Join(secrets, "\n")}, - {"VOLUMES", strings.Join(volumes, "\n")}, + secretsOverview := strings.Join(secrets, "\n") + if len(secrets) == 0 { + secretsOverview = config.NO_SECRETS_DEFAULT } - overview := formatter.CreateOverview("MOVE OVERVIEW", rows) + volumesOverview := strings.Join(volumes, "\n") + if len(volumes) == 0 { + volumesOverview = config.NO_VOLUMES_DEFAULT + } + + rows := [][]string{ + {i18n.G("DOMAIN"), domain}, + {i18n.G("RECIPE"), app.Recipe.Name}, + {i18n.G("OLD SERVER"), server}, + {i18n.G("NEW SERVER"), newServer}, + {i18n.G("SECRETS"), secretsOverview}, + {i18n.G("VOLUMES"), volumesOverview}, + } + + overview := formatter.CreateOverview(i18n.G("MOVE OVERVIEW"), rows) fmt.Println(overview) } @@ -179,17 +189,17 @@ func PromptProcced() error { } if Dry { - return fmt.Errorf("dry run") + return errors.New(i18n.G("dry run")) } response := false - prompt := &survey.Confirm{Message: "proceed?"} + prompt := &survey.Confirm{Message: i18n.G("proceed?")} if err := survey.AskOne(prompt, &response); err != nil { return err } if !response { - return errors.New("cancelled") + return errors.New(i18n.G("cancelled")) } return nil diff --git a/pkg/config/abra.go b/pkg/config/abra.go index 48781b00..9a0718cc 100644 --- a/pkg/config/abra.go +++ b/pkg/config/abra.go @@ -118,6 +118,8 @@ var ( NO_DOMAIN_DEFAULT = "N/A" NO_VERSION_DEFAULT = "N/A" + NO_SECRETS_DEFAULT = "N/A" + NO_VOLUMES_DEFAULT = "N/A" UNKNOWN_DEFAULT = "unknown" ) diff --git a/pkg/upstream/stack/remove.go b/pkg/upstream/stack/remove.go index e4011580..35f0052d 100644 --- a/pkg/upstream/stack/remove.go +++ b/pkg/upstream/stack/remove.go @@ -84,7 +84,7 @@ func RunRemove(ctx context.Context, client *apiclient.Client, opts Remove) error continue } - log.Debug("polling undeploy status") + log.Info(i18n.G("polling undeploy status")) timeout, err := waitOnTasks(ctx, client, namespace) if timeout { errs = append(errs, err.Error()) -- 2.49.0 From 9c37b9b748c0d7a2bac0815a8f6af8e1f8a33d22 Mon Sep 17 00:00:00 2001 From: decentral1se Date: Mon, 1 Sep 2025 11:17:11 +0200 Subject: [PATCH 3/4] test: app move (basics) --- pkg/client/client.go | 27 ++++++++++------- tests/integration/app_move.bats | 42 +++++++++++++++++++++++++++ tests/integration/helpers/common.bash | 1 + tests/integration/helpers/server.bash | 18 ++++++++++++ 4 files changed, 78 insertions(+), 10 deletions(-) create mode 100644 tests/integration/app_move.bats diff --git a/pkg/client/client.go b/pkg/client/client.go index b73a2a2c..69cc53a6 100644 --- a/pkg/client/client.go +++ b/pkg/client/client.go @@ -6,6 +6,7 @@ import ( "errors" "net/http" "os" + "strings" "time" contextPkg "coopcloud.tech/abra/pkg/context" @@ -38,18 +39,24 @@ func WithTimeout(timeout int) Opt { func New(serverName string, opts ...Opt) (*client.Client, error) { var clientOpts []client.Opt - if serverName != "default" { - context, err := GetContext(serverName) - if err != nil { - return nil, errors.New(i18n.G("unknown server, run \"abra server add %s\"?", serverName)) - } + ctx, err := GetContext(serverName) + if err != nil { + return nil, errors.New(i18n.G("unknown server, run \"abra server add %s\"?", serverName)) + } - ctxEndpoint, err := contextPkg.GetContextEndpoint(context) - if err != nil { - return nil, err - } + ctxEndpoint, err := contextPkg.GetContextEndpoint(ctx) + if err != nil { + return nil, err + } + var isUnix bool + if strings.Contains(ctxEndpoint, "unix://") { + isUnix = true + } + + if serverName != "default" && !isUnix { conf := &Conf{} + for _, opt := range opts { opt(conf) } @@ -93,7 +100,7 @@ func New(serverName string, opts ...Opt) (*client.Client, error) { } if info.Swarm.LocalNodeState == "inactive" { - if serverName != "default" { + if serverName != "default" && !isUnix { return cl, errors.New(i18n.G("swarm mode not enabled on %s?", serverName)) } diff --git a/tests/integration/app_move.bats b/tests/integration/app_move.bats new file mode 100644 index 00000000..28673242 --- /dev/null +++ b/tests/integration/app_move.bats @@ -0,0 +1,42 @@ +#!/usr/bin/env bash + +setup_file(){ + load "$PWD/tests/integration/helpers/common" + _common_setup + _add_server + _add_move_server + _new_app +} + +teardown_file(){ + _rm_app + _rm_server + _rm_move_server +} + +setup(){ + load "$PWD/tests/integration/helpers/common" + _common_setup + _ensure_catalogue +} + +teardown(){ + _undeploy_app +} + +@test "validate app argument" { + run $ABRA app move + assert_failure + + run $ABRA app move DOESNTEXIST + assert_failure + + run $ABRA app move "$TEST_APP_DOMAIN" DOESNTEXIST + assert_failure +} + +@test "move app fails if not deployed" { + run $ABRA app move "$TEST_APP_DOMAIN" "$TEST_MOVE_SERVER" + assert_failure + assert_output --partial 'must first be deployed' +} diff --git a/tests/integration/helpers/common.bash b/tests/integration/helpers/common.bash index 892be7b2..0ad6ebaf 100644 --- a/tests/integration/helpers/common.bash +++ b/tests/integration/helpers/common.bash @@ -17,6 +17,7 @@ _common_setup() { export TEST_APP_NAME="$(basename "${BATS_TEST_FILENAME//./_}")" export TEST_APP_DOMAIN="$TEST_APP_NAME.$TEST_SERVER" + export TEST_MOVE_SERVER="default2" export TEST_RECIPE="abra-test-recipe" _ensure_swarm diff --git a/tests/integration/helpers/server.bash b/tests/integration/helpers/server.bash index e3bcb320..76d6164d 100644 --- a/tests/integration/helpers/server.bash +++ b/tests/integration/helpers/server.bash @@ -10,6 +10,15 @@ _add_server() { assert_exists "$ABRA_DIR/servers/$TEST_SERVER" } +_add_move_server() { + run docker context create default2 --docker "host=unix:///var/run/docker.sock" + assert_success + + run mkdir -p "$ABRA_DIR/servers/default2" + assert_success + assert_exists "$ABRA_DIR/servers/default2" +} + _rm_server() { if [[ "$TEST_SERVER" == "default" ]]; then run rm -rf "$ABRA_DIR/servers/default" @@ -20,6 +29,15 @@ _rm_server() { assert_not_exists "$ABRA_DIR/servers/$TEST_SERVER" } +_rm_move_server() { + run docker context rm default2 + assert_success + + run rm -rf "$ABRA_DIR/servers/default2" + assert_success + assert_not_exists "$ABRA_DIR/servers/default2" +} + _rm_default_server(){ run rm -rf "$ABRA_DIR/servers/default" assert_success -- 2.49.0 From f0e2b012c65cf29ee154c7a683e818269e40f1d1 Mon Sep 17 00:00:00 2001 From: decentral1se Date: Mon, 1 Sep 2025 07:42:59 +0200 Subject: [PATCH 4/4] chore: make i18n --- pkg/i18n/locales/abra.pot | 247 +++++++++++++++++++++++++++++++++-- pkg/i18n/locales/es.po | 268 +++++++++++++++++++++++++++++++++++--- 2 files changed, 482 insertions(+), 33 deletions(-) diff --git a/pkg/i18n/locales/abra.pot b/pkg/i18n/locales/abra.pot index 1b2f494b..1022f84d 100644 --- a/pkg/i18n/locales/abra.pot +++ b/pkg/i18n/locales/abra.pot @@ -7,7 +7,7 @@ msgid "" msgstr "Project-Id-Version: \n" "Report-Msgid-Bugs-To: EMAIL\n" - "POT-Creation-Date: 2025-08-30 12:43+0200\n" + "POT-Creation-Date: 2025-09-01 11:17+0200\n" "PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" "Last-Translator: FULL NAME \n" "Language-Team: LANGUAGE \n" @@ -76,6 +76,11 @@ msgid " # list apps of all servers without live status\n" " abra app ls -r gitea" msgstr "" +#: ./cli/app/move.go:60 +msgid " # move an app\n" + " abra app move nextcloud.1312.net myserver.com" +msgstr "" + #: ./cli/app/cmd.go:41 msgid " # pass args/flags without \"--\"\n" " abra app cmd 1312.net app my_cmd_arg foo --user bar\n" @@ -221,7 +226,7 @@ msgstr "" msgid "%s does not exist for %s, use /bin/sh as fallback" msgstr "" -#: ./cli/app/cmd.go:114 ./cli/internal/deploy.go:151 +#: ./cli/app/cmd.go:114 ./cli/internal/deploy.go:214 #, c-format msgid "%s does not exist for %s?" msgstr "" @@ -246,6 +251,16 @@ msgstr "" msgid "%s doesn't have a %s function" msgstr "" +#: ./cli/app/move.go:158 ./cli/app/move.go:318 +#, c-format +msgid "%s failed on %s: output:%s err:%s" +msgstr "" + +#: ./cli/app/move.go:178 +#, c-format +msgid "%s failed to extract %s on %s: output:%s err:%s" +msgstr "" + #: ./pkg/upstream/stack/stack.go:169 #, c-format msgid "%s has been detected as deployed: %v" @@ -351,6 +366,11 @@ msgstr "" msgid "%s missing from %s.env" msgstr "" +#: ./cli/app/move.go:100 +#, c-format +msgid "%s must first be deployed on %s before moving" +msgstr "" + #: ./cli/recipe/upgrade.go:151 #, c-format msgid "%s not considered semver-like" @@ -406,6 +426,11 @@ msgstr "" msgid "%s successfully stored on server" msgstr "" +#: ./cli/app/move.go:211 +#, c-format +msgid "%s was successfully moved from %s to %s 🎉" +msgstr "" + #: ./cli/recipe/new.go:60 #, c-format msgid "%s/example.git" @@ -711,7 +736,7 @@ msgstr "" msgid "DEPLOYED LABELS" msgstr "" -#: ./cli/app/list.go:222 ./cli/internal/deploy.go:75 +#: ./cli/app/list.go:222 ./cli/internal/deploy.go:75 ./cli/internal/deploy.go:173 msgid "DOMAIN" msgstr "" @@ -925,6 +950,10 @@ msgstr "" msgid "List volumes associated with an app" msgstr "" +#: ./cli/internal/deploy.go:181 +msgid "MOVE OVERVIEW" +msgstr "" + #. translators: Short description for `app backup` command group #: ./cli/app/backup.go:246 msgid "Manage app backups" @@ -959,6 +988,32 @@ msgstr "" msgid "Manage the recipe catalogue" msgstr "" +#: ./cli/app/move.go:42 +msgid "Move an app to a differnt server.\n" + "\n" + "This command will migrate an app config and copy secrets and volumes from the\n" + "old server to the new one. The app MUST be deployed on the old server before\n" + "doing the move. The app will be undeployed from the current server but not\n" + "deployed on the new server.\n" + "\n" + "This command requires the \"cat\" command to be present on the app containers\n" + "when retrieving secrets. Some \"distroless\" images will not support this. Not\n" + "all apps are therefore moveable. Rsync is required on your local machine for\n" + "transferring volumes.\n" + "\n" + "Do not forget to update your DNS records. Don't panic, it might take a while\n" + "for the dust to settle after you move an app. If anything goes wrong, you can\n" + "always move the app config file to the original server and deploy it there\n" + "again. No data is removed from the old server.\n" + "\n" + "Use \"--dry-run/-r\" to see which secrets and volumes will be moved." +msgstr "" + +#. translators: Short description for `app move` command +#: ./cli/app/move.go:41 +msgid "Moves an app to a different server" +msgstr "" + #: ./cli/recipe/new.go:123 msgid "N" msgstr "" @@ -975,6 +1030,10 @@ msgstr "" msgid "NEW DEPLOYMENT" msgstr "" +#: ./cli/internal/deploy.go:176 +msgid "NEW SERVER" +msgstr "" + #: ./cli/app/secret.go:147 msgid "NOT" msgstr "" @@ -991,6 +1050,10 @@ msgid "Notify on new versions for deployed apps.\n" "Use \"--major/-m\" to include new major versions." msgstr "" +#: ./cli/internal/deploy.go:175 +msgid "OLD SERVER" +msgstr "" + #: ./cli/app/volume.go:54 msgid "ON SERVER" msgstr "" @@ -1015,7 +1078,7 @@ msgstr "" msgid "README.md metadata filled in" msgstr "" -#: ./cli/app/list.go:222 ./cli/internal/deploy.go:76 +#: ./cli/app/list.go:222 ./cli/internal/deploy.go:76 ./cli/internal/deploy.go:174 msgid "RECIPE" msgstr "" @@ -1135,6 +1198,10 @@ msgstr "" msgid "S" msgstr "" +#: ./cli/internal/deploy.go:177 +msgid "SECRETS" +msgstr "" + #: ./cli/app/new.go:204 msgid "SECRETS OVERVIEW" msgstr "" @@ -1481,6 +1548,10 @@ msgstr "" msgid "VERSION" msgstr "" +#: ./cli/internal/deploy.go:178 +msgid "VOLUMES" +msgstr "" + #: ./cli/recipe/reset.go:24 msgid "WARNING: this will delete your changes. Be Careful." msgstr "" @@ -1756,6 +1827,11 @@ msgstr "" msgid "bad status: %s" msgstr "" +#: ./cli/app/move.go:110 +#, c-format +msgid "bailing out: %s" +msgstr "" + #: ./pkg/upstream/convert/volume.go:113 msgid "bind options are incompatible with type tmpfs" msgstr "" @@ -1810,6 +1886,10 @@ msgstr "" msgid "can't separate key from value: %s (this variable is probably unset)" msgstr "" +#: ./cli/internal/deploy.go:202 +msgid "cancelled" +msgstr "" + #: ./pkg/catalogue/catalogue.go:59 ./pkg/recipe/git.go:245 #, c-format msgid "cannot ensure %s is up-to-date, no git remotes configured" @@ -2139,11 +2219,16 @@ msgstr "" msgid "create remote directory: %s" msgstr "" -#: ./pkg/client/client.go:88 +#: ./pkg/client/client.go:95 #, c-format msgid "created client for %s" msgstr "" +#: ./cli/app/move.go:134 +#, c-format +msgid "created secret on %s: %s" +msgstr "" + #: ./cli/recipe/release.go:428 #, c-format msgid "created tag %s at %s" @@ -2159,6 +2244,11 @@ msgstr "" msgid "creating %s" msgstr "" +#: ./cli/app/move.go:154 +#, c-format +msgid "creating %s on %s" +msgstr "" + #: ./cli/server/add.go:175 #, c-format msgid "creating context with domain %s" @@ -2174,6 +2264,11 @@ msgstr "" msgid "creating secret %s" msgstr "" +#: ./cli/app/move.go:144 +#, c-format +msgid "creating volume %s on %s" +msgstr "" + #: ./pkg/lint/recipe.go:22 msgid "critical" msgstr "" @@ -2373,6 +2468,10 @@ msgstr "" msgid "download [flags]" msgstr "" +#: ./cli/internal/deploy.go:192 +msgid "dry run" +msgstr "" + #: ./pkg/git/add.go:22 #, c-format msgid "dry run: adding %s" @@ -2415,7 +2514,7 @@ msgstr "" msgid "dry run: remote %s (%s) not created" msgstr "" -#: ./cli/catalogue/catalogue.go:301 ./cli/recipe/release.go:639 ./cli/recipe/sync.go:269 +#: ./cli/app/move.go:347 ./cli/catalogue/catalogue.go:301 ./cli/recipe/release.go:639 ./cli/recipe/sync.go:269 msgid "dry-run" msgstr "" @@ -2537,6 +2636,16 @@ msgstr "" msgid "expected 1 service but found %v: %s" msgstr "" +#: ./cli/app/move.go:174 +#, c-format +msgid "extracting %s on %s" +msgstr "" + +#: ./cli/app/move.go:313 +#, c-format +msgid "extracting secret %s on %s" +msgstr "" + #. translators: `abra recipe fetch` aliases. use a comma separated list of aliases #. with no spaces in between #: ./cli/app/deploy.go:347 ./cli/app/remove.go:163 ./cli/app/rollback.go:329 ./cli/app/secret.go:593 ./cli/app/upgrade.go:439 ./cli/app/volume.go:217 ./cli/recipe/fetch.go:20 ./cli/recipe/fetch.go:138 @@ -2563,6 +2672,16 @@ msgstr "" msgid "failed to commit changes: %s" msgstr "" +#: ./cli/app/move.go:164 +#, c-format +msgid "failed to copy %s from %s to local machine: output:%s err:%s" +msgstr "" + +#: ./cli/app/move.go:171 +#, c-format +msgid "failed to copy %s from local machine to %s: output:%s err:%s" +msgstr "" + #: ./pkg/upstream/stack/stack.go:531 #, c-format msgid "failed to create %s" @@ -2583,6 +2702,11 @@ msgstr "" msgid "failed to create secret %s" msgstr "" +#: ./cli/app/move.go:150 +#, c-format +msgid "failed to create volume %s on %s: %s" +msgstr "" + #: ./cli/updater/updater.go:263 #, c-format msgid "failed to determine deployed version of %s" @@ -2611,6 +2735,11 @@ msgstr "" msgid "failed to match chosen service" msgstr "" +#: ./cli/app/move.go:204 +#, c-format +msgid "failed to migrate app config: %s" +msgstr "" + #: ./pkg/client/registry.go:20 #, c-format msgid "failed to parse image %s, saw: %s" @@ -2621,6 +2750,21 @@ msgstr "" msgid "failed to publish new release: %s" msgstr "" +#: ./cli/app/move.go:185 ./cli/app/move.go:191 +#, c-format +msgid "failed to remove %s from %s: output:%s err:%s" +msgstr "" + +#: ./cli/app/move.go:197 +#, c-format +msgid "failed to remove %s on local machine: output:%s err:%s" +msgstr "" + +#: ./cli/app/move.go:119 +#, c-format +msgid "failed to remove app from %s: %s" +msgstr "" + #: ./pkg/upstream/stack/remove.go:183 #, c-format msgid "failed to remove config %s: %s" @@ -2665,6 +2809,11 @@ msgstr "" msgid "failed to select default branch in %s" msgstr "" +#: ./cli/app/move.go:132 +#, c-format +msgid "failed to store secret on %s: %s" +msgstr "" + #: ./cli/recipe/release.go:269 ./cli/recipe/release.go:554 #, c-format msgid "failed to tag release: %s" @@ -3259,9 +3408,11 @@ msgstr "" msgid "ls" msgstr "" +#. translators: `abra app move` aliases. use a comma separated list of aliases +#. with no spaces in between #. translators: `abra man` aliases. use a comma separated list of aliases #. with no spaces in between -#: ./cli/app/list.go:318 ./cli/app/ps.go:205 ./cli/app/secret.go:553 ./cli/app/secret.go:649 ./cli/recipe/list.go:104 ./cli/recipe/upgrade.go:376 ./cli/recipe/version.go:139 ./cli/run.go:128 ./cli/server/list.go:106 ./cli/updater/updater.go:560 +#: ./cli/app/list.go:318 ./cli/app/move.go:34 ./cli/app/ps.go:205 ./cli/app/secret.go:553 ./cli/app/secret.go:649 ./cli/recipe/list.go:104 ./cli/recipe/upgrade.go:376 ./cli/recipe/version.go:139 ./cli/run.go:128 ./cli/server/list.go:106 ./cli/updater/updater.go:560 msgid "m" msgstr "" @@ -3293,6 +3444,11 @@ msgstr "" msgid "man [flags]" msgstr "" +#: ./cli/app/move.go:202 +#, c-format +msgid "migrating app config from %s to %s" +msgstr "" + #: ./cli/internal/recipe.go:48 ./cli/internal/recipe.go:68 ./cli/internal/recipe.go:82 ./cli/recipe/release.go:655 ./cli/recipe/sync.go:285 ./cli/recipe/upgrade.go:359 msgid "minor" msgstr "" @@ -3322,6 +3478,16 @@ msgstr "" msgid "missing version for secret? (%s)" msgstr "" +#. translators: `app move` command +#: ./cli/app/move.go:38 +msgid "move [flags]" +msgstr "" + +#: ./cli/app/move.go:138 +#, c-format +msgid "moving volume %s from %s to %s" +msgstr "" + #: ./cli/app/secret.go:292 msgid "must provide argument if --no-input is passed" msgstr "" @@ -3527,6 +3693,10 @@ msgstr "" msgid "no server provided" msgstr "" +#: ./cli/app/move.go:81 +msgid "no server provided?" +msgstr "" + #: ./cli/app/cmd.go:174 #, c-format msgid "no service %s for %s?" @@ -3588,7 +3758,7 @@ msgstr "" msgid "no-tty" msgstr "" -#: ./cli/internal/deploy.go:159 +#: ./cli/internal/deploy.go:222 #, c-format msgid "not enough arguments: %s" msgstr "" @@ -3761,6 +3931,10 @@ msgstr "" msgid "print machine-readable output" msgstr "" +#: ./cli/internal/deploy.go:196 +msgid "proceed?" +msgstr "" + #: ./pkg/recipe/git.go:398 #, c-format msgid "processing %s for %s" @@ -3808,7 +3982,7 @@ msgstr "" #. with no spaces in between #. translators: `abra recipe` aliases. use a comma separated list of aliases #. with no spaces in between -#: ./cli/app/backup.go:327 ./cli/app/list.go:303 ./cli/app/run.go:23 ./cli/app/upgrade.go:463 ./cli/catalogue/catalogue.go:302 ./cli/recipe/recipe.go:12 ./cli/recipe/release.go:640 ./cli/recipe/sync.go:270 +#: ./cli/app/backup.go:327 ./cli/app/list.go:303 ./cli/app/move.go:348 ./cli/app/run.go:23 ./cli/app/upgrade.go:463 ./cli/catalogue/catalogue.go:302 ./cli/recipe/recipe.go:12 ./cli/recipe/release.go:640 ./cli/recipe/sync.go:270 msgid "r" msgstr "" @@ -3988,6 +4162,16 @@ msgstr "" msgid "removed freshly created tag %s" msgstr "" +#: ./cli/app/move.go:182 ./cli/app/move.go:188 +#, c-format +msgid "removing %s from %s" +msgstr "" + +#: ./cli/app/move.go:194 +#, c-format +msgid "removing %s from local machine" +msgstr "" + #: ./cli/server/prune.go:64 msgid "removing all images, not only dangling ones" msgstr "" @@ -4045,7 +4229,7 @@ msgstr "" msgid "repo set config: %s" msgstr "" -#: ./cli/catalogue/catalogue.go:304 ./cli/recipe/release.go:642 ./cli/recipe/sync.go:272 +#: ./cli/app/move.go:350 ./cli/catalogue/catalogue.go:304 ./cli/recipe/release.go:642 ./cli/recipe/sync.go:272 msgid "report changes that would be made" msgstr "" @@ -4186,6 +4370,16 @@ msgstr "" msgid "rs" msgstr "" +#: ./cli/app/move.go:168 +#, c-format +msgid "rsyncing %s (renaming to %s) to %s from local machine" +msgstr "" + +#: ./cli/app/move.go:161 +#, c-format +msgid "rsyncing %s from %s to local machine" +msgstr "" + #: ./cli/recipe/lint.go:41 msgid "rule" msgstr "" @@ -4213,7 +4407,7 @@ msgstr "" msgid "running backup %s on %s with exec config %v" msgstr "" -#: ./cli/internal/deploy.go:189 +#: ./cli/internal/deploy.go:252 #, c-format msgid "running command %s %s within the context of %s_%s" msgstr "" @@ -4233,7 +4427,7 @@ msgstr "" msgid "running command: %s" msgstr "" -#: ./cli/internal/deploy.go:167 +#: ./cli/internal/deploy.go:230 #, c-format msgid "running post-command '%s %s' in container %s" msgstr "" @@ -4444,6 +4638,11 @@ msgstr "" msgid "skipped" msgstr "" +#: ./cli/app/move.go:280 +#, c-format +msgid "skipping %s as it does not match %s" +msgstr "" + #: ./pkg/lint/recipe.go:55 #, c-format msgid "skipping %s based on skip condition" @@ -4588,12 +4787,12 @@ msgstr "" msgid "successfully created %s" msgstr "" -#: ./pkg/client/client.go:97 +#: ./pkg/client/client.go:104 #, c-format msgid "swarm mode not enabled on %s?" msgstr "" -#: ./pkg/client/client.go:100 +#: ./pkg/client/client.go:107 msgid "swarm mode not enabled on local server?" msgstr "" @@ -4814,6 +5013,11 @@ msgstr "" msgid "unable to fetch tags in %s: %s" msgstr "" +#: ./cli/app/move.go:299 +#, c-format +msgid "unable to get container matching %s: %s" +msgstr "" + #: ./pkg/recipe/git.go:274 #, c-format msgid "unable to git pull in %s: %s" @@ -4893,6 +5097,11 @@ msgstr "" msgid "unable to read version for %s from synced label. Did you try running \"abra recipe sync %s\" already?" msgstr "" +#: ./cli/app/move.go:208 +#, c-format +msgid "unable to remove %s: %s" +msgstr "" + #: ./cli/recipe/fetch.go:81 #, c-format msgid "unable to remove default remote in %s: %s" @@ -4918,6 +5127,11 @@ msgstr "" msgid "unable to resolve IPv4 for %s" msgstr "" +#: ./cli/app/move.go:105 +#, c-format +msgid "unable to retrieve %s resources on %s: %s" +msgstr "" + #: ./cli/app/list.go:159 #, c-format msgid "unable to retrieve tags for %s: %s" @@ -4977,6 +5191,11 @@ msgstr "" msgid "undeploy succeeded 🟢" msgstr "" +#: ./cli/app/move.go:113 +#, c-format +msgid "undeploying %s on %s" +msgstr "" + #: ./pkg/upstream/stack/loader.go:108 #, c-format msgid "unexpected environment %q" diff --git a/pkg/i18n/locales/es.po b/pkg/i18n/locales/es.po index 11c2327f..3a97bdbf 100644 --- a/pkg/i18n/locales/es.po +++ b/pkg/i18n/locales/es.po @@ -2,7 +2,7 @@ msgid "" msgstr "" "Project-Id-Version: \n" "Report-Msgid-Bugs-To: EMAIL\n" -"POT-Creation-Date: 2025-08-30 12:43+0200\n" +"POT-Creation-Date: 2025-09-01 11:17+0200\n" "PO-Revision-Date: 2025-08-29 21:45+0000\n" "Last-Translator: chasqui \n" "Language-Team: Spanish args/flags without \"--\"\n" @@ -231,7 +237,7 @@ msgstr "" msgid "%s does not exist for %s, use /bin/sh as fallback" msgstr "" -#: cli/app/cmd.go:114 cli/internal/deploy.go:151 +#: cli/app/cmd.go:114 cli/internal/deploy.go:214 #, c-format msgid "%s does not exist for %s?" msgstr "" @@ -256,6 +262,16 @@ msgstr "" msgid "%s doesn't have a %s function" msgstr "" +#: cli/app/move.go:158 cli/app/move.go:318 +#, c-format +msgid "%s failed on %s: output:%s err:%s" +msgstr "" + +#: cli/app/move.go:178 +#, c-format +msgid "%s failed to extract %s on %s: output:%s err:%s" +msgstr "" + #: pkg/upstream/stack/stack.go:169 #, c-format msgid "%s has been detected as deployed: %v" @@ -365,6 +381,11 @@ msgstr "" msgid "%s missing from %s.env" msgstr "" +#: cli/app/move.go:100 +#, c-format +msgid "%s must first be deployed on %s before moving" +msgstr "" + #: cli/recipe/upgrade.go:151 #, c-format msgid "%s not considered semver-like" @@ -422,6 +443,11 @@ msgstr "" msgid "%s successfully stored on server" msgstr "" +#: cli/app/move.go:211 +#, c-format +msgid "%s was successfully moved from %s to %s 🎉" +msgstr "" + #: cli/recipe/new.go:60 #, c-format msgid "%s/example.git" @@ -764,7 +790,7 @@ msgstr "" msgid "DEPLOYED LABELS" msgstr "" -#: cli/app/list.go:222 cli/internal/deploy.go:75 +#: cli/app/list.go:222 cli/internal/deploy.go:75 cli/internal/deploy.go:173 msgid "DOMAIN" msgstr "" @@ -993,6 +1019,10 @@ msgstr "📋 Listar los contenidos de una captura o instantánea 📸" msgid "List volumes associated with an app" msgstr "📋 Listar volúmenes 📦 asociados a una plataforma 🚀" +#: cli/internal/deploy.go:181 +msgid "MOVE OVERVIEW" +msgstr "" + #. translators: Short description for `app backup` command group #: cli/app/backup.go:246 msgid "Manage app backups" @@ -1027,6 +1057,40 @@ msgstr "⚙️ Administrar servidores (huertas digitales) 🕋" msgid "Manage the recipe catalogue" msgstr "⚙️ Administrar catálogo 📋 de recetas 🧑‍🍳" +#: cli/app/move.go:42 +msgid "" +"Move an app to a differnt server.\n" +"\n" +"This command will migrate an app config and copy secrets and volumes from " +"the\n" +"old server to the new one. The app MUST be deployed on the old server " +"before\n" +"doing the move. The app will be undeployed from the current server but not\n" +"deployed on the new server.\n" +"\n" +"This command requires the \"cat\" command to be present on the app " +"containers\n" +"when retrieving secrets. Some \"distroless\" images will not support this. " +"Not\n" +"all apps are therefore moveable. Rsync is required on your local machine " +"for\n" +"transferring volumes.\n" +"\n" +"Do not forget to update your DNS records. Don't panic, it might take a " +"while\n" +"for the dust to settle after you move an app. If anything goes wrong, you " +"can\n" +"always move the app config file to the original server and deploy it there\n" +"again. No data is removed from the old server.\n" +"\n" +"Use \"--dry-run/-r\" to see which secrets and volumes will be moved." +msgstr "" + +#. translators: Short description for `app move` command +#: cli/app/move.go:41 +msgid "Moves an app to a different server" +msgstr "" + #: cli/recipe/new.go:123 msgid "N" msgstr "" @@ -1044,6 +1108,10 @@ msgstr "" msgid "NEW DEPLOYMENT" msgstr "" +#: cli/internal/deploy.go:176 +msgid "NEW SERVER" +msgstr "" + #: cli/app/secret.go:147 msgid "NOT" msgstr "" @@ -1061,6 +1129,10 @@ msgid "" "Use \"--major/-m\" to include new major versions." msgstr "" +#: cli/internal/deploy.go:175 +msgid "OLD SERVER" +msgstr "" + #: cli/app/volume.go:54 msgid "ON SERVER" msgstr "" @@ -1087,7 +1159,7 @@ msgstr "" msgid "README.md metadata filled in" msgstr "" -#: cli/app/list.go:222 cli/internal/deploy.go:76 +#: cli/app/list.go:222 cli/internal/deploy.go:76 cli/internal/deploy.go:174 msgid "RECIPE" msgstr "" @@ -1227,6 +1299,10 @@ msgstr "💻 Ejecutar comandos en una plataforma 🚀" msgid "S" msgstr "" +#: cli/internal/deploy.go:177 +msgid "SECRETS" +msgstr "" + #: cli/app/new.go:204 msgid "SECRETS OVERVIEW" msgstr "" @@ -1618,6 +1694,10 @@ msgstr "" msgid "VERSION" msgstr "" +#: cli/internal/deploy.go:178 +msgid "VOLUMES" +msgstr "" + #: cli/recipe/reset.go:24 msgid "WARNING: this will delete your changes. Be Careful." msgstr "" @@ -1919,6 +1999,11 @@ msgstr "" msgid "bad status: %s" msgstr "" +#: cli/app/move.go:110 +#, c-format +msgid "bailing out: %s" +msgstr "" + #: pkg/upstream/convert/volume.go:113 msgid "bind options are incompatible with type tmpfs" msgstr "" @@ -1974,6 +2059,10 @@ msgstr "" msgid "can't separate key from value: %s (this variable is probably unset)" msgstr "" +#: cli/internal/deploy.go:202 +msgid "cancelled" +msgstr "" + #: pkg/catalogue/catalogue.go:59 pkg/recipe/git.go:245 #, c-format msgid "cannot ensure %s is up-to-date, no git remotes configured" @@ -2315,11 +2404,16 @@ msgstr "" msgid "create remote directory: %s" msgstr "" -#: pkg/client/client.go:88 +#: pkg/client/client.go:95 #, c-format msgid "created client for %s" msgstr "" +#: cli/app/move.go:134 +#, fuzzy, c-format +msgid "created secret on %s: %s" +msgstr "🥷 Genera secretos (contraseñas) automáticamente 🤖" + #: cli/recipe/release.go:428 #, c-format msgid "created tag %s at %s" @@ -2335,6 +2429,11 @@ msgstr "" msgid "creating %s" msgstr "" +#: cli/app/move.go:154 +#, c-format +msgid "creating %s on %s" +msgstr "" + #: cli/server/add.go:175 #, c-format msgid "creating context with domain %s" @@ -2350,6 +2449,11 @@ msgstr "" msgid "creating secret %s" msgstr "" +#: cli/app/move.go:144 +#, c-format +msgid "creating volume %s on %s" +msgstr "" + #: pkg/lint/recipe.go:22 msgid "critical" msgstr "" @@ -2553,6 +2657,10 @@ msgstr "" msgid "download [flags]" msgstr "descargar [flags]" +#: cli/internal/deploy.go:192 +msgid "dry run" +msgstr "" + #: pkg/git/add.go:22 #, c-format msgid "dry run: adding %s" @@ -2595,7 +2703,7 @@ msgstr "" msgid "dry run: remote %s (%s) not created" msgstr "" -#: cli/catalogue/catalogue.go:301 cli/recipe/release.go:639 +#: cli/app/move.go:347 cli/catalogue/catalogue.go:301 cli/recipe/release.go:639 #: cli/recipe/sync.go:269 msgid "dry-run" msgstr "" @@ -2722,6 +2830,16 @@ msgstr "" msgid "expected 1 service but found %v: %s" msgstr "" +#: cli/app/move.go:174 +#, c-format +msgid "extracting %s on %s" +msgstr "" + +#: cli/app/move.go:313 +#, c-format +msgid "extracting secret %s on %s" +msgstr "" + #. translators: `abra recipe fetch` aliases. use a comma separated list of aliases #. with no spaces in between #: cli/app/deploy.go:347 cli/app/remove.go:163 cli/app/rollback.go:329 @@ -2750,6 +2868,16 @@ msgstr "" msgid "failed to commit changes: %s" msgstr "" +#: cli/app/move.go:164 +#, c-format +msgid "failed to copy %s from %s to local machine: output:%s err:%s" +msgstr "" + +#: cli/app/move.go:171 +#, c-format +msgid "failed to copy %s from local machine to %s: output:%s err:%s" +msgstr "" + #: pkg/upstream/stack/stack.go:531 #, c-format msgid "failed to create %s" @@ -2770,6 +2898,11 @@ msgstr "" msgid "failed to create secret %s" msgstr "" +#: cli/app/move.go:150 +#, fuzzy, c-format +msgid "failed to create volume %s on %s: %s" +msgstr "🥷 Genera secretos (contraseñas) automáticamente 🤖" + #: cli/updater/updater.go:263 #, c-format msgid "failed to determine deployed version of %s" @@ -2798,6 +2931,11 @@ msgstr "" msgid "failed to match chosen service" msgstr "" +#: cli/app/move.go:204 +#, c-format +msgid "failed to migrate app config: %s" +msgstr "" + #: pkg/client/registry.go:20 #, c-format msgid "failed to parse image %s, saw: %s" @@ -2808,6 +2946,21 @@ msgstr "" msgid "failed to publish new release: %s" msgstr "" +#: cli/app/move.go:185 cli/app/move.go:191 +#, c-format +msgid "failed to remove %s from %s: output:%s err:%s" +msgstr "" + +#: cli/app/move.go:197 +#, c-format +msgid "failed to remove %s on local machine: output:%s err:%s" +msgstr "" + +#: cli/app/move.go:119 +#, c-format +msgid "failed to remove app from %s: %s" +msgstr "" + #: pkg/upstream/stack/remove.go:183 #, c-format msgid "failed to remove config %s: %s" @@ -2852,6 +3005,11 @@ msgstr "" msgid "failed to select default branch in %s" msgstr "" +#: cli/app/move.go:132 +#, fuzzy, c-format +msgid "failed to store secret on %s: %s" +msgstr "🥷 Genera secretos (contraseñas) automáticamente 🤖" + #: cli/recipe/release.go:269 cli/recipe/release.go:554 #, c-format msgid "failed to tag release: %s" @@ -3458,12 +3616,14 @@ msgstr "" msgid "ls" msgstr "plataformas" +#. translators: `abra app move` aliases. use a comma separated list of aliases +#. with no spaces in between #. translators: `abra man` aliases. use a comma separated list of aliases #. with no spaces in between -#: cli/app/list.go:318 cli/app/ps.go:205 cli/app/secret.go:553 -#: cli/app/secret.go:649 cli/recipe/list.go:104 cli/recipe/upgrade.go:376 -#: cli/recipe/version.go:139 cli/run.go:128 cli/server/list.go:106 -#: cli/updater/updater.go:560 +#: cli/app/list.go:318 cli/app/move.go:34 cli/app/ps.go:205 +#: cli/app/secret.go:553 cli/app/secret.go:649 cli/recipe/list.go:104 +#: cli/recipe/upgrade.go:376 cli/recipe/version.go:139 cli/run.go:128 +#: cli/server/list.go:106 cli/updater/updater.go:560 msgid "m" msgstr "" @@ -3499,6 +3659,11 @@ msgstr "" msgid "man [flags]" msgstr "manual [flags]" +#: cli/app/move.go:202 +#, c-format +msgid "migrating app config from %s to %s" +msgstr "" + #: cli/internal/recipe.go:48 cli/internal/recipe.go:68 #: cli/internal/recipe.go:82 cli/recipe/release.go:655 cli/recipe/sync.go:285 #: cli/recipe/upgrade.go:359 @@ -3530,6 +3695,17 @@ msgstr "" msgid "missing version for secret? (%s)" msgstr "" +#. translators: `app move` command +#: cli/app/move.go:38 +#, fuzzy +msgid "move [flags]" +msgstr "borrar [flags]" + +#: cli/app/move.go:138 +#, c-format +msgid "moving volume %s from %s to %s" +msgstr "" + #: cli/app/secret.go:292 msgid "must provide argument if --no-input is passed" msgstr "" @@ -3743,6 +3919,10 @@ msgstr "" msgid "no server provided" msgstr "" +#: cli/app/move.go:81 +msgid "no server provided?" +msgstr "" + #: cli/app/cmd.go:174 #, c-format msgid "no service %s for %s?" @@ -3806,7 +3986,7 @@ msgstr "" msgid "no-tty" msgstr "" -#: cli/internal/deploy.go:159 +#: cli/internal/deploy.go:222 #, c-format msgid "not enough arguments: %s" msgstr "" @@ -3973,8 +4153,9 @@ msgid "polling deployment status" msgstr "" #: pkg/upstream/stack/remove.go:87 +#, fuzzy msgid "polling undeploy status" -msgstr "" +msgstr "📋 Revisar el estado de una plataforma" #: cli/run.go:185 msgid "prefer offline & filesystem access" @@ -3990,6 +4171,10 @@ msgstr "" msgid "print machine-readable output" msgstr "" +#: cli/internal/deploy.go:196 +msgid "proceed?" +msgstr "" + #: pkg/recipe/git.go:398 #, c-format msgid "processing %s for %s" @@ -4037,8 +4222,8 @@ msgstr "" #. with no spaces in between #. translators: `abra recipe` aliases. use a comma separated list of aliases #. with no spaces in between -#: cli/app/backup.go:327 cli/app/list.go:303 cli/app/run.go:23 -#: cli/app/upgrade.go:463 cli/catalogue/catalogue.go:302 +#: cli/app/backup.go:327 cli/app/list.go:303 cli/app/move.go:348 +#: cli/app/run.go:23 cli/app/upgrade.go:463 cli/catalogue/catalogue.go:302 #: cli/recipe/recipe.go:12 cli/recipe/release.go:640 cli/recipe/sync.go:270 msgid "r" msgstr "" @@ -4221,6 +4406,16 @@ msgstr "" msgid "removed freshly created tag %s" msgstr "" +#: cli/app/move.go:182 cli/app/move.go:188 +#, c-format +msgid "removing %s from %s" +msgstr "" + +#: cli/app/move.go:194 +#, c-format +msgid "removing %s from local machine" +msgstr "" + #: cli/server/prune.go:64 msgid "removing all images, not only dangling ones" msgstr "" @@ -4278,7 +4473,7 @@ msgstr "" msgid "repo set config: %s" msgstr "" -#: cli/catalogue/catalogue.go:304 cli/recipe/release.go:642 +#: cli/app/move.go:350 cli/catalogue/catalogue.go:304 cli/recipe/release.go:642 #: cli/recipe/sync.go:272 msgid "report changes that would be made" msgstr "" @@ -4421,6 +4616,16 @@ msgstr "" msgid "rs" msgstr "" +#: cli/app/move.go:168 +#, c-format +msgid "rsyncing %s (renaming to %s) to %s from local machine" +msgstr "" + +#: cli/app/move.go:161 +#, c-format +msgid "rsyncing %s from %s to local machine" +msgstr "" + #: cli/recipe/lint.go:41 msgid "rule" msgstr "" @@ -4448,7 +4653,7 @@ msgstr "" msgid "running backup %s on %s with exec config %v" msgstr "" -#: cli/internal/deploy.go:189 +#: cli/internal/deploy.go:252 #, c-format msgid "running command %s %s within the context of %s_%s" msgstr "" @@ -4468,7 +4673,7 @@ msgstr "" msgid "running command: %s" msgstr "" -#: cli/internal/deploy.go:167 +#: cli/internal/deploy.go:230 #, c-format msgid "running post-command '%s %s' in container %s" msgstr "" @@ -4686,6 +4891,11 @@ msgstr "" msgid "skipped" msgstr "" +#: cli/app/move.go:280 +#, c-format +msgid "skipping %s as it does not match %s" +msgstr "" + #: pkg/lint/recipe.go:55 #, c-format msgid "skipping %s based on skip condition" @@ -4832,12 +5042,12 @@ msgstr "" msgid "successfully created %s" msgstr "" -#: pkg/client/client.go:97 +#: pkg/client/client.go:104 #, c-format msgid "swarm mode not enabled on %s?" msgstr "" -#: pkg/client/client.go:100 +#: pkg/client/client.go:107 msgid "swarm mode not enabled on local server?" msgstr "" @@ -5060,6 +5270,11 @@ msgstr "" msgid "unable to fetch tags in %s: %s" msgstr "" +#: cli/app/move.go:299 +#, c-format +msgid "unable to get container matching %s: %s" +msgstr "" + #: pkg/recipe/git.go:274 #, c-format msgid "unable to git pull in %s: %s" @@ -5142,6 +5357,11 @@ msgid "" "recipe sync %s\" already?" msgstr "" +#: cli/app/move.go:208 +#, c-format +msgid "unable to remove %s: %s" +msgstr "" + #: cli/recipe/fetch.go:81 #, c-format msgid "unable to remove default remote in %s: %s" @@ -5168,6 +5388,11 @@ msgstr "" msgid "unable to resolve IPv4 for %s" msgstr "" +#: cli/app/move.go:105 +#, c-format +msgid "unable to retrieve %s resources on %s: %s" +msgstr "" + #: cli/app/list.go:159 #, c-format msgid "unable to retrieve tags for %s: %s" @@ -5227,6 +5452,11 @@ msgstr "desarmar [flags]" msgid "undeploy succeeded 🟢" msgstr "" +#: cli/app/move.go:113 +#, c-format +msgid "undeploying %s on %s" +msgstr "" + #: pkg/upstream/stack/loader.go:108 #, c-format msgid "unexpected environment %q" -- 2.49.0