package framework import ( "bytes" "fmt" "io" "os/exec" "runtime" "strings" "time" "github.com/onsi/gomega" log "github.com/sirupsen/logrus" ) func (b CmdContext) makeCmd() *exec.Cmd { return exec.Command(b.command, b.args...) } // CmdContext is used to build, customize and execute a command. // Add more functions to customize the context as needed. type CmdContext struct { command string args []string envs []string dir string stdin io.Reader timeout <-chan time.Time retries RetriesContext } // RetriesContext is used to tweak retry loop. type RetriesContext struct { count int interval time.Duration } // NewCommand creates a command context. func NewCommand(command string, args ...string) *CmdContext { return &CmdContext{ command: command, args: args, retries: RetriesContext{interval: time.Second}, } } func dockerExecutable() string { if runtime.GOOS == "windows" { return "./bin/windows/docker.exe" } return "./bin/docker" } // NewDockerCommand creates a docker builder. func NewDockerCommand(args ...string) *CmdContext { return NewCommand(dockerExecutable(), args...) } // WithinDirectory tells Docker the cwd. func (b *CmdContext) WithinDirectory(path string) *CmdContext { b.dir = path return b } // WithEnvs set envs in context. func (b *CmdContext) WithEnvs(envs []string) *CmdContext { b.envs = envs return b } // WithTimeout controls maximum duration. func (b *CmdContext) WithTimeout(t <-chan time.Time) *CmdContext { b.timeout = t return b } // WithRetries sets how many times to retry the command before issuing an error func (b *CmdContext) WithRetries(count int) *CmdContext { b.retries.count = count return b } // Every interval between 2 retries func (b *CmdContext) Every(interval time.Duration) *CmdContext { b.retries.interval = interval return b } // WithStdinData feeds via stdin. func (b CmdContext) WithStdinData(data string) *CmdContext { b.stdin = strings.NewReader(data) return &b } // WithStdinReader feeds via stdin. func (b CmdContext) WithStdinReader(reader io.Reader) *CmdContext { b.stdin = reader return &b } // ExecOrDie runs a docker command. func (b CmdContext) ExecOrDie() string { str, err := b.Exec() log.Debugf("stdout: %s", str) gomega.Expect(err).NotTo(gomega.HaveOccurred()) return str } // Exec runs a docker command. func (b CmdContext) Exec() (string, error) { retry := b.retries.count for ; ; retry-- { cmd := b.makeCmd() cmd.Dir = b.dir cmd.Stdin = b.stdin if b.envs != nil { cmd.Env = b.envs } stdout, err := Execute(cmd, b.timeout) if err == nil || retry < 1 { return stdout, err } time.Sleep(b.retries.interval) } } // Execute executes a command. // The command cannot be re-used afterwards. func Execute(cmd *exec.Cmd, timeout <-chan time.Time) (string, error) { var stdout, stderr bytes.Buffer cmd.Stdout = mergeWriter(cmd.Stdout, &stdout) cmd.Stderr = mergeWriter(cmd.Stderr, &stderr) log.Infof("Execute '%s %s'", cmd.Path, strings.Join(cmd.Args[1:], " ")) // skip arg[0] as it is printed separately if err := cmd.Start(); err != nil { return "", fmt.Errorf("error starting %v:\nCommand stdout:\n%v\nstderr:\n%v\nerror:\n%v", cmd, stdout.String(), stderr.String(), err) } errCh := make(chan error, 1) go func() { errCh <- cmd.Wait() }() select { case err := <-errCh: if err != nil { log.Debugf("%s %s failed: %v", cmd.Path, strings.Join(cmd.Args[1:], " "), err) return stderr.String(), fmt.Errorf("error running %v:\nCommand stdout:\n%v\nstderr:\n%v\nerror:\n%v", cmd, stdout.String(), stderr.String(), err) } case <-timeout: log.Debugf("%s %s timed-out", cmd.Path, strings.Join(cmd.Args[1:], " ")) if err := cmd.Process.Kill(); err != nil { return "", err } return "", fmt.Errorf( "timed out waiting for command %v:\nCommand stdout:\n%v\nstderr:\n%v", cmd.Args, stdout.String(), stderr.String()) } if stderr.String() != "" { log.Debugf("stderr: %s", stderr.String()) } return stdout.String(), nil } func mergeWriter(other io.Writer, buf io.Writer) io.Writer { if other != nil { return io.MultiWriter(other, buf) } return buf } // Powershell runs a powershell command. func Powershell(input string) (string, error) { output, err := Execute(exec.Command("powershell", "-NoProfile", "-NonInteractive", "-ExecutionPolicy", "Unrestricted", "-Command", input), nil) if err != nil { return "", fmt.Errorf("fail to execute %s: %s", input, err) } return strings.TrimSpace(output), nil }