Browse code

pull client API version negotiation out of the CLI and into the client

Signed-off-by: Royce Remer <royceremer@gmail.com>

Royce Remer authored on 2017/06/21 14:58:16
Showing 3 changed files
... ...
@@ -55,8 +55,11 @@ import (
55 55
 	"strings"
56 56
 
57 57
 	"github.com/docker/docker/api"
58
+	"github.com/docker/docker/api/types"
59
+	"github.com/docker/docker/api/types/versions"
58 60
 	"github.com/docker/go-connections/sockets"
59 61
 	"github.com/docker/go-connections/tlsconfig"
62
+	"golang.org/x/net/context"
60 63
 )
61 64
 
62 65
 // ErrRedirect is the error returned by checkRedirect when the request is non-GET.
... ...
@@ -238,13 +241,29 @@ func (cli *Client) ClientVersion() string {
238 238
 	return cli.version
239 239
 }
240 240
 
241
-// UpdateClientVersion updates the version string associated with this
242
-// instance of the Client. This operation doesn't acquire a mutex.
243
-func (cli *Client) UpdateClientVersion(v string) {
244
-	if !cli.manualOverride {
245
-		cli.version = v
241
+// NegotiateAPIVersion updates the version string associated with this
242
+// instance of the Client to match the latest version the server supports
243
+func (cli *Client) NegotiateAPIVersion(ctx context.Context) {
244
+	ping, _ := cli.Ping(ctx)
245
+	cli.NegotiateAPIVersionPing(ping)
246
+}
247
+
248
+// NegotiateAPIVersionPing updates the version string associated with this
249
+// instance of the Client to match the latest version the server supports
250
+func (cli *Client) NegotiateAPIVersionPing(p types.Ping) {
251
+	if cli.manualOverride {
252
+		return
246 253
 	}
247 254
 
255
+	// try the latest version before versioning headers existed
256
+	if p.APIVersion == "" {
257
+		p.APIVersion = "1.24"
258
+	}
259
+
260
+	// if server version is lower than the current cli, downgrade
261
+	if versions.LessThan(p.APIVersion, cli.ClientVersion()) {
262
+		cli.version = p.APIVersion
263
+	}
248 264
 }
249 265
 
250 266
 // DaemonHost returns the host associated with this instance of the Client.
... ...
@@ -2,8 +2,6 @@ package client
2 2
 
3 3
 import (
4 4
 	"bytes"
5
-	"encoding/json"
6
-	"io/ioutil"
7 5
 	"net/http"
8 6
 	"net/url"
9 7
 	"os"
... ...
@@ -14,7 +12,6 @@ import (
14 14
 	"github.com/docker/docker/api"
15 15
 	"github.com/docker/docker/api/types"
16 16
 	"github.com/stretchr/testify/assert"
17
-	"golang.org/x/net/context"
18 17
 )
19 18
 
20 19
 func TestNewEnvClient(t *testing.T) {
... ...
@@ -81,57 +78,27 @@ func TestNewEnvClient(t *testing.T) {
81 81
 			expectedVersion: "1.22",
82 82
 		},
83 83
 	}
84
+
85
+	env := envToMap()
86
+	defer mapToEnv(env)
84 87
 	for _, c := range cases {
85
-		recoverEnvs := setupEnvs(t, c.envs)
88
+		mapToEnv(env)
89
+		mapToEnv(c.envs)
86 90
 		apiclient, err := NewEnvClient()
87 91
 		if c.expectedError != "" {
88
-			if err == nil {
89
-				t.Errorf("expected an error for %v", c)
90
-			} else if err.Error() != c.expectedError {
91
-				t.Errorf("expected an error %s, got %s, for %v", c.expectedError, err.Error(), c)
92
-			}
92
+			assert.Error(t, err)
93
+			assert.Equal(t, c.expectedError, err.Error())
93 94
 		} else {
94
-			if err != nil {
95
-				t.Error(err)
96
-			}
95
+			assert.NoError(t, err)
97 96
 			version := apiclient.ClientVersion()
98
-			if version != c.expectedVersion {
99
-				t.Errorf("expected %s, got %s, for %v", c.expectedVersion, version, c)
100
-			}
97
+			assert.Equal(t, c.expectedVersion, version)
101 98
 		}
102 99
 
103 100
 		if c.envs["DOCKER_TLS_VERIFY"] != "" {
104 101
 			// pedantic checking that this is handled correctly
105 102
 			tr := apiclient.client.Transport.(*http.Transport)
106
-			if tr.TLSClientConfig == nil {
107
-				t.Error("no TLS config found when DOCKER_TLS_VERIFY enabled")
108
-			}
109
-
110
-			if tr.TLSClientConfig.InsecureSkipVerify {
111
-				t.Error("TLS verification should be enabled")
112
-			}
113
-		}
114
-
115
-		recoverEnvs(t)
116
-	}
117
-}
118
-
119
-func setupEnvs(t *testing.T, envs map[string]string) func(*testing.T) {
120
-	oldEnvs := map[string]string{}
121
-	for key, value := range envs {
122
-		oldEnv := os.Getenv(key)
123
-		oldEnvs[key] = oldEnv
124
-		err := os.Setenv(key, value)
125
-		if err != nil {
126
-			t.Error(err)
127
-		}
128
-	}
129
-	return func(t *testing.T) {
130
-		for key, value := range oldEnvs {
131
-			err := os.Setenv(key, value)
132
-			if err != nil {
133
-				t.Error(err)
134
-			}
103
+			assert.NotNil(t, tr.TLSClientConfig)
104
+			assert.Equal(t, tr.TLSClientConfig.InsecureSkipVerify, false)
135 105
 		}
136 106
 	}
137 107
 }
... ...
@@ -161,14 +128,10 @@ func TestGetAPIPath(t *testing.T) {
161 161
 			t.Fatal(err)
162 162
 		}
163 163
 		g := c.getAPIPath(cs.p, cs.q)
164
-		if g != cs.e {
165
-			t.Fatalf("Expected %s, got %s", cs.e, g)
166
-		}
164
+		assert.Equal(t, g, cs.e)
167 165
 
168 166
 		err = c.Close()
169
-		if nil != err {
170
-			t.Fatalf("close client failed, error message: %s", err)
171
-		}
167
+		assert.NoError(t, err)
172 168
 	}
173 169
 }
174 170
 
... ...
@@ -189,101 +152,148 @@ func TestParseHost(t *testing.T) {
189 189
 
190 190
 	for _, cs := range cases {
191 191
 		p, a, b, e := ParseHost(cs.host)
192
-		if cs.err && e == nil {
193
-			t.Fatalf("expected error, got nil")
194
-		}
195
-		if !cs.err && e != nil {
196
-			t.Fatal(e)
197
-		}
198
-		if cs.proto != p {
199
-			t.Fatalf("expected proto %s, got %s", cs.proto, p)
200
-		}
201
-		if cs.addr != a {
202
-			t.Fatalf("expected addr %s, got %s", cs.addr, a)
203
-		}
204
-		if cs.base != b {
205
-			t.Fatalf("expected base %s, got %s", cs.base, b)
192
+		// if we expected an error to be returned...
193
+		if cs.err {
194
+			assert.Error(t, e)
206 195
 		}
196
+		assert.Equal(t, cs.proto, p)
197
+		assert.Equal(t, cs.addr, a)
198
+		assert.Equal(t, cs.base, b)
207 199
 	}
208 200
 }
209 201
 
210
-func TestUpdateClientVersion(t *testing.T) {
211
-	client := &Client{
212
-		client: newMockClient(func(req *http.Request) (*http.Response, error) {
213
-			splitQuery := strings.Split(req.URL.Path, "/")
214
-			queryVersion := splitQuery[1]
215
-			b, err := json.Marshal(types.Version{
216
-				APIVersion: queryVersion,
217
-			})
218
-			if err != nil {
219
-				return nil, err
220
-			}
221
-			return &http.Response{
222
-				StatusCode: http.StatusOK,
223
-				Body:       ioutil.NopCloser(bytes.NewReader(b)),
224
-			}, nil
225
-		}),
202
+func TestNewEnvClientSetsDefaultVersion(t *testing.T) {
203
+	env := envToMap()
204
+	defer mapToEnv(env)
205
+
206
+	envMap := map[string]string{
207
+		"DOCKER_HOST":        "",
208
+		"DOCKER_API_VERSION": "",
209
+		"DOCKER_TLS_VERIFY":  "",
210
+		"DOCKER_CERT_PATH":   "",
226 211
 	}
212
+	mapToEnv(envMap)
227 213
 
228
-	cases := []struct {
229
-		v string
230
-	}{
231
-		{"1.20"},
232
-		{"v1.21"},
233
-		{"1.22"},
234
-		{"v1.22"},
214
+	client, err := NewEnvClient()
215
+	if err != nil {
216
+		t.Fatal(err)
235 217
 	}
218
+	assert.Equal(t, client.version, api.DefaultVersion)
236 219
 
237
-	for _, cs := range cases {
238
-		client.UpdateClientVersion(cs.v)
239
-		r, err := client.ServerVersion(context.Background())
240
-		if err != nil {
241
-			t.Fatal(err)
242
-		}
243
-		if strings.TrimPrefix(r.APIVersion, "v") != strings.TrimPrefix(cs.v, "v") {
244
-			t.Fatalf("Expected %s, got %s", cs.v, r.APIVersion)
245
-		}
220
+	expected := "1.22"
221
+	os.Setenv("DOCKER_API_VERSION", expected)
222
+	client, err = NewEnvClient()
223
+	if err != nil {
224
+		t.Fatal(err)
246 225
 	}
226
+	assert.Equal(t, expected, client.version)
247 227
 }
248 228
 
249
-func TestNewEnvClientSetsDefaultVersion(t *testing.T) {
250
-	// Unset environment variables
251
-	envVarKeys := []string{
252
-		"DOCKER_HOST",
253
-		"DOCKER_API_VERSION",
254
-		"DOCKER_TLS_VERIFY",
255
-		"DOCKER_CERT_PATH",
229
+// TestNegotiateAPIVersionEmpty asserts that client.Client can
230
+// negotiate a compatible APIVersion when omitted
231
+func TestNegotiateAPIVersionEmpty(t *testing.T) {
232
+	env := envToMap()
233
+	defer mapToEnv(env)
234
+
235
+	envMap := map[string]string{
236
+		"DOCKER_API_VERSION": "",
256 237
 	}
257
-	envVarValues := make(map[string]string)
258
-	for _, key := range envVarKeys {
259
-		envVarValues[key] = os.Getenv(key)
260
-		os.Setenv(key, "")
238
+	mapToEnv(envMap)
239
+
240
+	client, err := NewEnvClient()
241
+	if err != nil {
242
+		t.Fatal(err)
261 243
 	}
262 244
 
245
+	ping := types.Ping{
246
+		APIVersion:   "",
247
+		OSType:       "linux",
248
+		Experimental: false,
249
+	}
250
+
251
+	// set our version to something new
252
+	client.version = "1.25"
253
+
254
+	// if no version from server, expect the earliest
255
+	// version before APIVersion was implemented
256
+	expected := "1.24"
257
+
258
+	// test downgrade
259
+	client.NegotiateAPIVersionPing(ping)
260
+	assert.Equal(t, expected, client.version)
261
+}
262
+
263
+// TestNegotiateAPIVersion asserts that client.Client can
264
+// negotiate a compatible APIVersion with the server
265
+func TestNegotiateAPIVersion(t *testing.T) {
263 266
 	client, err := NewEnvClient()
264 267
 	if err != nil {
265 268
 		t.Fatal(err)
266 269
 	}
267
-	if client.version != api.DefaultVersion {
268
-		t.Fatalf("Expected %s, got %s", api.DefaultVersion, client.version)
270
+
271
+	expected := "1.21"
272
+
273
+	ping := types.Ping{
274
+		APIVersion:   expected,
275
+		OSType:       "linux",
276
+		Experimental: false,
277
+	}
278
+
279
+	// set our version to something new
280
+	client.version = "1.22"
281
+
282
+	// test downgrade
283
+	client.NegotiateAPIVersionPing(ping)
284
+	assert.Equal(t, expected, client.version)
285
+}
286
+
287
+// TestNegotiateAPIVersionOverride asserts that we honor
288
+// the environment variable DOCKER_API_VERSION when negotianing versions
289
+func TestNegotiateAPVersionOverride(t *testing.T) {
290
+	env := envToMap()
291
+	defer mapToEnv(env)
292
+
293
+	envMap := map[string]string{
294
+		"DOCKER_API_VERSION": "9.99",
269 295
 	}
296
+	mapToEnv(envMap)
270 297
 
271
-	expected := "1.22"
272
-	os.Setenv("DOCKER_API_VERSION", expected)
273
-	client, err = NewEnvClient()
298
+	client, err := NewEnvClient()
274 299
 	if err != nil {
275 300
 		t.Fatal(err)
276 301
 	}
277
-	if client.version != expected {
278
-		t.Fatalf("Expected %s, got %s", expected, client.version)
302
+
303
+	ping := types.Ping{
304
+		APIVersion:   "1.24",
305
+		OSType:       "linux",
306
+		Experimental: false,
279 307
 	}
280 308
 
281
-	// Restore environment variables
282
-	for _, key := range envVarKeys {
283
-		os.Setenv(key, envVarValues[key])
309
+	expected := envMap["DOCKER_API_VERSION"]
310
+
311
+	// test that we honored the env var
312
+	client.NegotiateAPIVersionPing(ping)
313
+	assert.Equal(t, expected, client.version)
314
+}
315
+
316
+// mapToEnv takes a map of environment variables and sets them
317
+func mapToEnv(env map[string]string) {
318
+	for k, v := range env {
319
+		os.Setenv(k, v)
284 320
 	}
285 321
 }
286 322
 
323
+// envToMap returns a map of environment variables
324
+func envToMap() map[string]string {
325
+	env := make(map[string]string)
326
+	for _, e := range os.Environ() {
327
+		kv := strings.SplitAfterN(e, "=", 2)
328
+		env[kv[0]] = kv[1]
329
+	}
330
+
331
+	return env
332
+}
333
+
287 334
 type roundTripFunc func(*http.Request) (*http.Response, error)
288 335
 
289 336
 func (rtf roundTripFunc) RoundTrip(req *http.Request) (*http.Response, error) {
... ...
@@ -33,7 +33,8 @@ type CommonAPIClient interface {
33 33
 	ClientVersion() string
34 34
 	DaemonHost() string
35 35
 	ServerVersion(ctx context.Context) (types.Version, error)
36
-	UpdateClientVersion(v string)
36
+	NegotiateAPIVersion(ctx context.Context)
37
+	NegotiateAPIVersionPing(types.Ping)
37 38
 }
38 39
 
39 40
 // ContainerAPIClient defines API client methods for the containers