refactor!: do not set default timeout

See toolshed/abra#596

Quite some `i18n.G` additions along the way!
This commit is contained in:
2025-08-25 11:38:07 +02:00
parent 44a7d288af
commit 6a52575ae0
10 changed files with 99 additions and 79 deletions

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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