gopkg.in/docker/docker.v20@v20.10.27/client/client_test.go (about)

     1  package client // import "github.com/docker/docker/client"
     2  
     3  import (
     4  	"bytes"
     5  	"context"
     6  	"io"
     7  	"net/http"
     8  	"net/url"
     9  	"os"
    10  	"runtime"
    11  	"strings"
    12  	"testing"
    13  
    14  	"github.com/docker/docker/api"
    15  	"github.com/docker/docker/api/types"
    16  	"gotest.tools/v3/assert"
    17  	is "gotest.tools/v3/assert/cmp"
    18  	"gotest.tools/v3/env"
    19  	"gotest.tools/v3/skip"
    20  )
    21  
    22  func TestNewClientWithOpsFromEnv(t *testing.T) {
    23  	skip.If(t, runtime.GOOS == "windows")
    24  
    25  	testcases := []struct {
    26  		doc             string
    27  		envs            map[string]string
    28  		expectedError   string
    29  		expectedVersion string
    30  	}{
    31  		{
    32  			doc:             "default api version",
    33  			envs:            map[string]string{},
    34  			expectedVersion: api.DefaultVersion,
    35  		},
    36  		{
    37  			doc: "invalid cert path",
    38  			envs: map[string]string{
    39  				"DOCKER_CERT_PATH": "invalid/path",
    40  			},
    41  			expectedError: "Could not load X509 key pair: open invalid/path/cert.pem: no such file or directory",
    42  		},
    43  		{
    44  			doc: "default api version with cert path",
    45  			envs: map[string]string{
    46  				"DOCKER_CERT_PATH": "testdata/",
    47  			},
    48  			expectedVersion: api.DefaultVersion,
    49  		},
    50  		{
    51  			doc: "default api version with cert path and tls verify",
    52  			envs: map[string]string{
    53  				"DOCKER_CERT_PATH":  "testdata/",
    54  				"DOCKER_TLS_VERIFY": "1",
    55  			},
    56  			expectedVersion: api.DefaultVersion,
    57  		},
    58  		{
    59  			doc: "default api version with cert path and host",
    60  			envs: map[string]string{
    61  				"DOCKER_CERT_PATH": "testdata/",
    62  				"DOCKER_HOST":      "https://notaunixsocket",
    63  			},
    64  			expectedVersion: api.DefaultVersion,
    65  		},
    66  		{
    67  			doc: "invalid docker host",
    68  			envs: map[string]string{
    69  				"DOCKER_HOST": "host",
    70  			},
    71  			expectedError: "unable to parse docker host `host`",
    72  		},
    73  		{
    74  			doc: "invalid docker host, with good format",
    75  			envs: map[string]string{
    76  				"DOCKER_HOST": "invalid://url",
    77  			},
    78  			expectedVersion: api.DefaultVersion,
    79  		},
    80  		{
    81  			doc: "override api version",
    82  			envs: map[string]string{
    83  				"DOCKER_API_VERSION": "1.22",
    84  			},
    85  			expectedVersion: "1.22",
    86  		},
    87  	}
    88  
    89  	defer env.PatchAll(t, nil)()
    90  	for _, c := range testcases {
    91  		env.PatchAll(t, c.envs)
    92  		apiclient, err := NewClientWithOpts(FromEnv)
    93  		if c.expectedError != "" {
    94  			assert.Check(t, is.Error(err, c.expectedError), c.doc)
    95  		} else {
    96  			assert.Check(t, err, c.doc)
    97  			version := apiclient.ClientVersion()
    98  			assert.Check(t, is.Equal(c.expectedVersion, version), c.doc)
    99  		}
   100  
   101  		if c.envs["DOCKER_TLS_VERIFY"] != "" {
   102  			// pedantic checking that this is handled correctly
   103  			tr := apiclient.client.Transport.(*http.Transport)
   104  			assert.Assert(t, tr.TLSClientConfig != nil, c.doc)
   105  			assert.Check(t, is.Equal(tr.TLSClientConfig.InsecureSkipVerify, false), c.doc)
   106  		}
   107  	}
   108  }
   109  
   110  func TestGetAPIPath(t *testing.T) {
   111  	testcases := []struct {
   112  		version  string
   113  		path     string
   114  		query    url.Values
   115  		expected string
   116  	}{
   117  		{"", "/containers/json", nil, "/containers/json"},
   118  		{"", "/containers/json", url.Values{}, "/containers/json"},
   119  		{"", "/containers/json", url.Values{"s": []string{"c"}}, "/containers/json?s=c"},
   120  		{"1.22", "/containers/json", nil, "/v1.22/containers/json"},
   121  		{"1.22", "/containers/json", url.Values{}, "/v1.22/containers/json"},
   122  		{"1.22", "/containers/json", url.Values{"s": []string{"c"}}, "/v1.22/containers/json?s=c"},
   123  		{"v1.22", "/containers/json", nil, "/v1.22/containers/json"},
   124  		{"v1.22", "/containers/json", url.Values{}, "/v1.22/containers/json"},
   125  		{"v1.22", "/containers/json", url.Values{"s": []string{"c"}}, "/v1.22/containers/json?s=c"},
   126  		{"v1.22", "/networks/kiwl$%^", nil, "/v1.22/networks/kiwl$%25%5E"},
   127  	}
   128  
   129  	ctx := context.TODO()
   130  	for _, testcase := range testcases {
   131  		c := Client{version: testcase.version, basePath: "/"}
   132  		actual := c.getAPIPath(ctx, testcase.path, testcase.query)
   133  		assert.Check(t, is.Equal(actual, testcase.expected))
   134  	}
   135  }
   136  
   137  func TestParseHostURL(t *testing.T) {
   138  	testcases := []struct {
   139  		host        string
   140  		expected    *url.URL
   141  		expectedErr string
   142  	}{
   143  		{
   144  			host:        "",
   145  			expectedErr: "unable to parse docker host",
   146  		},
   147  		{
   148  			host:        "foobar",
   149  			expectedErr: "unable to parse docker host",
   150  		},
   151  		{
   152  			host:     "foo://bar",
   153  			expected: &url.URL{Scheme: "foo", Host: "bar"},
   154  		},
   155  		{
   156  			host:     "tcp://localhost:2476",
   157  			expected: &url.URL{Scheme: "tcp", Host: "localhost:2476"},
   158  		},
   159  		{
   160  			host:     "tcp://localhost:2476/path",
   161  			expected: &url.URL{Scheme: "tcp", Host: "localhost:2476", Path: "/path"},
   162  		},
   163  	}
   164  
   165  	for _, testcase := range testcases {
   166  		actual, err := ParseHostURL(testcase.host)
   167  		if testcase.expectedErr != "" {
   168  			assert.Check(t, is.ErrorContains(err, testcase.expectedErr))
   169  		}
   170  		assert.Check(t, is.DeepEqual(testcase.expected, actual))
   171  	}
   172  }
   173  
   174  func TestNewClientWithOpsFromEnvSetsDefaultVersion(t *testing.T) {
   175  	defer env.PatchAll(t, map[string]string{
   176  		"DOCKER_HOST":        "",
   177  		"DOCKER_API_VERSION": "",
   178  		"DOCKER_TLS_VERIFY":  "",
   179  		"DOCKER_CERT_PATH":   "",
   180  	})()
   181  
   182  	client, err := NewClientWithOpts(FromEnv)
   183  	if err != nil {
   184  		t.Fatal(err)
   185  	}
   186  	assert.Check(t, is.Equal(client.version, api.DefaultVersion))
   187  
   188  	expected := "1.22"
   189  	os.Setenv("DOCKER_API_VERSION", expected)
   190  	client, err = NewClientWithOpts(FromEnv)
   191  	if err != nil {
   192  		t.Fatal(err)
   193  	}
   194  	assert.Check(t, is.Equal(expected, client.version))
   195  }
   196  
   197  // TestNegotiateAPIVersionEmpty asserts that client.Client can
   198  // negotiate a compatible APIVersion when omitted
   199  func TestNegotiateAPIVersionEmpty(t *testing.T) {
   200  	defer env.PatchAll(t, map[string]string{"DOCKER_API_VERSION": ""})()
   201  
   202  	client, err := NewClientWithOpts(FromEnv)
   203  	assert.NilError(t, err)
   204  
   205  	ping := types.Ping{
   206  		APIVersion:   "",
   207  		OSType:       "linux",
   208  		Experimental: false,
   209  	}
   210  
   211  	// set our version to something new
   212  	client.version = "1.25"
   213  
   214  	// if no version from server, expect the earliest
   215  	// version before APIVersion was implemented
   216  	expected := "1.24"
   217  
   218  	// test downgrade
   219  	client.NegotiateAPIVersionPing(ping)
   220  	assert.Check(t, is.Equal(expected, client.version))
   221  }
   222  
   223  // TestNegotiateAPIVersion asserts that client.Client can
   224  // negotiate a compatible APIVersion with the server
   225  func TestNegotiateAPIVersion(t *testing.T) {
   226  	client, err := NewClientWithOpts(FromEnv)
   227  	assert.NilError(t, err)
   228  
   229  	expected := "1.21"
   230  	ping := types.Ping{
   231  		APIVersion:   expected,
   232  		OSType:       "linux",
   233  		Experimental: false,
   234  	}
   235  
   236  	// set our version to something new
   237  	client.version = "1.22"
   238  
   239  	// test downgrade
   240  	client.NegotiateAPIVersionPing(ping)
   241  	assert.Check(t, is.Equal(expected, client.version))
   242  
   243  	// set the client version to something older, and verify that we keep the
   244  	// original setting.
   245  	expected = "1.20"
   246  	client.version = expected
   247  	client.NegotiateAPIVersionPing(ping)
   248  	assert.Check(t, is.Equal(expected, client.version))
   249  
   250  }
   251  
   252  // TestNegotiateAPIVersionOverride asserts that we honor
   253  // the environment variable DOCKER_API_VERSION when negotiating versions
   254  func TestNegotiateAPVersionOverride(t *testing.T) {
   255  	expected := "9.99"
   256  	defer env.PatchAll(t, map[string]string{"DOCKER_API_VERSION": expected})()
   257  
   258  	client, err := NewClientWithOpts(FromEnv)
   259  	assert.NilError(t, err)
   260  
   261  	ping := types.Ping{
   262  		APIVersion:   "1.24",
   263  		OSType:       "linux",
   264  		Experimental: false,
   265  	}
   266  
   267  	// test that we honored the env var
   268  	client.NegotiateAPIVersionPing(ping)
   269  	assert.Check(t, is.Equal(expected, client.version))
   270  }
   271  
   272  func TestNegotiateAPIVersionAutomatic(t *testing.T) {
   273  	var pingVersion string
   274  	httpClient := newMockClient(func(req *http.Request) (*http.Response, error) {
   275  		resp := &http.Response{StatusCode: http.StatusOK, Header: http.Header{}}
   276  		resp.Header.Set("API-Version", pingVersion)
   277  		resp.Body = io.NopCloser(strings.NewReader("OK"))
   278  		return resp, nil
   279  	})
   280  
   281  	client, err := NewClientWithOpts(
   282  		WithHTTPClient(httpClient),
   283  		WithAPIVersionNegotiation(),
   284  	)
   285  	assert.NilError(t, err)
   286  
   287  	ctx := context.Background()
   288  	assert.Equal(t, client.ClientVersion(), api.DefaultVersion)
   289  
   290  	// First request should trigger negotiation
   291  	pingVersion = "1.35"
   292  	_, _ = client.Info(ctx)
   293  	assert.Equal(t, client.ClientVersion(), "1.35")
   294  
   295  	// Once successfully negotiated, subsequent requests should not re-negotiate
   296  	pingVersion = "1.25"
   297  	_, _ = client.Info(ctx)
   298  	assert.Equal(t, client.ClientVersion(), "1.35")
   299  }
   300  
   301  // TestNegotiateAPIVersionWithEmptyVersion asserts that initializing a client
   302  // with an empty version string does still allow API-version negotiation
   303  func TestNegotiateAPIVersionWithEmptyVersion(t *testing.T) {
   304  	client, err := NewClientWithOpts(WithVersion(""))
   305  	assert.NilError(t, err)
   306  
   307  	client.NegotiateAPIVersionPing(types.Ping{APIVersion: "1.35"})
   308  	assert.Equal(t, client.version, "1.35")
   309  }
   310  
   311  // TestNegotiateAPIVersionWithFixedVersion asserts that initializing a client
   312  // with an fixed version disables API-version negotiation
   313  func TestNegotiateAPIVersionWithFixedVersion(t *testing.T) {
   314  	client, err := NewClientWithOpts(WithVersion("1.35"))
   315  	assert.NilError(t, err)
   316  
   317  	client.NegotiateAPIVersionPing(types.Ping{APIVersion: "1.31"})
   318  	assert.Equal(t, client.version, "1.35")
   319  }
   320  
   321  type roundTripFunc func(*http.Request) (*http.Response, error)
   322  
   323  func (rtf roundTripFunc) RoundTrip(req *http.Request) (*http.Response, error) {
   324  	return rtf(req)
   325  }
   326  
   327  type bytesBufferClose struct {
   328  	*bytes.Buffer
   329  }
   330  
   331  func (bbc bytesBufferClose) Close() error {
   332  	return nil
   333  }
   334  
   335  func TestClientRedirect(t *testing.T) {
   336  	client := &http.Client{
   337  		CheckRedirect: CheckRedirect,
   338  		Transport: roundTripFunc(func(req *http.Request) (*http.Response, error) {
   339  			if req.URL.String() == "/bla" {
   340  				return &http.Response{StatusCode: 404}, nil
   341  			}
   342  			return &http.Response{
   343  				StatusCode: 301,
   344  				Header:     map[string][]string{"Location": {"/bla"}},
   345  				Body:       bytesBufferClose{bytes.NewBuffer(nil)},
   346  			}, nil
   347  		}),
   348  	}
   349  
   350  	cases := []struct {
   351  		httpMethod  string
   352  		expectedErr *url.Error
   353  		statusCode  int
   354  	}{
   355  		{http.MethodGet, nil, 301},
   356  		{http.MethodPost, &url.Error{Op: "Post", URL: "/bla", Err: ErrRedirect}, 301},
   357  		{http.MethodPut, &url.Error{Op: "Put", URL: "/bla", Err: ErrRedirect}, 301},
   358  		{http.MethodDelete, &url.Error{Op: "Delete", URL: "/bla", Err: ErrRedirect}, 301},
   359  	}
   360  
   361  	for _, tc := range cases {
   362  		req, err := http.NewRequest(tc.httpMethod, "/redirectme", nil)
   363  		assert.Check(t, err)
   364  		resp, err := client.Do(req)
   365  		assert.Check(t, is.Equal(tc.statusCode, resp.StatusCode))
   366  		if tc.expectedErr == nil {
   367  			assert.Check(t, is.Nil(err))
   368  		} else {
   369  			urlError, ok := err.(*url.Error)
   370  			assert.Assert(t, ok, "%T is not *url.Error", err)
   371  			assert.Check(t, is.Equal(*tc.expectedErr, *urlError))
   372  		}
   373  	}
   374  }