1
0
Fork 0
mirror of https://github.com/moby/moby.git synced 2022-11-09 12:21:53 -05:00

Merge pull request #32779 from rremer/auto-update-client

pull client API version negotiation out of the CLI and into the client
This commit is contained in:
Brian Goff 2017-06-21 17:35:22 -07:00 committed by GitHub
commit 8a672ba1aa
3 changed files with 158 additions and 128 deletions

View file

@ -55,8 +55,11 @@ import (
"strings" "strings"
"github.com/docker/docker/api" "github.com/docker/docker/api"
"github.com/docker/docker/api/types"
"github.com/docker/docker/api/types/versions"
"github.com/docker/go-connections/sockets" "github.com/docker/go-connections/sockets"
"github.com/docker/go-connections/tlsconfig" "github.com/docker/go-connections/tlsconfig"
"golang.org/x/net/context"
) )
// ErrRedirect is the error returned by checkRedirect when the request is non-GET. // ErrRedirect is the error returned by checkRedirect when the request is non-GET.
@ -238,13 +241,29 @@ func (cli *Client) ClientVersion() string {
return cli.version return cli.version
} }
// UpdateClientVersion updates the version string associated with this // NegotiateAPIVersion updates the version string associated with this
// instance of the Client. This operation doesn't acquire a mutex. // instance of the Client to match the latest version the server supports
func (cli *Client) UpdateClientVersion(v string) { func (cli *Client) NegotiateAPIVersion(ctx context.Context) {
if !cli.manualOverride { ping, _ := cli.Ping(ctx)
cli.version = v cli.NegotiateAPIVersionPing(ping)
}
// NegotiateAPIVersionPing updates the version string associated with this
// instance of the Client to match the latest version the server supports
func (cli *Client) NegotiateAPIVersionPing(p types.Ping) {
if cli.manualOverride {
return
} }
// try the latest version before versioning headers existed
if p.APIVersion == "" {
p.APIVersion = "1.24"
}
// if server version is lower than the current cli, downgrade
if versions.LessThan(p.APIVersion, cli.ClientVersion()) {
cli.version = p.APIVersion
}
} }
// DaemonHost returns the host associated with this instance of the Client. // DaemonHost returns the host associated with this instance of the Client.

View file

@ -2,8 +2,6 @@ package client
import ( import (
"bytes" "bytes"
"encoding/json"
"io/ioutil"
"net/http" "net/http"
"net/url" "net/url"
"os" "os"
@ -14,7 +12,6 @@ import (
"github.com/docker/docker/api" "github.com/docker/docker/api"
"github.com/docker/docker/api/types" "github.com/docker/docker/api/types"
"github.com/stretchr/testify/assert" "github.com/stretchr/testify/assert"
"golang.org/x/net/context"
) )
func TestNewEnvClient(t *testing.T) { func TestNewEnvClient(t *testing.T) {
@ -81,57 +78,27 @@ func TestNewEnvClient(t *testing.T) {
expectedVersion: "1.22", expectedVersion: "1.22",
}, },
} }
env := envToMap()
defer mapToEnv(env)
for _, c := range cases { for _, c := range cases {
recoverEnvs := setupEnvs(t, c.envs) mapToEnv(env)
mapToEnv(c.envs)
apiclient, err := NewEnvClient() apiclient, err := NewEnvClient()
if c.expectedError != "" { if c.expectedError != "" {
if err == nil { assert.Error(t, err)
t.Errorf("expected an error for %v", c) assert.Equal(t, c.expectedError, err.Error())
} else if err.Error() != c.expectedError {
t.Errorf("expected an error %s, got %s, for %v", c.expectedError, err.Error(), c)
}
} else { } else {
if err != nil { assert.NoError(t, err)
t.Error(err)
}
version := apiclient.ClientVersion() version := apiclient.ClientVersion()
if version != c.expectedVersion { assert.Equal(t, c.expectedVersion, version)
t.Errorf("expected %s, got %s, for %v", c.expectedVersion, version, c)
}
} }
if c.envs["DOCKER_TLS_VERIFY"] != "" { if c.envs["DOCKER_TLS_VERIFY"] != "" {
// pedantic checking that this is handled correctly // pedantic checking that this is handled correctly
tr := apiclient.client.Transport.(*http.Transport) tr := apiclient.client.Transport.(*http.Transport)
if tr.TLSClientConfig == nil { assert.NotNil(t, tr.TLSClientConfig)
t.Error("no TLS config found when DOCKER_TLS_VERIFY enabled") assert.Equal(t, tr.TLSClientConfig.InsecureSkipVerify, false)
}
if tr.TLSClientConfig.InsecureSkipVerify {
t.Error("TLS verification should be enabled")
}
}
recoverEnvs(t)
}
}
func setupEnvs(t *testing.T, envs map[string]string) func(*testing.T) {
oldEnvs := map[string]string{}
for key, value := range envs {
oldEnv := os.Getenv(key)
oldEnvs[key] = oldEnv
err := os.Setenv(key, value)
if err != nil {
t.Error(err)
}
}
return func(t *testing.T) {
for key, value := range oldEnvs {
err := os.Setenv(key, value)
if err != nil {
t.Error(err)
}
} }
} }
} }
@ -161,14 +128,10 @@ func TestGetAPIPath(t *testing.T) {
t.Fatal(err) t.Fatal(err)
} }
g := c.getAPIPath(cs.p, cs.q) g := c.getAPIPath(cs.p, cs.q)
if g != cs.e { assert.Equal(t, g, cs.e)
t.Fatalf("Expected %s, got %s", cs.e, g)
}
err = c.Close() err = c.Close()
if nil != err { assert.NoError(t, err)
t.Fatalf("close client failed, error message: %s", err)
}
} }
} }
@ -189,84 +152,33 @@ func TestParseHost(t *testing.T) {
for _, cs := range cases { for _, cs := range cases {
p, a, b, e := ParseHost(cs.host) p, a, b, e := ParseHost(cs.host)
if cs.err && e == nil { // if we expected an error to be returned...
t.Fatalf("expected error, got nil") if cs.err {
} assert.Error(t, e)
if !cs.err && e != nil {
t.Fatal(e)
}
if cs.proto != p {
t.Fatalf("expected proto %s, got %s", cs.proto, p)
}
if cs.addr != a {
t.Fatalf("expected addr %s, got %s", cs.addr, a)
}
if cs.base != b {
t.Fatalf("expected base %s, got %s", cs.base, b)
}
}
}
func TestUpdateClientVersion(t *testing.T) {
client := &Client{
client: newMockClient(func(req *http.Request) (*http.Response, error) {
splitQuery := strings.Split(req.URL.Path, "/")
queryVersion := splitQuery[1]
b, err := json.Marshal(types.Version{
APIVersion: queryVersion,
})
if err != nil {
return nil, err
}
return &http.Response{
StatusCode: http.StatusOK,
Body: ioutil.NopCloser(bytes.NewReader(b)),
}, nil
}),
}
cases := []struct {
v string
}{
{"1.20"},
{"v1.21"},
{"1.22"},
{"v1.22"},
}
for _, cs := range cases {
client.UpdateClientVersion(cs.v)
r, err := client.ServerVersion(context.Background())
if err != nil {
t.Fatal(err)
}
if strings.TrimPrefix(r.APIVersion, "v") != strings.TrimPrefix(cs.v, "v") {
t.Fatalf("Expected %s, got %s", cs.v, r.APIVersion)
} }
assert.Equal(t, cs.proto, p)
assert.Equal(t, cs.addr, a)
assert.Equal(t, cs.base, b)
} }
} }
func TestNewEnvClientSetsDefaultVersion(t *testing.T) { func TestNewEnvClientSetsDefaultVersion(t *testing.T) {
// Unset environment variables env := envToMap()
envVarKeys := []string{ defer mapToEnv(env)
"DOCKER_HOST",
"DOCKER_API_VERSION", envMap := map[string]string{
"DOCKER_TLS_VERIFY", "DOCKER_HOST": "",
"DOCKER_CERT_PATH", "DOCKER_API_VERSION": "",
} "DOCKER_TLS_VERIFY": "",
envVarValues := make(map[string]string) "DOCKER_CERT_PATH": "",
for _, key := range envVarKeys {
envVarValues[key] = os.Getenv(key)
os.Setenv(key, "")
} }
mapToEnv(envMap)
client, err := NewEnvClient() client, err := NewEnvClient()
if err != nil { if err != nil {
t.Fatal(err) t.Fatal(err)
} }
if client.version != api.DefaultVersion { assert.Equal(t, client.version, api.DefaultVersion)
t.Fatalf("Expected %s, got %s", api.DefaultVersion, client.version)
}
expected := "1.22" expected := "1.22"
os.Setenv("DOCKER_API_VERSION", expected) os.Setenv("DOCKER_API_VERSION", expected)
@ -274,14 +186,112 @@ func TestNewEnvClientSetsDefaultVersion(t *testing.T) {
if err != nil { if err != nil {
t.Fatal(err) t.Fatal(err)
} }
if client.version != expected { assert.Equal(t, expected, client.version)
t.Fatalf("Expected %s, got %s", expected, client.version) }
// TestNegotiateAPIVersionEmpty asserts that client.Client can
// negotiate a compatible APIVersion when omitted
func TestNegotiateAPIVersionEmpty(t *testing.T) {
env := envToMap()
defer mapToEnv(env)
envMap := map[string]string{
"DOCKER_API_VERSION": "",
}
mapToEnv(envMap)
client, err := NewEnvClient()
if err != nil {
t.Fatal(err)
} }
// Restore environment variables ping := types.Ping{
for _, key := range envVarKeys { APIVersion: "",
os.Setenv(key, envVarValues[key]) OSType: "linux",
Experimental: false,
} }
// set our version to something new
client.version = "1.25"
// if no version from server, expect the earliest
// version before APIVersion was implemented
expected := "1.24"
// test downgrade
client.NegotiateAPIVersionPing(ping)
assert.Equal(t, expected, client.version)
}
// TestNegotiateAPIVersion asserts that client.Client can
// negotiate a compatible APIVersion with the server
func TestNegotiateAPIVersion(t *testing.T) {
client, err := NewEnvClient()
if err != nil {
t.Fatal(err)
}
expected := "1.21"
ping := types.Ping{
APIVersion: expected,
OSType: "linux",
Experimental: false,
}
// set our version to something new
client.version = "1.22"
// test downgrade
client.NegotiateAPIVersionPing(ping)
assert.Equal(t, expected, client.version)
}
// TestNegotiateAPIVersionOverride asserts that we honor
// the environment variable DOCKER_API_VERSION when negotianing versions
func TestNegotiateAPVersionOverride(t *testing.T) {
env := envToMap()
defer mapToEnv(env)
envMap := map[string]string{
"DOCKER_API_VERSION": "9.99",
}
mapToEnv(envMap)
client, err := NewEnvClient()
if err != nil {
t.Fatal(err)
}
ping := types.Ping{
APIVersion: "1.24",
OSType: "linux",
Experimental: false,
}
expected := envMap["DOCKER_API_VERSION"]
// test that we honored the env var
client.NegotiateAPIVersionPing(ping)
assert.Equal(t, expected, client.version)
}
// mapToEnv takes a map of environment variables and sets them
func mapToEnv(env map[string]string) {
for k, v := range env {
os.Setenv(k, v)
}
}
// envToMap returns a map of environment variables
func envToMap() map[string]string {
env := make(map[string]string)
for _, e := range os.Environ() {
kv := strings.SplitAfterN(e, "=", 2)
env[kv[0]] = kv[1]
}
return env
} }
type roundTripFunc func(*http.Request) (*http.Response, error) type roundTripFunc func(*http.Request) (*http.Response, error)

View file

@ -33,7 +33,8 @@ type CommonAPIClient interface {
ClientVersion() string ClientVersion() string
DaemonHost() string DaemonHost() string
ServerVersion(ctx context.Context) (types.Version, error) ServerVersion(ctx context.Context) (types.Version, error)
UpdateClientVersion(v string) NegotiateAPIVersion(ctx context.Context)
NegotiateAPIVersionPing(types.Ping)
} }
// ContainerAPIClient defines API client methods for the containers // ContainerAPIClient defines API client methods for the containers