exec.go 6.0 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236
  1. package framework
  2. import (
  3. "bytes"
  4. "fmt"
  5. "io"
  6. "io/ioutil"
  7. "os"
  8. "os/exec"
  9. "path/filepath"
  10. "runtime"
  11. "strings"
  12. "time"
  13. "github.com/onsi/gomega"
  14. log "github.com/sirupsen/logrus"
  15. "github.com/stretchr/testify/require"
  16. "github.com/stretchr/testify/suite"
  17. )
  18. func (b CmdContext) makeCmd() *exec.Cmd {
  19. return exec.Command(b.command, b.args...)
  20. }
  21. // CmdContext is used to build, customize and execute a command.
  22. // Add more functions to customize the context as needed.
  23. type CmdContext struct {
  24. command string
  25. args []string
  26. envs []string
  27. dir string
  28. stdin io.Reader
  29. timeout <-chan time.Time
  30. retries RetriesContext
  31. }
  32. // RetriesContext is used to tweak retry loop.
  33. type RetriesContext struct {
  34. count int
  35. interval time.Duration
  36. }
  37. // Suite is used to store context information for e2e tests
  38. type Suite struct {
  39. suite.Suite
  40. ConfigDir string
  41. BinDir string
  42. }
  43. // SetupSuite is run before running any tests
  44. func (s *Suite) SetupSuite() {
  45. d, _ := ioutil.TempDir("", "")
  46. s.BinDir = d
  47. gomega.RegisterFailHandler(func(message string, callerSkip ...int) {
  48. log.Error(message)
  49. cp := filepath.Join(s.ConfigDir, "config.json")
  50. d, _ := ioutil.ReadFile(cp)
  51. fmt.Printf("Contents of %s:\n%s\n\nContents of config dir:\n", cp, string(d))
  52. out, _ := s.NewCommand("find", s.ConfigDir).Exec()
  53. fmt.Println(out)
  54. s.T().Fail()
  55. })
  56. s.linkClassicDocker()
  57. }
  58. // TearDownSuite is run after all tests
  59. func (s *Suite) TearDownSuite() {
  60. _ = os.RemoveAll(s.BinDir)
  61. }
  62. func (s *Suite) linkClassicDocker() {
  63. p, err := exec.LookPath("docker")
  64. gomega.Expect(err).To(gomega.BeNil())
  65. err = os.Symlink(p, filepath.Join(s.BinDir, "docker-classic"))
  66. gomega.Expect(err).To(gomega.BeNil())
  67. err = os.Setenv("PATH", fmt.Sprintf("%s:%s", s.BinDir, os.Getenv("PATH")))
  68. gomega.Expect(err).To(gomega.BeNil())
  69. }
  70. // BeforeTest is run before each test
  71. func (s *Suite) BeforeTest(suite, test string) {
  72. d, _ := ioutil.TempDir("", "")
  73. s.ConfigDir = d
  74. }
  75. // AfterTest is run after each test
  76. func (s *Suite) AfterTest(suite, test string) {
  77. err := os.RemoveAll(s.ConfigDir)
  78. require.NoError(s.T(), err)
  79. }
  80. // NewCommand creates a command context.
  81. func (s *Suite) NewCommand(command string, args ...string) *CmdContext {
  82. var envs []string
  83. if s.ConfigDir != "" {
  84. envs = append(os.Environ(), fmt.Sprintf("DOCKER_CONFIG=%s", s.ConfigDir))
  85. }
  86. return &CmdContext{
  87. command: command,
  88. args: args,
  89. envs: envs,
  90. retries: RetriesContext{interval: time.Second},
  91. }
  92. }
  93. func dockerExecutable() string {
  94. if runtime.GOOS == "windows" {
  95. return "../../bin/docker.exe"
  96. }
  97. return "../../bin/docker"
  98. }
  99. // NewDockerCommand creates a docker builder.
  100. func (s *Suite) NewDockerCommand(args ...string) *CmdContext {
  101. return s.NewCommand(dockerExecutable(), args...)
  102. }
  103. // WithinDirectory tells Docker the cwd.
  104. func (b *CmdContext) WithinDirectory(path string) *CmdContext {
  105. b.dir = path
  106. return b
  107. }
  108. // WithEnvs set envs in context.
  109. func (b *CmdContext) WithEnvs(envs []string) *CmdContext {
  110. b.envs = envs
  111. return b
  112. }
  113. // WithTimeout controls maximum duration.
  114. func (b *CmdContext) WithTimeout(t <-chan time.Time) *CmdContext {
  115. b.timeout = t
  116. return b
  117. }
  118. // WithRetries sets how many times to retry the command before issuing an error
  119. func (b *CmdContext) WithRetries(count int) *CmdContext {
  120. b.retries.count = count
  121. return b
  122. }
  123. // Every interval between 2 retries
  124. func (b *CmdContext) Every(interval time.Duration) *CmdContext {
  125. b.retries.interval = interval
  126. return b
  127. }
  128. // WithStdinData feeds via stdin.
  129. func (b CmdContext) WithStdinData(data string) *CmdContext {
  130. b.stdin = strings.NewReader(data)
  131. return &b
  132. }
  133. // WithStdinReader feeds via stdin.
  134. func (b CmdContext) WithStdinReader(reader io.Reader) *CmdContext {
  135. b.stdin = reader
  136. return &b
  137. }
  138. // ExecOrDie runs a docker command.
  139. func (b CmdContext) ExecOrDie() string {
  140. str, err := b.Exec()
  141. log.Debugf("stdout: %s", str)
  142. gomega.Expect(err).NotTo(gomega.HaveOccurred())
  143. return str
  144. }
  145. // Exec runs a docker command.
  146. func (b CmdContext) Exec() (string, error) {
  147. retry := b.retries.count
  148. for ; ; retry-- {
  149. cmd := b.makeCmd()
  150. cmd.Dir = b.dir
  151. cmd.Stdin = b.stdin
  152. if b.envs != nil {
  153. cmd.Env = b.envs
  154. }
  155. stdout, err := Execute(cmd, b.timeout)
  156. if err == nil || retry < 1 {
  157. return stdout, err
  158. }
  159. time.Sleep(b.retries.interval)
  160. }
  161. }
  162. // Execute executes a command.
  163. // The command cannot be re-used afterwards.
  164. func Execute(cmd *exec.Cmd, timeout <-chan time.Time) (string, error) {
  165. var stdout, stderr bytes.Buffer
  166. cmd.Stdout = mergeWriter(cmd.Stdout, &stdout)
  167. cmd.Stderr = mergeWriter(cmd.Stderr, &stderr)
  168. log.Infof("Execute '%s %s'", cmd.Path, strings.Join(cmd.Args[1:], " ")) // skip arg[0] as it is printed separately
  169. if err := cmd.Start(); err != nil {
  170. return "", fmt.Errorf("error starting %v:\nCommand stdout:\n%v\nstderr:\n%v\nerror:\n%v", cmd, stdout.String(), stderr.String(), err)
  171. }
  172. errCh := make(chan error, 1)
  173. go func() {
  174. errCh <- cmd.Wait()
  175. }()
  176. select {
  177. case err := <-errCh:
  178. if err != nil {
  179. log.Debugf("%s %s failed: %v", cmd.Path, strings.Join(cmd.Args[1:], " "), err)
  180. return stderr.String(), fmt.Errorf("error running %v:\nCommand stdout:\n%v\nstderr:\n%v\nerror:\n%v", cmd, stdout.String(), stderr.String(), err)
  181. }
  182. case <-timeout:
  183. log.Debugf("%s %s timed-out", cmd.Path, strings.Join(cmd.Args[1:], " "))
  184. if err := cmd.Process.Kill(); err != nil {
  185. return "", err
  186. }
  187. return "", fmt.Errorf(
  188. "timed out waiting for command %v:\nCommand stdout:\n%v\nstderr:\n%v",
  189. cmd.Args, stdout.String(), stderr.String())
  190. }
  191. if stderr.String() != "" {
  192. log.Debugf("stderr: %s", stderr.String())
  193. }
  194. return stdout.String(), nil
  195. }
  196. func mergeWriter(other io.Writer, buf io.Writer) io.Writer {
  197. if other != nil {
  198. return io.MultiWriter(other, buf)
  199. }
  200. return buf
  201. }
  202. // Powershell runs a powershell command.
  203. func Powershell(input string) (string, error) {
  204. output, err := Execute(exec.Command("powershell", "-NoProfile", "-NonInteractive", "-ExecutionPolicy", "Unrestricted", "-Command", input), nil)
  205. if err != nil {
  206. return "", fmt.Errorf("fail to execute %s: %s", input, err)
  207. }
  208. return strings.TrimSpace(output), nil
  209. }