From a2940a84b37adb1c21add9c7f7fb470fbabc1bf7 Mon Sep 17 00:00:00 2001 From: decentral1se Date: Mon, 25 Aug 2025 11:35:56 +0200 Subject: [PATCH 1/5] chore: restructure unit testing resources --- pkg/test/test.go | 101 +++++++++--------- .../{test_folder => test_dir}/bar.env | 0 .../{test_folder => test_dir}/cruft.txt | 0 .../folder1 => test_dir/dir1}/readme.txt | 0 .../folder2 => test_dir/dir2}/fsociety00.dat | 0 .../{test_folder => test_dir}/foo.env | 0 tests/resources/test_recipe/.env.sample | 6 ++ tests/resources/test_recipe/compose.yml | 15 +++ .../test_server/test_app.example.com.env | 6 ++ .../servers/evil.corp/ecloud.env | 3 - 10 files changed, 77 insertions(+), 54 deletions(-) rename tests/resources/{test_folder => test_dir}/bar.env (100%) rename tests/resources/{test_folder => test_dir}/cruft.txt (100%) rename tests/resources/{test_folder/folder1 => test_dir/dir1}/readme.txt (100%) rename tests/resources/{test_folder/folder2 => test_dir/dir2}/fsociety00.dat (100%) rename tests/resources/{test_folder => test_dir}/foo.env (100%) create mode 100644 tests/resources/test_recipe/.env.sample create mode 100644 tests/resources/test_recipe/compose.yml create mode 100644 tests/resources/test_server/test_app.example.com.env delete mode 100644 tests/resources/valid_abra_config/servers/evil.corp/ecloud.env diff --git a/pkg/test/test.go b/pkg/test/test.go index 92e88656..d8768c4d 100644 --- a/pkg/test/test.go +++ b/pkg/test/test.go @@ -1,6 +1,7 @@ package test import ( + "fmt" "os" "path" @@ -11,70 +12,68 @@ import ( ) var ( - TestFolder = os.ExpandEnv("$PWD/../../tests/resources/test_folder") - ValidAbraConf = os.ExpandEnv("$PWD/../../tests/resources/valid_abra_config") + AppName = "test_app.example.com" + 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() { - testAppLink := os.ExpandEnv("$HOME/.abra/servers/foo.com") - if err := os.Remove(testAppLink); err != nil { - log.Fatal(err) - } + testAppLink := os.ExpandEnv("$ABRA_DIR/servers/test_server") + os.Remove(testAppLink) - testRecipeLink := os.ExpandEnv("$HOME/.abra/recipes/test") - if err := os.Remove(testRecipeLink); err != nil { - log.Fatal(err) - } + testRecipeLink := os.ExpandEnv("$ABRA_DIR/recipes/test_recipe") + os.Remove(testRecipeLink) } -// MkServerAppRecipe symlinks the test server / app / recipe. func MkServerAppRecipe() { RmServerAppRecipe() - testAppDir := os.ExpandEnv("$PWD/../../tests/resources/testapp") - testAppLink := os.ExpandEnv("$HOME/.abra/servers/foo.com") + if err := os.Mkdir(os.ExpandEnv("$ABRA_DIR/servers"), 0700); err != nil { + 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 { log.Fatal(err) } - testRecipeDir := os.ExpandEnv("$PWD/../../tests/resources/testrecipe") - testRecipeLink := os.ExpandEnv("$HOME/.abra/recipes/test") + testRecipeDir := os.ExpandEnv("$PWD/../../tests/resources/test_recipe") + testRecipeLink := os.ExpandEnv("$ABRA_DIR/recipes/test_recipe") if err := os.Symlink(testRecipeDir, testRecipeLink); err != nil { log.Fatal(err) } diff --git a/tests/resources/test_folder/bar.env b/tests/resources/test_dir/bar.env similarity index 100% rename from tests/resources/test_folder/bar.env rename to tests/resources/test_dir/bar.env diff --git a/tests/resources/test_folder/cruft.txt b/tests/resources/test_dir/cruft.txt similarity index 100% rename from tests/resources/test_folder/cruft.txt rename to tests/resources/test_dir/cruft.txt diff --git a/tests/resources/test_folder/folder1/readme.txt b/tests/resources/test_dir/dir1/readme.txt similarity index 100% rename from tests/resources/test_folder/folder1/readme.txt rename to tests/resources/test_dir/dir1/readme.txt diff --git a/tests/resources/test_folder/folder2/fsociety00.dat b/tests/resources/test_dir/dir2/fsociety00.dat similarity index 100% rename from tests/resources/test_folder/folder2/fsociety00.dat rename to tests/resources/test_dir/dir2/fsociety00.dat diff --git a/tests/resources/test_folder/foo.env b/tests/resources/test_dir/foo.env similarity index 100% rename from tests/resources/test_folder/foo.env rename to tests/resources/test_dir/foo.env diff --git a/tests/resources/test_recipe/.env.sample b/tests/resources/test_recipe/.env.sample new file mode 100644 index 00000000..f2f4dcb2 --- /dev/null +++ b/tests/resources/test_recipe/.env.sample @@ -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 diff --git a/tests/resources/test_recipe/compose.yml b/tests/resources/test_recipe/compose.yml new file mode 100644 index 00000000..8d8ff83a --- /dev/null +++ b/tests/resources/test_recipe/compose.yml @@ -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 diff --git a/tests/resources/test_server/test_app.example.com.env b/tests/resources/test_server/test_app.example.com.env new file mode 100644 index 00000000..dd74c315 --- /dev/null +++ b/tests/resources/test_server/test_app.example.com.env @@ -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 \ No newline at end of file diff --git a/tests/resources/valid_abra_config/servers/evil.corp/ecloud.env b/tests/resources/valid_abra_config/servers/evil.corp/ecloud.env deleted file mode 100644 index db05f8ca..00000000 --- a/tests/resources/valid_abra_config/servers/evil.corp/ecloud.env +++ /dev/null @@ -1,3 +0,0 @@ -RECIPE=ecloud -DOMAIN=ecloud.evil.corp -SMTP_AUTHTYPE=login \ No newline at end of file -- 2.49.0 From 97377dea395444b03bd3835079d1535f39244c25 Mon Sep 17 00:00:00 2001 From: decentral1se Date: Mon, 25 Aug 2025 11:36:33 +0200 Subject: [PATCH 2/5] test(integration): ensure timeout handling --- tests/integration/app_deploy.bats | 18 ++++++++++++++++++ tests/integration/app_rollback.bats | 28 ++++++++++++++++++++++++++++ tests/integration/app_undeploy.bats | 22 ++++++++++++++++++++++ tests/integration/app_upgrade.bats | 27 +++++++++++++++++++++++++++ 4 files changed, 95 insertions(+) diff --git a/tests/integration/app_deploy.bats b/tests/integration/app_deploy.bats index 87c3e79c..b3b04204 100644 --- a/tests/integration/app_deploy.bats +++ b/tests/integration/app_deploy.bats @@ -444,6 +444,24 @@ teardown(){ 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 @test "no chaos version label if no chaos" { _deploy_app diff --git a/tests/integration/app_rollback.bats b/tests/integration/app_rollback.bats index 53e17d95..deb9e340 100644 --- a/tests/integration/app_rollback.bats +++ b/tests/integration/app_rollback.bats @@ -110,6 +110,34 @@ teardown(){ 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 @test "force rollback to previous version" { run $ABRA app deploy "$TEST_APP_DOMAIN" "0.1.1+1.20.2" --no-input --no-converge-checks diff --git a/tests/integration/app_undeploy.bats b/tests/integration/app_undeploy.bats index c991f8ac..f9d3ddd2 100644 --- a/tests/integration/app_undeploy.bats +++ b/tests/integration/app_undeploy.bats @@ -116,6 +116,28 @@ teardown(){ 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 @test "undeploy chaos deployment" { run $ABRA app deploy "$TEST_APP_DOMAIN" --no-input --chaos diff --git a/tests/integration/app_upgrade.bats b/tests/integration/app_upgrade.bats index 47cd22a3..3328fd5f 100644 --- a/tests/integration/app_upgrade.bats +++ b/tests/integration/app_upgrade.bats @@ -152,6 +152,33 @@ teardown(){ 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 @test "show single release note" { run $ABRA app deploy "$TEST_APP_DOMAIN" "0.1.1+1.20.2" --no-input --no-converge-checks -- 2.49.0 From 44a7d288af076930b103026087dc2ec37540c8ce Mon Sep 17 00:00:00 2001 From: decentral1se Date: Mon, 25 Aug 2025 11:37:49 +0200 Subject: [PATCH 3/5] test(unit): ensure timeout handling --- pkg/app/compose_test.go | 62 +++++++++++++++++++++++++++++++++++++++++ 1 file changed, 62 insertions(+) create mode 100644 pkg/app/compose_test.go diff --git a/pkg/app/compose_test.go b/pkg/app/compose_test.go new file mode 100644 index 00000000..f66e7565 --- /dev/null +++ b/pkg/app/compose_test.go @@ -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) + } +} -- 2.49.0 From 6a52575ae06bdb2b3cf046073bd9ec2b613384d4 Mon Sep 17 00:00:00 2001 From: decentral1se Date: Mon, 25 Aug 2025 11:38:07 +0200 Subject: [PATCH 4/5] refactor!: do not set default timeout See https://git.coopcloud.tech/toolshed/abra/issues/596 Quite some `i18n.G` additions along the way! --- cli/app/deploy.go | 2 - cli/app/rollback.go | 2 - cli/app/undeploy.go | 2 - cli/app/upgrade.go | 2 - pkg/app/compose.go | 17 ++++++-- pkg/envfile/envfile_test.go | 4 +- pkg/ui/deploy.go | 5 ++- pkg/upstream/stack/loader.go | 13 +++--- pkg/upstream/stack/remove.go | 52 +++++++++++++++--------- pkg/upstream/stack/stack.go | 79 +++++++++++++++++++----------------- 10 files changed, 99 insertions(+), 79 deletions(-) diff --git a/cli/app/deploy.go b/cli/app/deploy.go index 19cd8872..754865f4 100644 --- a/cli/app/deploy.go +++ b/cli/app/deploy.go @@ -200,8 +200,6 @@ checkout as-is. Recipe commit hashes are also supported as values for log.Fatal(err) } - log.Debug(i18n.G("set waiting timeout to %d second(s)", stack.WaitTimeout)) - serviceNames, err := appPkg.GetAppServiceNames(app.Name) if err != nil { log.Fatal(err) diff --git a/cli/app/rollback.go b/cli/app/rollback.go index 0218dd0a..1d49aaec 100644 --- a/cli/app/rollback.go +++ b/cli/app/rollback.go @@ -199,8 +199,6 @@ beforehand. See "abra app backup" for more.`), log.Fatal(err) } - log.Debug(i18n.G("set waiting timeout to %d second(s)", stack.WaitTimeout)) - serviceNames, err := appPkg.GetAppServiceNames(app.Name) if err != nil { log.Fatal(err) diff --git a/cli/app/undeploy.go b/cli/app/undeploy.go index ecb32365..3f5124dd 100644 --- a/cli/app/undeploy.go +++ b/cli/app/undeploy.go @@ -85,8 +85,6 @@ Passing "--prune/-p" does not remove those volumes.`), log.Fatal(err) } - log.Info(i18n.G("initialising undeploy")) - rmOpts := stack.Remove{ Namespaces: []string{stackName}, Detach: false, diff --git a/cli/app/upgrade.go b/cli/app/upgrade.go index be574c45..489030d6 100644 --- a/cli/app/upgrade.go +++ b/cli/app/upgrade.go @@ -237,8 +237,6 @@ beforehand. See "abra app backup" for more.`), log.Fatal(err) } - log.Debug(i18n.G("set waiting timeout to %d second(s)", stack.WaitTimeout)) - serviceNames, err := appPkg.GetAppServiceNames(app.Name) if err != nil { log.Fatal(err) diff --git a/pkg/app/compose.go b/pkg/app/compose.go index dd5070b1..781d5877 100644 --- a/pkg/app/compose.go +++ b/pkg/app/compose.go @@ -1,6 +1,7 @@ package app import ( + "errors" "fmt" "strconv" @@ -87,13 +88,21 @@ func GetLabel(compose *composetypes.Config, stackName string, label string) stri 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) { - timeout := 50 // Default Timeout - var err error = nil + var timeout int + if timeoutLabel := GetLabel(compose, stackName, "timeout"); timeoutLabel != "" { log.Debug(i18n.G("timeout label: %s", timeoutLabel)) + + var err error 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 } diff --git a/pkg/envfile/envfile_test.go b/pkg/envfile/envfile_test.go index f669ce90..9ae75da6 100644 --- a/pkg/envfile/envfile_test.go +++ b/pkg/envfile/envfile_test.go @@ -15,7 +15,7 @@ import ( ) func TestGetAllFoldersInDirectory(t *testing.T) { - folders, err := config.GetAllFoldersInDirectory(testPkg.TestFolder) + folders, err := config.GetAllFoldersInDirectory(testPkg.TestDir) if err != nil { t.Fatal(err) } @@ -25,7 +25,7 @@ func TestGetAllFoldersInDirectory(t *testing.T) { } func TestGetAllFilesInDirectory(t *testing.T) { - files, err := config.GetAllFilesInDirectory(testPkg.TestFolder) + files, err := config.GetAllFilesInDirectory(testPkg.TestDir) if err != nil { t.Fatal(err) } diff --git a/pkg/ui/deploy.go b/pkg/ui/deploy.go index 20a8c87a..92bb41ce 100644 --- a/pkg/ui/deploy.go +++ b/pkg/ui/deploy.go @@ -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() }) return tea.Batch(cmds...) diff --git a/pkg/upstream/stack/loader.go b/pkg/upstream/stack/loader.go index ab0e609e..1910129a 100644 --- a/pkg/upstream/stack/loader.go +++ b/pkg/upstream/stack/loader.go @@ -7,10 +7,12 @@ import ( "sort" "strings" + "coopcloud.tech/abra/pkg/i18n" "coopcloud.tech/abra/pkg/log" "github.com/docker/cli/cli/compose/loader" "github.com/docker/cli/cli/compose/schema" composetypes "github.com/docker/cli/cli/compose/types" + "github.com/pkg/errors" ) // 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...) if err != nil { if fpe, ok := err.(*loader.ForbiddenPropertiesError); ok { - return nil, fmt.Errorf("compose file contains unsupported options: %s", - propertyWarnings(fpe.Properties)) + return nil, errors.New(i18n.G("compose file contains unsupported options: %s", propertyWarnings(fpe.Properties))) } return nil, err } @@ -51,14 +52,12 @@ func LoadComposefile(opts Deploy, appEnv map[string]string, options ...func(*loa unsupportedProperties := loader.GetUnsupportedProperties(dicts...) if len(unsupportedProperties) > 0 { - log.Warnf("%s: ignoring unsupported options: %s", - recipeName, strings.Join(unsupportedProperties, ", ")) + log.Warn(i18n.G("%s: ignoring unsupported options: %s", recipeName, strings.Join(unsupportedProperties, ", "))) } deprecatedProperties := loader.GetDeprecatedProperties(dicts...) if len(deprecatedProperties) > 0 { - log.Warnf("%s: ignoring deprecated options: %s", - recipeName, propertyWarnings(deprecatedProperties)) + log.Warn(i18n.G("%s: ignoring deprecated options: %s", recipeName, propertyWarnings(deprecatedProperties))) } return config, nil } @@ -106,7 +105,7 @@ func buildEnvironment(env []string) (map[string]string, error) { for _, s := range env { // if value is empty, s is like "K=", not "K". 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) result[kv[0]] = kv[1] diff --git a/pkg/upstream/stack/remove.go b/pkg/upstream/stack/remove.go index a2fde29d..21825e30 100644 --- a/pkg/upstream/stack/remove.go +++ b/pkg/upstream/stack/remove.go @@ -9,6 +9,7 @@ import ( "strings" "time" + "coopcloud.tech/abra/pkg/i18n" "coopcloud.tech/abra/pkg/log" "github.com/docker/docker/api/types" "github.com/docker/docker/api/types/network" @@ -21,6 +22,12 @@ import ( // RunRemove is the swarm implementation of docker stack remove 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) signal.Notify(sigIntCh, os.Interrupt) 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 { - log.Warnf("nothing found in stack: %s", namespace) + log.Warn(i18n.G("nothing found in stack: %s", namespace)) continue } @@ -72,17 +79,17 @@ func RunRemove(ctx context.Context, client *apiclient.Client, opts Remove) error hasError = removeNetworks(ctx, client, networks) || 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 } - log.Info("polling undeploy status") + log.Info(i18n.G("polling undeploy status")) timeout, err := waitOnTasks(ctx, client, namespace) if timeout { errs = append(errs, err.Error()) } else { 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: return nil 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: return err } @@ -121,10 +128,10 @@ func removeServices( var hasError bool sort.Slice(services, sortServiceByName(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 { 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 @@ -137,10 +144,10 @@ func removeNetworks( ) bool { var hasError bool 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 { 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 @@ -153,10 +160,10 @@ func removeSecrets( ) bool { var hasError bool 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 { 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 @@ -169,10 +176,10 @@ func removeConfigs( ) bool { var hasError bool 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 { 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 @@ -206,12 +213,17 @@ func terminalState(state swarm.TaskState) bool { func waitOnTasks(ctx context.Context, client apiclient.APIClient, namespace string) (bool, error) { var timedOut bool - log.Debugf("waiting on undeploy tasks (timeout=%v secs)", WaitTimeout) - go func() { - t := time.Duration(WaitTimeout) * time.Second - <-time.After(t) - log.Debug("timed out on undeploy") + if WaitTimeout == 0 { + return + } + + 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 }() @@ -219,7 +231,7 @@ func waitOnTasks(ctx context.Context, client apiclient.APIClient, namespace stri for { tasks, err := getStackTasks(ctx, client, namespace) 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 { @@ -234,7 +246,7 @@ func waitOnTasks(ctx context.Context, client apiclient.APIClient, namespace stri } if timedOut { - return true, fmt.Errorf("deployment timed out 🟠") + return true, errors.New(i18n.G("deployment timed out 🟠")) } } diff --git a/pkg/upstream/stack/stack.go b/pkg/upstream/stack/stack.go index b2bf1230..628ee414 100644 --- a/pkg/upstream/stack/stack.go +++ b/pkg/upstream/stack/stack.go @@ -14,6 +14,7 @@ import ( tea "github.com/charmbracelet/bubbletea" "coopcloud.tech/abra/pkg/config" + "coopcloud.tech/abra/pkg/i18n" "coopcloud.tech/abra/pkg/log" "coopcloud.tech/abra/pkg/ui" "coopcloud.tech/abra/pkg/upstream/convert" @@ -39,8 +40,9 @@ const ( ResolveImageNever = "never" ) -// Timeout to wait until docker services converge, default is 50s (random choice) -var WaitTimeout = 50 +// Timeout to wait until docker services converge. This timeout is disabled by +// default but can be configured by passing a TIMEOUT=... in the app .env +var WaitTimeout = 0 type StackStatus struct { 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 { boolVal, err := strconv.ParseBool(isChaos) 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 } @@ -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 } - 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 } @@ -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{}) { oldServices, err := GetStackServices(ctx, cl, namespace.Name()) if err != nil { - log.Warnf("failed to list services: %s", err) + log.Warn(i18n.G("failed to list services: %s", err)) } pruneServices := []swarm.Service{} @@ -201,7 +203,11 @@ func RunDeploy( dontWait bool, filters filters.Args, ) 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 { return err @@ -230,7 +236,7 @@ func validateResolveImageFlag(opts *Deploy) error { case ResolveImageAlways, ResolveImageChanged, ResolveImageNever: return nil 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 { - log.Warn("skipping converge logic checks") + log.Warn(i18n.G("skipping converge logic checks")) return nil } @@ -339,11 +345,11 @@ func validateExternalNetworks(ctx context.Context, client dockerClient.NetworkAP network, err := client.NetworkInspect(ctx, networkName, networktypes.InspectOptions{}) switch { 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: return err 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 @@ -356,13 +362,13 @@ func createSecrets(ctx context.Context, cl *dockerClient.Client, secrets []swarm case err == nil: // secret already exists, then we update that 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): // 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 { - 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: return err @@ -378,13 +384,13 @@ func createConfigs(ctx context.Context, cl *dockerClient.Client, configs []swarm case err == nil: // config already exists, then we update that 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): // config does not exist, then we create a new one. log.Debugf("creating config %s", configSpec.Name) 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: return err @@ -413,9 +419,9 @@ func createNetworks(ctx context.Context, cl *dockerClient.Client, namespace conv 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 { - return errors.Wrapf(err, "failed to create network %s", name) + return errors.Wrap(err, i18n.G("failed to create network %s", name)) } } return nil @@ -455,16 +461,16 @@ func deployServices( if sendAuth { dockerCLI, err := command.NewDockerCli() 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) 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 { - log.Debugf("updating %s", name) + log.Debug(i18n.G("updating %s", name)) updateOpts := types.ServiceUpdateOptions{EncodedRegistryAuth: encodedAuth} @@ -499,7 +505,7 @@ func deployServices( response, err := cl.ServiceUpdate(ctx, service.ID, service.Version, serviceSpec, updateOpts) 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 { @@ -511,7 +517,7 @@ func deployServices( ID: service.ID, }) } else { - log.Debugf("creating %s", name) + log.Debug(i18n.G("creating %s", name)) createOpts := types.ServiceCreateOptions{EncodedRegistryAuth: encodedAuth} @@ -522,7 +528,7 @@ func deployServices( serviceCreateResponse, err := cl.ServiceCreate(ctx, serviceSpec, createOpts) 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{ @@ -567,7 +573,7 @@ func WaitOnServices(ctx context.Context, cl *dockerClient.Client, opts WaitOpts) tui := tea.NewProgram(model) if !opts.Quiet { - log.Info("polling deployment status") + log.Info(i18n.G("polling deployment status")) } m, err := log.Without( @@ -576,7 +582,7 @@ func WaitOnServices(ctx context.Context, cl *dockerClient.Client, opts WaitOpts) }, ) 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) @@ -584,16 +590,16 @@ func WaitOnServices(ctx context.Context, cl *dockerClient.Client, opts WaitOpts) var errs []error if deployModel.Failed { - errs = append(errs, fmt.Errorf("deploy failed 🛑")) + errs = append(errs, errors.New(i18n.G("deploy failed 🛑"))) } else if deployModel.TimedOut { - errs = append(errs, fmt.Errorf("deploy timed out 🟠")) + errs = append(errs, errors.New(i18n.G("deploy timed out 🟠"))) } else { - errs = append(errs, fmt.Errorf("deploy in progress 🟠")) + errs = append(errs, errors.New(i18n.G("deploy in progress 🟠"))) } for _, s := range *deployModel.Streams { 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 { - 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) 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() s := strings.Join(*deployModel.Logs, "\n") 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...) } if !opts.Quiet { - log.Info("deploy succeeded 🟢") + log.Info(i18n.G("deploy succeeded 🟢")) } return nil @@ -646,8 +652,7 @@ func GetStacks(cl *dockerClient.Client) ([]*formatter.Stack, error) { labels := service.Spec.Labels name, ok := labels[convert.LabelNamespace] if !ok { - return nil, errors.Errorf("cannot get label %s for %s", - convert.LabelNamespace, service.ID) + return nil, errors.New(i18n.G("cannot get label %s for %s", convert.LabelNamespace, service.ID)) } ztack, ok := m[name] if !ok { -- 2.49.0 From f39eab8f1ec03d29c692f8fc5626051aeb91b272 Mon Sep 17 00:00:00 2001 From: decentral1se Date: Mon, 25 Aug 2025 11:55:33 +0200 Subject: [PATCH 5/5] test: pass ABRA_DIR to unit test on CI --- .drone.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/.drone.yml b/.drone.yml index 764d5766..d5d9809d 100644 --- a/.drone.yml +++ b/.drone.yml @@ -10,6 +10,7 @@ steps: - name: make test image: golang:1.24 environment: + ABRA_DIR: $HOME/.abra CATL_URL: https://git.coopcloud.tech/toolshed/recipes-catalogue-json.git commands: - mkdir -p $HOME/.abra -- 2.49.0