Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add options to redirect compose command stdout and stderr to custom writers #470

Closed
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
98 changes: 30 additions & 68 deletions compose.go
@@ -1,7 +1,6 @@
package testcontainers

import (
"bytes"
"context"
"fmt"
"io"
Expand All @@ -11,7 +10,6 @@ import (
"path/filepath"
"runtime"
"strings"
"sync"

"github.com/docker/docker/api/types"
"github.com/docker/docker/api/types/filters"
Expand All @@ -34,6 +32,8 @@ type DockerCompose interface {
WithCommand([]string) DockerCompose
WithEnv(map[string]string) DockerCompose
WithExposedService(string, int, wait.Strategy) DockerCompose
WithStdoutPipe(io.Writer) DockerCompose
WithStderrPipe(io.Writer) DockerCompose
}

type waitService struct {
Expand All @@ -54,6 +54,8 @@ type LocalDockerCompose struct {
Services map[string]interface{}
waitStrategySupplied bool
WaitStrategyMap map[waitService]wait.Strategy
cmdStdout io.Writer
cmdStderr io.Writer
}

type (
Expand Down Expand Up @@ -113,6 +115,8 @@ func NewLocalDockerCompose(filePaths []string, identifier string, opts ...LocalD
dc.waitStrategySupplied = false
dc.WaitStrategyMap = make(map[waitService]wait.Strategy)

dc.cmdStdout = os.Stdout
dc.cmdStderr = os.Stderr
return dc
}

Expand Down Expand Up @@ -215,6 +219,18 @@ func (dc *LocalDockerCompose) WithExposedService(service string, port int, strat
return dc
}

// WithStdoutPipe redirects standard output of the command to the specified writer
func (dc *LocalDockerCompose) WithStdoutPipe(w io.Writer) DockerCompose {
dc.cmdStdout = w
return dc
}

// WithStderrPipe redirects standard error of the command to the specified writer
func (dc *LocalDockerCompose) WithStderrPipe(w io.Writer) DockerCompose {
dc.cmdStderr = w
return dc
}

// validate checks if the files to be run in the compose are valid YAML files, setting up
// references to all services in them
func (dc *LocalDockerCompose) validate() error {
Expand Down Expand Up @@ -249,19 +265,15 @@ func (dc *LocalDockerCompose) validate() error {
// ExecError is super struct that holds any information about an execution error, so the client code
// can handle the result
type ExecError struct {
Command []string
StdoutOutput []byte
StderrOutput []byte
Error error
Stdout error
Stderr error
Command []string
Error error
Stdout error
Stderr error
}

// execute executes a program with arguments and environment variables inside a specific directory
func execute(
dirContext string, environment map[string]string, binary string, args []string) ExecError {

var errStdout, errStderr error
dirContext string, environment map[string]string, binary string, args []string, cmdStdout, cmdStderr io.Writer) ExecError {

cmd := exec.Command(binary, args...)
cmd.Dir = dirContext
Expand All @@ -271,11 +283,8 @@ func execute(
cmd.Env = append(cmd.Env, key+"="+value)
}

stdoutIn, _ := cmd.StdoutPipe()
stderrIn, _ := cmd.StderrPipe()

stdout := newCapturingPassThroughWriter(os.Stdout)
stderr := newCapturingPassThroughWriter(os.Stderr)
cmd.Stdout = cmdStdout
cmd.Stderr = cmdStderr

err := cmd.Start()
if err != nil {
Expand All @@ -284,38 +293,19 @@ func execute(

return ExecError{
// add information about the CMD and arguments used
Command: execCmd,
StdoutOutput: stdout.Bytes(),
StderrOutput: stderr.Bytes(),
Error: err,
Stderr: errStderr,
Stdout: errStdout,
Command: execCmd,
Error: err,
}
}

var wg sync.WaitGroup
wg.Add(1)

go func() {
_, errStdout = io.Copy(stdout, stdoutIn)
wg.Done()
}()

_, errStderr = io.Copy(stderr, stderrIn)
wg.Wait()

err = cmd.Wait()

execCmd := []string{"Reading std", dirContext, binary}
execCmd = append(execCmd, args...)

return ExecError{
Command: execCmd,
StdoutOutput: stdout.Bytes(),
StderrOutput: stderr.Bytes(),
Error: err,
Stderr: errStderr,
Stdout: errStdout,
Command: execCmd,
Error: err,
}
}

Expand Down Expand Up @@ -345,7 +335,7 @@ func executeCompose(dc *LocalDockerCompose, args []string) ExecError {
}
cmds = append(cmds, args...)

execErr := execute(pwd, environment, dc.Executable, cmds)
execErr := execute(pwd, environment, dc.Executable, cmds, dc.cmdStdout, dc.cmdStderr)
err := execErr.Error
if err != nil {
args := strings.Join(dc.Cmd, " ")
Expand All @@ -368,34 +358,6 @@ func executeCompose(dc *LocalDockerCompose, args []string) ExecError {
return execErr
}

// capturingPassThroughWriter is a writer that remembers
// data written to it and passes it to w
type capturingPassThroughWriter struct {
buf bytes.Buffer
w io.Writer
}

// newCapturingPassThroughWriter creates new capturingPassThroughWriter
func newCapturingPassThroughWriter(w io.Writer) *capturingPassThroughWriter {
return &capturingPassThroughWriter{
w: w,
}
}

func (w *capturingPassThroughWriter) Write(d []byte) (int, error) {
w.buf.Write(d)
return w.w.Write(d)
}

// Bytes returns bytes written to the writer
func (w *capturingPassThroughWriter) Bytes() []byte {
b := w.buf.Bytes()
if b == nil {
b = []byte{}
}
return b
}

// Which checks if a binary is present in PATH
func which(binary string) error {
_, err := exec.LookPath(binary)
Expand Down
6 changes: 1 addition & 5 deletions compose_test.go
Expand Up @@ -454,17 +454,13 @@ func checkIfError(t *testing.T, err ExecError) {
t.Fatalf("An error in Stderr happened when running %v: %v", err.Command, err.Stderr)
}

assert.NotNil(t, err.StdoutOutput)
assert.NotNil(t, err.StderrOutput)
}

func executeAndGetOutput(command string, args []string) (string, ExecError) {
cmd := exec.Command(command, args...)
out, err := cmd.CombinedOutput()

return string(out), ExecError{
Error: err,
StderrOutput: out,
StdoutOutput: out,
Error: err,
}
}