Compare commits
15 Commits
main
...
feature/3w
Author | SHA1 | Date | |
---|---|---|---|
24970360fa | |||
b3bd253684 | |||
32c2ea5b53 | |||
12b01ace71 | |||
58d15c35d8 | |||
a02be5705e | |||
371b70b537 | |||
7558550d96 | |||
d94335941f | |||
88ea705a33 | |||
faa8cd12d9 | |||
65ed2f6113 | |||
746485cfb0 | |||
2b1bece9b6 | |||
c05d0fef24 |
1
.gitignore
vendored
1
.gitignore
vendored
@ -1,4 +1,3 @@
|
||||
*.tar.gz
|
||||
*fmtcoverage.html
|
||||
.e2e.env
|
||||
.envrc
|
||||
|
@ -5,7 +5,6 @@
|
||||
|
||||
- 3wordchant
|
||||
- ammaratef45
|
||||
- apfelwurm
|
||||
- basebuilder
|
||||
- cassowary
|
||||
- chasqui
|
||||
@ -13,7 +12,6 @@
|
||||
- decentral1se
|
||||
- fauno
|
||||
- frando
|
||||
- iexos
|
||||
- kawaiipunk
|
||||
- knoflook
|
||||
- mayel
|
||||
|
@ -1,20 +1,14 @@
|
||||
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: strings.Split(appAliases, ","),
|
||||
Aliases: []string{i18n.G("a")},
|
||||
// translators: Short description for `app` command group
|
||||
Short: i18n.G("Manage apps"),
|
||||
}
|
||||
|
@ -2,7 +2,6 @@ package app
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"strings"
|
||||
|
||||
"coopcloud.tech/abra/cli/internal"
|
||||
"coopcloud.tech/abra/pkg/autocomplete"
|
||||
@ -12,14 +11,10 @@ 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: strings.Split(appBackupListAliases, ","),
|
||||
Aliases: []string{i18n.G("ls")},
|
||||
// translators: Short description for `app backup list` command
|
||||
Short: i18n.G("List the contents of a snapshot"),
|
||||
Args: cobra.ExactArgs(1),
|
||||
@ -68,14 +63,10 @@ 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: strings.Split(appBackupDownloadAliases, ","),
|
||||
Aliases: []string{i18n.G("d")},
|
||||
// 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.
|
||||
@ -143,14 +134,10 @@ 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: strings.Split(appBackupCreateAliases, ","),
|
||||
Aliases: []string{i18n.G("c")},
|
||||
// translators: Short description for `app backup create` command
|
||||
Short: i18n.G("Create a new snapshot"),
|
||||
Args: cobra.ExactArgs(1),
|
||||
@ -193,14 +180,10 @@ 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: strings.Split(appBackupSnapshotsAliases, ","),
|
||||
Aliases: []string{i18n.G("s")},
|
||||
// translators: Short description for `app backup snapshots` command
|
||||
Short: i18n.G("List all snapshots"),
|
||||
Args: cobra.ExactArgs(1),
|
||||
@ -234,14 +217,10 @@ 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: strings.Split(appBackupAliases, ","),
|
||||
Aliases: []string{i18n.G("b")},
|
||||
// translators: Short description for `app backup` command group
|
||||
Short: i18n.G("Manage app backups"),
|
||||
}
|
||||
|
@ -2,7 +2,6 @@ package app
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"strings"
|
||||
|
||||
"coopcloud.tech/abra/cli/internal"
|
||||
appPkg "coopcloud.tech/abra/pkg/app"
|
||||
@ -14,14 +13,10 @@ 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: strings.Split(appCheckAliases, ","),
|
||||
Aliases: []string{i18n.G("chk")},
|
||||
// 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.
|
||||
|
@ -18,14 +18,10 @@ 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: strings.Split(appCmdAliases, ","),
|
||||
Aliases: []string{i18n.G("cmd")},
|
||||
// translators: Short description for `app cmd` command
|
||||
Short: i18n.G("Run app commands"),
|
||||
Long: i18n.G(`Run an app specific command.
|
||||
@ -198,14 +194,10 @@ 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: strings.Split(appCmdListAliases, ","),
|
||||
Aliases: []string{i18n.G("ls")},
|
||||
// translators: Short description for `app cmd list` command
|
||||
Short: i18n.G("List all available commands"),
|
||||
Args: cobra.MinimumNArgs(1),
|
||||
|
@ -3,7 +3,6 @@ package app
|
||||
import (
|
||||
"os"
|
||||
"os/exec"
|
||||
"strings"
|
||||
|
||||
appPkg "coopcloud.tech/abra/pkg/app"
|
||||
"coopcloud.tech/abra/pkg/autocomplete"
|
||||
@ -13,14 +12,10 @@ 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: strings.Split(appConfigAliases, ","),
|
||||
Aliases: []string{i18n.G("cfg")},
|
||||
// translators: Short description for `app config` command
|
||||
Short: i18n.G("Edit app config"),
|
||||
Example: i18n.G(" abra config 1312.net"),
|
||||
|
@ -25,14 +25,10 @@ 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: strings.Split(appCpAliases, ","),
|
||||
Aliases: []string{i18n.G("c")},
|
||||
// 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
|
||||
|
@ -14,6 +14,7 @@ import (
|
||||
|
||||
appPkg "coopcloud.tech/abra/pkg/app"
|
||||
"coopcloud.tech/abra/pkg/client"
|
||||
"coopcloud.tech/abra/pkg/deploy"
|
||||
"coopcloud.tech/abra/pkg/dns"
|
||||
"coopcloud.tech/abra/pkg/formatter"
|
||||
"coopcloud.tech/abra/pkg/i18n"
|
||||
@ -24,14 +25,10 @@ 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: strings.Split(appDeployAliases, ","),
|
||||
Aliases: []string{i18n.G("d")},
|
||||
// translators: Short description for `app deploy` command
|
||||
Short: i18n.G("Deploy an app"),
|
||||
Long: i18n.G(`Deploy an app.
|
||||
@ -191,12 +188,35 @@ checkout as-is. Recipe commit hashes are also supported as values for
|
||||
deployedVersion = deployMeta.Version
|
||||
}
|
||||
|
||||
// Gather secrets
|
||||
secretInfo, err := deploy.GatherSecretsForDeploy(cl, app)
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
|
||||
// Gather configs
|
||||
configInfo, err := deploy.GatherConfigsForDeploy(cl, app, compose, abraShEnv)
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
|
||||
// Gather images
|
||||
imageInfo, err := deploy.GatherImagesForDeploy(cl, app, compose)
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
|
||||
// Show deploy overview
|
||||
|
||||
if err := internal.DeployOverview(
|
||||
app,
|
||||
deployedVersion,
|
||||
toDeployVersion,
|
||||
"",
|
||||
deployWarnMessages,
|
||||
strings.Join(secretInfo, "\n"),
|
||||
strings.Join(configInfo, "\n"),
|
||||
strings.Join(imageInfo, "\n"),
|
||||
); err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
|
@ -3,7 +3,6 @@ package app
|
||||
import (
|
||||
"fmt"
|
||||
"sort"
|
||||
"strings"
|
||||
|
||||
"coopcloud.tech/abra/cli/internal"
|
||||
"coopcloud.tech/abra/pkg/autocomplete"
|
||||
@ -12,14 +11,10 @@ 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: strings.Split(appEnvAliases, ","),
|
||||
Aliases: []string{i18n.G("e")},
|
||||
// translators: Short description for `app env` command
|
||||
Short: i18n.G("Show app .env values"),
|
||||
Example: i18n.G(" abra app env 1312.net"),
|
||||
|
@ -4,7 +4,6 @@ import (
|
||||
"context"
|
||||
"fmt"
|
||||
"sort"
|
||||
"strings"
|
||||
|
||||
"coopcloud.tech/abra/cli/internal"
|
||||
"coopcloud.tech/abra/pkg/autocomplete"
|
||||
@ -20,14 +19,10 @@ 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: strings.Split(appLabelsAliases, ","),
|
||||
Aliases: []string{i18n.G("lb")},
|
||||
// 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."),
|
||||
|
@ -39,14 +39,10 @@ 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: strings.Split(appListAliases, ","),
|
||||
Aliases: []string{i18n.G("ls")},
|
||||
// translators: Short description for `app list` command
|
||||
Short: i18n.G("List all managed apps"),
|
||||
Long: i18n.G(`Generate a report of all managed apps.
|
||||
|
@ -2,7 +2,6 @@ package app
|
||||
|
||||
import (
|
||||
"context"
|
||||
"strings"
|
||||
|
||||
"coopcloud.tech/abra/cli/internal"
|
||||
appPkg "coopcloud.tech/abra/pkg/app"
|
||||
@ -15,14 +14,10 @@ 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: strings.Split(appLogsAliases, ","),
|
||||
Aliases: []string{i18n.G("l")},
|
||||
// translators: Short description for `app logs` command
|
||||
Short: i18n.G("Tail app logs"),
|
||||
Args: cobra.RangeArgs(1, 2),
|
||||
|
350
cli/app/move.go
350
cli/app/move.go
@ -1,350 +0,0 @@
|
||||
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"),
|
||||
)
|
||||
}
|
@ -3,7 +3,6 @@ package app
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"strings"
|
||||
|
||||
"coopcloud.tech/abra/cli/internal"
|
||||
"coopcloud.tech/abra/pkg/app"
|
||||
@ -43,14 +42,10 @@ 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: strings.Split(appNewAliases, ","),
|
||||
Aliases: []string{i18n.G("n")},
|
||||
// translators: Short description for `app new` command
|
||||
Short: i18n.G("Create a new app"),
|
||||
Long: appNewDescription,
|
||||
@ -149,27 +144,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)
|
||||
}
|
||||
@ -191,10 +186,6 @@ 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 {
|
||||
|
@ -24,14 +24,10 @@ 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: strings.Split(appPsAliases, ","),
|
||||
Aliases: []string{i18n.G("p")},
|
||||
// translators: Short description for `app ps` command
|
||||
Short: i18n.G("Check app deployment status"),
|
||||
Args: cobra.ExactArgs(1),
|
||||
|
@ -3,7 +3,6 @@ package app
|
||||
import (
|
||||
"context"
|
||||
"os"
|
||||
"strings"
|
||||
|
||||
"coopcloud.tech/abra/cli/internal"
|
||||
"coopcloud.tech/abra/pkg/autocomplete"
|
||||
@ -16,14 +15,10 @@ 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: strings.Split(appRemoveAliases, ","),
|
||||
Aliases: []string{i18n.G("rm")},
|
||||
// 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.
|
||||
|
@ -3,7 +3,6 @@ package app
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"strings"
|
||||
|
||||
"coopcloud.tech/abra/cli/internal"
|
||||
appPkg "coopcloud.tech/abra/pkg/app"
|
||||
@ -18,14 +17,10 @@ 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: strings.Split(appRestartAliases, ","),
|
||||
Aliases: []string{i18n.G("re")},
|
||||
// translators: Short description for `app restart` command
|
||||
Short: i18n.G("Restart an app"),
|
||||
Long: i18n.G(`This command restarts services within a deployed app.
|
||||
|
@ -12,14 +12,10 @@ 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: strings.Split(appRestoreAliases, ","),
|
||||
Aliases: []string{i18n.G("rs")},
|
||||
// translators: Short description for `app restore` command
|
||||
Short: i18n.G("Restore a snapshot"),
|
||||
Long: i18n.G(`Snapshots are restored while apps are deployed.
|
||||
|
@ -8,6 +8,7 @@ import (
|
||||
appPkg "coopcloud.tech/abra/pkg/app"
|
||||
"coopcloud.tech/abra/pkg/autocomplete"
|
||||
"coopcloud.tech/abra/pkg/config"
|
||||
"coopcloud.tech/abra/pkg/deploy"
|
||||
"coopcloud.tech/abra/pkg/envfile"
|
||||
"coopcloud.tech/abra/pkg/formatter"
|
||||
"coopcloud.tech/abra/pkg/i18n"
|
||||
@ -22,14 +23,10 @@ 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: strings.Split(appRollbackAliases, ","),
|
||||
Aliases: []string{i18n.G("rl")},
|
||||
// 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.
|
||||
@ -190,6 +187,24 @@ beforehand. See "abra app backup" for more.`),
|
||||
}
|
||||
appPkg.SetUpdateLabel(compose, stackName, app.Env)
|
||||
|
||||
// Gather secrets
|
||||
secretInfo, err := deploy.GatherSecretsForDeploy(cl, app)
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
|
||||
// Gather configs
|
||||
configInfo, err := deploy.GatherConfigsForDeploy(cl, app, compose, abraShEnv)
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
|
||||
// Gather images
|
||||
imageInfo, err := deploy.GatherImagesForDeploy(cl, app, compose)
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
|
||||
// NOTE(d1): no release notes implemeneted for rolling back
|
||||
if err := internal.DeployOverview(
|
||||
app,
|
||||
@ -197,6 +212,9 @@ beforehand. See "abra app backup" for more.`),
|
||||
chosenDowngrade,
|
||||
"",
|
||||
downgradeWarnMessages,
|
||||
strings.Join(secretInfo, "\n"),
|
||||
strings.Join(configInfo, "\n"),
|
||||
strings.Join(imageInfo, "\n"),
|
||||
); err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
|
@ -3,7 +3,6 @@ package app
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"strings"
|
||||
|
||||
"coopcloud.tech/abra/cli/internal"
|
||||
"coopcloud.tech/abra/pkg/autocomplete"
|
||||
@ -18,14 +17,10 @@ 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: strings.Split(appRunAliases, ","),
|
||||
Aliases: []string{i18n.G("r")},
|
||||
// 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
|
||||
|
@ -24,14 +24,10 @@ 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: strings.Split(appSecretGenerateAliases, ","),
|
||||
Aliases: []string{i18n.G("g")},
|
||||
// translators: Short description for `app secret generate` command
|
||||
Short: i18n.G("Generate secrets"),
|
||||
Args: cobra.RangeArgs(1, 3),
|
||||
@ -150,14 +146,10 @@ 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: strings.Split(appSecretInsertAliases, ","),
|
||||
Aliases: []string{i18n.G("i")},
|
||||
// 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.
|
||||
@ -247,7 +239,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); err != nil {
|
||||
if err := client.StoreSecret(cl, secretName, data, app.Server); err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
|
||||
@ -329,14 +321,10 @@ 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: strings.Split(appSecretRemoveAliases, ","),
|
||||
Aliases: []string{i18n.G("rm")},
|
||||
// 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.
|
||||
@ -448,14 +436,10 @@ 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: strings.Split(appSecretLsAliases, ","),
|
||||
Aliases: []string{i18n.G("ls")},
|
||||
// translators: Short description for `app secret list` command
|
||||
Short: i18n.G("List all secrets"),
|
||||
Args: cobra.MinimumNArgs(1),
|
||||
|
@ -17,14 +17,10 @@ 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: strings.Split(appServicesAliases, ","),
|
||||
Aliases: []string{i18n.G("sr")},
|
||||
// translators: Short description for `app services` command
|
||||
Short: i18n.G("Display all services of an app"),
|
||||
Args: cobra.ExactArgs(1),
|
||||
|
@ -3,7 +3,6 @@ package app
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"strings"
|
||||
|
||||
"coopcloud.tech/abra/cli/internal"
|
||||
appPkg "coopcloud.tech/abra/pkg/app"
|
||||
@ -19,15 +18,12 @@ 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]"),
|
||||
Use: i18n.G("undeploy <domain> [flags]"),
|
||||
Aliases: []string{i18n.G("un")},
|
||||
// translators: Short description for `app undeploy` command
|
||||
Aliases: strings.Split(appUndeployAliases, ","),
|
||||
Short: i18n.G("Undeploy an app"),
|
||||
Long: i18n.G(`This does not destroy any application data.
|
||||
|
||||
However, you should remain vigilant, as your swarm installation will consider
|
||||
@ -71,6 +67,9 @@ Passing "--prune/-p" does not remove those volumes.`),
|
||||
config.NO_DOMAIN_DEFAULT,
|
||||
"",
|
||||
nil,
|
||||
"",
|
||||
"",
|
||||
"",
|
||||
); err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
|
@ -7,11 +7,11 @@ import (
|
||||
"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"
|
||||
"coopcloud.tech/abra/pkg/deploy"
|
||||
"coopcloud.tech/abra/pkg/envfile"
|
||||
"coopcloud.tech/abra/pkg/formatter"
|
||||
"coopcloud.tech/abra/pkg/i18n"
|
||||
@ -25,14 +25,10 @@ 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: strings.Split(appUpgradeAliases, ","),
|
||||
Aliases: []string{i18n.G("up")},
|
||||
// translators: Short description for `app upgrade` command
|
||||
Short: i18n.G("Upgrade an app"),
|
||||
Long: i18n.G(`Upgrade an app.
|
||||
@ -216,6 +212,24 @@ beforehand. See "abra app backup" for more.`),
|
||||
}
|
||||
}
|
||||
|
||||
// Gather secrets
|
||||
secretInfo, err := deploy.GatherSecretsForDeploy(cl, app)
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
|
||||
// Gather configs
|
||||
configInfo, err := deploy.GatherConfigsForDeploy(cl, app, compose, abraShEnv)
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
|
||||
// Gather images
|
||||
imageInfo, err := deploy.GatherImagesForDeploy(cl, app, compose)
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
|
||||
if showReleaseNotes {
|
||||
fmt.Print(upgradeReleaseNotes)
|
||||
return
|
||||
@ -234,6 +248,9 @@ beforehand. See "abra app backup" for more.`),
|
||||
chosenUpgrade,
|
||||
upgradeReleaseNotes,
|
||||
upgradeWarnMessages,
|
||||
strings.Join(secretInfo, "\n"),
|
||||
strings.Join(configInfo, "\n"),
|
||||
strings.Join(imageInfo, "\n"),
|
||||
); err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
@ -311,7 +328,7 @@ func chooseUpgrade(
|
||||
}
|
||||
|
||||
func getReleaseNotes(
|
||||
app app.App,
|
||||
app appPkg.App,
|
||||
versions []string,
|
||||
chosenUpgrade string,
|
||||
deployMeta stack.DeployMeta,
|
||||
@ -335,7 +352,7 @@ func getReleaseNotes(
|
||||
|
||||
if parsedVersion.IsGreaterThan(parsedDeployedVersion) &&
|
||||
parsedVersion.IsLessThan(parsedChosenUpgrade) {
|
||||
note, err := app.Recipe.GetReleaseNotes(version, app.Domain)
|
||||
note, err := app.Recipe.GetReleaseNotes(version)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
@ -356,7 +373,7 @@ func getReleaseNotes(
|
||||
|
||||
// ensureUpgradesAvailable ensures that there are available upgrades.
|
||||
func ensureUpgradesAvailable(
|
||||
app app.App,
|
||||
app appPkg.App,
|
||||
versions []string,
|
||||
availableUpgrades *[]string,
|
||||
deployMeta stack.DeployMeta,
|
||||
@ -388,7 +405,7 @@ func ensureUpgradesAvailable(
|
||||
// validateUpgradeVersionArg validates the specific version.
|
||||
func validateUpgradeVersionArg(
|
||||
specificVersion string,
|
||||
app app.App,
|
||||
app appPkg.App,
|
||||
deployMeta stack.DeployMeta,
|
||||
) error {
|
||||
parsedSpecificVersion, err := tagcmp.Parse(specificVersion)
|
||||
@ -415,7 +432,7 @@ func validateUpgradeVersionArg(
|
||||
|
||||
// ensureDeployed ensures the app is deployed and if so, returns deployment
|
||||
// meta info.
|
||||
func ensureDeployed(cl *dockerClient.Client, app app.App) (stack.DeployMeta, error) {
|
||||
func ensureDeployed(cl *dockerClient.Client, app appPkg.App) (stack.DeployMeta, error) {
|
||||
log.Debug(i18n.G("checking whether %s is already deployed", app.StackName()))
|
||||
|
||||
deployMeta, err := stack.IsDeployed(context.Background(), cl, app.StackName())
|
||||
|
@ -3,7 +3,6 @@ package app
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"strings"
|
||||
|
||||
"coopcloud.tech/abra/cli/internal"
|
||||
"coopcloud.tech/abra/pkg/autocomplete"
|
||||
@ -16,14 +15,10 @@ 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: strings.Split(appVolumeListAliases, ","),
|
||||
Aliases: []string{i18n.G("ls")},
|
||||
// translators: Short description for `app list` command
|
||||
Short: i18n.G("List volumes associated with an app"),
|
||||
Args: cobra.ExactArgs(1),
|
||||
@ -79,10 +74,6 @@ 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]"),
|
||||
@ -103,7 +94,7 @@ Passing "--force/-f" will select all volumes for removal. Be careful.`),
|
||||
|
||||
# delete specific volume
|
||||
abra app volume rm 1312.net my_volume`),
|
||||
Aliases: strings.Split(appVolumeRemoveAliases, ","),
|
||||
Aliases: []string{i18n.G("rm")},
|
||||
Args: cobra.MinimumNArgs(1),
|
||||
ValidArgsFunction: func(
|
||||
cmd *cobra.Command,
|
||||
@ -199,14 +190,10 @@ 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: strings.Split(appVolumeAliases, ","),
|
||||
Aliases: []string{i18n.G("vl")},
|
||||
Short: i18n.G("Manage app volumes"),
|
||||
}
|
||||
|
||||
|
@ -7,7 +7,6 @@ import (
|
||||
"os"
|
||||
"path"
|
||||
"slices"
|
||||
"strings"
|
||||
|
||||
"coopcloud.tech/abra/cli/internal"
|
||||
"coopcloud.tech/abra/pkg/autocomplete"
|
||||
@ -22,14 +21,10 @@ 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: strings.Split(appCatalogueSyncAliases, ","),
|
||||
Aliases: []string{i18n.G("g")},
|
||||
// translators: Short description for `catalogue sync` command
|
||||
Short: i18n.G("Sync recipe catalogue for latest changes"),
|
||||
Args: cobra.NoArgs,
|
||||
@ -46,14 +41,10 @@ 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: strings.Split(appCatalogueAliases, ","),
|
||||
Aliases: []string{i18n.G("g")},
|
||||
// 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.
|
||||
|
@ -2,20 +2,14 @@ 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]"),
|
||||
Aliases: strings.Split(autocompleteAliases, ","),
|
||||
Use: i18n.G("autocomplete [bash|zsh|fish|powershell]"),
|
||||
// translators: Short description for `autocomplete` command
|
||||
Short: i18n.G("Generate autocompletion script"),
|
||||
Long: i18n.G(`To load completions:
|
||||
|
@ -50,6 +50,9 @@ func DeployOverview(
|
||||
toDeployVersion string,
|
||||
releaseNotes string,
|
||||
warnMessages []string,
|
||||
secrets string,
|
||||
configs string,
|
||||
images string,
|
||||
) error {
|
||||
deployConfig := "compose.yml"
|
||||
if composeFiles, ok := app.Env["COMPOSE_FILE"]; ok {
|
||||
@ -80,6 +83,24 @@ func DeployOverview(
|
||||
{i18n.G("CURRENT DEPLOYMENT"), formatter.BoldDirtyDefault(deployedVersion)},
|
||||
{i18n.G("ENV VERSION"), formatter.BoldDirtyDefault(envVersion)},
|
||||
{i18n.G("NEW DEPLOYMENT"), formatter.BoldDirtyDefault(toDeployVersion)},
|
||||
{"", ""},
|
||||
{i18n.G("IMAGES"), images},
|
||||
}
|
||||
|
||||
if len(secrets) > 0 {
|
||||
secretsRows := [][]string{
|
||||
{"", ""},
|
||||
{i18n.G("SECRETS"), secrets},
|
||||
}
|
||||
rows = append(rows, secretsRows...)
|
||||
}
|
||||
|
||||
if len(configs) > 0 {
|
||||
configsRows := [][]string{
|
||||
{"", ""},
|
||||
{i18n.G("CONFIGS"), configs},
|
||||
}
|
||||
rows = append(rows, configsRows...)
|
||||
}
|
||||
|
||||
deployType := getDeployType(deployedVersion, toDeployVersion)
|
||||
@ -142,69 +163,6 @@ 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>|... "
|
||||
|
@ -1,8 +1,6 @@
|
||||
package recipe
|
||||
|
||||
import (
|
||||
"strings"
|
||||
|
||||
"coopcloud.tech/abra/cli/internal"
|
||||
"coopcloud.tech/abra/pkg/autocomplete"
|
||||
gitPkg "coopcloud.tech/abra/pkg/git"
|
||||
@ -11,14 +9,10 @@ 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: strings.Split(recipeDiffAliases, ","),
|
||||
Aliases: []string{i18n.G("d")},
|
||||
// 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."),
|
||||
|
@ -2,7 +2,6 @@ package recipe
|
||||
|
||||
import (
|
||||
"os"
|
||||
"strings"
|
||||
|
||||
"coopcloud.tech/abra/cli/internal"
|
||||
"coopcloud.tech/abra/pkg/autocomplete"
|
||||
@ -15,14 +14,10 @@ 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: strings.Split(recipeFetchAliases, ","),
|
||||
Aliases: []string{i18n.G("f")},
|
||||
// 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.`),
|
||||
|
@ -1,8 +1,6 @@
|
||||
package recipe
|
||||
|
||||
import (
|
||||
"strings"
|
||||
|
||||
"coopcloud.tech/abra/cli/internal"
|
||||
"coopcloud.tech/abra/pkg/autocomplete"
|
||||
"coopcloud.tech/abra/pkg/formatter"
|
||||
@ -12,16 +10,12 @@ 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: strings.Split(recipeLintAliases, ","),
|
||||
Aliases: []string{i18n.G("l")},
|
||||
Args: cobra.MinimumNArgs(1),
|
||||
ValidArgsFunction: func(
|
||||
cmd *cobra.Command,
|
||||
|
@ -14,16 +14,12 @@ 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: strings.Split(recipeListAliases, ","),
|
||||
Aliases: []string{i18n.G("ls")},
|
||||
Args: cobra.NoArgs,
|
||||
Run: func(cmd *cobra.Command, args []string) {
|
||||
catl, err := recipe.ReadRecipeCatalogue(internal.Offline)
|
||||
|
@ -5,7 +5,6 @@ import (
|
||||
"fmt"
|
||||
"os"
|
||||
"path"
|
||||
"strings"
|
||||
"text/template"
|
||||
|
||||
"coopcloud.tech/abra/pkg/autocomplete"
|
||||
@ -31,18 +30,13 @@ 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: 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),
|
||||
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),
|
||||
ValidArgsFunction: func(
|
||||
cmd *cobra.Command,
|
||||
args []string,
|
||||
|
@ -1,21 +1,15 @@
|
||||
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: strings.Split(recipeAliases, ","),
|
||||
Aliases: []string{i18n.G("r")},
|
||||
// translators: Short description for `recipe` command group
|
||||
Short: i18n.G("Manage recipes"),
|
||||
Long: i18n.G(`A recipe is a blueprint for an app.
|
||||
|
@ -23,14 +23,10 @@ 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: strings.Split(recipeReleaseAliases, ","),
|
||||
Aliases: []string{i18n.G("rl")},
|
||||
// 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.
|
||||
|
@ -1,8 +1,6 @@
|
||||
package recipe
|
||||
|
||||
import (
|
||||
"strings"
|
||||
|
||||
"coopcloud.tech/abra/cli/internal"
|
||||
"coopcloud.tech/abra/pkg/autocomplete"
|
||||
"coopcloud.tech/abra/pkg/i18n"
|
||||
@ -11,14 +9,10 @@ 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: strings.Split(recipeResetAliases, ","),
|
||||
Aliases: []string{i18n.G("rs")},
|
||||
// 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."),
|
||||
|
@ -3,7 +3,6 @@ package recipe
|
||||
import (
|
||||
"fmt"
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
"coopcloud.tech/abra/cli/internal"
|
||||
"coopcloud.tech/abra/pkg/autocomplete"
|
||||
@ -19,14 +18,10 @@ 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: strings.Split(recipeSyncAliases, ","),
|
||||
Aliases: []string{i18n.G("s")},
|
||||
// translators: Short description for `recipe sync` command
|
||||
Short: i18n.G("Sync recipe version label"),
|
||||
Long: i18n.G(`Generate labels for the main recipe service.
|
||||
|
@ -37,14 +37,10 @@ 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: strings.Split(recipeUpgradeAliases, ","),
|
||||
Aliases: []string{i18n.G("u")},
|
||||
// translators: Short description for `recipe upgrade` command
|
||||
Short: i18n.G("Upgrade recipe image tags"),
|
||||
Long: i18n.G(`Upgrade a given <recipe> configuration.
|
||||
|
@ -3,7 +3,6 @@ package recipe
|
||||
import (
|
||||
"fmt"
|
||||
"sort"
|
||||
"strings"
|
||||
|
||||
"coopcloud.tech/abra/cli/internal"
|
||||
"coopcloud.tech/abra/pkg/autocomplete"
|
||||
@ -14,14 +13,10 @@ 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: strings.Split(recipeVersionsAliases, ","),
|
||||
Aliases: []string{i18n.G("v")},
|
||||
// translators: Short description for `recipe versions` command
|
||||
Short: i18n.G("List recipe versions"),
|
||||
Args: cobra.ExactArgs(1),
|
||||
|
55
cli/run.go
55
cli/run.go
@ -4,7 +4,6 @@ import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"os"
|
||||
"strings"
|
||||
|
||||
"coopcloud.tech/abra/cli/app"
|
||||
"coopcloud.tech/abra/cli/catalogue"
|
||||
@ -20,65 +19,23 @@ 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 🎩🐇"),
|
||||
// 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),
|
||||
Short: i18n.G("The Co-op Cloud command-line utility belt 🎩🐇"),
|
||||
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},
|
||||
@ -121,16 +78,11 @@ Config:
|
||||
}
|
||||
|
||||
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: strings.Split(manAliases, ","),
|
||||
Aliases: []string{"m"},
|
||||
// translators: Short description for `man` command
|
||||
Short: i18n.G("Generate manpage"),
|
||||
Example: i18n.G(` # generate the man pages into /usr/local/share/man/man1
|
||||
@ -258,7 +210,6 @@ Config:
|
||||
app.AppRestartCommand,
|
||||
app.AppRestoreCommand,
|
||||
app.AppRollbackCommand,
|
||||
app.AppMoveCommand,
|
||||
app.AppRunCommand,
|
||||
app.AppSecretCommand,
|
||||
app.AppServicesCommand,
|
||||
|
@ -3,7 +3,6 @@ package server
|
||||
import (
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
|
||||
"coopcloud.tech/abra/cli/internal"
|
||||
"coopcloud.tech/abra/pkg/autocomplete"
|
||||
@ -18,14 +17,10 @@ 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: strings.Split(serverAddAliases, ","),
|
||||
Aliases: []string{i18n.G("a")},
|
||||
// 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.
|
||||
|
@ -14,14 +14,10 @@ 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: strings.Split(serverListAliases, ","),
|
||||
Aliases: []string{i18n.G("ls")},
|
||||
// translators: Short description for `server list` command
|
||||
Short: i18n.G("List managed servers"),
|
||||
Args: cobra.NoArgs,
|
||||
|
@ -1,8 +1,6 @@
|
||||
package server
|
||||
|
||||
import (
|
||||
"strings"
|
||||
|
||||
"coopcloud.tech/abra/cli/internal"
|
||||
"coopcloud.tech/abra/pkg/autocomplete"
|
||||
"coopcloud.tech/abra/pkg/client"
|
||||
@ -13,14 +11,10 @@ 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: strings.Split(serverPruneliases, ","),
|
||||
Aliases: []string{i18n.G("p")},
|
||||
// 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.
|
||||
|
@ -3,7 +3,6 @@ package server
|
||||
import (
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
|
||||
"coopcloud.tech/abra/cli/internal"
|
||||
"coopcloud.tech/abra/pkg/autocomplete"
|
||||
@ -14,14 +13,10 @@ 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: strings.Split(serverRemoveAliases, ","),
|
||||
Aliases: []string{i18n.G("rm")},
|
||||
// translators: Short description for `server remove` command
|
||||
Short: i18n.G("Remove a managed server"),
|
||||
Long: i18n.G(`Remove a managed server.
|
||||
|
@ -1,21 +1,15 @@
|
||||
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: strings.Split(serverAliases, ","),
|
||||
Aliases: []string{i18n.G("s")},
|
||||
// translators: Short description for `server` command group
|
||||
Short: i18n.G("Manage servers"),
|
||||
}
|
||||
|
@ -30,15 +30,11 @@ 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: strings.Split(notifyAliases, ","),
|
||||
Aliases: []string{i18n.G("n")},
|
||||
// translators: Short description for `notify` command
|
||||
Short: i18n.G("Check for available upgrades"),
|
||||
Long: i18n.G(`Notify on new versions for deployed apps.
|
||||
@ -75,15 +71,11 @@ 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: strings.Split(upgradeAliases, ","),
|
||||
Aliases: []string{i18n.G("u")},
|
||||
// translators: Short description for `app upgrade` command
|
||||
Short: i18n.G("Upgrade apps"),
|
||||
Long: i18n.G(`Upgrade an app by specifying stack name and recipe.
|
||||
|
@ -4,7 +4,6 @@ package cli
|
||||
import (
|
||||
"fmt"
|
||||
"os/exec"
|
||||
"strings"
|
||||
|
||||
"coopcloud.tech/abra/cli/internal"
|
||||
"coopcloud.tech/abra/pkg/i18n"
|
||||
@ -12,15 +11,11 @@ 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: strings.Split(upgradeAliases, ","),
|
||||
Aliases: []string{"u"},
|
||||
// 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.
|
||||
|
@ -390,12 +390,7 @@ func TemplateAppEnvSample(r recipe.Recipe, appName, server, domain string) error
|
||||
return err
|
||||
}
|
||||
|
||||
newContents := strings.Replace(
|
||||
string(read),
|
||||
fmt.Sprintf("%s.example.com", r.Name),
|
||||
domain,
|
||||
-1,
|
||||
)
|
||||
newContents := strings.Replace(string(read), r.Name+".example.com", domain, -1)
|
||||
|
||||
err = os.WriteFile(appEnvPath, []byte(newContents), 0)
|
||||
if err != nil {
|
||||
|
@ -6,7 +6,6 @@ import (
|
||||
"errors"
|
||||
"net/http"
|
||||
"os"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
contextPkg "coopcloud.tech/abra/pkg/context"
|
||||
@ -39,24 +38,18 @@ func WithTimeout(timeout int) Opt {
|
||||
func New(serverName string, opts ...Opt) (*client.Client, error) {
|
||||
var clientOpts []client.Opt
|
||||
|
||||
ctx, err := GetContext(serverName)
|
||||
if err != nil {
|
||||
return nil, errors.New(i18n.G("unknown server, run \"abra server add %s\"?", serverName))
|
||||
}
|
||||
if serverName != "default" {
|
||||
context, err := GetContext(serverName)
|
||||
if err != nil {
|
||||
return nil, errors.New(i18n.G("unknown server, run \"abra server add %s\"?", serverName))
|
||||
}
|
||||
|
||||
ctxEndpoint, err := contextPkg.GetContextEndpoint(ctx)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
ctxEndpoint, err := contextPkg.GetContextEndpoint(context)
|
||||
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)
|
||||
}
|
||||
@ -100,7 +93,7 @@ func New(serverName string, opts ...Opt) (*client.Client, error) {
|
||||
}
|
||||
|
||||
if info.Swarm.LocalNodeState == "inactive" {
|
||||
if serverName != "default" && !isUnix {
|
||||
if serverName != "default" {
|
||||
return cl, errors.New(i18n.G("swarm mode not enabled on %s?", serverName))
|
||||
}
|
||||
|
||||
|
@ -3,6 +3,7 @@ package client
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"strings"
|
||||
|
||||
"coopcloud.tech/abra/pkg/i18n"
|
||||
"github.com/docker/docker/api/types/filters"
|
||||
@ -37,3 +38,12 @@ func RemoveConfigs(cl *client.Client, ctx context.Context, configNames []string,
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func GetConfigNameAndVersion(fullName string, stackName string) (string, string, error) {
|
||||
name := strings.TrimPrefix(fullName, stackName+"_")
|
||||
if lastUnderscore := strings.LastIndex(name, "_"); lastUnderscore != -1 {
|
||||
return name[0:lastUnderscore], name[lastUnderscore+1:], nil
|
||||
} else {
|
||||
return "", "", errors.New(i18n.G("can't parse version from config '%s'", fullName))
|
||||
}
|
||||
}
|
||||
|
@ -7,7 +7,7 @@ import (
|
||||
"github.com/docker/docker/client"
|
||||
)
|
||||
|
||||
func StoreSecret(cl *client.Client, secretName, secretValue string) error {
|
||||
func StoreSecret(cl *client.Client, secretName, secretValue, server string) error {
|
||||
ann := swarm.Annotations{Name: secretName}
|
||||
spec := swarm.SecretSpec{Annotations: ann, Data: []byte(secretValue)}
|
||||
|
||||
@ -17,3 +17,11 @@ func StoreSecret(cl *client.Client, secretName, secretValue string) error {
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func GetSecretNames(secrets []swarm.Secret) []string {
|
||||
var secretNames []string
|
||||
for _, secret := range secrets {
|
||||
secretNames = append(secretNames, secret.Spec.Name)
|
||||
}
|
||||
return secretNames
|
||||
}
|
||||
|
@ -118,8 +118,6 @@ var (
|
||||
|
||||
NO_DOMAIN_DEFAULT = "N/A"
|
||||
NO_VERSION_DEFAULT = "N/A"
|
||||
NO_SECRETS_DEFAULT = "N/A"
|
||||
NO_VOLUMES_DEFAULT = "N/A"
|
||||
|
||||
UNKNOWN_DEFAULT = "unknown"
|
||||
)
|
||||
|
226
pkg/deploy/utils.go
Normal file
226
pkg/deploy/utils.go
Normal file
@ -0,0 +1,226 @@
|
||||
package deploy
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
"regexp"
|
||||
"sort"
|
||||
"strings"
|
||||
|
||||
appPkg "coopcloud.tech/abra/pkg/app"
|
||||
"coopcloud.tech/abra/pkg/client"
|
||||
"coopcloud.tech/abra/pkg/log"
|
||||
"coopcloud.tech/abra/pkg/secret"
|
||||
|
||||
composetypes "github.com/docker/cli/cli/compose/types"
|
||||
"github.com/docker/docker/api/types/swarm"
|
||||
dockerClient "github.com/docker/docker/client"
|
||||
)
|
||||
|
||||
// GetConfigsForStack retrieves all Docker configs attached to services in a given stack.
|
||||
func GetConfigsForStack(cl *dockerClient.Client, app appPkg.App) (map[string]string, error) {
|
||||
filters, err := app.Filters(false, false)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// List all services in the stack
|
||||
services, err := cl.ServiceList(context.Background(), swarm.ServiceListOptions{
|
||||
Filters: filters,
|
||||
})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// Collect unique config names with versions
|
||||
configs := make(map[string]string)
|
||||
for _, service := range services {
|
||||
if service.Spec.TaskTemplate.ContainerSpec != nil {
|
||||
for _, configRef := range service.Spec.TaskTemplate.ContainerSpec.Configs {
|
||||
configName := configRef.ConfigName
|
||||
if configName == "" {
|
||||
continue
|
||||
}
|
||||
configBaseName, configVersion, err := client.GetConfigNameAndVersion(configName, app.StackName())
|
||||
if err != nil {
|
||||
log.Warn(err)
|
||||
continue
|
||||
}
|
||||
|
||||
existingConfigVersion, ok := configs[configBaseName]
|
||||
if !ok {
|
||||
// First time seeing this, add to map
|
||||
configs[configBaseName] = configVersion
|
||||
} else {
|
||||
// Just make sure the versions are the same..
|
||||
if existingConfigVersion != configVersion {
|
||||
log.Warnf("different versions for config '%s', '%s' and %s'", configBaseName, existingConfigVersion, configVersion)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return configs, nil
|
||||
}
|
||||
|
||||
func GetImageNameAndTag(imageName string) (string, string, error) {
|
||||
imageParts := regexp.MustCompile("^([^:]*):([^@]*)@?").FindSubmatch([]byte(imageName))
|
||||
|
||||
if len(imageParts) == 0 {
|
||||
return "", "", errors.New("can't determine image version for image '%s'")
|
||||
}
|
||||
|
||||
imageBaseName := string(imageParts[1])
|
||||
imageTag := string(imageParts[2])
|
||||
|
||||
return imageBaseName, imageTag, nil
|
||||
}
|
||||
|
||||
// GetImagesForStack retrieves all Docker images for services in a given stack.
|
||||
func GetImagesForStack(cl *dockerClient.Client, app appPkg.App) (map[string]string, error) {
|
||||
filters, err := app.Filters(false, false)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// List all services in the stack
|
||||
services, err := cl.ServiceList(context.Background(), swarm.ServiceListOptions{
|
||||
Filters: filters,
|
||||
})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// Collect unique image names with versions
|
||||
images := make(map[string]string)
|
||||
for _, service := range services {
|
||||
if service.Spec.TaskTemplate.ContainerSpec != nil {
|
||||
imageName := service.Spec.TaskTemplate.ContainerSpec.Image
|
||||
|
||||
imageBaseName, imageTag, err := GetImageNameAndTag(imageName)
|
||||
if err != nil {
|
||||
log.Warn(err)
|
||||
continue
|
||||
}
|
||||
|
||||
existingImageVersion, ok := images[imageBaseName]
|
||||
if !ok {
|
||||
// First time seeing this, add to map
|
||||
images[imageBaseName] = imageTag
|
||||
} else {
|
||||
// Just make sure the versions are the same..
|
||||
if existingImageVersion != imageTag {
|
||||
log.Warnf("different versions for image '%s', '%s' and %s'", imageBaseName, existingImageVersion, imageTag)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return images, nil
|
||||
}
|
||||
|
||||
func GatherSecretsForDeploy(cl *dockerClient.Client, app appPkg.App) ([]string, error) {
|
||||
|
||||
secStats, err := secret.PollSecretsStatus(cl, app)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
var secretInfo []string
|
||||
|
||||
// Sort secrets to ensure reproducible output
|
||||
sort.Slice(secStats, func(i, j int) bool {
|
||||
return secStats[i].LocalName < secStats[j].LocalName
|
||||
})
|
||||
for _, secStat := range secStats {
|
||||
secretInfo = append(secretInfo, fmt.Sprintf("%s: %s", secStat.LocalName, secStat.Version))
|
||||
}
|
||||
return secretInfo, nil
|
||||
}
|
||||
|
||||
func GatherConfigsForDeploy(cl *dockerClient.Client, app appPkg.App, compose *composetypes.Config, abraShEnv map[string]string) ([]string, error) {
|
||||
// Get current configs from existing deployment
|
||||
currentConfigs, err := GetConfigsForStack(cl, app)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
log.Debugf("Deployed config names: %v", currentConfigs)
|
||||
|
||||
// Get new configs from the compose specification
|
||||
newConfigs := compose.Configs
|
||||
|
||||
var configInfo []string
|
||||
for configName := range newConfigs {
|
||||
log.Debugf("Searching abra.sh for version for %s", configName)
|
||||
versionKey := strings.ToUpper(configName) + "_VERSION"
|
||||
newVersion, exists := abraShEnv[versionKey]
|
||||
if !exists {
|
||||
log.Warnf("No version found for config %s", configName)
|
||||
configInfo = append(configInfo, fmt.Sprintf("%s: ? (missing version)", configName))
|
||||
continue
|
||||
}
|
||||
|
||||
if currentVersion, exists := currentConfigs[configName]; exists {
|
||||
if currentVersion == newVersion {
|
||||
configInfo = append(configInfo, fmt.Sprintf("%s: %s (unchanged)", configName, newVersion))
|
||||
} else {
|
||||
configInfo = append(configInfo, fmt.Sprintf("%s: %s → %s", configName, currentVersion, newVersion))
|
||||
}
|
||||
} else {
|
||||
configInfo = append(configInfo, fmt.Sprintf("%s: %s (new)", configName, newVersion))
|
||||
}
|
||||
}
|
||||
|
||||
return configInfo, nil
|
||||
}
|
||||
|
||||
func GatherImagesForDeploy(cl *dockerClient.Client, app appPkg.App, compose *composetypes.Config) ([]string, error) {
|
||||
|
||||
// Get current images from existing deployment
|
||||
currentImages, err := GetImagesForStack(cl, app)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
log.Infof("Deployed images: %v", currentImages)
|
||||
|
||||
// Proposed new images from the compose files
|
||||
newImages := make(map[string]string)
|
||||
|
||||
for _, service := range compose.Services {
|
||||
imageBaseName, imageTag, err := GetImageNameAndTag(service.Image)
|
||||
if err != nil {
|
||||
log.Warn(err)
|
||||
continue
|
||||
}
|
||||
existingImageVersion, ok := newImages[imageBaseName]
|
||||
if !ok {
|
||||
// First time seeing this, add to map
|
||||
newImages[imageBaseName] = imageTag
|
||||
} else {
|
||||
// Just make sure the versions are the same..
|
||||
if existingImageVersion != imageTag {
|
||||
log.Warnf("different versions for image '%s', '%s' and %s'", imageBaseName, existingImageVersion, imageTag)
|
||||
}
|
||||
}
|
||||
}
|
||||
log.Infof("Proposed images: %v", newImages)
|
||||
|
||||
var imageInfo []string
|
||||
for newImageName, newImageVersion := range newImages {
|
||||
if currentVersion, exists := currentImages[newImageName]; exists {
|
||||
if currentVersion == newImageVersion {
|
||||
imageInfo = append(imageInfo, fmt.Sprintf("%s: %s (unchanged)", newImageName, newImageVersion))
|
||||
} else {
|
||||
imageInfo = append(imageInfo, fmt.Sprintf("%s: %s → %s", newImageName, currentVersion, newImageVersion))
|
||||
}
|
||||
} else {
|
||||
imageInfo = append(imageInfo, fmt.Sprintf("%s: %s (new)", newImageName, newImageVersion))
|
||||
}
|
||||
}
|
||||
|
||||
return imageInfo, nil
|
||||
}
|
File diff suppressed because it is too large
Load Diff
Binary file not shown.
File diff suppressed because it is too large
Load Diff
@ -5,7 +5,6 @@ import (
|
||||
"fmt"
|
||||
"os"
|
||||
"path"
|
||||
"strings"
|
||||
|
||||
"coopcloud.tech/abra/pkg/envfile"
|
||||
"coopcloud.tech/abra/pkg/formatter"
|
||||
@ -21,7 +20,7 @@ func (r Recipe) SampleEnv() (map[string]string, error) {
|
||||
}
|
||||
|
||||
// GetReleaseNotes prints release notes for the recipe version
|
||||
func (r Recipe) GetReleaseNotes(version, appDomain string) (string, error) {
|
||||
func (r Recipe) GetReleaseNotes(version string) (string, error) {
|
||||
if version == "" {
|
||||
return "", nil
|
||||
}
|
||||
@ -37,14 +36,7 @@ func (r Recipe) GetReleaseNotes(version, appDomain string) (string, error) {
|
||||
title := formatter.BoldStyle.Render(i18n.G("%s release notes:", version))
|
||||
withTitle := fmt.Sprintf("%s\n%s\n", title, releaseNotes)
|
||||
|
||||
templatedDomain := strings.Replace(
|
||||
withTitle,
|
||||
fmt.Sprintf("%s.example.com", r.Name),
|
||||
appDomain,
|
||||
-1,
|
||||
)
|
||||
|
||||
return templatedDomain, nil
|
||||
return withTitle, nil
|
||||
}
|
||||
|
||||
return "", nil
|
||||
|
@ -218,7 +218,7 @@ func GenerateSecrets(cl *dockerClient.Client, secrets map[string]Secret, server
|
||||
return
|
||||
}
|
||||
|
||||
if err := client.StoreSecret(cl, secret.RemoteName, password); err != nil {
|
||||
if err := client.StoreSecret(cl, secret.RemoteName, password, server); 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); err != nil {
|
||||
if err := client.StoreSecret(cl, secret.RemoteName, passphrase, server); err != nil {
|
||||
if strings.Contains(err.Error(), "AlreadyExists") {
|
||||
log.Warnf(i18n.G("%s already exists", secret.RemoteName))
|
||||
ch <- nil
|
||||
@ -280,7 +280,7 @@ type secretStatus struct {
|
||||
type secretStatuses []secretStatus
|
||||
|
||||
// PollSecretsStatus checks status of secrets by comparing the local recipe
|
||||
// config and deploymend server state.
|
||||
// config and deployed server state.
|
||||
func PollSecretsStatus(cl *dockerClient.Client, app appPkg.App) (secretStatuses, error) {
|
||||
var secStats secretStatuses
|
||||
|
||||
@ -306,7 +306,7 @@ func PollSecretsStatus(cl *dockerClient.Client, app appPkg.App) (secretStatuses,
|
||||
|
||||
remoteSecretNames := make(map[string]bool)
|
||||
for _, cont := range secretList {
|
||||
remoteSecretNames[cont.Spec.Annotations.Name] = true
|
||||
remoteSecretNames[cont.Spec.Name] = true
|
||||
}
|
||||
|
||||
for secretName, val := range secretsConfig {
|
||||
|
@ -1,42 +0,0 @@
|
||||
#!/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'
|
||||
}
|
@ -257,16 +257,3 @@ 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"
|
||||
}
|
||||
|
@ -205,18 +205,6 @@ 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
|
||||
|
@ -17,7 +17,6 @@ _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
|
||||
|
@ -10,15 +10,6 @@ _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"
|
||||
@ -29,15 +20,6 @@ _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
|
||||
|
Reference in New Issue
Block a user