|
|
@@ -0,0 +1,177 @@
|
|
|
+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
|
|
|
+}
|