Compare commits

...

18 Commits

Author SHA1 Message Date
e1f029d2db chore: make i18n
All checks were successful
continuous-integration/drone/push Build is passing
2025-09-03 00:25:23 +02:00
cf2952dc65 chore: add missing i18n on --latest [ci skip] 2025-09-03 00:24:55 +02:00
2291712661 fix: abra app move docs/patches
All checks were successful
continuous-integration/drone/push Build is passing
2025-09-01 13:48:10 +02:00
f0e2b012c6 chore: make i18n
All checks were successful
continuous-integration/drone/push Build is passing
2025-09-01 11:17:44 +02:00
9c37b9b748 test: app move (basics) 2025-09-01 11:17:39 +02:00
824f314472 refactor: app move review pass 2025-09-01 11:17:22 +02:00
61849a358c feat(app): Adds abra app move command 2025-09-01 06:50:11 +02:00
8c7b06a7bb chore: add missing authors [ci skip] 2025-08-30 13:40:39 +02:00
4c9abbf925 feat: template example domain in release notes
All checks were successful
continuous-integration/drone/pr Build is passing
continuous-integration/drone/push Build is passing
See toolshed/organising#521
2025-08-30 12:45:48 +02:00
09176801e1 feat: warn for secret generation
All checks were successful
continuous-integration/drone/pr Build is passing
continuous-integration/drone/push Build is passing
2025-08-30 12:27:29 +02:00
36d4648114 feat: add config dir help
All checks were successful
continuous-integration/drone/pr Build is passing
continuous-integration/drone/push Build is passing
See toolshed/organising#470
2025-08-30 12:12:40 +02:00
83ca2a63d1 fix: support ValidArgs translation
All checks were successful
continuous-integration/drone/push Build is passing
See #632
2025-08-30 12:02:13 +02:00
e25ce5d1a0 chore: make i18n
All checks were successful
continuous-integration/drone/push Build is passing
2025-08-30 11:46:48 +02:00
4cb5091d50 Merge remote-tracking branch 'weblate/main' 2025-08-30 11:45:59 +02:00
4bfbc53b94 feat: support alias translation
All checks were successful
continuous-integration/drone/push Build is passing
See #627
2025-08-30 11:39:49 +02:00
52f02ad9b9 translate: support usage translations
All checks were successful
continuous-integration/drone/pr Build is passing
continuous-integration/drone/push Build is passing
See #628
2025-08-30 10:46:09 +02:00
c0acc3663b Translated using Weblate (Spanish)
Currently translated at 10.8% (114 of 1048 strings)

Translation: Co-op Cloud/abra
Translate-URL: https://translate.coopcloud.tech/projects/co-op-cloud/abra/es/
2025-08-29 21:45:43 +00:00
c2819b9366 Translated using Weblate (Spanish)
Currently translated at 10.8% (114 of 1048 strings)

Translation: Co-op Cloud/abra
Translate-URL: https://translate.coopcloud.tech/projects/co-op-cloud/abra/es/
2025-08-29 08:00:42 +00:00
62 changed files with 3175 additions and 1553 deletions

1
.gitignore vendored
View File

@ -1,3 +1,4 @@
*.tar.gz
*fmtcoverage.html
.e2e.env
.envrc

View File

@ -5,6 +5,7 @@
- 3wordchant
- ammaratef45
- apfelwurm
- basebuilder
- cassowary
- chasqui
@ -12,6 +13,7 @@
- decentral1se
- fauno
- frando
- iexos
- kawaiipunk
- knoflook
- mayel

View File

@ -1,14 +1,20 @@
package app
import (
"strings"
"coopcloud.tech/abra/pkg/i18n"
"github.com/spf13/cobra"
)
// translators: `abra app` aliases. use a comma separated list of aliases with
// no spaces in between
var appAliases = i18n.G("a")
var AppCommand = &cobra.Command{
// translators: `app` command group
Use: i18n.G("app [cmd] [args] [flags]"),
Aliases: []string{i18n.G("a")},
Aliases: strings.Split(appAliases, ","),
// translators: Short description for `app` command group
Short: i18n.G("Manage apps"),
}

View File

@ -2,6 +2,7 @@ package app
import (
"fmt"
"strings"
"coopcloud.tech/abra/cli/internal"
"coopcloud.tech/abra/pkg/autocomplete"
@ -11,10 +12,14 @@ import (
"github.com/spf13/cobra"
)
// translators: `abra app backup list` aliases. use a comma separated list of aliases with
// no spaces in between
var appBackupListAliases = i18n.G("ls")
var AppBackupListCommand = &cobra.Command{
// translators: `app backup list` command
Use: i18n.G("list <domain> [flags]"),
Aliases: []string{i18n.G("ls")},
Aliases: strings.Split(appBackupListAliases, ","),
// translators: Short description for `app backup list` command
Short: i18n.G("List the contents of a snapshot"),
Args: cobra.ExactArgs(1),
@ -63,10 +68,14 @@ var AppBackupListCommand = &cobra.Command{
},
}
// translators: `abra app backup download` aliases. use a comma separated list of aliases with
// no spaces in between
var appBackupDownloadAliases = i18n.G("d")
var AppBackupDownloadCommand = &cobra.Command{
// translators: `app backup download` command
Use: i18n.G("download <domain> [flags]"),
Aliases: []string{i18n.G("d")},
Aliases: strings.Split(appBackupDownloadAliases, ","),
// translators: Short description for `app backup download` command
Short: i18n.G("Download a snapshot"),
Long: i18n.G(`Downloads a backup.tar.gz to the current working directory.
@ -134,10 +143,14 @@ var AppBackupDownloadCommand = &cobra.Command{
},
}
// translators: `abra app backup create` aliases. use a comma separated list of aliases with
// no spaces in between
var appBackupCreateAliases = i18n.G("c")
var AppBackupCreateCommand = &cobra.Command{
// translators: `app backup create` command
Use: i18n.G("create <domain> [flags]"),
Aliases: []string{i18n.G("c")},
Aliases: strings.Split(appBackupCreateAliases, ","),
// translators: Short description for `app backup create` command
Short: i18n.G("Create a new snapshot"),
Args: cobra.ExactArgs(1),
@ -180,10 +193,14 @@ var AppBackupCreateCommand = &cobra.Command{
},
}
// translators: `abra app backup snapshots` aliases. use a comma separated list of aliases with
// no spaces in between
var appBackupSnapshotsAliases = i18n.G("s")
var AppBackupSnapshotsCommand = &cobra.Command{
// translators: `app backup snapshots` command
Use: i18n.G("snapshots <domain> [flags]"),
Aliases: []string{i18n.G("s")},
Aliases: strings.Split(appBackupSnapshotsAliases, ","),
// translators: Short description for `app backup snapshots` command
Short: i18n.G("List all snapshots"),
Args: cobra.ExactArgs(1),
@ -217,10 +234,14 @@ var AppBackupSnapshotsCommand = &cobra.Command{
},
}
// translators: `abra app backup` aliases. use a comma separated list of aliases with
// no spaces in between
var appBackupAliases = i18n.G("b")
var AppBackupCommand = &cobra.Command{
// translators: `app backup` command group
Use: i18n.G("backup [cmd] [args] [flags]"),
Aliases: []string{i18n.G("b")},
Aliases: strings.Split(appBackupAliases, ","),
// translators: Short description for `app backup` command group
Short: i18n.G("Manage app backups"),
}

View File

@ -2,6 +2,7 @@ package app
import (
"fmt"
"strings"
"coopcloud.tech/abra/cli/internal"
appPkg "coopcloud.tech/abra/pkg/app"
@ -13,10 +14,14 @@ import (
"github.com/spf13/cobra"
)
// translators: `abra app check` aliases. use a comma separated list of aliases with
// no spaces in between
var appCheckAliases = i18n.G("chk")
var AppCheckCommand = &cobra.Command{
// translators: `app check` command
Use: i18n.G("check <domain> [flags]"),
Aliases: []string{i18n.G("chk")},
Aliases: strings.Split(appCheckAliases, ","),
// translators: Short description for `app check` command
Short: i18n.G("Ensure an app is well configured"),
Long: i18n.G(`Compare env vars in both the app ".env" and recipe ".env.sample" file.

View File

@ -18,10 +18,14 @@ import (
"github.com/spf13/cobra"
)
// translators: `abra app cmd` aliases. use a comma separated list of aliases with
// no spaces in between
var appCmdAliases = i18n.G("cmd")
var AppCmdCommand = &cobra.Command{
// translators: `app command` command
Use: i18n.G("command <domain> [service | --local] <cmd> [[args] [flags] | [flags] -- [args]]"),
Aliases: []string{i18n.G("cmd")},
Aliases: strings.Split(appCmdAliases, ","),
// translators: Short description for `app cmd` command
Short: i18n.G("Run app commands"),
Long: i18n.G(`Run an app specific command.
@ -194,10 +198,14 @@ does not).`),
},
}
// translators: `abra app command list` aliases. use a comma separated list of
// aliases with no spaces in between
var appCmdListAliases = i18n.G("ls")
var AppCmdListCommand = &cobra.Command{
// translators: `app cmd list` command
Use: i18n.G("list <domain> [flags]"),
Aliases: []string{i18n.G("ls")},
Aliases: strings.Split(appCmdListAliases, ","),
// translators: Short description for `app cmd list` command
Short: i18n.G("List all available commands"),
Args: cobra.MinimumNArgs(1),

View File

@ -3,6 +3,7 @@ package app
import (
"os"
"os/exec"
"strings"
appPkg "coopcloud.tech/abra/pkg/app"
"coopcloud.tech/abra/pkg/autocomplete"
@ -12,10 +13,14 @@ import (
"github.com/spf13/cobra"
)
// translators: `abra app config` aliases. use a comma separated list of
// aliases with no spaces in between
var appConfigAliases = i18n.G("cfg")
var AppConfigCommand = &cobra.Command{
// translators: `app config` command
Use: i18n.G("config <domain> [flags]"),
Aliases: []string{i18n.G("cfg")},
Aliases: strings.Split(appConfigAliases, ","),
// translators: Short description for `app config` command
Short: i18n.G("Edit app config"),
Example: i18n.G(" abra config 1312.net"),

View File

@ -25,10 +25,14 @@ import (
"github.com/spf13/cobra"
)
// translators: `abra app cp` aliases. use a comma separated list of aliases with
// no spaces in between
var appCpAliases = i18n.G("c")
var AppCpCommand = &cobra.Command{
// translators: `app cp` command
Use: i18n.G("cp <domain> <src> <dst> [flags]"),
Aliases: []string{i18n.G("c")},
Aliases: strings.Split(appCpAliases, ","),
// translators: Short description for `app cp` command
Short: i18n.G("Copy files to/from a deployed app service"),
Example: i18n.G(` # copy myfile.txt to the root of the app service

View File

@ -24,10 +24,14 @@ import (
"github.com/spf13/cobra"
)
// translators: `abra app deploy` aliases. use a comma separated list of aliases with
// no spaces in between
var appDeployAliases = i18n.G("d")
var AppDeployCommand = &cobra.Command{
// translators: `app deploy` command
Use: i18n.G("deploy <domain> [version] [flags]"),
Aliases: []string{i18n.G("d")},
Aliases: strings.Split(appDeployAliases, ","),
// translators: Short description for `app deploy` command
Short: i18n.G("Deploy an app"),
Long: i18n.G(`Deploy an app.
@ -363,8 +367,8 @@ func init() {
AppDeployCommand.PersistentFlags().BoolVarP(
&internal.DeployLatest,
"latest",
"l",
i18n.G("latest"),
i18n.G("l"),
false,
i18n.G("deploy latest recipe version"),
)

View File

@ -3,6 +3,7 @@ package app
import (
"fmt"
"sort"
"strings"
"coopcloud.tech/abra/cli/internal"
"coopcloud.tech/abra/pkg/autocomplete"
@ -11,10 +12,14 @@ import (
"github.com/spf13/cobra"
)
// translators: `abra app env` aliases. use a comma separated list of aliases with
// no spaces in between
var appEnvAliases = i18n.G("e")
var AppEnvCommand = &cobra.Command{
// translators: `app env` command
Use: i18n.G("env <domain> [flags]"),
Aliases: []string{i18n.G("e")},
Aliases: strings.Split(appEnvAliases, ","),
// translators: Short description for `app env` command
Short: i18n.G("Show app .env values"),
Example: i18n.G(" abra app env 1312.net"),

View File

@ -4,6 +4,7 @@ import (
"context"
"fmt"
"sort"
"strings"
"coopcloud.tech/abra/cli/internal"
"coopcloud.tech/abra/pkg/autocomplete"
@ -19,10 +20,14 @@ import (
"github.com/spf13/cobra"
)
// translators: `abra app labels` aliases. use a comma separated list of
// aliases with no spaces in between
var appLabelsAliases = i18n.G("lb")
var AppLabelsCommand = &cobra.Command{
// translators: `app labels` command
Use: i18n.G("labels <domain> [flags]"),
Aliases: []string{i18n.G("lb")},
Aliases: strings.Split(appLabelsAliases, ","),
// translators: Short description for `app labels` command
Short: i18n.G("Show deployment labels"),
Long: i18n.G("Both local recipe and live deployment labels are shown."),

View File

@ -39,10 +39,14 @@ type serverStatus struct {
UpgradeCount int `json:"upgradeCount"`
}
// translators: `abra app list` aliases. use a comma separated list of aliases with
// no spaces in between
var appListAliases = i18n.G("ls")
var AppListCommand = &cobra.Command{
// translators: `app list` command
Use: i18n.G("list [flags]"),
Aliases: []string{i18n.G("ls")},
Aliases: strings.Split(appListAliases, ","),
// translators: Short description for `app list` command
Short: i18n.G("List all managed apps"),
Long: i18n.G(`Generate a report of all managed apps.

View File

@ -2,6 +2,7 @@ package app
import (
"context"
"strings"
"coopcloud.tech/abra/cli/internal"
appPkg "coopcloud.tech/abra/pkg/app"
@ -14,10 +15,14 @@ import (
"github.com/spf13/cobra"
)
// translators: `abra app logs` aliases. use a comma separated list of aliases with
// no spaces in between
var appLogsAliases = i18n.G("l")
var AppLogsCommand = &cobra.Command{
// translators: `app logs` command
Use: i18n.G("logs <domain> [service] [flags]"),
Aliases: []string{i18n.G("l")},
Aliases: strings.Split(appLogsAliases, ","),
// translators: Short description for `app logs` command
Short: i18n.G("Tail app logs"),
Args: cobra.RangeArgs(1, 2),

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

@ -0,0 +1,350 @@
package app
import (
"context"
"errors"
"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"
"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"
"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"
)
// 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{
// translators: `app move` command
Use: i18n.G("move <domain> <server> [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 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.
The "tar" command is required on both the old and new server as well as "sudo"
permissions. The "rsync" command 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,
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(i18n.G("no server provided?"))
}
newServer := internal.ValidateServer([]string{args[1]})
if err := app.Recipe.Ensure(internal.GetEnsureContext()); err != nil {
log.Fatal(err)
}
currentServerClient, err := client.New(app.Server)
if err != nil {
log.Fatal(err)
}
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 {
log.Fatal(i18n.G("bailing out: %s", err))
}
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(), currentServerClient, rmOpts); err != nil {
log.Fatal(i18n.G("failed to remove app from %s: %s", err, app.Server))
}
newServerClient, 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(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.Info(i18n.G("moving volume %s from %s to %s", v.Name, app.Server, newServer))
// 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.Fatal(i18n.G("failed to create volume %s on %s: %s", v.Name, newServer, err))
}
filename := fmt.Sprintf("%s_outgoing.tar.gz", v.Name)
log.Debug(i18n.G("creating %s on %s", filename, app.Server))
tarCmd := fmt.Sprintf("sudo tar --same-owner -czhpf %s -C /var/lib/docker/volumes %s", filename, v.Name)
cmd := exec.Command("ssh", app.Server, "-tt", tarCmd)
if out, err := cmd.CombinedOutput(); err != nil {
log.Fatal(i18n.G("%s failed on %s: output:%s err:%s", tarCmd, app.Server, string(out), err))
}
log.Debug(i18n.G("rsyncing %s from %s to local machine", filename, app.Server))
cmd = exec.Command("rsync", "-a", "-v", fmt.Sprintf("%s:%s", app.Server, filename), filename)
if out, err := cmd.CombinedOutput(); err != nil {
log.Fatal(i18n.G("failed to copy %s from %s to local machine: output:%s err:%s", filename, app.Server, string(out), err))
}
log.Debug(i18n.G("rsyncing %s to %s from local machine", filename, filename, newServer))
cmd = exec.Command("rsync", "-a", "-v", filename, fmt.Sprintf("%s:%s", newServer, filename))
if out, err := cmd.CombinedOutput(); err != nil {
log.Fatal(i18n.G("failed to copy %s from local machine to %s: output:%s err:%s", filename, newServer, string(out), err))
}
log.Debug(i18n.G("extracting %s on %s", filename, newServer))
tarExtractCmd := fmt.Sprintf("sudo tar --same-owner -xzpf %s -C /var/lib/docker/volumes", filename)
cmd = exec.Command("ssh", newServer, "-tt", tarExtractCmd)
if out, err := cmd.CombinedOutput(); err != nil {
log.Fatal(i18n.G("%s failed to extract %s on %s: output:%s err:%s", tarExtractCmd, filename, newServer, string(out), err))
}
// Remove tar files
log.Debug(i18n.G("removing %s from %s", filename, newServer))
cmd = exec.Command("ssh", newServer, "-tt", fmt.Sprintf("sudo rm -rf %s", filename))
if out, err := cmd.CombinedOutput(); err != nil {
log.Fatal(i18n.G("failed to remove %s from %s: output:%s err:%s", filename, newServer, string(out), err))
}
log.Debug(i18n.G("removing %s from %s", filename, app.Server))
cmd = exec.Command("ssh", app.Server, "-tt", fmt.Sprintf("sudo rm -rf %s", filename))
if out, err := cmd.CombinedOutput(); err != nil {
log.Fatal(i18n.G("failed to remove %s from %s: output:%s err:%s", filename, app.Server, string(out), err))
}
log.Debug(i18n.G("removing %s from local machine", filename))
cmd = exec.Command("rm", "-r", "-f", filename)
if out, err := cmd.CombinedOutput(); err != nil {
log.Fatal(i18n.G("failed to remove %s on local machine: output:%s err:%s", filename, string(out), 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(i18n.G("unable to remove %s: %s", app.Path, err))
}
log.Info(i18n.G("%s was successfully moved from %s to %s 🎉", app.Name, app.Server, newServer))
},
}
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 {
return nil, err
}
secretConfigs, err := secret.ReadSecretsConfig(app.Path, composeFiles, app.StackName())
if err != nil {
return nil, err
}
opts := stack.Deploy{Composefiles: composeFiles, Namespace: app.StackName()}
compose, err := appPkg.GetAppComposeConfig(app.Name, opts, app.Env)
if err != nil {
return nil, 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 {
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 {
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
}
}
}
}
f := filters.NewArgs()
f.Add("name", s.Spec.Name)
targetContainer, err := containerPkg.GetContainer(context.Background(), cl, f, true)
if err != nil {
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
}
}
for secretName, secretID := range secretNames {
if _, ok := resources.Secrets[secretName]; ok {
continue
}
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 {
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
}
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,
i18n.G("dry-run"),
i18n.G("r"),
false,
i18n.G("report changes that would be made"),
)
}

View File

@ -3,6 +3,7 @@ package app
import (
"errors"
"fmt"
"strings"
"coopcloud.tech/abra/cli/internal"
"coopcloud.tech/abra/pkg/app"
@ -42,10 +43,14 @@ You can use the "--pass/-P" to store these generated passwords locally in a
pass store (see passwordstore.org for more). The pass command must be available
on your $PATH.`)
// translators: `abra app new` aliases. use a comma separated list of aliases with
// no spaces in between
var appNewAliases = i18n.G("n")
var AppNewCommand = &cobra.Command{
// translators: `app new` command
Use: i18n.G("new [recipe] [version] [flags]"),
Aliases: []string{i18n.G("n")},
Aliases: strings.Split(appNewAliases, ","),
// translators: Short description for `app new` command
Short: i18n.G("Create a new app"),
Long: appNewDescription,
@ -144,27 +149,27 @@ var AppNewCommand = &cobra.Command{
log.Fatal(err)
}
sampleEnv, err := recipe.SampleEnv()
if err != nil {
log.Fatal(err)
}
composeFiles, err := recipe.GetComposeFiles(sampleEnv)
if err != nil {
log.Fatal(err)
}
secretsConfig, err := secret.ReadSecretsConfig(
recipe.SampleEnvPath,
composeFiles,
appPkg.StackName(appDomain),
)
if err != nil {
log.Fatal(err)
}
var appSecrets AppSecrets
if generateSecrets {
sampleEnv, err := recipe.SampleEnv()
if err != nil {
log.Fatal(err)
}
composeFiles, err := recipe.GetComposeFiles(sampleEnv)
if err != nil {
log.Fatal(err)
}
secretsConfig, err := secret.ReadSecretsConfig(
recipe.SampleEnvPath,
composeFiles,
appPkg.StackName(appDomain),
)
if err != nil {
log.Fatal(err)
}
if err := promptForSecrets(recipe.Name, secretsConfig); err != nil {
log.Fatal(err)
}
@ -186,6 +191,10 @@ var AppNewCommand = &cobra.Command{
log.Info(i18n.G("%s created (version: %s)", appDomain, recipeVersion))
if len(secretsConfig) > 0 {
log.Warn(i18n.G("%s requires secret generation before deploying, run \"abra app secret generate %s --all\"", recipe.Name, appDomain))
}
if len(appSecrets) > 0 {
rows := [][]string{}
for k, v := range appSecrets {

View File

@ -24,10 +24,14 @@ import (
"github.com/spf13/cobra"
)
// translators: `abra app ps` aliases. use a comma separated list of aliases
// with no spaces in between
var appPsAliases = i18n.G("p")
var AppPsCommand = &cobra.Command{
// translators: `app ps` command
Use: i18n.G("ps <domain> [flags]"),
Aliases: []string{i18n.G("p")},
Aliases: strings.Split(appPsAliases, ","),
// translators: Short description for `app ps` command
Short: i18n.G("Check app deployment status"),
Args: cobra.ExactArgs(1),

View File

@ -3,6 +3,7 @@ package app
import (
"context"
"os"
"strings"
"coopcloud.tech/abra/cli/internal"
"coopcloud.tech/abra/pkg/autocomplete"
@ -15,10 +16,14 @@ import (
"github.com/spf13/cobra"
)
// translators: `abra app remove` aliases. use a comma separated list of aliases with
// no spaces in between
var appRemoveAliases = i18n.G("rm")
var AppRemoveCommand = &cobra.Command{
// translators: `app remove` command
Use: i18n.G("remove <domain> [flags]"),
Aliases: []string{i18n.G("rm")},
Aliases: strings.Split(appRemoveAliases, ","),
// translators: Short description for `app remove` command
Short: i18n.G("Remove all app data, locally and remotely"),
Long: i18n.G(`Remove everything related to an app which is already undeployed.

View File

@ -3,6 +3,7 @@ package app
import (
"context"
"fmt"
"strings"
"coopcloud.tech/abra/cli/internal"
appPkg "coopcloud.tech/abra/pkg/app"
@ -17,10 +18,14 @@ import (
"github.com/spf13/cobra"
)
// translators: `abra app restart` aliases. use a comma separated list of aliases with
// no spaces in between
var appRestartAliases = i18n.G("re")
var AppRestartCommand = &cobra.Command{
// translators: `app restart` command
Use: i18n.G("restart <domain> [[service] | --all-services] [flags]"),
Aliases: []string{i18n.G("re")},
Aliases: strings.Split(appRestartAliases, ","),
// translators: Short description for `app restart` command
Short: i18n.G("Restart an app"),
Long: i18n.G(`This command restarts services within a deployed app.

View File

@ -12,10 +12,14 @@ import (
"github.com/spf13/cobra"
)
// translators: `abra app restore` aliases. use a comma separated list of
// aliases with no spaces in between
var appRestoreAliases = i18n.G("rs")
var AppRestoreCommand = &cobra.Command{
// translators: `app restore` command
Use: i18n.G("restore <domain> [flags]"),
Aliases: []string{i18n.G("rs")},
Aliases: strings.Split(appRestoreAliases, ","),
// translators: Short description for `app restore` command
Short: i18n.G("Restore a snapshot"),
Long: i18n.G(`Snapshots are restored while apps are deployed.

View File

@ -2,6 +2,7 @@ package app
import (
"errors"
"strings"
"coopcloud.tech/abra/pkg/app"
appPkg "coopcloud.tech/abra/pkg/app"
@ -21,10 +22,14 @@ import (
"github.com/spf13/cobra"
)
// translators: `abra app rollback` aliases. use a comma separated list of
// aliases with no spaces in between
var appRollbackAliases = i18n.G("rl")
var AppRollbackCommand = &cobra.Command{
// translators: `app rollback` command
Use: i18n.G("rollback <domain> [version] [flags]"),
Aliases: []string{i18n.G("rl")},
Aliases: strings.Split(appRollbackAliases, ","),
// translators: Short description for `app rollback` command
Short: i18n.G("Roll an app back to a previous version"),
Long: i18n.G(`This command rolls an app back to a previous version.

View File

@ -3,6 +3,7 @@ package app
import (
"context"
"fmt"
"strings"
"coopcloud.tech/abra/cli/internal"
"coopcloud.tech/abra/pkg/autocomplete"
@ -17,10 +18,14 @@ import (
"github.com/spf13/cobra"
)
// translators: `abra app run` aliases. use a comma separated list of aliases
// with no spaces in between
var appRunAliases = i18n.G("r")
var AppRunCommand = &cobra.Command{
// translators: `app run` command
Use: i18n.G("run <domain> <service> <cmd> [[args] [flags] | [flags] -- [args]]"),
Aliases: []string{i18n.G("r")},
Aliases: strings.Split(appRunAliases, ","),
// translators: Short description for `app run` command
Short: i18n.G("Run a command inside a service container"),
Example: i18n.G(` # run <cmd> with args/flags

View File

@ -24,10 +24,14 @@ import (
"github.com/spf13/cobra"
)
// translators: `abra app secret generate` aliases. use a comma separated list of aliases with
// no spaces in between
var appSecretGenerateAliases = i18n.G("g")
var AppSecretGenerateCommand = &cobra.Command{
// translators: `app secret generate` command
Use: i18n.G("generate <domain> [[secret] [version] | --all] [flags]"),
Aliases: []string{i18n.G("g")},
Aliases: strings.Split(appSecretGenerateAliases, ","),
// translators: Short description for `app secret generate` command
Short: i18n.G("Generate secrets"),
Args: cobra.RangeArgs(1, 3),
@ -146,10 +150,14 @@ var AppSecretGenerateCommand = &cobra.Command{
},
}
// translators: `abra app secret insert` aliases. use a comma separated list of aliases with
// no spaces in between
var appSecretInsertAliases = i18n.G("i")
var AppSecretInsertCommand = &cobra.Command{
// translators: `app secret insert` command
Use: i18n.G("insert <domain> <secret> <version> [<data>] [flags]"),
Aliases: []string{i18n.G("i")},
Aliases: strings.Split(appSecretInsertAliases, ","),
// translators: Short description for `app secret insert` command
Short: i18n.G("Insert secret"),
Long: i18n.G(`This command inserts a secret into an app environment.
@ -239,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)
}
@ -321,10 +329,14 @@ func secretRm(cl *dockerClient.Client, app appPkg.App, secretName, parsed string
return nil
}
// translators: `abra app secret remove` aliases. use a comma separated list of aliases with
// no spaces in between
var appSecretRemoveAliases = i18n.G("rm")
var AppSecretRmCommand = &cobra.Command{
// translators: `app secret remove` command
Use: i18n.G("remove <domain> [[secret] | --all] [flags]"),
Aliases: []string{i18n.G("rm")},
Aliases: strings.Split(appSecretRemoveAliases, ","),
// translators: Short description for `app secret remove` command
Short: i18n.G("Remove a secret"),
Long: i18n.G(`This command removes a secret from an app environment.
@ -436,10 +448,14 @@ match those configured in the recipe beforehand.`),
},
}
// translators: `abra app secret ls` aliases. use a comma separated list of aliases with
// no spaces in between
var appSecretLsAliases = i18n.G("ls")
var AppSecretLsCommand = &cobra.Command{
// translators: `app secret list` command
Use: i18n.G("list <domain>"),
Aliases: []string{i18n.G("ls")},
Aliases: strings.Split(appSecretLsAliases, ","),
// translators: Short description for `app secret list` command
Short: i18n.G("List all secrets"),
Args: cobra.MinimumNArgs(1),

View File

@ -17,10 +17,14 @@ import (
"github.com/spf13/cobra"
)
// translators: `abra app services` aliases. use a comma separated list of
// aliases with no spaces in between
var appServicesAliases = i18n.G("sr")
var AppServicesCommand = &cobra.Command{
// translators: `app services` command
Use: i18n.G("services <domain> [flags]"),
Aliases: []string{i18n.G("sr")},
Aliases: strings.Split(appServicesAliases, ","),
// translators: Short description for `app services` command
Short: i18n.G("Display all services of an app"),
Args: cobra.ExactArgs(1),

View File

@ -3,6 +3,7 @@ package app
import (
"context"
"fmt"
"strings"
"coopcloud.tech/abra/cli/internal"
appPkg "coopcloud.tech/abra/pkg/app"
@ -18,12 +19,15 @@ import (
"github.com/spf13/cobra"
)
// translators: `abra app undeploy` aliases. use a comma separated list of aliases with
// no spaces in between
var appUndeployAliases = i18n.G("un")
var AppUndeployCommand = &cobra.Command{
// translators: `app undeploy` command
Use: i18n.G("undeploy <domain> [flags]"),
Aliases: []string{i18n.G("un")},
Use: i18n.G("undeploy <domain> [flags]"),
// translators: Short description for `app undeploy` command
Short: i18n.G("Undeploy an app"),
Aliases: strings.Split(appUndeployAliases, ","),
Long: i18n.G(`This does not destroy any application data.
However, you should remain vigilant, as your swarm installation will consider

View File

@ -25,10 +25,14 @@ import (
"github.com/spf13/cobra"
)
// translators: `abra app upgrade` aliases. use a comma separated list of aliases with
// no spaces in between
var appUpgradeAliases = i18n.G("up")
var AppUpgradeCommand = &cobra.Command{
// translators: `app upgrade` command
Use: i18n.G("upgrade <domain> [version] [flags]"),
Aliases: []string{i18n.G("up")},
Aliases: strings.Split(appUpgradeAliases, ","),
// translators: Short description for `app upgrade` command
Short: i18n.G("Upgrade an app"),
Long: i18n.G(`Upgrade an app.
@ -331,7 +335,7 @@ func getReleaseNotes(
if parsedVersion.IsGreaterThan(parsedDeployedVersion) &&
parsedVersion.IsLessThan(parsedChosenUpgrade) {
note, err := app.Recipe.GetReleaseNotes(version)
note, err := app.Recipe.GetReleaseNotes(version, app.Domain)
if err != nil {
return err
}

View File

@ -3,6 +3,7 @@ package app
import (
"context"
"fmt"
"strings"
"coopcloud.tech/abra/cli/internal"
"coopcloud.tech/abra/pkg/autocomplete"
@ -15,10 +16,14 @@ import (
"github.com/spf13/cobra"
)
// translators: `abra app volume list` aliases. use a comma separated list of aliases with
// no spaces in between
var appVolumeListAliases = i18n.G("ls")
var AppVolumeListCommand = &cobra.Command{
// translators: `app volume list` command
Use: i18n.G("list <domain> [flags]"),
Aliases: []string{i18n.G("ls")},
Aliases: strings.Split(appVolumeListAliases, ","),
// translators: Short description for `app list` command
Short: i18n.G("List volumes associated with an app"),
Args: cobra.ExactArgs(1),
@ -74,6 +79,10 @@ var AppVolumeListCommand = &cobra.Command{
},
}
// translators: `abra app volume remove` aliases. use a comma separated list of aliases with
// no spaces in between
var appVolumeRemoveAliases = i18n.G("rm")
var AppVolumeRemoveCommand = &cobra.Command{
// translators: `app volume remove` command
Use: i18n.G("remove <domain> [volume] [flags]"),
@ -94,7 +103,7 @@ Passing "--force/-f" will select all volumes for removal. Be careful.`),
# delete specific volume
abra app volume rm 1312.net my_volume`),
Aliases: []string{i18n.G("rm")},
Aliases: strings.Split(appVolumeRemoveAliases, ","),
Args: cobra.MinimumNArgs(1),
ValidArgsFunction: func(
cmd *cobra.Command,
@ -190,10 +199,14 @@ Passing "--force/-f" will select all volumes for removal. Be careful.`),
},
}
// translators: `abra app volume` aliases. use a comma separated list of aliases with
// no spaces in between
var appVolumeAliases = i18n.G("vl")
var AppVolumeCommand = &cobra.Command{
// translators: `app volume` command group
Use: i18n.G("volume [cmd] [args] [flags]"),
Aliases: []string{i18n.G("vl")},
Aliases: strings.Split(appVolumeAliases, ","),
Short: i18n.G("Manage app volumes"),
}

View File

@ -7,6 +7,7 @@ import (
"os"
"path"
"slices"
"strings"
"coopcloud.tech/abra/cli/internal"
"coopcloud.tech/abra/pkg/autocomplete"
@ -21,10 +22,14 @@ import (
"github.com/spf13/cobra"
)
// translators: `abra catalogue sync` aliases. use a comma separated list of aliases with
// no spaces in between
var appCatalogueSyncAliases = i18n.G("s")
var CatalogueSyncCommand = &cobra.Command{
// translators: `catalogue sync` command
Use: i18n.G("sync [flags]"),
Aliases: []string{i18n.G("g")},
Aliases: strings.Split(appCatalogueSyncAliases, ","),
// translators: Short description for `catalogue sync` command
Short: i18n.G("Sync recipe catalogue for latest changes"),
Args: cobra.NoArgs,
@ -41,10 +46,14 @@ var CatalogueSyncCommand = &cobra.Command{
},
}
// translators: `abra catalogue` aliases. use a comma separated list of aliases with
// no spaces in between
var appCatalogueAliases = i18n.G("g")
var CatalogueGenerateCommand = &cobra.Command{
// translators: `catalogue generate` command
Use: i18n.G("generate [recipe] [flags]"),
Aliases: []string{i18n.G("g")},
Aliases: strings.Split(appCatalogueAliases, ","),
// translators: Short description for `catalogue generate` command
Short: i18n.G("Generate the recipe catalogue"),
Long: i18n.G(`Generate a new copy of the recipe catalogue.

View File

@ -2,14 +2,20 @@ package cli
import (
"os"
"strings"
"coopcloud.tech/abra/pkg/i18n"
"github.com/spf13/cobra"
)
// translators: `abra autocomplete` aliases. use a comma separated list of
// aliases with no spaces in between
var autocompleteAliases = i18n.G("ac")
var AutocompleteCommand = &cobra.Command{
// translators: `autocomplete` command
Use: i18n.G("autocomplete [bash|zsh|fish|powershell]"),
Use: i18n.G("autocomplete [bash|zsh|fish|powershell]"),
Aliases: strings.Split(autocompleteAliases, ","),
// translators: Short description for `autocomplete` command
Short: i18n.G("Generate autocompletion script"),
Long: i18n.G(`To load completions:

View File

@ -142,6 +142,69 @@ 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
}
secretsOverview := strings.Join(secrets, "\n")
if len(secrets) == 0 {
secretsOverview = config.NO_SECRETS_DEFAULT
}
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)
}
func PromptProcced() error {
if NoInput {
return nil
}
if Dry {
return errors.New(i18n.G("dry run"))
}
response := false
prompt := &survey.Confirm{Message: i18n.G("proceed?")}
if err := survey.AskOne(prompt, &response); err != nil {
return err
}
if !response {
return errors.New(i18n.G("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:
// "<service> <command> <arguments>|<service> <command> <arguments>|... "

View File

@ -1,6 +1,8 @@
package recipe
import (
"strings"
"coopcloud.tech/abra/cli/internal"
"coopcloud.tech/abra/pkg/autocomplete"
gitPkg "coopcloud.tech/abra/pkg/git"
@ -9,10 +11,14 @@ import (
"github.com/spf13/cobra"
)
// translators: `abra recipe diff` aliases. use a comma separated list of aliases
// with no spaces in between
var recipeDiffAliases = i18n.G("d")
var RecipeDiffCommand = &cobra.Command{
// translators: `recipe diff` command
Use: i18n.G("diff <recipe> [flags]"),
Aliases: []string{i18n.G("d")},
Aliases: strings.Split(recipeDiffAliases, ","),
// translators: Short description for `recipe diff` command
Short: i18n.G("Show unstaged changes in recipe config"),
Long: i18n.G("This command requires /usr/bin/git."),

View File

@ -2,6 +2,7 @@ package recipe
import (
"os"
"strings"
"coopcloud.tech/abra/cli/internal"
"coopcloud.tech/abra/pkg/autocomplete"
@ -14,10 +15,14 @@ import (
"github.com/spf13/cobra"
)
// translators: `abra recipe fetch` aliases. use a comma separated list of aliases
// with no spaces in between
var recipeFetchAliases = i18n.G("f")
var RecipeFetchCommand = &cobra.Command{
// translators: `recipe fetch` command
Use: i18n.G("fetch [recipe | --all] [flags]"),
Aliases: []string{i18n.G("f")},
Aliases: strings.Split(recipeFetchAliases, ","),
// translators: Short description for `recipe fetch` command
Short: i18n.G("Clone recipe(s) locally"),
Long: i18n.G(`Using "--force/-f" Git syncs an existing recipe. It does not erase unstaged changes.`),

View File

@ -1,6 +1,8 @@
package recipe
import (
"strings"
"coopcloud.tech/abra/cli/internal"
"coopcloud.tech/abra/pkg/autocomplete"
"coopcloud.tech/abra/pkg/formatter"
@ -10,12 +12,16 @@ import (
"github.com/spf13/cobra"
)
// translators: `abra recipe lint` aliases. use a comma separated list of
// aliases with no spaces in between
var recipeLintAliases = i18n.G("l")
var RecipeLintCommand = &cobra.Command{
// translators: `recipe lint` command
Use: i18n.G("lint <recipe> [flags]"),
// translators: Short description for `recipe lint` command
Short: i18n.G("Lint a recipe"),
Aliases: []string{i18n.G("l")},
Aliases: strings.Split(recipeLintAliases, ","),
Args: cobra.MinimumNArgs(1),
ValidArgsFunction: func(
cmd *cobra.Command,

View File

@ -14,12 +14,16 @@ import (
"github.com/spf13/cobra"
)
// translators: `abra recipe list` aliases. use a comma separated list of
// aliases with no spaces in between
var recipeListAliases = i18n.G("ls")
var RecipeListCommand = &cobra.Command{
// translators: `recipe list` command
Use: i18n.G("list"),
// translators: Short description for `recipe list` command
Short: i18n.G("List recipes"),
Aliases: []string{i18n.G("ls")},
Aliases: strings.Split(recipeListAliases, ","),
Args: cobra.NoArgs,
Run: func(cmd *cobra.Command, args []string) {
catl, err := recipe.ReadRecipeCatalogue(internal.Offline)

View File

@ -5,6 +5,7 @@ import (
"fmt"
"os"
"path"
"strings"
"text/template"
"coopcloud.tech/abra/pkg/autocomplete"
@ -30,13 +31,18 @@ type recipeMetadata struct {
SSO string
}
// translators: `abra recipe new` aliases. use a comma separated list of
// aliases with no spaces in between
var recipeNewAliases = i18n.G("n")
var RecipeNewCommand = &cobra.Command{
// translators: `recipe new` command
Use: i18n.G("new <recipe> [flags]"),
Aliases: []string{i18n.G("n")},
Short: i18n.G("Create a new recipe"),
Long: i18n.G(`A community managed recipe template is used.`),
Args: cobra.ExactArgs(1),
Aliases: strings.Split(recipeNewAliases, ","),
// translators: Short description for `abra recipe new` command
Short: i18n.G("Create a new recipe"),
Long: i18n.G(`A community managed recipe template is used.`),
Args: cobra.ExactArgs(1),
ValidArgsFunction: func(
cmd *cobra.Command,
args []string,

View File

@ -1,15 +1,21 @@
package recipe
import (
"strings"
"coopcloud.tech/abra/pkg/i18n"
"github.com/spf13/cobra"
)
// translators: `abra recipe` aliases. use a comma separated list of aliases
// with no spaces in between
var recipeAliases = i18n.G("r")
// RecipeCommand defines all recipe related sub-commands.
var RecipeCommand = &cobra.Command{
// translators: `recipe` command group
Use: i18n.G("recipe [cmd] [args] [flags]"),
Aliases: []string{i18n.G("r")},
Aliases: strings.Split(recipeAliases, ","),
// translators: Short description for `recipe` command group
Short: i18n.G("Manage recipes"),
Long: i18n.G(`A recipe is a blueprint for an app.

View File

@ -23,10 +23,14 @@ import (
"github.com/spf13/cobra"
)
// translators: `abra recipe release` aliases. use a comma separated list of
// aliases with no spaces in between
var recipeReleaseAliases = i18n.G("rl")
var RecipeReleaseCommand = &cobra.Command{
// translators: `recipe release` command
Use: i18n.G("release <recipe> [version] [flags]"),
Aliases: []string{i18n.G("rl")},
Aliases: strings.Split(recipeReleaseAliases, ","),
// translators: Short description for `recipe release` command
Short: i18n.G("Release a new recipe version"),
Long: i18n.G(`Create a new version of a recipe.

View File

@ -1,6 +1,8 @@
package recipe
import (
"strings"
"coopcloud.tech/abra/cli/internal"
"coopcloud.tech/abra/pkg/autocomplete"
"coopcloud.tech/abra/pkg/i18n"
@ -9,10 +11,14 @@ import (
"github.com/spf13/cobra"
)
// translators: `abra recipe reset` aliases. use a comma separated list of
// aliases with no spaces in between
var recipeResetAliases = i18n.G("rs")
var RecipeResetCommand = &cobra.Command{
// translators: `recipe reset` command
Use: i18n.G("reset <recipe> [flags]"),
Aliases: []string{i18n.G("rs")},
Aliases: strings.Split(recipeResetAliases, ","),
// translators: Short description for `recipe reset` command
Short: i18n.G("Remove all unstaged changes from recipe config"),
Long: i18n.G("WARNING: this will delete your changes. Be Careful."),

View File

@ -3,6 +3,7 @@ package recipe
import (
"fmt"
"strconv"
"strings"
"coopcloud.tech/abra/cli/internal"
"coopcloud.tech/abra/pkg/autocomplete"
@ -18,10 +19,14 @@ import (
"github.com/spf13/cobra"
)
// translators: `abra recipe reset` aliases. use a comma separated list of
// aliases with no spaces in between
var recipeSyncAliases = i18n.G("s")
var RecipeSyncCommand = &cobra.Command{
// translators: `recipe sync` command
Use: i18n.G("sync <recipe> [version] [flags]"),
Aliases: []string{i18n.G("s")},
Aliases: strings.Split(recipeSyncAliases, ","),
// translators: Short description for `recipe sync` command
Short: i18n.G("Sync recipe version label"),
Long: i18n.G(`Generate labels for the main recipe service.

View File

@ -37,10 +37,14 @@ type anUpgrade struct {
UpgradeTags []string `json:"upgrades"`
}
// translators: `abra recipe upgrade` aliases. use a comma separated list of
// aliases with no spaces in between
var recipeUpgradeAliases = i18n.G("u")
var RecipeUpgradeCommand = &cobra.Command{
// translators: `recipe upgrade` command
Use: i18n.G("upgrade <recipe> [flags]"),
Aliases: []string{i18n.G("u")},
Aliases: strings.Split(recipeUpgradeAliases, ","),
// translators: Short description for `recipe upgrade` command
Short: i18n.G("Upgrade recipe image tags"),
Long: i18n.G(`Upgrade a given <recipe> configuration.

View File

@ -3,6 +3,7 @@ package recipe
import (
"fmt"
"sort"
"strings"
"coopcloud.tech/abra/cli/internal"
"coopcloud.tech/abra/pkg/autocomplete"
@ -13,10 +14,14 @@ import (
"github.com/spf13/cobra"
)
// translators: `abra recipe versions` aliases. use a comma separated list of aliases
// with no spaces in between
var recipeVersionsAliases = i18n.G("v")
var RecipeVersionCommand = &cobra.Command{
// translators: `recipe versions` command
Use: i18n.G("versions <recipe> [flags]"),
Aliases: []string{i18n.G("v")},
Aliases: strings.Split(recipeVersionsAliases, ","),
// translators: Short description for `recipe versions` command
Short: i18n.G("List recipe versions"),
Args: cobra.ExactArgs(1),

View File

@ -4,6 +4,7 @@ import (
"errors"
"fmt"
"os"
"strings"
"coopcloud.tech/abra/cli/app"
"coopcloud.tech/abra/cli/catalogue"
@ -19,23 +20,65 @@ import (
"github.com/spf13/cobra/doc"
)
var (
// translators: `abra` usage template. please translate only words like
// "Aliases" and "Example" and nothing inside the {{ ... }}
usageTemplate = i18n.G(`Usage:{{if .Runnable}}
{{.UseLine}}{{end}}{{if .HasAvailableSubCommands}}
{{.CommandPath}} [command]{{end}}{{if gt (len .Aliases) 0}}
Aliases:
{{.NameAndAliases}}{{end}}{{if .HasExample}}
Examples:
{{.Example}}{{end}}{{if .HasAvailableSubCommands}}
Available Commands:{{range .Commands}}{{if (or .IsAvailableCommand (eq .Name "help"))}}
{{rpad .Name .NamePadding }} {{.Short}}{{end}}{{end}}{{end}}{{if .HasAvailableLocalFlags}}
Flags:
{{.LocalFlags.FlagUsages | trimTrailingWhitespaces}}{{end}}{{if .HasAvailableInheritedFlags}}
Global Flags:
{{.InheritedFlags.FlagUsages | trimTrailingWhitespaces}}{{end}}{{if .HasHelpSubCommands}}
Additional help topics:{{range .Commands}}{{if .IsAdditionalHelpTopicCommand}}
{{rpad .CommandPath .CommandPathPadding}} {{.Short}}{{end}}{{end}}{{end}}{{if .HasAvailableSubCommands}}
Use "{{.CommandPath}} [command] --help" for more information about a command.{{end}}
`)
)
func Run(version, commit string) {
rootCmd := &cobra.Command{
// translators: `abra` binary name
Use: i18n.G("abra [cmd] [args] [flags]"),
// translators: Short description for `abra` binary
Short: i18n.G("The Co-op Cloud command-line utility belt 🎩🐇"),
Short: i18n.G("The Co-op Cloud command-line utility belt 🎩🐇"),
// translators: Long description for `abra` binary. This needs to be
// translated in the same way as the Short description so that everything
// matches up
Long: i18n.G(`The Co-op Cloud command-line utility belt 🎩🐇
Config:
$ABRA_DIR: %s`, config.ABRA_DIR),
Version: fmt.Sprintf("%s-%s", version, commit[:7]),
ValidArgs: []string{
// translators: `abra app` command for autocompletion
i18n.G("app"),
// translators: `abra autocomplete` command for autocompletion
i18n.G("autocomplete"),
// translators: `abra catalogue` command for autocompletion
i18n.G("catalogue"),
// translators: `abra man` command for autocompletion
i18n.G("man"),
// translators: `abra recipe` command for autocompletion
i18n.G("recipe"),
// translators: `abra server` command for autocompletion
i18n.G("server"),
// translators: `abra upgrade` command for autocompletion
i18n.G("upgrade"),
},
PersistentPreRunE: func(cmd *cobra.Command, args []string) error {
dirs := []map[string]os.FileMode{
{config.ABRA_DIR: 0764},
@ -78,11 +121,16 @@ func Run(version, commit string) {
}
rootCmd.CompletionOptions.DisableDefaultCmd = true
rootCmd.SetUsageTemplate(usageTemplate)
// translators: `abra man` aliases. use a comma separated list of aliases
// with no spaces in between
manAliases := i18n.G("m")
manCommand := &cobra.Command{
// translators: `man` command
Use: i18n.G("man [flags]"),
Aliases: []string{"m"},
Aliases: strings.Split(manAliases, ","),
// translators: Short description for `man` command
Short: i18n.G("Generate manpage"),
Example: i18n.G(` # generate the man pages into /usr/local/share/man/man1
@ -210,6 +258,7 @@ func Run(version, commit string) {
app.AppRestartCommand,
app.AppRestoreCommand,
app.AppRollbackCommand,
app.AppMoveCommand,
app.AppRunCommand,
app.AppSecretCommand,
app.AppServicesCommand,

View File

@ -3,6 +3,7 @@ package server
import (
"os"
"path/filepath"
"strings"
"coopcloud.tech/abra/cli/internal"
"coopcloud.tech/abra/pkg/autocomplete"
@ -17,10 +18,14 @@ import (
"github.com/spf13/cobra"
)
// translators: `abra server add` aliases. use a comma separated list of
// aliases with no spaces in between
var serverAddAliases = i18n.G("a")
var ServerAddCommand = &cobra.Command{
// translators: `server add` command
Use: i18n.G("add [[server] | --local] [flags]"),
Aliases: []string{i18n.G("a")},
Aliases: strings.Split(serverAddAliases, ","),
// translators: Short description for `server add` command
Short: i18n.G("Add a new server"),
Long: i18n.G(`Add a new server to your configuration so that it can be managed by Abra.

View File

@ -14,10 +14,14 @@ import (
"github.com/spf13/cobra"
)
// translators: `abra server list` aliases. use a comma separated list of
// aliases with no spaces in between
var serverListAliases = i18n.G("ls")
var ServerListCommand = &cobra.Command{
// translators: `server list` command
Use: i18n.G("list [flags]"),
Aliases: []string{i18n.G("ls")},
Aliases: strings.Split(serverListAliases, ","),
// translators: Short description for `server list` command
Short: i18n.G("List managed servers"),
Args: cobra.NoArgs,

View File

@ -1,6 +1,8 @@
package server
import (
"strings"
"coopcloud.tech/abra/cli/internal"
"coopcloud.tech/abra/pkg/autocomplete"
"coopcloud.tech/abra/pkg/client"
@ -11,10 +13,14 @@ import (
"github.com/spf13/cobra"
)
// translators: `abra server prune` aliases. use a comma separated list of
// aliases with no spaces in between
var serverPruneliases = i18n.G("p")
var ServerPruneCommand = &cobra.Command{
// translators: `server prune` command
Use: i18n.G("prune <server> [flags]"),
Aliases: []string{i18n.G("p")},
Aliases: strings.Split(serverPruneliases, ","),
// translators: Short description for `server prune` command
Short: i18n.G("Prune resources on a server"),
Long: i18n.G(`Prunes unused containers, networks, and dangling images.

View File

@ -3,6 +3,7 @@ package server
import (
"os"
"path/filepath"
"strings"
"coopcloud.tech/abra/cli/internal"
"coopcloud.tech/abra/pkg/autocomplete"
@ -13,10 +14,14 @@ import (
"github.com/spf13/cobra"
)
// translators: `abra server remove` aliases. use a comma separated list of
// aliases with no spaces in between
var serverRemoveAliases = i18n.G("rm")
var ServerRemoveCommand = &cobra.Command{
// translators: `server remove` command
Use: i18n.G("remove <server> [flags]"),
Aliases: []string{i18n.G("rm")},
Aliases: strings.Split(serverRemoveAliases, ","),
// translators: Short description for `server remove` command
Short: i18n.G("Remove a managed server"),
Long: i18n.G(`Remove a managed server.

View File

@ -1,15 +1,21 @@
package server
import (
"strings"
"coopcloud.tech/abra/pkg/i18n"
"github.com/spf13/cobra"
)
// translators: `abra server` aliases. use a comma separated list of aliases
// with no spaces in between
var serverAliases = i18n.G("s")
// ServerCommand defines the `abra server` command and its subcommands
var ServerCommand = &cobra.Command{
// translators: `server` command group
Use: i18n.G("server [cmd] [args] [flags]"),
Aliases: []string{i18n.G("s")},
Aliases: strings.Split(serverAliases, ","),
// translators: Short description for `server` command group
Short: i18n.G("Manage servers"),
}

View File

@ -30,11 +30,15 @@ import (
const SERVER = "localhost"
// translators: `kadabra notify` aliases. use a comma separated list of aliases
// with no spaces in between
var notifyAliases = i18n.G("n")
// NotifyCommand checks for available upgrades.
var NotifyCommand = &cobra.Command{
// translators: `notify` command
Use: i18n.G("notify [flags]"),
Aliases: []string{i18n.G("n")},
Aliases: strings.Split(notifyAliases, ","),
// translators: Short description for `notify` command
Short: i18n.G("Check for available upgrades"),
Long: i18n.G(`Notify on new versions for deployed apps.
@ -71,11 +75,15 @@ Use "--major/-m" to include new major versions.`),
},
}
// translators: `kadabra upgrade` aliases. use a comma separated list of aliases with
// no spaces in between
var upgradeAliases = i18n.G("u")
// UpgradeCommand upgrades apps.
var UpgradeCommand = &cobra.Command{
// translators: `app upgrade` command
Use: i18n.G("upgrade [[stack] [recipe] | --all] [flags]"),
Aliases: []string{i18n.G("u")},
Aliases: strings.Split(upgradeAliases, ","),
// translators: Short description for `app upgrade` command
Short: i18n.G("Upgrade apps"),
Long: i18n.G(`Upgrade an app by specifying stack name and recipe.

View File

@ -4,6 +4,7 @@ package cli
import (
"fmt"
"os/exec"
"strings"
"coopcloud.tech/abra/cli/internal"
"coopcloud.tech/abra/pkg/i18n"
@ -11,11 +12,15 @@ import (
"github.com/spf13/cobra"
)
// translators: `abra upgrade` aliases. use a comma separated list of aliases with
// no spaces in between
var upgradeAliases = i18n.G("u")
// UpgradeCommand upgrades abra in-place.
var UpgradeCommand = &cobra.Command{
// translators: `upgrade` command
Use: i18n.G("upgrade [flags]"),
Aliases: []string{"u"},
Aliases: strings.Split(upgradeAliases, ","),
// translators: Short description for `upgrade` command
Short: i18n.G("Upgrade abra"),
Long: i18n.G(`Upgrade abra in-place with the latest stable or release candidate.

View File

@ -390,7 +390,12 @@ func TemplateAppEnvSample(r recipe.Recipe, appName, server, domain string) error
return err
}
newContents := strings.Replace(string(read), r.Name+".example.com", domain, -1)
newContents := strings.Replace(
string(read),
fmt.Sprintf("%s.example.com", r.Name),
domain,
-1,
)
err = os.WriteFile(appEnvPath, []byte(newContents), 0)
if err != nil {

View File

@ -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))
}

View File

@ -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)}

View File

@ -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"
)

File diff suppressed because it is too large Load Diff

Binary file not shown.

File diff suppressed because it is too large Load Diff

View File

@ -5,6 +5,7 @@ import (
"fmt"
"os"
"path"
"strings"
"coopcloud.tech/abra/pkg/envfile"
"coopcloud.tech/abra/pkg/formatter"
@ -20,7 +21,7 @@ func (r Recipe) SampleEnv() (map[string]string, error) {
}
// GetReleaseNotes prints release notes for the recipe version
func (r Recipe) GetReleaseNotes(version string) (string, error) {
func (r Recipe) GetReleaseNotes(version, appDomain string) (string, error) {
if version == "" {
return "", nil
}
@ -36,7 +37,14 @@ func (r Recipe) GetReleaseNotes(version string) (string, error) {
title := formatter.BoldStyle.Render(i18n.G("%s release notes:", version))
withTitle := fmt.Sprintf("%s\n%s\n", title, releaseNotes)
return withTitle, nil
templatedDomain := strings.Replace(
withTitle,
fmt.Sprintf("%s.example.com", r.Name),
appDomain,
-1,
)
return templatedDomain, nil
}
return "", nil

View File

@ -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

View File

@ -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'
}

View File

@ -257,3 +257,16 @@ teardown(){
assert_success
assert_exists "$ABRA_DIR/servers/$TEST_SERVER/$TEST_APP_DOMAIN.env"
}
@test "warn about secrets if present" {
run $ABRA app new "$TEST_RECIPE" --domain "$TEST_APP_DOMAIN"
assert_success
assert_output --partial "requires secret generation"
}
@test "do not warn about secrets if not present" {
# NOTE(d1): here's hoping this won't flake, custom-html is pretty stable
run $ABRA app new custom-html --domain "$TEST_APP_DOMAIN"
assert_success
refute_output --partial "requires secret generation"
}

View File

@ -205,6 +205,18 @@ teardown(){
refute_output --partial 'release notes baz' # 0.2.0+1.21.0
}
# bats test_tags=slow
@test "template <recipe>.example.com in release note" {
run $ABRA app deploy "$TEST_APP_DOMAIN" "0.3.4+1.21.0" --no-input --no-converge-checks
assert_success
assert_output --partial '0.3.4+1.21.0'
run $ABRA app upgrade "$TEST_APP_DOMAIN" "0.3.5+1.21.0" --no-input --no-converge-checks
assert_success
assert_output --partial '0.3.5+1.21.0'
refute_output --partial 'abra-test-recipe.local' # 0.3.5+1.21.0
}
# bats test_tags=slow
@test "show multiple release notes" {
run $ABRA app deploy "$TEST_APP_DOMAIN" "0.1.0+1.20.0" --no-input --no-converge-checks

View File

@ -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

View File

@ -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