package command import ( "bytes" "context" "fmt" "io" "net" "net/http" "net/http/httptest" "os" "path/filepath" "runtime" "strings" "testing" "time" "github.com/docker/cli/cli/config" "github.com/docker/cli/cli/config/configfile" "github.com/docker/cli/cli/flags" "github.com/docker/cli/cli/streams" "github.com/docker/docker/api" "github.com/docker/docker/api/types" "github.com/docker/docker/client" "github.com/pkg/errors" "gotest.tools/v3/assert" "gotest.tools/v3/fs" ) func TestNewAPIClientFromFlags(t *testing.T) { host := "unix://path" if runtime.GOOS == "windows" { host = "npipe://./" } opts := &flags.ClientOptions{Hosts: []string{host}} apiClient, err := NewAPIClientFromFlags(opts, &configfile.ConfigFile{}) assert.NilError(t, err) assert.Equal(t, apiClient.DaemonHost(), host) assert.Equal(t, apiClient.ClientVersion(), api.DefaultVersion) } func TestNewAPIClientFromFlagsForDefaultSchema(t *testing.T) { host := ":2375" slug := "tcp://localhost" if runtime.GOOS == "windows" { slug = "tcp://127.0.0.1" } opts := &flags.ClientOptions{Hosts: []string{host}} apiClient, err := NewAPIClientFromFlags(opts, &configfile.ConfigFile{}) assert.NilError(t, err) assert.Equal(t, apiClient.DaemonHost(), slug+host) assert.Equal(t, apiClient.ClientVersion(), api.DefaultVersion) } func TestNewAPIClientFromFlagsWithCustomHeaders(t *testing.T) { var received map[string]string ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { received = map[string]string{ "My-Header": r.Header.Get("My-Header"), "User-Agent": r.Header.Get("User-Agent"), } _, _ = w.Write([]byte("OK")) })) defer ts.Close() host := strings.Replace(ts.URL, "http://", "tcp://", 1) opts := &flags.ClientOptions{Hosts: []string{host}} configFile := &configfile.ConfigFile{ HTTPHeaders: map[string]string{ "My-Header": "Custom-Value", }, } apiClient, err := NewAPIClientFromFlags(opts, configFile) assert.NilError(t, err) assert.Equal(t, apiClient.DaemonHost(), host) assert.Equal(t, apiClient.ClientVersion(), api.DefaultVersion) // verify User-Agent is not appended to the configfile. see https://github.com/docker/cli/pull/2756 assert.DeepEqual(t, configFile.HTTPHeaders, map[string]string{"My-Header": "Custom-Value"}) expectedHeaders := map[string]string{ "My-Header": "Custom-Value", "User-Agent": UserAgent(), } _, err = apiClient.Ping(context.Background()) assert.NilError(t, err) assert.DeepEqual(t, received, expectedHeaders) } func TestNewAPIClientFromFlagsWithCustomHeadersFromEnv(t *testing.T) { var received http.Header ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { received = r.Header.Clone() _, _ = w.Write([]byte("OK")) })) defer ts.Close() host := strings.Replace(ts.URL, "http://", "tcp://", 1) opts := &flags.ClientOptions{Hosts: []string{host}} configFile := &configfile.ConfigFile{ HTTPHeaders: map[string]string{ "My-Header": "Custom-Value from config-file", }, } // envOverrideHTTPHeaders should override the HTTPHeaders from the config-file, // so "My-Header" should not be present. t.Setenv(envOverrideHTTPHeaders, `one=one-value,"two=two,value",three=,four=four-value,four=four-value-override`) apiClient, err := NewAPIClientFromFlags(opts, configFile) assert.NilError(t, err) assert.Equal(t, apiClient.DaemonHost(), host) assert.Equal(t, apiClient.ClientVersion(), api.DefaultVersion) expectedHeaders := http.Header{ "One": []string{"one-value"}, "Two": []string{"two,value"}, "Three": []string{""}, "Four": []string{"four-value-override"}, "User-Agent": []string{UserAgent()}, } _, err = apiClient.Ping(context.Background()) assert.NilError(t, err) assert.DeepEqual(t, received, expectedHeaders) } func TestNewAPIClientFromFlagsWithAPIVersionFromEnv(t *testing.T) { customVersion := "v3.3.3" t.Setenv("DOCKER_API_VERSION", customVersion) t.Setenv("DOCKER_HOST", ":2375") opts := &flags.ClientOptions{} configFile := &configfile.ConfigFile{} apiclient, err := NewAPIClientFromFlags(opts, configFile) assert.NilError(t, err) assert.Equal(t, apiclient.ClientVersion(), customVersion) } type fakeClient struct { client.Client pingFunc func() (types.Ping, error) version string negotiated bool } func (c *fakeClient) Ping(_ context.Context) (types.Ping, error) { return c.pingFunc() } func (c *fakeClient) ClientVersion() string { return c.version } func (c *fakeClient) NegotiateAPIVersionPing(types.Ping) { c.negotiated = true } func TestInitializeFromClient(t *testing.T) { const defaultVersion = "v1.55" testcases := []struct { doc string pingFunc func() (types.Ping, error) expectedServer ServerInfo negotiated bool }{ { doc: "successful ping", pingFunc: func() (types.Ping, error) { return types.Ping{Experimental: true, OSType: "linux", APIVersion: "v1.30"}, nil }, expectedServer: ServerInfo{HasExperimental: true, OSType: "linux"}, negotiated: true, }, { doc: "failed ping, no API version", pingFunc: func() (types.Ping, error) { return types.Ping{}, errors.New("failed") }, expectedServer: ServerInfo{HasExperimental: true}, }, { doc: "failed ping, with API version", pingFunc: func() (types.Ping, error) { return types.Ping{APIVersion: "v1.33"}, errors.New("failed") }, expectedServer: ServerInfo{HasExperimental: true}, negotiated: true, }, } for _, testcase := range testcases { testcase := testcase t.Run(testcase.doc, func(t *testing.T) { apiclient := &fakeClient{ pingFunc: testcase.pingFunc, version: defaultVersion, } cli := &DockerCli{client: apiclient} err := cli.Initialize(flags.NewClientOptions()) assert.NilError(t, err) assert.DeepEqual(t, cli.ServerInfo(), testcase.expectedServer) assert.Equal(t, apiclient.negotiated, testcase.negotiated) }) } } // Makes sure we don't hang forever on the initial connection. // https://github.com/docker/cli/issues/3652 func TestInitializeFromClientHangs(t *testing.T) { dir := t.TempDir() socket := filepath.Join(dir, "my.sock") l, err := net.Listen("unix", socket) assert.NilError(t, err) receiveReqCh := make(chan bool) timeoutCtx, cancel := context.WithTimeout(context.Background(), time.Second) defer cancel() // Simulate a server that hangs on connections. ts := httptest.NewUnstartedServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { select { case <-timeoutCtx.Done(): case receiveReqCh <- true: // Blocks until someone receives on the channel. } _, _ = w.Write([]byte("OK")) })) ts.Listener = l ts.Start() defer ts.Close() opts := &flags.ClientOptions{Hosts: []string{"unix://" + socket}} configFile := &configfile.ConfigFile{} apiClient, err := NewAPIClientFromFlags(opts, configFile) assert.NilError(t, err) initializedCh := make(chan bool) go func() { cli := &DockerCli{client: apiClient, initTimeout: time.Millisecond} err := cli.Initialize(flags.NewClientOptions()) assert.Check(t, err) cli.CurrentVersion() close(initializedCh) }() select { case <-timeoutCtx.Done(): t.Fatal("timeout waiting for initialization to complete") case <-initializedCh: } select { case <-timeoutCtx.Done(): t.Fatal("server never received an init request") case <-receiveReqCh: } } // The CLI no longer disables/hides experimental CLI features, however, we need // to verify that existing configuration files do not break func TestExperimentalCLI(t *testing.T) { defaultVersion := "v1.55" testcases := []struct { doc string configfile string }{ { doc: "default", configfile: `{}`, }, { doc: "experimental", configfile: `{ "experimental": "enabled" }`, }, } for _, testcase := range testcases { testcase := testcase t.Run(testcase.doc, func(t *testing.T) { dir := fs.NewDir(t, testcase.doc, fs.WithFile("config.json", testcase.configfile)) defer dir.Remove() apiclient := &fakeClient{ version: defaultVersion, pingFunc: func() (types.Ping, error) { return types.Ping{Experimental: true, OSType: "linux", APIVersion: defaultVersion}, nil }, } cli := &DockerCli{client: apiclient, err: streams.NewOut(os.Stderr)} config.SetDir(dir.Path()) err := cli.Initialize(flags.NewClientOptions()) assert.NilError(t, err) }) } } func TestNewDockerCliAndOperators(t *testing.T) { // Test default operations and also overriding default ones cli, err := NewDockerCli( WithContentTrust(true), ) assert.NilError(t, err) // Check streams are initialized assert.Check(t, cli.In() != nil) assert.Check(t, cli.Out() != nil) assert.Check(t, cli.Err() != nil) assert.Equal(t, cli.ContentTrustEnabled(), true) // Apply can modify a dockerCli after construction inbuf := bytes.NewBuffer([]byte("input")) outbuf := bytes.NewBuffer(nil) errbuf := bytes.NewBuffer(nil) err = cli.Apply( WithInputStream(io.NopCloser(inbuf)), WithOutputStream(outbuf), WithErrorStream(errbuf), ) assert.NilError(t, err) // Check input stream inputStream, err := io.ReadAll(cli.In()) assert.NilError(t, err) assert.Equal(t, string(inputStream), "input") // Check output stream fmt.Fprintf(cli.Out(), "output") outputStream, err := io.ReadAll(outbuf) assert.NilError(t, err) assert.Equal(t, string(outputStream), "output") // Check error stream fmt.Fprintf(cli.Err(), "error") errStream, err := io.ReadAll(errbuf) assert.NilError(t, err) assert.Equal(t, string(errStream), "error") } func TestInitializeShouldAlwaysCreateTheContextStore(t *testing.T) { cli, err := NewDockerCli() assert.NilError(t, err) assert.NilError(t, cli.Initialize(flags.NewClientOptions(), WithInitializeClient(func(cli *DockerCli) (client.APIClient, error) { return client.NewClientWithOpts() }))) assert.Check(t, cli.ContextStore() != nil) } func TestHooksEnabled(t *testing.T) { t.Run("disabled by default", func(t *testing.T) { cli, err := NewDockerCli() assert.NilError(t, err) assert.Check(t, !cli.HooksEnabled()) }) t.Run("enabled in configFile", func(t *testing.T) { configFile := `{ "features": { "hooks": "true" }}` dir := fs.NewDir(t, "", fs.WithFile("config.json", configFile)) defer dir.Remove() cli, err := NewDockerCli() assert.NilError(t, err) config.SetDir(dir.Path()) assert.Check(t, cli.HooksEnabled()) }) t.Run("env var overrides configFile", func(t *testing.T) { configFile := `{ "features": { "hooks": "true" }}` t.Setenv("DOCKER_CLI_HOOKS", "false") dir := fs.NewDir(t, "", fs.WithFile("config.json", configFile)) defer dir.Remove() cli, err := NewDockerCli() assert.NilError(t, err) config.SetDir(dir.Path()) assert.Check(t, !cli.HooksEnabled()) }) t.Run("legacy env var overrides configFile", func(t *testing.T) { configFile := `{ "features": { "hooks": "true" }}` t.Setenv("DOCKER_CLI_HINTS", "false") dir := fs.NewDir(t, "", fs.WithFile("config.json", configFile)) defer dir.Remove() cli, err := NewDockerCli() assert.NilError(t, err) config.SetDir(dir.Path()) assert.Check(t, !cli.HooksEnabled()) }) }