robocar-base/vendor/github.com/testcontainers/testcontainers-go/compose.go

270 lines
6.3 KiB
Go

package testcontainers
import (
"bytes"
"fmt"
"io"
"io/ioutil"
"os"
"os/exec"
"path/filepath"
"runtime"
"strings"
"sync"
"gopkg.in/yaml.v2"
)
const (
envProjectName = "COMPOSE_PROJECT_NAME"
envComposeFile = "COMPOSE_FILE"
)
// DockerCompose defines the contract for running Docker Compose
type DockerCompose interface {
Down() ExecError
Invoke() ExecError
WithCommand([]string) DockerCompose
WithEnv(map[string]string) DockerCompose
}
// LocalDockerCompose represents a Docker Compose execution using local binary
// docker-compose or docker-compose.exe, depending on the underlying platform
type LocalDockerCompose struct {
Executable string
ComposeFilePaths []string
absComposeFilePaths []string
Identifier string
Cmd []string
Env map[string]string
Services map[string]interface{}
}
// NewLocalDockerCompose returns an instance of the local Docker Compose, using an
// array of Docker Compose file paths and an identifier for the Compose execution.
//
// It will iterate through the array adding '-f compose-file-path' flags to the local
// Docker Compose execution. The identifier represents the name of the execution,
// which will define the name of the underlying Docker network and the name of the
// running Compose services.
func NewLocalDockerCompose(filePaths []string, identifier string) *LocalDockerCompose {
dc := &LocalDockerCompose{}
dc.Executable = "docker-compose"
if runtime.GOOS == "windows" {
dc.Executable = "docker-compose.exe"
}
dc.ComposeFilePaths = filePaths
dc.absComposeFilePaths = make([]string, len(filePaths))
for i, cfp := range dc.ComposeFilePaths {
abs, _ := filepath.Abs(cfp)
dc.absComposeFilePaths[i] = abs
}
dc.validate()
dc.Identifier = strings.ToLower(identifier)
return dc
}
// Down executes docker-compose down
func (dc *LocalDockerCompose) Down() ExecError {
return executeCompose(dc, []string{"down"})
}
func (dc *LocalDockerCompose) getDockerComposeEnvironment() map[string]string {
environment := map[string]string{}
composeFileEnvVariableValue := ""
for _, abs := range dc.absComposeFilePaths {
composeFileEnvVariableValue += abs + string(os.PathListSeparator)
}
environment[envProjectName] = dc.Identifier
environment[envComposeFile] = composeFileEnvVariableValue
return environment
}
// Invoke invokes the docker compose
func (dc *LocalDockerCompose) Invoke() ExecError {
return executeCompose(dc, dc.Cmd)
}
// WithCommand assigns the command
func (dc *LocalDockerCompose) WithCommand(cmd []string) DockerCompose {
dc.Cmd = cmd
return dc
}
// WithEnv assigns the environment
func (dc *LocalDockerCompose) WithEnv(env map[string]string) DockerCompose {
dc.Env = env
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 {
type compose struct {
Services map[string]interface{}
}
for _, abs := range dc.absComposeFilePaths {
c := compose{}
yamlFile, err := ioutil.ReadFile(abs)
if err != nil {
return err
}
err = yaml.Unmarshal(yamlFile, &c)
if err != nil {
return err
}
dc.Services = c.Services
}
return nil
}
// 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
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
cmd := exec.Command(binary, args...)
cmd.Dir = dirContext
cmd.Env = os.Environ()
for key, value := range environment {
cmd.Env = append(cmd.Env, key+"="+value)
}
stdoutIn, _ := cmd.StdoutPipe()
stderrIn, _ := cmd.StderrPipe()
stdout := newCapturingPassThroughWriter(os.Stdout)
stderr := newCapturingPassThroughWriter(os.Stderr)
err := cmd.Start()
if err != nil {
execCmd := []string{"Starting command", dirContext, binary}
execCmd = append(execCmd, args...)
return ExecError{
// add information about the CMD and arguments used
Command: execCmd,
Error: err,
Stderr: errStderr,
Stdout: errStdout,
}
}
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,
Error: err,
Stderr: errStderr,
Stdout: errStdout,
}
}
func executeCompose(dc *LocalDockerCompose, args []string) ExecError {
if which(dc.Executable) != nil {
return ExecError{
Command: []string{dc.Executable},
Error: fmt.Errorf("Local Docker Compose not found. Is %s on the PATH?", dc.Executable),
}
}
environment := dc.getDockerComposeEnvironment()
for k, v := range dc.Env {
environment[k] = v
}
cmds := []string{}
pwd := "."
if len(dc.absComposeFilePaths) > 0 {
pwd, _ = filepath.Split(dc.absComposeFilePaths[0])
for _, abs := range dc.absComposeFilePaths {
cmds = append(cmds, "-f", abs)
}
} else {
cmds = append(cmds, "-f", "docker-compose.yml")
}
cmds = append(cmds, args...)
execErr := execute(pwd, environment, dc.Executable, cmds)
err := execErr.Error
if err != nil {
args := strings.Join(dc.Cmd, " ")
return ExecError{
Command: []string{dc.Executable},
Error: fmt.Errorf("Local Docker compose exited abnormally whilst running %s: [%v]. %s", dc.Executable, args, err.Error()),
}
}
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 {
return w.buf.Bytes()
}
// Which checks if a binary is present in PATH
func which(binary string) error {
_, err := exec.LookPath(binary)
return err
}