Merge remote-tracking branch 'upsteam/main' into insecrt-secret-from-stdin
This commit is contained in:
@ -10,6 +10,7 @@ steps:
|
|||||||
- name: make test
|
- name: make test
|
||||||
image: golang:1.24
|
image: golang:1.24
|
||||||
environment:
|
environment:
|
||||||
|
ABRA_DIR: $HOME/.abra
|
||||||
CATL_URL: https://git.coopcloud.tech/toolshed/recipes-catalogue-json.git
|
CATL_URL: https://git.coopcloud.tech/toolshed/recipes-catalogue-json.git
|
||||||
commands:
|
commands:
|
||||||
- mkdir -p $HOME/.abra
|
- mkdir -p $HOME/.abra
|
||||||
|
@ -200,8 +200,6 @@ checkout as-is. Recipe commit hashes are also supported as values for
|
|||||||
log.Fatal(err)
|
log.Fatal(err)
|
||||||
}
|
}
|
||||||
|
|
||||||
log.Debug(i18n.G("set waiting timeout to %d second(s)", stack.WaitTimeout))
|
|
||||||
|
|
||||||
serviceNames, err := appPkg.GetAppServiceNames(app.Name)
|
serviceNames, err := appPkg.GetAppServiceNames(app.Name)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Fatal(err)
|
log.Fatal(err)
|
||||||
|
@ -199,8 +199,6 @@ beforehand. See "abra app backup" for more.`),
|
|||||||
log.Fatal(err)
|
log.Fatal(err)
|
||||||
}
|
}
|
||||||
|
|
||||||
log.Debug(i18n.G("set waiting timeout to %d second(s)", stack.WaitTimeout))
|
|
||||||
|
|
||||||
serviceNames, err := appPkg.GetAppServiceNames(app.Name)
|
serviceNames, err := appPkg.GetAppServiceNames(app.Name)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Fatal(err)
|
log.Fatal(err)
|
||||||
|
@ -2,6 +2,7 @@ package app
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
"io"
|
"io"
|
||||||
"os"
|
"os"
|
||||||
@ -17,6 +18,7 @@ import (
|
|||||||
"coopcloud.tech/abra/pkg/i18n"
|
"coopcloud.tech/abra/pkg/i18n"
|
||||||
"coopcloud.tech/abra/pkg/log"
|
"coopcloud.tech/abra/pkg/log"
|
||||||
"coopcloud.tech/abra/pkg/secret"
|
"coopcloud.tech/abra/pkg/secret"
|
||||||
|
"github.com/AlecAivazis/survey/v2"
|
||||||
"github.com/docker/docker/api/types"
|
"github.com/docker/docker/api/types"
|
||||||
dockerClient "github.com/docker/docker/client"
|
dockerClient "github.com/docker/docker/client"
|
||||||
"github.com/spf13/cobra"
|
"github.com/spf13/cobra"
|
||||||
@ -143,7 +145,7 @@ var AppSecretGenerateCommand = &cobra.Command{
|
|||||||
}
|
}
|
||||||
|
|
||||||
var AppSecretInsertCommand = &cobra.Command{
|
var AppSecretInsertCommand = &cobra.Command{
|
||||||
Use: i18n.G("insert <domain> <secret> <version> <data> [flags]"),
|
Use: i18n.G("insert <domain> <secret> <version> [<data>] [flags]"),
|
||||||
Aliases: []string{i18n.G("i")},
|
Aliases: []string{i18n.G("i")},
|
||||||
Short: i18n.G("Insert secret"),
|
Short: i18n.G("Insert secret"),
|
||||||
Long: i18n.G(`This command inserts a secret into an app environment.
|
Long: i18n.G(`This command inserts a secret into an app environment.
|
||||||
@ -195,29 +197,9 @@ environment. Typically, you can let Abra generate them for you on app creation
|
|||||||
|
|
||||||
name := args[1]
|
name := args[1]
|
||||||
version := args[2]
|
version := args[2]
|
||||||
var data string
|
data, err := readSecretData(args)
|
||||||
if len(args) == 4 {
|
if err != nil {
|
||||||
data = args[3]
|
log.Fatal(err)
|
||||||
} else if len(args) == 3 {
|
|
||||||
if insertFromFile {
|
|
||||||
log.Fatal("can not insert from file and read from stdin")
|
|
||||||
}
|
|
||||||
|
|
||||||
fi, err := os.Stdin.Stat()
|
|
||||||
if err != nil {
|
|
||||||
log.Fatal(err)
|
|
||||||
}
|
|
||||||
if fi.Mode()&os.ModeNamedPipe == 0 {
|
|
||||||
log.Fatal("need to provide secret data or stdin stream")
|
|
||||||
}
|
|
||||||
|
|
||||||
log.Debug("reading secret data from stdin")
|
|
||||||
bytes, err := io.ReadAll(os.Stdin)
|
|
||||||
if err != nil {
|
|
||||||
log.Fatalf("reading data from stdin: %s", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
data = string(bytes)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
composeFiles, err := app.Recipe.GetComposeFiles(app.Env)
|
composeFiles, err := app.Recipe.GetComposeFiles(app.Env)
|
||||||
@ -240,7 +222,7 @@ environment. Typically, you can let Abra generate them for you on app creation
|
|||||||
log.Fatal(i18n.G("no secret %s available for recipe %s?", name, app.Recipe.Name))
|
log.Fatal(i18n.G("no secret %s available for recipe %s?", name, app.Recipe.Name))
|
||||||
}
|
}
|
||||||
|
|
||||||
if insertFromFile && len(args) == 4 {
|
if insertFromFile {
|
||||||
raw, err := os.ReadFile(data)
|
raw, err := os.ReadFile(data)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Fatal(i18n.G("reading secret from file: %s", err))
|
log.Fatal(i18n.G("reading secret from file: %s", err))
|
||||||
@ -253,7 +235,7 @@ environment. Typically, you can let Abra generate them for you on app creation
|
|||||||
}
|
}
|
||||||
|
|
||||||
secretName := fmt.Sprintf("%s_%s_%s", app.StackName(), name, version)
|
secretName := fmt.Sprintf("%s_%s_%s", app.StackName(), name, version)
|
||||||
if err := client.StoreSecret(cl, secretName, data, app.Server); err != nil {
|
if err := client.StoreSecret(cl, secretName, version, app.Server); err != nil {
|
||||||
log.Fatal(err)
|
log.Fatal(err)
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -267,6 +249,55 @@ environment. Typically, you can let Abra generate them for you on app creation
|
|||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func readSecretData(args []string) (string, error) {
|
||||||
|
if len(args) == 4 {
|
||||||
|
return args[3], nil
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(args) != 3 {
|
||||||
|
return "", errors.New(i18n.G("need 3 or 4 arguments"))
|
||||||
|
}
|
||||||
|
// First check if data is provided by stdin
|
||||||
|
fi, err := os.Stdin.Stat()
|
||||||
|
if err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
if fi.Mode()&os.ModeNamedPipe != 0 {
|
||||||
|
// Can't insert from stdin and read from file
|
||||||
|
if insertFromFile {
|
||||||
|
return "", errors.New(i18n.G("can not insert from file and read from stdin"))
|
||||||
|
}
|
||||||
|
|
||||||
|
log.Debug(i18n.G("reading secret data from stdin"))
|
||||||
|
bytes, err := io.ReadAll(os.Stdin)
|
||||||
|
if err != nil {
|
||||||
|
return "", errors.New(i18n.G("reading data from stdin: %s", err))
|
||||||
|
}
|
||||||
|
|
||||||
|
return string(bytes), nil
|
||||||
|
}
|
||||||
|
if internal.NoInput {
|
||||||
|
return "", errors.New(i18n.G("must provide <data> argument if --no-input is passed"))
|
||||||
|
}
|
||||||
|
|
||||||
|
log.Debug(i18n.G("secret data not provided on command-line or stdin, prompting"))
|
||||||
|
var prompt survey.Prompt
|
||||||
|
if !insertFromFile {
|
||||||
|
prompt = &survey.Password{
|
||||||
|
Message: i18n.G("specify secret value"),
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
prompt = &survey.Input{
|
||||||
|
Message: i18n.G("specify secret file"),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
var data string
|
||||||
|
if err := survey.AskOne(prompt, &data); err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
return data, nil
|
||||||
|
}
|
||||||
|
|
||||||
// secretRm removes a secret.
|
// secretRm removes a secret.
|
||||||
func secretRm(cl *dockerClient.Client, app appPkg.App, secretName, parsed string) error {
|
func secretRm(cl *dockerClient.Client, app appPkg.App, secretName, parsed string) error {
|
||||||
if err := cl.SecretRemove(context.Background(), secretName); err != nil {
|
if err := cl.SecretRemove(context.Background(), secretName); err != nil {
|
||||||
|
@ -85,8 +85,6 @@ Passing "--prune/-p" does not remove those volumes.`),
|
|||||||
log.Fatal(err)
|
log.Fatal(err)
|
||||||
}
|
}
|
||||||
|
|
||||||
log.Info(i18n.G("initialising undeploy"))
|
|
||||||
|
|
||||||
rmOpts := stack.Remove{
|
rmOpts := stack.Remove{
|
||||||
Namespaces: []string{stackName},
|
Namespaces: []string{stackName},
|
||||||
Detach: false,
|
Detach: false,
|
||||||
|
@ -237,8 +237,6 @@ beforehand. See "abra app backup" for more.`),
|
|||||||
log.Fatal(err)
|
log.Fatal(err)
|
||||||
}
|
}
|
||||||
|
|
||||||
log.Debug(i18n.G("set waiting timeout to %d second(s)", stack.WaitTimeout))
|
|
||||||
|
|
||||||
serviceNames, err := appPkg.GetAppServiceNames(app.Name)
|
serviceNames, err := appPkg.GetAppServiceNames(app.Name)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Fatal(err)
|
log.Fatal(err)
|
||||||
|
@ -1,6 +1,7 @@
|
|||||||
package app
|
package app
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
"strconv"
|
"strconv"
|
||||||
|
|
||||||
@ -87,13 +88,21 @@ func GetLabel(compose *composetypes.Config, stackName string, label string) stri
|
|||||||
return ""
|
return ""
|
||||||
}
|
}
|
||||||
|
|
||||||
// GetTimeoutFromLabel reads the timeout value from docker label "coop-cloud.${STACK_NAME}.TIMEOUT" and returns 50 as default value
|
// GetTimeoutFromLabel reads the timeout value from docker label
|
||||||
|
// `coop-cloud.${STACK_NAME}.timeout=...` if present. A value is present if the
|
||||||
|
// operator uses a `TIMEOUT=...` in their app env.
|
||||||
func GetTimeoutFromLabel(compose *composetypes.Config, stackName string) (int, error) {
|
func GetTimeoutFromLabel(compose *composetypes.Config, stackName string) (int, error) {
|
||||||
timeout := 50 // Default Timeout
|
var timeout int
|
||||||
var err error = nil
|
|
||||||
if timeoutLabel := GetLabel(compose, stackName, "timeout"); timeoutLabel != "" {
|
if timeoutLabel := GetLabel(compose, stackName, "timeout"); timeoutLabel != "" {
|
||||||
log.Debug(i18n.G("timeout label: %s", timeoutLabel))
|
log.Debug(i18n.G("timeout label: %s", timeoutLabel))
|
||||||
|
|
||||||
|
var err error
|
||||||
timeout, err = strconv.Atoi(timeoutLabel)
|
timeout, err = strconv.Atoi(timeoutLabel)
|
||||||
|
if err != nil {
|
||||||
|
return timeout, errors.New(i18n.G("unable to convert timeout label %s to int: %s", timeoutLabel, err))
|
||||||
|
}
|
||||||
}
|
}
|
||||||
return timeout, err
|
|
||||||
|
return timeout, nil
|
||||||
}
|
}
|
||||||
|
62
pkg/app/compose_test.go
Normal file
62
pkg/app/compose_test.go
Normal file
@ -0,0 +1,62 @@
|
|||||||
|
package app_test
|
||||||
|
|
||||||
|
import (
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
appPkg "coopcloud.tech/abra/pkg/app"
|
||||||
|
testPkg "coopcloud.tech/abra/pkg/test"
|
||||||
|
stack "coopcloud.tech/abra/pkg/upstream/stack"
|
||||||
|
|
||||||
|
"github.com/stretchr/testify/assert"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestGetTimeoutFromLabel(t *testing.T) {
|
||||||
|
testPkg.MkServerAppRecipe()
|
||||||
|
defer testPkg.RmServerAppRecipe()
|
||||||
|
|
||||||
|
tests := []struct {
|
||||||
|
configuredTimeout string
|
||||||
|
expectedTimeout int
|
||||||
|
}{
|
||||||
|
{"0", 0},
|
||||||
|
{"DOESNTEXIST", 0}, // NOTE(d1): test when missing from .env
|
||||||
|
{"80", 80},
|
||||||
|
{"120", 120},
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, test := range tests {
|
||||||
|
app, err := appPkg.GetApp(testPkg.ExpectedAppFiles, testPkg.AppName)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if test.configuredTimeout != "DOESNTEXIST" {
|
||||||
|
app.Env["TIMEOUT"] = test.configuredTimeout
|
||||||
|
}
|
||||||
|
|
||||||
|
composeFiles, err := app.Recipe.GetComposeFiles(app.Env)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
deployOpts := stack.Deploy{
|
||||||
|
Composefiles: composeFiles,
|
||||||
|
Namespace: app.StackName(),
|
||||||
|
Prune: false,
|
||||||
|
ResolveImage: stack.ResolveImageAlways,
|
||||||
|
Detach: false,
|
||||||
|
}
|
||||||
|
|
||||||
|
compose, err := appPkg.GetAppComposeConfig(app.Name, deployOpts, app.Env)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
timeout, err := appPkg.GetTimeoutFromLabel(compose, app.StackName())
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
assert.Equal(t, timeout, test.expectedTimeout)
|
||||||
|
}
|
||||||
|
}
|
@ -15,7 +15,7 @@ import (
|
|||||||
)
|
)
|
||||||
|
|
||||||
func TestGetAllFoldersInDirectory(t *testing.T) {
|
func TestGetAllFoldersInDirectory(t *testing.T) {
|
||||||
folders, err := config.GetAllFoldersInDirectory(testPkg.TestFolder)
|
folders, err := config.GetAllFoldersInDirectory(testPkg.TestDir)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatal(err)
|
t.Fatal(err)
|
||||||
}
|
}
|
||||||
@ -25,7 +25,7 @@ func TestGetAllFoldersInDirectory(t *testing.T) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func TestGetAllFilesInDirectory(t *testing.T) {
|
func TestGetAllFilesInDirectory(t *testing.T) {
|
||||||
files, err := config.GetAllFilesInDirectory(testPkg.TestFolder)
|
files, err := config.GetAllFilesInDirectory(testPkg.TestDir)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatal(err)
|
t.Fatal(err)
|
||||||
}
|
}
|
||||||
|
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
101
pkg/test/test.go
101
pkg/test/test.go
@ -1,6 +1,7 @@
|
|||||||
package test
|
package test
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"fmt"
|
||||||
"os"
|
"os"
|
||||||
"path"
|
"path"
|
||||||
|
|
||||||
@ -11,70 +12,68 @@ import (
|
|||||||
)
|
)
|
||||||
|
|
||||||
var (
|
var (
|
||||||
TestFolder = os.ExpandEnv("$PWD/../../tests/resources/test_folder")
|
AppName = "test_app.example.com"
|
||||||
ValidAbraConf = os.ExpandEnv("$PWD/../../tests/resources/valid_abra_config")
|
ServerName = "test_server"
|
||||||
|
TFiles = []string{"bar.env", "foo.env"}
|
||||||
|
TFolders = []string{"dir1", "dir2"}
|
||||||
|
TestServer = os.ExpandEnv("$PWD/../../tests/resources/test_server")
|
||||||
|
TestDir = os.ExpandEnv("$PWD/../../tests/resources/test_dir")
|
||||||
|
|
||||||
|
ExpectedAppEnv = envfile.AppEnv{
|
||||||
|
"DOMAIN": "test_app.example.com",
|
||||||
|
"RECIPE": "test_recipe",
|
||||||
|
}
|
||||||
|
|
||||||
|
ExpectedApp = appPkg.App{
|
||||||
|
Name: AppName,
|
||||||
|
Recipe: recipe.Get(ExpectedAppEnv["RECIPE"]),
|
||||||
|
Domain: ExpectedAppEnv["DOMAIN"],
|
||||||
|
Env: ExpectedAppEnv,
|
||||||
|
Path: ExpectedAppFile.Path,
|
||||||
|
Server: ExpectedAppFile.Server,
|
||||||
|
}
|
||||||
|
|
||||||
|
ExpectedAppFile = appPkg.AppFile{
|
||||||
|
Path: path.Join(TestServer, fmt.Sprintf("%s.env", AppName)),
|
||||||
|
Server: ServerName,
|
||||||
|
}
|
||||||
|
|
||||||
|
ExpectedAppFiles = map[string]appPkg.AppFile{
|
||||||
|
AppName: ExpectedAppFile,
|
||||||
|
}
|
||||||
)
|
)
|
||||||
|
|
||||||
// make sure these are in alphabetical order
|
|
||||||
var (
|
|
||||||
TFolders = []string{"folder1", "folder2"}
|
|
||||||
TFiles = []string{"bar.env", "foo.env"}
|
|
||||||
)
|
|
||||||
|
|
||||||
var (
|
|
||||||
AppName = "ecloud"
|
|
||||||
ServerName = "evil.corp"
|
|
||||||
)
|
|
||||||
|
|
||||||
var ExpectedAppEnv = envfile.AppEnv{
|
|
||||||
"DOMAIN": "ecloud.evil.corp",
|
|
||||||
"RECIPE": "ecloud",
|
|
||||||
"SMTP_AUTHTYPE": "login",
|
|
||||||
}
|
|
||||||
|
|
||||||
var ExpectedApp = appPkg.App{
|
|
||||||
Name: AppName,
|
|
||||||
Recipe: recipe.Get(ExpectedAppEnv["RECIPE"]),
|
|
||||||
Domain: ExpectedAppEnv["DOMAIN"],
|
|
||||||
Env: ExpectedAppEnv,
|
|
||||||
Path: ExpectedAppFile.Path,
|
|
||||||
Server: ExpectedAppFile.Server,
|
|
||||||
}
|
|
||||||
|
|
||||||
var ExpectedAppFile = appPkg.AppFile{
|
|
||||||
Path: path.Join(ValidAbraConf, "servers", ServerName, AppName+".env"),
|
|
||||||
Server: ServerName,
|
|
||||||
}
|
|
||||||
|
|
||||||
var ExpectedAppFiles = map[string]appPkg.AppFile{
|
|
||||||
AppName: ExpectedAppFile,
|
|
||||||
}
|
|
||||||
|
|
||||||
// RmServerAppRecipe deletes the test server / app / recipe.
|
|
||||||
func RmServerAppRecipe() {
|
func RmServerAppRecipe() {
|
||||||
testAppLink := os.ExpandEnv("$HOME/.abra/servers/foo.com")
|
testAppLink := os.ExpandEnv("$ABRA_DIR/servers/test_server")
|
||||||
if err := os.Remove(testAppLink); err != nil {
|
os.Remove(testAppLink)
|
||||||
log.Fatal(err)
|
|
||||||
}
|
|
||||||
|
|
||||||
testRecipeLink := os.ExpandEnv("$HOME/.abra/recipes/test")
|
testRecipeLink := os.ExpandEnv("$ABRA_DIR/recipes/test_recipe")
|
||||||
if err := os.Remove(testRecipeLink); err != nil {
|
os.Remove(testRecipeLink)
|
||||||
log.Fatal(err)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// MkServerAppRecipe symlinks the test server / app / recipe.
|
|
||||||
func MkServerAppRecipe() {
|
func MkServerAppRecipe() {
|
||||||
RmServerAppRecipe()
|
RmServerAppRecipe()
|
||||||
|
|
||||||
testAppDir := os.ExpandEnv("$PWD/../../tests/resources/testapp")
|
if err := os.Mkdir(os.ExpandEnv("$ABRA_DIR/servers"), 0700); err != nil {
|
||||||
testAppLink := os.ExpandEnv("$HOME/.abra/servers/foo.com")
|
if !os.IsExist(err) {
|
||||||
|
log.Fatal(err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := os.Mkdir(os.ExpandEnv("$ABRA_DIR/recipes"), 0764); err != nil {
|
||||||
|
if !os.IsExist(err) {
|
||||||
|
log.Fatal(err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
testAppDir := os.ExpandEnv("$PWD/../../tests/resources/test_server")
|
||||||
|
testAppLink := os.ExpandEnv("$ABRA_DIR/servers/test_server")
|
||||||
if err := os.Symlink(testAppDir, testAppLink); err != nil {
|
if err := os.Symlink(testAppDir, testAppLink); err != nil {
|
||||||
log.Fatal(err)
|
log.Fatal(err)
|
||||||
}
|
}
|
||||||
|
|
||||||
testRecipeDir := os.ExpandEnv("$PWD/../../tests/resources/testrecipe")
|
testRecipeDir := os.ExpandEnv("$PWD/../../tests/resources/test_recipe")
|
||||||
testRecipeLink := os.ExpandEnv("$HOME/.abra/recipes/test")
|
testRecipeLink := os.ExpandEnv("$ABRA_DIR/recipes/test_recipe")
|
||||||
if err := os.Symlink(testRecipeDir, testRecipeLink); err != nil {
|
if err := os.Symlink(testRecipeDir, testRecipeLink); err != nil {
|
||||||
log.Fatal(err)
|
log.Fatal(err)
|
||||||
}
|
}
|
||||||
|
@ -197,7 +197,10 @@ func (m Model) Init() tea.Cmd {
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
cmds = append(cmds, func() tea.Msg { return deployTimeout(m) })
|
if m.timeout != 0 {
|
||||||
|
cmds = append(cmds, func() tea.Msg { return deployTimeout(m) })
|
||||||
|
}
|
||||||
|
|
||||||
cmds = append(cmds, func() tea.Msg { return m.gatherLogs() })
|
cmds = append(cmds, func() tea.Msg { return m.gatherLogs() })
|
||||||
|
|
||||||
return tea.Batch(cmds...)
|
return tea.Batch(cmds...)
|
||||||
|
@ -7,10 +7,12 @@ import (
|
|||||||
"sort"
|
"sort"
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
|
"coopcloud.tech/abra/pkg/i18n"
|
||||||
"coopcloud.tech/abra/pkg/log"
|
"coopcloud.tech/abra/pkg/log"
|
||||||
"github.com/docker/cli/cli/compose/loader"
|
"github.com/docker/cli/cli/compose/loader"
|
||||||
"github.com/docker/cli/cli/compose/schema"
|
"github.com/docker/cli/cli/compose/schema"
|
||||||
composetypes "github.com/docker/cli/cli/compose/types"
|
composetypes "github.com/docker/cli/cli/compose/types"
|
||||||
|
"github.com/pkg/errors"
|
||||||
)
|
)
|
||||||
|
|
||||||
// DontSkipValidation ensures validation is done for compose file loading
|
// DontSkipValidation ensures validation is done for compose file loading
|
||||||
@ -38,8 +40,7 @@ func LoadComposefile(opts Deploy, appEnv map[string]string, options ...func(*loa
|
|||||||
config, err := loader.Load(configDetails, options...)
|
config, err := loader.Load(configDetails, options...)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
if fpe, ok := err.(*loader.ForbiddenPropertiesError); ok {
|
if fpe, ok := err.(*loader.ForbiddenPropertiesError); ok {
|
||||||
return nil, fmt.Errorf("compose file contains unsupported options: %s",
|
return nil, errors.New(i18n.G("compose file contains unsupported options: %s", propertyWarnings(fpe.Properties)))
|
||||||
propertyWarnings(fpe.Properties))
|
|
||||||
}
|
}
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
@ -51,14 +52,12 @@ func LoadComposefile(opts Deploy, appEnv map[string]string, options ...func(*loa
|
|||||||
|
|
||||||
unsupportedProperties := loader.GetUnsupportedProperties(dicts...)
|
unsupportedProperties := loader.GetUnsupportedProperties(dicts...)
|
||||||
if len(unsupportedProperties) > 0 {
|
if len(unsupportedProperties) > 0 {
|
||||||
log.Warnf("%s: ignoring unsupported options: %s",
|
log.Warn(i18n.G("%s: ignoring unsupported options: %s", recipeName, strings.Join(unsupportedProperties, ", ")))
|
||||||
recipeName, strings.Join(unsupportedProperties, ", "))
|
|
||||||
}
|
}
|
||||||
|
|
||||||
deprecatedProperties := loader.GetDeprecatedProperties(dicts...)
|
deprecatedProperties := loader.GetDeprecatedProperties(dicts...)
|
||||||
if len(deprecatedProperties) > 0 {
|
if len(deprecatedProperties) > 0 {
|
||||||
log.Warnf("%s: ignoring deprecated options: %s",
|
log.Warn(i18n.G("%s: ignoring deprecated options: %s", recipeName, propertyWarnings(deprecatedProperties)))
|
||||||
recipeName, propertyWarnings(deprecatedProperties))
|
|
||||||
}
|
}
|
||||||
return config, nil
|
return config, nil
|
||||||
}
|
}
|
||||||
@ -106,7 +105,7 @@ func buildEnvironment(env []string) (map[string]string, error) {
|
|||||||
for _, s := range env {
|
for _, s := range env {
|
||||||
// if value is empty, s is like "K=", not "K".
|
// if value is empty, s is like "K=", not "K".
|
||||||
if !strings.Contains(s, "=") {
|
if !strings.Contains(s, "=") {
|
||||||
return result, fmt.Errorf("unexpected environment %q", s)
|
return result, errors.New(i18n.G("unexpected environment %q", s))
|
||||||
}
|
}
|
||||||
kv := strings.SplitN(s, "=", 2)
|
kv := strings.SplitN(s, "=", 2)
|
||||||
result[kv[0]] = kv[1]
|
result[kv[0]] = kv[1]
|
||||||
|
@ -9,6 +9,7 @@ import (
|
|||||||
"strings"
|
"strings"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
|
"coopcloud.tech/abra/pkg/i18n"
|
||||||
"coopcloud.tech/abra/pkg/log"
|
"coopcloud.tech/abra/pkg/log"
|
||||||
"github.com/docker/docker/api/types"
|
"github.com/docker/docker/api/types"
|
||||||
"github.com/docker/docker/api/types/network"
|
"github.com/docker/docker/api/types/network"
|
||||||
@ -21,6 +22,12 @@ import (
|
|||||||
|
|
||||||
// RunRemove is the swarm implementation of docker stack remove
|
// RunRemove is the swarm implementation of docker stack remove
|
||||||
func RunRemove(ctx context.Context, client *apiclient.Client, opts Remove) error {
|
func RunRemove(ctx context.Context, client *apiclient.Client, opts Remove) error {
|
||||||
|
log.Info(i18n.G("initialising undeploy"))
|
||||||
|
|
||||||
|
if WaitTimeout != 0 {
|
||||||
|
log.Debug(i18n.G("timeout: set to %d second(s)", WaitTimeout))
|
||||||
|
}
|
||||||
|
|
||||||
sigIntCh := make(chan os.Signal, 1)
|
sigIntCh := make(chan os.Signal, 1)
|
||||||
signal.Notify(sigIntCh, os.Interrupt)
|
signal.Notify(sigIntCh, os.Interrupt)
|
||||||
defer signal.Stop(sigIntCh)
|
defer signal.Stop(sigIntCh)
|
||||||
@ -62,7 +69,7 @@ func RunRemove(ctx context.Context, client *apiclient.Client, opts Remove) error
|
|||||||
}
|
}
|
||||||
|
|
||||||
if len(services)+len(networks)+len(secrets)+len(configs) == 0 {
|
if len(services)+len(networks)+len(secrets)+len(configs) == 0 {
|
||||||
log.Warnf("nothing found in stack: %s", namespace)
|
log.Warn(i18n.G("nothing found in stack: %s", namespace))
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -72,17 +79,17 @@ func RunRemove(ctx context.Context, client *apiclient.Client, opts Remove) error
|
|||||||
hasError = removeNetworks(ctx, client, networks) || hasError
|
hasError = removeNetworks(ctx, client, networks) || hasError
|
||||||
|
|
||||||
if hasError {
|
if hasError {
|
||||||
errs = append(errs, fmt.Sprintf("failed to remove some resources from stack: %s", namespace))
|
errs = append(errs, fmt.Sprint(i18n.G("failed to remove some resources from stack: %s", namespace)))
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
|
||||||
log.Info("polling undeploy status")
|
log.Info(i18n.G("polling undeploy status"))
|
||||||
timeout, err := waitOnTasks(ctx, client, namespace)
|
timeout, err := waitOnTasks(ctx, client, namespace)
|
||||||
if timeout {
|
if timeout {
|
||||||
errs = append(errs, err.Error())
|
errs = append(errs, err.Error())
|
||||||
} else {
|
} else {
|
||||||
if err != nil {
|
if err != nil {
|
||||||
errs = append(errs, fmt.Sprintf("failed to wait on tasks of stack: %s: %s", namespace, err))
|
errs = append(errs, fmt.Sprint(i18n.G("failed to wait on tasks of stack: %s: %s", namespace, err)))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -99,7 +106,7 @@ func RunRemove(ctx context.Context, client *apiclient.Client, opts Remove) error
|
|||||||
case <-waitCh:
|
case <-waitCh:
|
||||||
return nil
|
return nil
|
||||||
case <-sigIntCh:
|
case <-sigIntCh:
|
||||||
return fmt.Errorf("skipping as requested, undeploy still in progress 🟠")
|
return errors.New(i18n.G("skipping as requested, undeploy still in progress 🟠"))
|
||||||
case err := <-errCh:
|
case err := <-errCh:
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
@ -121,10 +128,10 @@ func removeServices(
|
|||||||
var hasError bool
|
var hasError bool
|
||||||
sort.Slice(services, sortServiceByName(services))
|
sort.Slice(services, sortServiceByName(services))
|
||||||
for _, service := range services {
|
for _, service := range services {
|
||||||
log.Debugf("removing service %s", service.Spec.Name)
|
log.Debug(i18n.G("removing service %s", service.Spec.Name))
|
||||||
if err := client.ServiceRemove(ctx, service.ID); err != nil {
|
if err := client.ServiceRemove(ctx, service.ID); err != nil {
|
||||||
hasError = true
|
hasError = true
|
||||||
log.Fatalf("failed to remove service %s: %s", service.ID, err)
|
log.Fatal(i18n.G("failed to remove service %s: %s", service.ID, err))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return hasError
|
return hasError
|
||||||
@ -137,10 +144,10 @@ func removeNetworks(
|
|||||||
) bool {
|
) bool {
|
||||||
var hasError bool
|
var hasError bool
|
||||||
for _, network := range networks {
|
for _, network := range networks {
|
||||||
log.Debugf("removing network %s", network.Name)
|
log.Debug(i18n.G("removing network %s", network.Name))
|
||||||
if err := client.NetworkRemove(ctx, network.ID); err != nil {
|
if err := client.NetworkRemove(ctx, network.ID); err != nil {
|
||||||
hasError = true
|
hasError = true
|
||||||
log.Fatalf("failed to remove network %s: %s", network.ID, err)
|
log.Fatal(i18n.G("failed to remove network %s: %s", network.ID, err))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return hasError
|
return hasError
|
||||||
@ -153,10 +160,10 @@ func removeSecrets(
|
|||||||
) bool {
|
) bool {
|
||||||
var hasError bool
|
var hasError bool
|
||||||
for _, secret := range secrets {
|
for _, secret := range secrets {
|
||||||
log.Debugf("removing secret %s", secret.Spec.Name)
|
log.Debug(i18n.G("removing secret %s", secret.Spec.Name))
|
||||||
if err := client.SecretRemove(ctx, secret.ID); err != nil {
|
if err := client.SecretRemove(ctx, secret.ID); err != nil {
|
||||||
hasError = true
|
hasError = true
|
||||||
log.Fatalf("Failed to remove secret %s: %s", secret.ID, err)
|
log.Fatal(i18n.G("failed to remove secret %s: %s", secret.ID, err))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return hasError
|
return hasError
|
||||||
@ -169,10 +176,10 @@ func removeConfigs(
|
|||||||
) bool {
|
) bool {
|
||||||
var hasError bool
|
var hasError bool
|
||||||
for _, config := range configs {
|
for _, config := range configs {
|
||||||
log.Debugf("removing config %s", config.Spec.Name)
|
log.Debug(i18n.G("removing config %s", config.Spec.Name))
|
||||||
if err := client.ConfigRemove(ctx, config.ID); err != nil {
|
if err := client.ConfigRemove(ctx, config.ID); err != nil {
|
||||||
hasError = true
|
hasError = true
|
||||||
log.Fatalf("failed to remove config %s: %s", config.ID, err)
|
log.Fatal(i18n.G("failed to remove config %s: %s", config.ID, err))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return hasError
|
return hasError
|
||||||
@ -206,12 +213,17 @@ func terminalState(state swarm.TaskState) bool {
|
|||||||
func waitOnTasks(ctx context.Context, client apiclient.APIClient, namespace string) (bool, error) {
|
func waitOnTasks(ctx context.Context, client apiclient.APIClient, namespace string) (bool, error) {
|
||||||
var timedOut bool
|
var timedOut bool
|
||||||
|
|
||||||
log.Debugf("waiting on undeploy tasks (timeout=%v secs)", WaitTimeout)
|
|
||||||
|
|
||||||
go func() {
|
go func() {
|
||||||
t := time.Duration(WaitTimeout) * time.Second
|
if WaitTimeout == 0 {
|
||||||
<-time.After(t)
|
return
|
||||||
log.Debug("timed out on undeploy")
|
}
|
||||||
|
|
||||||
|
log.Debug(i18n.G("timeout: waiting on undeploy tasks (timeout=%v secs)", WaitTimeout))
|
||||||
|
|
||||||
|
timeout := time.Duration(WaitTimeout) * time.Second
|
||||||
|
<-time.After(timeout)
|
||||||
|
|
||||||
|
log.Debug(i18n.G("timed out on undeploy (timeout=%v sec)", WaitTimeout))
|
||||||
timedOut = true
|
timedOut = true
|
||||||
}()
|
}()
|
||||||
|
|
||||||
@ -219,7 +231,7 @@ func waitOnTasks(ctx context.Context, client apiclient.APIClient, namespace stri
|
|||||||
for {
|
for {
|
||||||
tasks, err := getStackTasks(ctx, client, namespace)
|
tasks, err := getStackTasks(ctx, client, namespace)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return false, fmt.Errorf("failed to get tasks: %w", err)
|
return false, errors.New(i18n.G("failed to get tasks: %w", err))
|
||||||
}
|
}
|
||||||
|
|
||||||
for _, task := range tasks {
|
for _, task := range tasks {
|
||||||
@ -234,7 +246,7 @@ func waitOnTasks(ctx context.Context, client apiclient.APIClient, namespace stri
|
|||||||
}
|
}
|
||||||
|
|
||||||
if timedOut {
|
if timedOut {
|
||||||
return true, fmt.Errorf("deployment timed out 🟠")
|
return true, errors.New(i18n.G("deployment timed out 🟠"))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -14,6 +14,7 @@ import (
|
|||||||
tea "github.com/charmbracelet/bubbletea"
|
tea "github.com/charmbracelet/bubbletea"
|
||||||
|
|
||||||
"coopcloud.tech/abra/pkg/config"
|
"coopcloud.tech/abra/pkg/config"
|
||||||
|
"coopcloud.tech/abra/pkg/i18n"
|
||||||
"coopcloud.tech/abra/pkg/log"
|
"coopcloud.tech/abra/pkg/log"
|
||||||
"coopcloud.tech/abra/pkg/ui"
|
"coopcloud.tech/abra/pkg/ui"
|
||||||
"coopcloud.tech/abra/pkg/upstream/convert"
|
"coopcloud.tech/abra/pkg/upstream/convert"
|
||||||
@ -39,8 +40,9 @@ const (
|
|||||||
ResolveImageNever = "never"
|
ResolveImageNever = "never"
|
||||||
)
|
)
|
||||||
|
|
||||||
// Timeout to wait until docker services converge, default is 50s (random choice)
|
// Timeout to wait until docker services converge. This timeout is disabled by
|
||||||
var WaitTimeout = 50
|
// default but can be configured by passing a TIMEOUT=... in the app .env
|
||||||
|
var WaitTimeout = 0
|
||||||
|
|
||||||
type StackStatus struct {
|
type StackStatus struct {
|
||||||
Services []swarm.Service
|
Services []swarm.Service
|
||||||
@ -152,7 +154,7 @@ func IsDeployed(ctx context.Context, cl *dockerClient.Client, stackName string)
|
|||||||
if isChaos, ok := service.Spec.Labels[labelKey]; ok {
|
if isChaos, ok := service.Spec.Labels[labelKey]; ok {
|
||||||
boolVal, err := strconv.ParseBool(isChaos)
|
boolVal, err := strconv.ParseBool(isChaos)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return deployMeta, fmt.Errorf("unable to parse '%s' value as bool: %s", labelKey, err)
|
return deployMeta, errors.New(i18n.G("unable to parse '%s' value as bool: %s", labelKey, err))
|
||||||
}
|
}
|
||||||
deployMeta.IsChaos = boolVal
|
deployMeta.IsChaos = boolVal
|
||||||
}
|
}
|
||||||
@ -164,12 +166,12 @@ func IsDeployed(ctx context.Context, cl *dockerClient.Client, stackName string)
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
log.Debugf("%s has been detected as deployed: %v", stackName, deployMeta)
|
log.Debug(i18n.G("%s has been detected as deployed: %v", stackName, deployMeta))
|
||||||
|
|
||||||
return deployMeta, nil
|
return deployMeta, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
log.Debugf("%s has been detected as not deployed", stackName)
|
log.Debug(i18n.G("%s has been detected as not deployed", stackName))
|
||||||
|
|
||||||
return deployMeta, nil
|
return deployMeta, nil
|
||||||
}
|
}
|
||||||
@ -178,7 +180,7 @@ func IsDeployed(ctx context.Context, cl *dockerClient.Client, stackName string)
|
|||||||
func pruneServices(ctx context.Context, cl *dockerClient.Client, namespace convert.Namespace, services map[string]struct{}) {
|
func pruneServices(ctx context.Context, cl *dockerClient.Client, namespace convert.Namespace, services map[string]struct{}) {
|
||||||
oldServices, err := GetStackServices(ctx, cl, namespace.Name())
|
oldServices, err := GetStackServices(ctx, cl, namespace.Name())
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Warnf("failed to list services: %s", err)
|
log.Warn(i18n.G("failed to list services: %s", err))
|
||||||
}
|
}
|
||||||
|
|
||||||
pruneServices := []swarm.Service{}
|
pruneServices := []swarm.Service{}
|
||||||
@ -201,7 +203,11 @@ func RunDeploy(
|
|||||||
dontWait bool,
|
dontWait bool,
|
||||||
filters filters.Args,
|
filters filters.Args,
|
||||||
) error {
|
) error {
|
||||||
log.Info("initialising deployment")
|
log.Info(i18n.G("initialising deployment"))
|
||||||
|
|
||||||
|
if WaitTimeout != 0 {
|
||||||
|
log.Debug(i18n.G("timeout: set to %d second(s)", WaitTimeout))
|
||||||
|
}
|
||||||
|
|
||||||
if err := validateResolveImageFlag(&opts); err != nil {
|
if err := validateResolveImageFlag(&opts); err != nil {
|
||||||
return err
|
return err
|
||||||
@ -230,7 +236,7 @@ func validateResolveImageFlag(opts *Deploy) error {
|
|||||||
case ResolveImageAlways, ResolveImageChanged, ResolveImageNever:
|
case ResolveImageAlways, ResolveImageChanged, ResolveImageNever:
|
||||||
return nil
|
return nil
|
||||||
default:
|
default:
|
||||||
return errors.Errorf("invalid option %s for flag --resolve-image", opts.ResolveImage)
|
return errors.New(i18n.G("invalid option %s for flag --resolve-image", opts.ResolveImage))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -297,7 +303,7 @@ func deployCompose(
|
|||||||
}
|
}
|
||||||
|
|
||||||
if dontWait {
|
if dontWait {
|
||||||
log.Warn("skipping converge logic checks")
|
log.Warn(i18n.G("skipping converge logic checks"))
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -339,11 +345,11 @@ func validateExternalNetworks(ctx context.Context, client dockerClient.NetworkAP
|
|||||||
network, err := client.NetworkInspect(ctx, networkName, networktypes.InspectOptions{})
|
network, err := client.NetworkInspect(ctx, networkName, networktypes.InspectOptions{})
|
||||||
switch {
|
switch {
|
||||||
case dockerClient.IsErrNotFound(err):
|
case dockerClient.IsErrNotFound(err):
|
||||||
return errors.Errorf("network %q is declared as external, but could not be found. You need to create a swarm-scoped network before the stack is deployed, which you can do by running this on the server: docker network create -d overlay proxy", networkName)
|
return errors.New(i18n.G("network %q is declared as external, but could not be found. You need to create a swarm-scoped network before the stack is deployed, which you can do by running this on the server: docker network create -d overlay proxy", networkName))
|
||||||
case err != nil:
|
case err != nil:
|
||||||
return err
|
return err
|
||||||
case network.Scope != "swarm":
|
case network.Scope != "swarm":
|
||||||
return errors.Errorf("network %q is declared as external, but it is not in the right scope: %q instead of \"swarm\"", networkName, network.Scope)
|
return errors.New(i18n.G("network %q is declared as external, but it is not in the right scope: %q instead of \"swarm\"", networkName, network.Scope))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return nil
|
return nil
|
||||||
@ -356,13 +362,13 @@ func createSecrets(ctx context.Context, cl *dockerClient.Client, secrets []swarm
|
|||||||
case err == nil:
|
case err == nil:
|
||||||
// secret already exists, then we update that
|
// secret already exists, then we update that
|
||||||
if err := cl.SecretUpdate(ctx, secret.ID, secret.Meta.Version, secretSpec); err != nil {
|
if err := cl.SecretUpdate(ctx, secret.ID, secret.Meta.Version, secretSpec); err != nil {
|
||||||
return errors.Wrapf(err, "failed to update secret %s", secretSpec.Name)
|
return errors.Wrap(err, i18n.G("failed to update secret %s", secretSpec.Name))
|
||||||
}
|
}
|
||||||
case dockerClient.IsErrNotFound(err):
|
case dockerClient.IsErrNotFound(err):
|
||||||
// secret does not exist, then we create a new one.
|
// secret does not exist, then we create a new one.
|
||||||
log.Infof("creating secret %s", secretSpec.Name)
|
log.Info(i18n.G("creating secret %s", secretSpec.Name))
|
||||||
if _, err := cl.SecretCreate(ctx, secretSpec); err != nil {
|
if _, err := cl.SecretCreate(ctx, secretSpec); err != nil {
|
||||||
return errors.Wrapf(err, "failed to create secret %s", secretSpec.Name)
|
return errors.Wrap(err, i18n.G("failed to create secret %s", secretSpec.Name))
|
||||||
}
|
}
|
||||||
default:
|
default:
|
||||||
return err
|
return err
|
||||||
@ -378,13 +384,13 @@ func createConfigs(ctx context.Context, cl *dockerClient.Client, configs []swarm
|
|||||||
case err == nil:
|
case err == nil:
|
||||||
// config already exists, then we update that
|
// config already exists, then we update that
|
||||||
if err := cl.ConfigUpdate(ctx, config.ID, config.Meta.Version, configSpec); err != nil {
|
if err := cl.ConfigUpdate(ctx, config.ID, config.Meta.Version, configSpec); err != nil {
|
||||||
return errors.Wrapf(err, "failed to update config %s", configSpec.Name)
|
return errors.Wrap(err, i18n.G("failed to update config %s", configSpec.Name))
|
||||||
}
|
}
|
||||||
case dockerClient.IsErrNotFound(err):
|
case dockerClient.IsErrNotFound(err):
|
||||||
// config does not exist, then we create a new one.
|
// config does not exist, then we create a new one.
|
||||||
log.Debugf("creating config %s", configSpec.Name)
|
log.Debugf("creating config %s", configSpec.Name)
|
||||||
if _, err := cl.ConfigCreate(ctx, configSpec); err != nil {
|
if _, err := cl.ConfigCreate(ctx, configSpec); err != nil {
|
||||||
return errors.Wrapf(err, "failed to create config %s", configSpec.Name)
|
return errors.Wrap(err, i18n.G("failed to create config %s", configSpec.Name))
|
||||||
}
|
}
|
||||||
default:
|
default:
|
||||||
return err
|
return err
|
||||||
@ -413,9 +419,9 @@ func createNetworks(ctx context.Context, cl *dockerClient.Client, namespace conv
|
|||||||
createOpts.Driver = defaultNetworkDriver
|
createOpts.Driver = defaultNetworkDriver
|
||||||
}
|
}
|
||||||
|
|
||||||
log.Debugf("creating network %s", name)
|
log.Debug(i18n.G("creating network %s", name))
|
||||||
if _, err := cl.NetworkCreate(ctx, name, createOpts); err != nil {
|
if _, err := cl.NetworkCreate(ctx, name, createOpts); err != nil {
|
||||||
return errors.Wrapf(err, "failed to create network %s", name)
|
return errors.Wrap(err, i18n.G("failed to create network %s", name))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return nil
|
return nil
|
||||||
@ -455,16 +461,16 @@ func deployServices(
|
|||||||
if sendAuth {
|
if sendAuth {
|
||||||
dockerCLI, err := command.NewDockerCli()
|
dockerCLI, err := command.NewDockerCli()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Errorf("retrieving docker auth token: failed create docker cli: %s", err)
|
log.Error(i18n.G("retrieving docker auth token: failed create docker cli: %s", err))
|
||||||
}
|
}
|
||||||
encodedAuth, err = command.RetrieveAuthTokenFromImage(dockerCLI.ConfigFile(), image)
|
encodedAuth, err = command.RetrieveAuthTokenFromImage(dockerCLI.ConfigFile(), image)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Errorf("failed to retrieve registry auth for image %s: %s", image, err)
|
log.Error(i18n.G("failed to retrieve registry auth for image %s: %s", image, err))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if service, exists := existingServiceMap[name]; exists {
|
if service, exists := existingServiceMap[name]; exists {
|
||||||
log.Debugf("updating %s", name)
|
log.Debug(i18n.G("updating %s", name))
|
||||||
|
|
||||||
updateOpts := types.ServiceUpdateOptions{EncodedRegistryAuth: encodedAuth}
|
updateOpts := types.ServiceUpdateOptions{EncodedRegistryAuth: encodedAuth}
|
||||||
|
|
||||||
@ -499,7 +505,7 @@ func deployServices(
|
|||||||
|
|
||||||
response, err := cl.ServiceUpdate(ctx, service.ID, service.Version, serviceSpec, updateOpts)
|
response, err := cl.ServiceUpdate(ctx, service.ID, service.Version, serviceSpec, updateOpts)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, errors.Wrapf(err, "failed to update %s", name)
|
return nil, errors.Wrap(err, i18n.G("failed to update %s", name))
|
||||||
}
|
}
|
||||||
|
|
||||||
for _, warning := range response.Warnings {
|
for _, warning := range response.Warnings {
|
||||||
@ -511,7 +517,7 @@ func deployServices(
|
|||||||
ID: service.ID,
|
ID: service.ID,
|
||||||
})
|
})
|
||||||
} else {
|
} else {
|
||||||
log.Debugf("creating %s", name)
|
log.Debug(i18n.G("creating %s", name))
|
||||||
|
|
||||||
createOpts := types.ServiceCreateOptions{EncodedRegistryAuth: encodedAuth}
|
createOpts := types.ServiceCreateOptions{EncodedRegistryAuth: encodedAuth}
|
||||||
|
|
||||||
@ -522,7 +528,7 @@ func deployServices(
|
|||||||
|
|
||||||
serviceCreateResponse, err := cl.ServiceCreate(ctx, serviceSpec, createOpts)
|
serviceCreateResponse, err := cl.ServiceCreate(ctx, serviceSpec, createOpts)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, errors.Wrapf(err, "failed to create %s", name)
|
return nil, errors.Wrap(err, i18n.G("failed to create %s", name))
|
||||||
}
|
}
|
||||||
|
|
||||||
servicesMeta = append(servicesMeta, ui.ServiceMeta{
|
servicesMeta = append(servicesMeta, ui.ServiceMeta{
|
||||||
@ -567,7 +573,7 @@ func WaitOnServices(ctx context.Context, cl *dockerClient.Client, opts WaitOpts)
|
|||||||
tui := tea.NewProgram(model)
|
tui := tea.NewProgram(model)
|
||||||
|
|
||||||
if !opts.Quiet {
|
if !opts.Quiet {
|
||||||
log.Info("polling deployment status")
|
log.Info(i18n.G("polling deployment status"))
|
||||||
}
|
}
|
||||||
|
|
||||||
m, err := log.Without(
|
m, err := log.Without(
|
||||||
@ -576,7 +582,7 @@ func WaitOnServices(ctx context.Context, cl *dockerClient.Client, opts WaitOpts)
|
|||||||
},
|
},
|
||||||
)
|
)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("waitOnServices: error running TUI: %s", err)
|
return errors.New(i18n.G("waitOnServices: error running TUI: %s", err))
|
||||||
}
|
}
|
||||||
|
|
||||||
deployModel := m.(ui.Model)
|
deployModel := m.(ui.Model)
|
||||||
@ -584,16 +590,16 @@ func WaitOnServices(ctx context.Context, cl *dockerClient.Client, opts WaitOpts)
|
|||||||
var errs []error
|
var errs []error
|
||||||
|
|
||||||
if deployModel.Failed {
|
if deployModel.Failed {
|
||||||
errs = append(errs, fmt.Errorf("deploy failed 🛑"))
|
errs = append(errs, errors.New(i18n.G("deploy failed 🛑")))
|
||||||
} else if deployModel.TimedOut {
|
} else if deployModel.TimedOut {
|
||||||
errs = append(errs, fmt.Errorf("deploy timed out 🟠"))
|
errs = append(errs, errors.New(i18n.G("deploy timed out 🟠")))
|
||||||
} else {
|
} else {
|
||||||
errs = append(errs, fmt.Errorf("deploy in progress 🟠"))
|
errs = append(errs, errors.New(i18n.G("deploy in progress 🟠")))
|
||||||
}
|
}
|
||||||
|
|
||||||
for _, s := range *deployModel.Streams {
|
for _, s := range *deployModel.Streams {
|
||||||
if s.Err != nil {
|
if s.Err != nil {
|
||||||
errs = append(errs, fmt.Errorf("%s: %s", s.Name, s.Err))
|
errs = append(errs, errors.New(i18n.G("%s: %s", s.Name, s.Err)))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -605,28 +611,28 @@ func WaitOnServices(ctx context.Context, cl *dockerClient.Client, opts WaitOpts)
|
|||||||
)
|
)
|
||||||
|
|
||||||
if err := os.MkdirAll(filepath.Join(config.LOGS_DIR, opts.ServerName), 0o764); err != nil {
|
if err := os.MkdirAll(filepath.Join(config.LOGS_DIR, opts.ServerName), 0o764); err != nil {
|
||||||
return fmt.Errorf("waitOnServices: error creating log dir: %s", err)
|
return errors.New(i18n.G("waitOnServices: error creating log dir: %s", err))
|
||||||
}
|
}
|
||||||
|
|
||||||
file, err := os.Create(logsPath)
|
file, err := os.Create(logsPath)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("waitOnServices: error opening file: %s", err)
|
return errors.New(i18n.G("waitOnServices: error opening file: %s", err))
|
||||||
}
|
}
|
||||||
defer file.Close()
|
defer file.Close()
|
||||||
|
|
||||||
s := strings.Join(*deployModel.Logs, "\n")
|
s := strings.Join(*deployModel.Logs, "\n")
|
||||||
if _, err := file.WriteString(s); err != nil {
|
if _, err := file.WriteString(s); err != nil {
|
||||||
return fmt.Errorf("waitOnServices: writeFile: %s", err)
|
return errors.New(i18n.G("waitOnServices: writeFile: %s", err))
|
||||||
}
|
}
|
||||||
|
|
||||||
errs = append(errs, fmt.Errorf("logs: %s", logsPath))
|
errs = append(errs, errors.New(i18n.G("logs: %s", logsPath)))
|
||||||
}
|
}
|
||||||
|
|
||||||
return stdlibErr.Join(errs...)
|
return stdlibErr.Join(errs...)
|
||||||
}
|
}
|
||||||
|
|
||||||
if !opts.Quiet {
|
if !opts.Quiet {
|
||||||
log.Info("deploy succeeded 🟢")
|
log.Info(i18n.G("deploy succeeded 🟢"))
|
||||||
}
|
}
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
@ -646,8 +652,7 @@ func GetStacks(cl *dockerClient.Client) ([]*formatter.Stack, error) {
|
|||||||
labels := service.Spec.Labels
|
labels := service.Spec.Labels
|
||||||
name, ok := labels[convert.LabelNamespace]
|
name, ok := labels[convert.LabelNamespace]
|
||||||
if !ok {
|
if !ok {
|
||||||
return nil, errors.Errorf("cannot get label %s for %s",
|
return nil, errors.New(i18n.G("cannot get label %s for %s", convert.LabelNamespace, service.ID))
|
||||||
convert.LabelNamespace, service.ID)
|
|
||||||
}
|
}
|
||||||
ztack, ok := m[name]
|
ztack, ok := m[name]
|
||||||
if !ok {
|
if !ok {
|
||||||
|
@ -444,6 +444,24 @@ teardown(){
|
|||||||
assert_output --partial "$latestRelease"
|
assert_output --partial "$latestRelease"
|
||||||
}
|
}
|
||||||
|
|
||||||
|
# bats test_tags=slow
|
||||||
|
@test "ignore timeout when not present in env" {
|
||||||
|
run $ABRA app deploy "$TEST_APP_DOMAIN" --no-input --no-converge-checks --debug
|
||||||
|
assert_success
|
||||||
|
refute_output --partial "timeout: set to"
|
||||||
|
}
|
||||||
|
|
||||||
|
# bats test_tags=slow
|
||||||
|
@test "use timeout when present in env" {
|
||||||
|
run sed -i 's/#TIMEOUT=120/TIMEOUT=120/g' \
|
||||||
|
"$ABRA_DIR/servers/$TEST_SERVER/$TEST_APP_DOMAIN.env"
|
||||||
|
assert_success
|
||||||
|
|
||||||
|
run $ABRA app deploy "$TEST_APP_DOMAIN" --no-input --no-converge-checks --debug
|
||||||
|
assert_success
|
||||||
|
assert_output --partial "timeout: set to 120"
|
||||||
|
}
|
||||||
|
|
||||||
# bats test_tags=slow
|
# bats test_tags=slow
|
||||||
@test "no chaos version label if no chaos" {
|
@test "no chaos version label if no chaos" {
|
||||||
_deploy_app
|
_deploy_app
|
||||||
|
@ -110,6 +110,34 @@ teardown(){
|
|||||||
assert_output --partial "0.1.0+1.20.0"
|
assert_output --partial "0.1.0+1.20.0"
|
||||||
}
|
}
|
||||||
|
|
||||||
|
# bats test_tags=slow
|
||||||
|
@test "ignore timeout when not present in env" {
|
||||||
|
run $ABRA app deploy "$TEST_APP_DOMAIN" --no-input --no-converge-checks
|
||||||
|
assert_success
|
||||||
|
|
||||||
|
# NOTE(d1): only recipe versions >= 0.3.5+1.21.0 have the TIMEOUT not set to
|
||||||
|
# a default in the compose.yml. so we force a rollback to that version
|
||||||
|
# specifically
|
||||||
|
run $ABRA app rollback "$TEST_APP_DOMAIN" "0.3.5+1.21.0" \
|
||||||
|
--no-input --no-converge-checks --debug --force
|
||||||
|
assert_success
|
||||||
|
refute_output --partial "timeout: set to"
|
||||||
|
}
|
||||||
|
|
||||||
|
# bats test_tags=slow
|
||||||
|
@test "use timeout when present in env" {
|
||||||
|
run $ABRA app deploy "$TEST_APP_DOMAIN" --no-input --no-converge-checks
|
||||||
|
assert_success
|
||||||
|
|
||||||
|
run sed -i 's/#TIMEOUT=120/TIMEOUT=120/g' \
|
||||||
|
"$ABRA_DIR/servers/$TEST_SERVER/$TEST_APP_DOMAIN.env"
|
||||||
|
assert_success
|
||||||
|
|
||||||
|
run $ABRA app rollback "$TEST_APP_DOMAIN" --no-input --no-converge-checks --debug
|
||||||
|
assert_success
|
||||||
|
assert_output --partial "timeout: set to 120"
|
||||||
|
}
|
||||||
|
|
||||||
# bats test_tags=slow
|
# bats test_tags=slow
|
||||||
@test "force rollback to previous version" {
|
@test "force rollback to previous version" {
|
||||||
run $ABRA app deploy "$TEST_APP_DOMAIN" "0.1.1+1.20.2" --no-input --no-converge-checks
|
run $ABRA app deploy "$TEST_APP_DOMAIN" "0.1.1+1.20.2" --no-input --no-converge-checks
|
||||||
|
@ -206,7 +206,7 @@ teardown(){
|
|||||||
run $ABRA app secret insert "$TEST_APP_DOMAIN" bar
|
run $ABRA app secret insert "$TEST_APP_DOMAIN" bar
|
||||||
assert_failure
|
assert_failure
|
||||||
|
|
||||||
run $ABRA app secret insert "$TEST_APP_DOMAIN" bar baz
|
run $ABRA app secret insert "$TEST_APP_DOMAIN" bar baz --no-input
|
||||||
assert_failure
|
assert_failure
|
||||||
|
|
||||||
run bash -c "echo foo | $ABRA app secret insert $TEST_APP_DOMAIN bar baz -f"
|
run bash -c "echo foo | $ABRA app secret insert $TEST_APP_DOMAIN bar baz -f"
|
||||||
|
@ -116,6 +116,28 @@ teardown(){
|
|||||||
assert_success
|
assert_success
|
||||||
}
|
}
|
||||||
|
|
||||||
|
# bats test_tags=slow
|
||||||
|
@test "ignore timeout when not present in env" {
|
||||||
|
_deploy_app
|
||||||
|
|
||||||
|
run $ABRA app undeploy "$TEST_APP_DOMAIN" --no-input --debug
|
||||||
|
assert_success
|
||||||
|
refute_output --partial "timeout: set to"
|
||||||
|
}
|
||||||
|
|
||||||
|
# bats test_tags=slow
|
||||||
|
@test "use timeout when present in env" {
|
||||||
|
_deploy_app
|
||||||
|
|
||||||
|
run sed -i 's/#TIMEOUT=120/TIMEOUT=120/g' \
|
||||||
|
"$ABRA_DIR/servers/$TEST_SERVER/$TEST_APP_DOMAIN.env"
|
||||||
|
assert_success
|
||||||
|
|
||||||
|
run $ABRA app undeploy "$TEST_APP_DOMAIN" --no-input --debug
|
||||||
|
assert_success
|
||||||
|
assert_output --partial "timeout: set to 120"
|
||||||
|
}
|
||||||
|
|
||||||
# bats test_tags=slow
|
# bats test_tags=slow
|
||||||
@test "undeploy chaos deployment" {
|
@test "undeploy chaos deployment" {
|
||||||
run $ABRA app deploy "$TEST_APP_DOMAIN" --no-input --chaos
|
run $ABRA app deploy "$TEST_APP_DOMAIN" --no-input --chaos
|
||||||
|
@ -152,6 +152,33 @@ teardown(){
|
|||||||
assert_output --partial "$(_latest_release)"
|
assert_output --partial "$(_latest_release)"
|
||||||
}
|
}
|
||||||
|
|
||||||
|
# bats test_tags=slow
|
||||||
|
@test "ignore timeout when not present in env" {
|
||||||
|
run $ABRA app deploy "$TEST_APP_DOMAIN" "0.1.0+1.20.0" --no-input --no-converge-checks
|
||||||
|
assert_success
|
||||||
|
assert_output --partial '0.1.0+1.20.0'
|
||||||
|
|
||||||
|
run $ABRA app upgrade "$TEST_APP_DOMAIN" --no-input --no-converge-checks --debug
|
||||||
|
assert_success
|
||||||
|
refute_output --partial "timeout: set to"
|
||||||
|
}
|
||||||
|
|
||||||
|
# bats test_tags=slow
|
||||||
|
@test "use timeout when present in env" {
|
||||||
|
run $ABRA app deploy "$TEST_APP_DOMAIN" "0.1.0+1.20.0" --no-input --no-converge-checks
|
||||||
|
assert_success
|
||||||
|
assert_output --partial '0.1.0+1.20.0'
|
||||||
|
|
||||||
|
run sed -i 's/#TIMEOUT=120/TIMEOUT=120/g' \
|
||||||
|
"$ABRA_DIR/servers/$TEST_SERVER/$TEST_APP_DOMAIN.env"
|
||||||
|
assert_success
|
||||||
|
|
||||||
|
run $ABRA app upgrade "$TEST_APP_DOMAIN" --no-input --no-converge-checks --debug
|
||||||
|
assert_success
|
||||||
|
assert_output --partial "timeout: set to 120"
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
# bats test_tags=slow
|
# bats test_tags=slow
|
||||||
@test "show single release note" {
|
@test "show single release note" {
|
||||||
run $ABRA app deploy "$TEST_APP_DOMAIN" "0.1.1+1.20.2" --no-input --no-converge-checks
|
run $ABRA app deploy "$TEST_APP_DOMAIN" "0.1.1+1.20.2" --no-input --no-converge-checks
|
||||||
|
6
tests/resources/test_recipe/.env.sample
Normal file
6
tests/resources/test_recipe/.env.sample
Normal file
@ -0,0 +1,6 @@
|
|||||||
|
RECIPE=test_recipe
|
||||||
|
DOMAIN=test_app.example.com
|
||||||
|
|
||||||
|
# NOTE(d1): ensure commented out TIMEOUT doesn't get included
|
||||||
|
# see TestReadEnv in ./pkg/envfile
|
||||||
|
# TIMEOUT=120
|
15
tests/resources/test_recipe/compose.yml
Normal file
15
tests/resources/test_recipe/compose.yml
Normal file
@ -0,0 +1,15 @@
|
|||||||
|
---
|
||||||
|
version: "3.8"
|
||||||
|
|
||||||
|
services:
|
||||||
|
app:
|
||||||
|
image: nginx:1.29.0
|
||||||
|
networks:
|
||||||
|
- proxy
|
||||||
|
deploy:
|
||||||
|
labels:
|
||||||
|
- "coop-cloud.${STACK_NAME}.timeout=${TIMEOUT}"
|
||||||
|
|
||||||
|
networks:
|
||||||
|
proxy:
|
||||||
|
external: true
|
6
tests/resources/test_server/test_app.example.com.env
Normal file
6
tests/resources/test_server/test_app.example.com.env
Normal file
@ -0,0 +1,6 @@
|
|||||||
|
RECIPE=test_recipe
|
||||||
|
DOMAIN=test_app.example.com
|
||||||
|
|
||||||
|
# NOTE(d1): ensure commented out TIMEOUT doesn't get included
|
||||||
|
# see TestReadEnv in ./pkg/envfile
|
||||||
|
# TIMEOUT=120
|
@ -1,3 +0,0 @@
|
|||||||
RECIPE=ecloud
|
|
||||||
DOMAIN=ecloud.evil.corp
|
|
||||||
SMTP_AUTHTYPE=login
|
|
Reference in New Issue
Block a user