mirror of https://github.com/docker/cli.git
363 lines
9.7 KiB
Go
363 lines
9.7 KiB
Go
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/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 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: 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())
|
|
})
|
|
}
|