Compare commits

..

15 Commits

Author SHA1 Message Date
3wc
24970360fa Regen POT
All checks were successful
continuous-integration/drone/push Build is passing
2025-09-02 14:00:40 -04:00
3wc
b3bd253684 4matting
Some checks failed
continuous-integration/drone/push Build is failing
2025-09-02 13:59:45 -04:00
3wc
32c2ea5b53 Roll out pre-deploy changes to rollback and upgrade 2025-09-02 13:58:45 -04:00
3wc
12b01ace71 Show image differences in pre-deploy overview
Some checks failed
continuous-integration/drone/push Build is failing
2025-09-02 13:48:09 -04:00
3wc
58d15c35d8 Move secret- and config-gathering to separate file
Some checks failed
continuous-integration/drone/push Build is failing
2025-09-02 12:15:17 -04:00
3wc
a02be5705e Resolve circular import
Some checks failed
continuous-integration/drone/push Build is failing
2025-09-02 12:00:47 -04:00
3wc
371b70b537 Add GetSecretNamesForStack, tidy up GetConfigNamesForStack
Some checks failed
continuous-integration/drone/push Build is failing
2025-09-02 11:32:37 -04:00
3wc
7558550d96 Warn instead of error on missing config version
Some checks failed
continuous-integration/drone/push Build is failing
2025-09-01 21:03:16 -04:00
3wc
d94335941f Working config version comparison
Some checks failed
continuous-integration/drone/push Build is failing
2025-09-01 16:26:00 -04:00
3wc
88ea705a33 Skip empty sections in deploy overview
Some checks failed
continuous-integration/drone/push Build is failing
2025-09-01 15:33:56 -04:00
3wc
faa8cd12d9 Only show remote configs used in deployment 2025-09-01 15:22:40 -04:00
3wc
65ed2f6113 Add some spacing, might delete 2025-09-01 15:07:26 -04:00
3wc
746485cfb0 Tidy up a little 2025-09-01 14:56:50 -04:00
3wc
2b1bece9b6 WIP: Working secret and config versions during deploy overview
Some checks failed
continuous-integration/drone/push Build is failing
2025-09-01 14:49:49 -04:00
3wc
c05d0fef24 WIP: Initial stab at secrets/configs/images 2025-09-01 13:43:10 -04:00
64 changed files with 1920 additions and 3189 deletions

1
.gitignore vendored
View File

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

View File

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

View File

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

View File

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

View File

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

View 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),

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -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.`),

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

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); 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 {

View File

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

View File

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

View File

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

View File

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

View File

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