-
-
Notifications
You must be signed in to change notification settings - Fork 511
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Merge pull request #97 from mdelapenya/25-docker-compose
(#25) Create very basic implementation of Local Docker Compose
- Loading branch information
Showing
7 changed files
with
617 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,264 @@ | ||
package testcontainers | ||
|
||
import ( | ||
"bytes" | ||
"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 { | ||
panic("Local Docker Compose not found. Is " + dc.Executable + " on the PATH?") | ||
} | ||
|
||
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, " ") | ||
panic( | ||
"Local Docker compose exited abnormally whilst running " + | ||
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 | ||
} |
Oops, something went wrong.