diff --git a/go.mod b/go.mod index 4e664b2759..cb01074d54 100644 --- a/go.mod +++ b/go.mod @@ -10,6 +10,7 @@ require ( github.com/boumenot/gocover-cobertura v1.2.0 github.com/cbroglie/mustache v1.4.0 github.com/cespare/xxhash/v2 v2.3.0 + github.com/creack/pty v1.1.21 github.com/dustin/go-humanize v1.0.1 github.com/elastic/elastic-integration-corpus-generator-tool v0.10.0 github.com/elastic/go-elasticsearch/v7 v7.17.10 diff --git a/go.sum b/go.sum index 19b5e54d66..59cdaf44dc 100644 --- a/go.sum +++ b/go.sum @@ -71,8 +71,8 @@ github.com/cloudflare/circl v1.3.7/go.mod h1:sRTcRWXGLrKw6yIGJ+l7amYJFfAXbZG0kBS github.com/cpuguy83/go-md2man/v2 v2.0.3/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o= github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E= github.com/creack/pty v1.1.17/go.mod h1:MOBLtS5ELjhRRrroQr9kyvTxUAFNvYEK993ew/Vr4O4= -github.com/creack/pty v1.1.18 h1:n56/Zwd5o6whRC5PMGretI4IdRLlmBXYNjScPaBgsbY= -github.com/creack/pty v1.1.18/go.mod h1:MOBLtS5ELjhRRrroQr9kyvTxUAFNvYEK993ew/Vr4O4= +github.com/creack/pty v1.1.21 h1:1/QdRyBaHHJP61QkWMXlOIBfsgdDeeKfK8SYVUWJKf0= +github.com/creack/pty v1.1.21/go.mod h1:MOBLtS5ELjhRRrroQr9kyvTxUAFNvYEK993ew/Vr4O4= github.com/creasty/defaults v1.7.0 h1:eNdqZvc5B509z18lD8yc212CAqJNvfT1Jq6L8WowdBA= github.com/creasty/defaults v1.7.0/go.mod h1:iGzKe6pbEHnpMPtfDXZEr0NVxWnPTjb1bbDy08fPzYM= github.com/cyphar/filepath-securejoin v0.2.4 h1:Ugdm7cg7i6ZK6x3xDF1oEu1nfkyfH53EtKeQYTC3kyg= diff --git a/internal/compose/compose.go b/internal/compose/compose.go index 41f156da21..81163f6d30 100644 --- a/internal/compose/compose.go +++ b/internal/compose/compose.go @@ -5,6 +5,7 @@ package compose import ( + "bufio" "bytes" "context" "errors" @@ -12,12 +13,15 @@ import ( "io" "os" "os/exec" + "regexp" "runtime" "strconv" "strings" + "sync" "time" "github.com/Masterminds/semver/v3" + "github.com/creack/pty" "gopkg.in/yaml.v3" @@ -486,16 +490,66 @@ func (p *Project) runDockerComposeCmd(ctx context.Context, opts dockerComposeOpt } cmd.Env = append(os.Environ(), opts.env...) + ptty, tty, err := pty.Open() + if err != nil { + return fmt.Errorf("failed to open pseudo-tty to capture stderr: %w", err) + } + + var errBuffer bytes.Buffer + cmd.Stderr = tty + var stderr io.Writer = &errBuffer if logger.IsDebugMode() { cmd.Stdout = os.Stdout - cmd.Stderr = os.Stderr + stderr = io.MultiWriter(&errBuffer, os.Stderr) } if opts.stdout != nil { cmd.Stdout = opts.stdout } + var wg sync.WaitGroup + wg.Add(1) + go func() { + defer wg.Done() + io.Copy(stderr, ptty) + }() + logger.Debugf("running command: %s", cmd) - return cmd.Run() + err = cmd.Run() + ptty.Close() + tty.Close() + wg.Wait() + if err != nil { + if msg := cleanComposeError(errBuffer.String()); len(msg) > 0 { + return fmt.Errorf("%w: %s", err, msg) + } + } + return err +} + +const daemonResponse = `Error response from daemon:` + +// This regexp must match prefixes like WARN[0000], which may include escape sequences for colored letters +// or structured logs, starting with key=value pairs. +var composeLoggerPrefix = regexp.MustCompile(`^[^\s]+\[[0-9]+\]`) + +func cleanComposeError(msg string) string { + // If there is a daemon response, just return it. + if i := strings.Index(msg, daemonResponse); i >= 0 { + return strings.TrimSpace(msg[i+len(daemonResponse):]) + } + + // Filter out lines coming from the docker compose structured logger. + var cleanError strings.Builder + scanner := bufio.NewScanner(strings.NewReader(msg)) + for scanner.Scan() { + line := strings.TrimSpace(scanner.Text()) + if composeLoggerPrefix.MatchString(line) { + continue + } + fmt.Fprintln(&cleanError, line) + } + + return strings.TrimSpace(cleanError.String()) } func (p *Project) dockerComposeBaseCommand() (name string, args []string) { diff --git a/internal/stack/update.go b/internal/stack/update.go index 991622e641..ee7f6dfd80 100644 --- a/internal/stack/update.go +++ b/internal/stack/update.go @@ -25,7 +25,7 @@ func Update(ctx context.Context, options Options) error { err = dockerComposePull(ctx, options) if err != nil { - return fmt.Errorf("updating docker images failed: %w", err) + return fmt.Errorf("pulling docker images failed: %w", err) } return nil }