1package cli // import "github.com/docker/docker/integration-cli/cli"
2
3import (
4	"fmt"
5	"io"
6	"strings"
7	"time"
8
9	"github.com/docker/docker/integration-cli/daemon"
10	"github.com/docker/docker/integration-cli/environment"
11	"github.com/pkg/errors"
12	"gotest.tools/assert"
13	"gotest.tools/icmd"
14)
15
16var testEnv *environment.Execution
17
18// SetTestEnvironment sets a static test environment
19// TODO: decouple this package from environment
20func SetTestEnvironment(env *environment.Execution) {
21	testEnv = env
22}
23
24// CmdOperator defines functions that can modify a command
25type CmdOperator func(*icmd.Cmd) func()
26
27type testingT interface {
28	assert.TestingT
29	Fatal(args ...interface{})
30	Fatalf(string, ...interface{})
31}
32
33// DockerCmd executes the specified docker command and expect a success
34func DockerCmd(t testingT, args ...string) *icmd.Result {
35	return Docker(Args(args...)).Assert(t, icmd.Success)
36}
37
38// BuildCmd executes the specified docker build command and expect a success
39func BuildCmd(t testingT, name string, cmdOperators ...CmdOperator) *icmd.Result {
40	return Docker(Build(name), cmdOperators...).Assert(t, icmd.Success)
41}
42
43// InspectCmd executes the specified docker inspect command and expect a success
44func InspectCmd(t testingT, name string, cmdOperators ...CmdOperator) *icmd.Result {
45	return Docker(Inspect(name), cmdOperators...).Assert(t, icmd.Success)
46}
47
48// WaitRun will wait for the specified container to be running, maximum 5 seconds.
49func WaitRun(t testingT, name string, cmdOperators ...CmdOperator) {
50	WaitForInspectResult(t, name, "{{.State.Running}}", "true", 5*time.Second, cmdOperators...)
51}
52
53// WaitExited will wait for the specified container to state exit, subject
54// to a maximum time limit in seconds supplied by the caller
55func WaitExited(t testingT, name string, timeout time.Duration, cmdOperators ...CmdOperator) {
56	WaitForInspectResult(t, name, "{{.State.Status}}", "exited", timeout, cmdOperators...)
57}
58
59// WaitRestart will wait for the specified container to restart once
60func WaitRestart(t testingT, name string, timeout time.Duration, cmdOperators ...CmdOperator) {
61	WaitForInspectResult(t, name, "{{.RestartCount}}", "1", timeout, cmdOperators...)
62}
63
64// WaitForInspectResult waits for the specified expression to be equals to the specified expected string in the given time.
65func WaitForInspectResult(t testingT, name, expr, expected string, timeout time.Duration, cmdOperators ...CmdOperator) {
66	after := time.After(timeout)
67
68	args := []string{"inspect", "-f", expr, name}
69	for {
70		result := Docker(Args(args...), cmdOperators...)
71		if result.Error != nil {
72			if !strings.Contains(strings.ToLower(result.Stderr()), "no such") {
73				t.Fatalf("error executing docker inspect: %v\n%s",
74					result.Stderr(), result.Stdout())
75			}
76			select {
77			case <-after:
78				t.Fatal(result.Error)
79			default:
80				time.Sleep(10 * time.Millisecond)
81				continue
82			}
83		}
84
85		out := strings.TrimSpace(result.Stdout())
86		if out == expected {
87			break
88		}
89
90		select {
91		case <-after:
92			t.Fatalf("condition \"%q == %q\" not true in time (%v)", out, expected, timeout)
93		default:
94		}
95
96		time.Sleep(100 * time.Millisecond)
97	}
98}
99
100// Docker executes the specified docker command
101func Docker(cmd icmd.Cmd, cmdOperators ...CmdOperator) *icmd.Result {
102	for _, op := range cmdOperators {
103		deferFn := op(&cmd)
104		if deferFn != nil {
105			defer deferFn()
106		}
107	}
108	appendDocker(&cmd)
109	if err := validateArgs(cmd.Command...); err != nil {
110		return &icmd.Result{
111			Error: err,
112		}
113	}
114	return icmd.RunCmd(cmd)
115}
116
117// validateArgs is a checker to ensure tests are not running commands which are
118// not supported on platforms. Specifically on Windows this is 'busybox top'.
119func validateArgs(args ...string) error {
120	if testEnv.OSType != "windows" {
121		return nil
122	}
123	foundBusybox := -1
124	for key, value := range args {
125		if strings.ToLower(value) == "busybox" {
126			foundBusybox = key
127		}
128		if (foundBusybox != -1) && (key == foundBusybox+1) && (strings.ToLower(value) == "top") {
129			return errors.New("cannot use 'busybox top' in tests on Windows. Use runSleepingContainer()")
130		}
131	}
132	return nil
133}
134
135// Build executes the specified docker build command
136func Build(name string) icmd.Cmd {
137	return icmd.Command("build", "-t", name)
138}
139
140// Inspect executes the specified docker inspect command
141func Inspect(name string) icmd.Cmd {
142	return icmd.Command("inspect", name)
143}
144
145// Format sets the specified format with --format flag
146func Format(format string) func(*icmd.Cmd) func() {
147	return func(cmd *icmd.Cmd) func() {
148		cmd.Command = append(
149			[]string{cmd.Command[0]},
150			append([]string{"--format", fmt.Sprintf("{{%s}}", format)}, cmd.Command[1:]...)...,
151		)
152		return nil
153	}
154}
155
156func appendDocker(cmd *icmd.Cmd) {
157	cmd.Command = append([]string{testEnv.DockerBinary()}, cmd.Command...)
158}
159
160// Args build an icmd.Cmd struct from the specified arguments
161func Args(args ...string) icmd.Cmd {
162	switch len(args) {
163	case 0:
164		return icmd.Cmd{}
165	case 1:
166		return icmd.Command(args[0])
167	default:
168		return icmd.Command(args[0], args[1:]...)
169	}
170}
171
172// Daemon points to the specified daemon
173func Daemon(d *daemon.Daemon) func(*icmd.Cmd) func() {
174	return func(cmd *icmd.Cmd) func() {
175		cmd.Command = append([]string{"--host", d.Sock()}, cmd.Command...)
176		return nil
177	}
178}
179
180// WithTimeout sets the timeout for the command to run
181func WithTimeout(timeout time.Duration) func(cmd *icmd.Cmd) func() {
182	return func(cmd *icmd.Cmd) func() {
183		cmd.Timeout = timeout
184		return nil
185	}
186}
187
188// WithEnvironmentVariables sets the specified environment variables for the command to run
189func WithEnvironmentVariables(envs ...string) func(cmd *icmd.Cmd) func() {
190	return func(cmd *icmd.Cmd) func() {
191		cmd.Env = envs
192		return nil
193	}
194}
195
196// WithFlags sets the specified flags for the command to run
197func WithFlags(flags ...string) func(*icmd.Cmd) func() {
198	return func(cmd *icmd.Cmd) func() {
199		cmd.Command = append(cmd.Command, flags...)
200		return nil
201	}
202}
203
204// InDir sets the folder in which the command should be executed
205func InDir(path string) func(*icmd.Cmd) func() {
206	return func(cmd *icmd.Cmd) func() {
207		cmd.Dir = path
208		return nil
209	}
210}
211
212// WithStdout sets the standard output writer of the command
213func WithStdout(writer io.Writer) func(*icmd.Cmd) func() {
214	return func(cmd *icmd.Cmd) func() {
215		cmd.Stdout = writer
216		return nil
217	}
218}
219
220// WithStdin sets the standard input reader for the command
221func WithStdin(stdin io.Reader) func(*icmd.Cmd) func() {
222	return func(cmd *icmd.Cmd) func() {
223		cmd.Stdin = stdin
224		return nil
225	}
226}
227