1package command
2
3import (
4	"bytes"
5	"context"
6	"crypto/x509"
7	"fmt"
8	"io/ioutil"
9	"net/http"
10	"os"
11	"runtime"
12	"testing"
13
14	cliconfig "github.com/docker/cli/cli/config"
15	"github.com/docker/cli/cli/config/configfile"
16	"github.com/docker/cli/cli/flags"
17	"github.com/docker/docker/api"
18	"github.com/docker/docker/api/types"
19	"github.com/docker/docker/client"
20	"github.com/pkg/errors"
21	"gotest.tools/v3/assert"
22	is "gotest.tools/v3/assert/cmp"
23	"gotest.tools/v3/env"
24	"gotest.tools/v3/fs"
25)
26
27func TestNewAPIClientFromFlags(t *testing.T) {
28	host := "unix://path"
29	if runtime.GOOS == "windows" {
30		host = "npipe://./"
31	}
32	opts := &flags.CommonOptions{Hosts: []string{host}}
33	configFile := &configfile.ConfigFile{
34		HTTPHeaders: map[string]string{
35			"My-Header": "Custom-Value",
36		},
37	}
38	apiclient, err := NewAPIClientFromFlags(opts, configFile)
39	assert.NilError(t, err)
40	assert.Check(t, is.Equal(host, apiclient.DaemonHost()))
41
42	expectedHeaders := map[string]string{
43		"My-Header":  "Custom-Value",
44		"User-Agent": UserAgent(),
45	}
46	assert.Check(t, is.DeepEqual(expectedHeaders, apiclient.(*client.Client).CustomHTTPHeaders()))
47	assert.Check(t, is.Equal(api.DefaultVersion, apiclient.ClientVersion()))
48	assert.DeepEqual(t, configFile.HTTPHeaders, map[string]string{"My-Header": "Custom-Value"})
49}
50
51func TestNewAPIClientFromFlagsForDefaultSchema(t *testing.T) {
52	host := ":2375"
53	opts := &flags.CommonOptions{Hosts: []string{host}}
54	configFile := &configfile.ConfigFile{
55		HTTPHeaders: map[string]string{
56			"My-Header": "Custom-Value",
57		},
58	}
59	apiclient, err := NewAPIClientFromFlags(opts, configFile)
60	assert.NilError(t, err)
61	assert.Check(t, is.Equal("tcp://localhost"+host, apiclient.DaemonHost()))
62
63	expectedHeaders := map[string]string{
64		"My-Header":  "Custom-Value",
65		"User-Agent": UserAgent(),
66	}
67	assert.Check(t, is.DeepEqual(expectedHeaders, apiclient.(*client.Client).CustomHTTPHeaders()))
68	assert.Check(t, is.Equal(api.DefaultVersion, apiclient.ClientVersion()))
69}
70
71func TestNewAPIClientFromFlagsWithAPIVersionFromEnv(t *testing.T) {
72	customVersion := "v3.3.3"
73	defer env.Patch(t, "DOCKER_API_VERSION", customVersion)()
74	defer env.Patch(t, "DOCKER_HOST", ":2375")()
75
76	opts := &flags.CommonOptions{}
77	configFile := &configfile.ConfigFile{}
78	apiclient, err := NewAPIClientFromFlags(opts, configFile)
79	assert.NilError(t, err)
80	assert.Check(t, is.Equal(customVersion, apiclient.ClientVersion()))
81}
82
83func TestNewAPIClientFromFlagsWithHttpProxyEnv(t *testing.T) {
84	defer env.Patch(t, "HTTP_PROXY", "http://proxy.acme.com:1234")()
85	defer env.Patch(t, "DOCKER_HOST", "tcp://docker.acme.com:2376")()
86
87	opts := &flags.CommonOptions{}
88	configFile := &configfile.ConfigFile{}
89	apiclient, err := NewAPIClientFromFlags(opts, configFile)
90	assert.NilError(t, err)
91	transport, ok := apiclient.HTTPClient().Transport.(*http.Transport)
92	assert.Assert(t, ok)
93	assert.Assert(t, transport.Proxy != nil)
94	request, err := http.NewRequest(http.MethodGet, "tcp://docker.acme.com:2376", nil)
95	assert.NilError(t, err)
96	url, err := transport.Proxy(request)
97	assert.NilError(t, err)
98	assert.Check(t, is.Equal("http://proxy.acme.com:1234", url.String()))
99}
100
101type fakeClient struct {
102	client.Client
103	pingFunc   func() (types.Ping, error)
104	version    string
105	negotiated bool
106}
107
108func (c *fakeClient) Ping(_ context.Context) (types.Ping, error) {
109	return c.pingFunc()
110}
111
112func (c *fakeClient) ClientVersion() string {
113	return c.version
114}
115
116func (c *fakeClient) NegotiateAPIVersionPing(types.Ping) {
117	c.negotiated = true
118}
119
120func TestInitializeFromClient(t *testing.T) {
121	defaultVersion := "v1.55"
122
123	var testcases = []struct {
124		doc            string
125		pingFunc       func() (types.Ping, error)
126		expectedServer ServerInfo
127		negotiated     bool
128	}{
129		{
130			doc: "successful ping",
131			pingFunc: func() (types.Ping, error) {
132				return types.Ping{Experimental: true, OSType: "linux", APIVersion: "v1.30"}, nil
133			},
134			expectedServer: ServerInfo{HasExperimental: true, OSType: "linux"},
135			negotiated:     true,
136		},
137		{
138			doc: "failed ping, no API version",
139			pingFunc: func() (types.Ping, error) {
140				return types.Ping{}, errors.New("failed")
141			},
142			expectedServer: ServerInfo{HasExperimental: true},
143		},
144		{
145			doc: "failed ping, with API version",
146			pingFunc: func() (types.Ping, error) {
147				return types.Ping{APIVersion: "v1.33"}, errors.New("failed")
148			},
149			expectedServer: ServerInfo{HasExperimental: true},
150			negotiated:     true,
151		},
152	}
153
154	for _, testcase := range testcases {
155		testcase := testcase
156		t.Run(testcase.doc, func(t *testing.T) {
157			apiclient := &fakeClient{
158				pingFunc: testcase.pingFunc,
159				version:  defaultVersion,
160			}
161
162			cli := &DockerCli{client: apiclient}
163			cli.initializeFromClient()
164			assert.Check(t, is.DeepEqual(testcase.expectedServer, cli.serverInfo))
165			assert.Check(t, is.Equal(testcase.negotiated, apiclient.negotiated))
166		})
167	}
168}
169
170// The CLI no longer disables/hides experimental CLI features, however, we need
171// to verify that existing configuration files do not break
172func TestExperimentalCLI(t *testing.T) {
173	defaultVersion := "v1.55"
174
175	var testcases = []struct {
176		doc        string
177		configfile string
178	}{
179		{
180			doc:        "default",
181			configfile: `{}`,
182		},
183		{
184			doc: "experimental",
185			configfile: `{
186	"experimental": "enabled"
187}`,
188		},
189	}
190
191	for _, testcase := range testcases {
192		testcase := testcase
193		t.Run(testcase.doc, func(t *testing.T) {
194			dir := fs.NewDir(t, testcase.doc, fs.WithFile("config.json", testcase.configfile))
195			defer dir.Remove()
196			apiclient := &fakeClient{
197				version: defaultVersion,
198				pingFunc: func() (types.Ping, error) {
199					return types.Ping{Experimental: true, OSType: "linux", APIVersion: defaultVersion}, nil
200				},
201			}
202
203			cli := &DockerCli{client: apiclient, err: os.Stderr}
204			cliconfig.SetDir(dir.Path())
205			err := cli.Initialize(flags.NewClientOptions())
206			assert.NilError(t, err)
207			// For backward-compatibility, HasExperimental will always be "true"
208			assert.Check(t, is.Equal(true, cli.ClientInfo().HasExperimental))
209		})
210	}
211}
212
213func TestGetClientWithPassword(t *testing.T) {
214	expected := "password"
215
216	var testcases = []struct {
217		doc             string
218		password        string
219		retrieverErr    error
220		retrieverGiveup bool
221		newClientErr    error
222		expectedErr     string
223	}{
224		{
225			doc:      "successful connect",
226			password: expected,
227		},
228		{
229			doc:             "password retriever exhausted",
230			retrieverGiveup: true,
231			retrieverErr:    errors.New("failed"),
232			expectedErr:     "private key is encrypted, but could not get passphrase",
233		},
234		{
235			doc:          "password retriever error",
236			retrieverErr: errors.New("failed"),
237			expectedErr:  "failed",
238		},
239		{
240			doc:          "newClient error",
241			newClientErr: errors.New("failed to connect"),
242			expectedErr:  "failed to connect",
243		},
244	}
245
246	for _, testcase := range testcases {
247		testcase := testcase
248		t.Run(testcase.doc, func(t *testing.T) {
249			passRetriever := func(_, _ string, _ bool, attempts int) (passphrase string, giveup bool, err error) {
250				// Always return an invalid pass first to test iteration
251				switch attempts {
252				case 0:
253					return "something else", false, nil
254				default:
255					return testcase.password, testcase.retrieverGiveup, testcase.retrieverErr
256				}
257			}
258
259			newClient := func(currentPassword string) (client.APIClient, error) {
260				if testcase.newClientErr != nil {
261					return nil, testcase.newClientErr
262				}
263				if currentPassword == expected {
264					return &client.Client{}, nil
265				}
266				return &client.Client{}, x509.IncorrectPasswordError
267			}
268
269			_, err := getClientWithPassword(passRetriever, newClient)
270			if testcase.expectedErr != "" {
271				assert.ErrorContains(t, err, testcase.expectedErr)
272				return
273			}
274
275			assert.NilError(t, err)
276		})
277	}
278}
279
280func TestNewDockerCliAndOperators(t *testing.T) {
281	// Test default operations and also overriding default ones
282	cli, err := NewDockerCli(
283		WithContentTrust(true),
284	)
285	assert.NilError(t, err)
286	// Check streams are initialized
287	assert.Check(t, cli.In() != nil)
288	assert.Check(t, cli.Out() != nil)
289	assert.Check(t, cli.Err() != nil)
290	assert.Equal(t, cli.ContentTrustEnabled(), true)
291
292	// Apply can modify a dockerCli after construction
293	inbuf := bytes.NewBuffer([]byte("input"))
294	outbuf := bytes.NewBuffer(nil)
295	errbuf := bytes.NewBuffer(nil)
296	err = cli.Apply(
297		WithInputStream(ioutil.NopCloser(inbuf)),
298		WithOutputStream(outbuf),
299		WithErrorStream(errbuf),
300	)
301	assert.NilError(t, err)
302	// Check input stream
303	inputStream, err := ioutil.ReadAll(cli.In())
304	assert.NilError(t, err)
305	assert.Equal(t, string(inputStream), "input")
306	// Check output stream
307	fmt.Fprintf(cli.Out(), "output")
308	outputStream, err := ioutil.ReadAll(outbuf)
309	assert.NilError(t, err)
310	assert.Equal(t, string(outputStream), "output")
311	// Check error stream
312	fmt.Fprintf(cli.Err(), "error")
313	errStream, err := ioutil.ReadAll(errbuf)
314	assert.NilError(t, err)
315	assert.Equal(t, string(errStream), "error")
316}
317
318func TestInitializeShouldAlwaysCreateTheContextStore(t *testing.T) {
319	cli, err := NewDockerCli()
320	assert.NilError(t, err)
321	assert.NilError(t, cli.Initialize(flags.NewClientOptions(), WithInitializeClient(func(cli *DockerCli) (client.APIClient, error) {
322		return client.NewClientWithOpts()
323	})))
324	assert.Check(t, cli.ContextStore() != nil)
325}
326