Browse code

Add configs support to client

Signed-off-by: Aaron Lehmann <aaron.lehmann@docker.com>

Aaron Lehmann authored on 2017/03/16 07:04:32
Showing 12 changed files
1 1
new file mode 100644
... ...
@@ -0,0 +1,22 @@
0
+package client
1
+
2
+import (
3
+	"encoding/json"
4
+
5
+	"github.com/docker/docker/api/types"
6
+	"github.com/docker/docker/api/types/swarm"
7
+	"golang.org/x/net/context"
8
+)
9
+
10
+// ConfigCreate creates a new Config.
11
+func (cli *Client) ConfigCreate(ctx context.Context, config swarm.ConfigSpec) (types.ConfigCreateResponse, error) {
12
+	var response types.ConfigCreateResponse
13
+	resp, err := cli.post(ctx, "/configs/create", nil, config, nil)
14
+	if err != nil {
15
+		return response, err
16
+	}
17
+
18
+	err = json.NewDecoder(resp.body).Decode(&response)
19
+	ensureReaderClosed(resp)
20
+	return response, err
21
+}
0 22
new file mode 100644
... ...
@@ -0,0 +1,57 @@
0
+package client
1
+
2
+import (
3
+	"bytes"
4
+	"encoding/json"
5
+	"fmt"
6
+	"io/ioutil"
7
+	"net/http"
8
+	"strings"
9
+	"testing"
10
+
11
+	"github.com/docker/docker/api/types"
12
+	"github.com/docker/docker/api/types/swarm"
13
+	"golang.org/x/net/context"
14
+)
15
+
16
+func TestConfigCreateError(t *testing.T) {
17
+	client := &Client{
18
+		client: newMockClient(errorMock(http.StatusInternalServerError, "Server error")),
19
+	}
20
+	_, err := client.ConfigCreate(context.Background(), swarm.ConfigSpec{})
21
+	if err == nil || err.Error() != "Error response from daemon: Server error" {
22
+		t.Fatalf("expected a Server Error, got %v", err)
23
+	}
24
+}
25
+
26
+func TestConfigCreate(t *testing.T) {
27
+	expectedURL := "/configs/create"
28
+	client := &Client{
29
+		client: newMockClient(func(req *http.Request) (*http.Response, error) {
30
+			if !strings.HasPrefix(req.URL.Path, expectedURL) {
31
+				return nil, fmt.Errorf("Expected URL '%s', got '%s'", expectedURL, req.URL)
32
+			}
33
+			if req.Method != "POST" {
34
+				return nil, fmt.Errorf("expected POST method, got %s", req.Method)
35
+			}
36
+			b, err := json.Marshal(types.ConfigCreateResponse{
37
+				ID: "test_config",
38
+			})
39
+			if err != nil {
40
+				return nil, err
41
+			}
42
+			return &http.Response{
43
+				StatusCode: http.StatusCreated,
44
+				Body:       ioutil.NopCloser(bytes.NewReader(b)),
45
+			}, nil
46
+		}),
47
+	}
48
+
49
+	r, err := client.ConfigCreate(context.Background(), swarm.ConfigSpec{})
50
+	if err != nil {
51
+		t.Fatal(err)
52
+	}
53
+	if r.ID != "test_config" {
54
+		t.Fatalf("expected `test_config`, got %s", r.ID)
55
+	}
56
+}
0 57
new file mode 100644
... ...
@@ -0,0 +1,34 @@
0
+package client
1
+
2
+import (
3
+	"bytes"
4
+	"encoding/json"
5
+	"io/ioutil"
6
+	"net/http"
7
+
8
+	"github.com/docker/docker/api/types/swarm"
9
+	"golang.org/x/net/context"
10
+)
11
+
12
+// ConfigInspectWithRaw returns the config information with raw data
13
+func (cli *Client) ConfigInspectWithRaw(ctx context.Context, id string) (swarm.Config, []byte, error) {
14
+	resp, err := cli.get(ctx, "/configs/"+id, nil, nil)
15
+	if err != nil {
16
+		if resp.statusCode == http.StatusNotFound {
17
+			return swarm.Config{}, nil, configNotFoundError{id}
18
+		}
19
+		return swarm.Config{}, nil, err
20
+	}
21
+	defer ensureReaderClosed(resp)
22
+
23
+	body, err := ioutil.ReadAll(resp.body)
24
+	if err != nil {
25
+		return swarm.Config{}, nil, err
26
+	}
27
+
28
+	var config swarm.Config
29
+	rdr := bytes.NewReader(body)
30
+	err = json.NewDecoder(rdr).Decode(&config)
31
+
32
+	return config, body, err
33
+}
0 34
new file mode 100644
... ...
@@ -0,0 +1,65 @@
0
+package client
1
+
2
+import (
3
+	"bytes"
4
+	"encoding/json"
5
+	"fmt"
6
+	"io/ioutil"
7
+	"net/http"
8
+	"strings"
9
+	"testing"
10
+
11
+	"github.com/docker/docker/api/types/swarm"
12
+	"golang.org/x/net/context"
13
+)
14
+
15
+func TestConfigInspectError(t *testing.T) {
16
+	client := &Client{
17
+		client: newMockClient(errorMock(http.StatusInternalServerError, "Server error")),
18
+	}
19
+
20
+	_, _, err := client.ConfigInspectWithRaw(context.Background(), "nothing")
21
+	if err == nil || err.Error() != "Error response from daemon: Server error" {
22
+		t.Fatalf("expected a Server Error, got %v", err)
23
+	}
24
+}
25
+
26
+func TestConfigInspectConfigNotFound(t *testing.T) {
27
+	client := &Client{
28
+		client: newMockClient(errorMock(http.StatusNotFound, "Server error")),
29
+	}
30
+
31
+	_, _, err := client.ConfigInspectWithRaw(context.Background(), "unknown")
32
+	if err == nil || !IsErrConfigNotFound(err) {
33
+		t.Fatalf("expected a configNotFoundError error, got %v", err)
34
+	}
35
+}
36
+
37
+func TestConfigInspect(t *testing.T) {
38
+	expectedURL := "/configs/config_id"
39
+	client := &Client{
40
+		client: newMockClient(func(req *http.Request) (*http.Response, error) {
41
+			if !strings.HasPrefix(req.URL.Path, expectedURL) {
42
+				return nil, fmt.Errorf("Expected URL '%s', got '%s'", expectedURL, req.URL)
43
+			}
44
+			content, err := json.Marshal(swarm.Config{
45
+				ID: "config_id",
46
+			})
47
+			if err != nil {
48
+				return nil, err
49
+			}
50
+			return &http.Response{
51
+				StatusCode: http.StatusOK,
52
+				Body:       ioutil.NopCloser(bytes.NewReader(content)),
53
+			}, nil
54
+		}),
55
+	}
56
+
57
+	configInspect, _, err := client.ConfigInspectWithRaw(context.Background(), "config_id")
58
+	if err != nil {
59
+		t.Fatal(err)
60
+	}
61
+	if configInspect.ID != "config_id" {
62
+		t.Fatalf("expected `config_id`, got %s", configInspect.ID)
63
+	}
64
+}
0 65
new file mode 100644
... ...
@@ -0,0 +1,35 @@
0
+package client
1
+
2
+import (
3
+	"encoding/json"
4
+	"net/url"
5
+
6
+	"github.com/docker/docker/api/types"
7
+	"github.com/docker/docker/api/types/filters"
8
+	"github.com/docker/docker/api/types/swarm"
9
+	"golang.org/x/net/context"
10
+)
11
+
12
+// ConfigList returns the list of configs.
13
+func (cli *Client) ConfigList(ctx context.Context, options types.ConfigListOptions) ([]swarm.Config, error) {
14
+	query := url.Values{}
15
+
16
+	if options.Filters.Len() > 0 {
17
+		filterJSON, err := filters.ToParam(options.Filters)
18
+		if err != nil {
19
+			return nil, err
20
+		}
21
+
22
+		query.Set("filters", filterJSON)
23
+	}
24
+
25
+	resp, err := cli.get(ctx, "/configs", query, nil)
26
+	if err != nil {
27
+		return nil, err
28
+	}
29
+
30
+	var configs []swarm.Config
31
+	err = json.NewDecoder(resp.body).Decode(&configs)
32
+	ensureReaderClosed(resp)
33
+	return configs, err
34
+}
0 35
new file mode 100644
... ...
@@ -0,0 +1,94 @@
0
+package client
1
+
2
+import (
3
+	"bytes"
4
+	"encoding/json"
5
+	"fmt"
6
+	"io/ioutil"
7
+	"net/http"
8
+	"strings"
9
+	"testing"
10
+
11
+	"github.com/docker/docker/api/types"
12
+	"github.com/docker/docker/api/types/filters"
13
+	"github.com/docker/docker/api/types/swarm"
14
+	"golang.org/x/net/context"
15
+)
16
+
17
+func TestConfigListError(t *testing.T) {
18
+	client := &Client{
19
+		client: newMockClient(errorMock(http.StatusInternalServerError, "Server error")),
20
+	}
21
+
22
+	_, err := client.ConfigList(context.Background(), types.ConfigListOptions{})
23
+	if err == nil || err.Error() != "Error response from daemon: Server error" {
24
+		t.Fatalf("expected a Server Error, got %v", err)
25
+	}
26
+}
27
+
28
+func TestConfigList(t *testing.T) {
29
+	expectedURL := "/configs"
30
+
31
+	filters := filters.NewArgs()
32
+	filters.Add("label", "label1")
33
+	filters.Add("label", "label2")
34
+
35
+	listCases := []struct {
36
+		options             types.ConfigListOptions
37
+		expectedQueryParams map[string]string
38
+	}{
39
+		{
40
+			options: types.ConfigListOptions{},
41
+			expectedQueryParams: map[string]string{
42
+				"filters": "",
43
+			},
44
+		},
45
+		{
46
+			options: types.ConfigListOptions{
47
+				Filters: filters,
48
+			},
49
+			expectedQueryParams: map[string]string{
50
+				"filters": `{"label":{"label1":true,"label2":true}}`,
51
+			},
52
+		},
53
+	}
54
+	for _, listCase := range listCases {
55
+		client := &Client{
56
+			client: newMockClient(func(req *http.Request) (*http.Response, error) {
57
+				if !strings.HasPrefix(req.URL.Path, expectedURL) {
58
+					return nil, fmt.Errorf("Expected URL '%s', got '%s'", expectedURL, req.URL)
59
+				}
60
+				query := req.URL.Query()
61
+				for key, expected := range listCase.expectedQueryParams {
62
+					actual := query.Get(key)
63
+					if actual != expected {
64
+						return nil, fmt.Errorf("%s not set in URL query properly. Expected '%s', got %s", key, expected, actual)
65
+					}
66
+				}
67
+				content, err := json.Marshal([]swarm.Config{
68
+					{
69
+						ID: "config_id1",
70
+					},
71
+					{
72
+						ID: "config_id2",
73
+					},
74
+				})
75
+				if err != nil {
76
+					return nil, err
77
+				}
78
+				return &http.Response{
79
+					StatusCode: http.StatusOK,
80
+					Body:       ioutil.NopCloser(bytes.NewReader(content)),
81
+				}, nil
82
+			}),
83
+		}
84
+
85
+		configs, err := client.ConfigList(context.Background(), listCase.options)
86
+		if err != nil {
87
+			t.Fatal(err)
88
+		}
89
+		if len(configs) != 2 {
90
+			t.Fatalf("expected 2 configs, got %v", configs)
91
+		}
92
+	}
93
+}
0 94
new file mode 100644
... ...
@@ -0,0 +1,10 @@
0
+package client
1
+
2
+import "golang.org/x/net/context"
3
+
4
+// ConfigRemove removes a Config.
5
+func (cli *Client) ConfigRemove(ctx context.Context, id string) error {
6
+	resp, err := cli.delete(ctx, "/configs/"+id, nil, nil)
7
+	ensureReaderClosed(resp)
8
+	return err
9
+}
0 10
new file mode 100644
... ...
@@ -0,0 +1,47 @@
0
+package client
1
+
2
+import (
3
+	"bytes"
4
+	"fmt"
5
+	"io/ioutil"
6
+	"net/http"
7
+	"strings"
8
+	"testing"
9
+
10
+	"golang.org/x/net/context"
11
+)
12
+
13
+func TestConfigRemoveError(t *testing.T) {
14
+	client := &Client{
15
+		client: newMockClient(errorMock(http.StatusInternalServerError, "Server error")),
16
+	}
17
+
18
+	err := client.ConfigRemove(context.Background(), "config_id")
19
+	if err == nil || err.Error() != "Error response from daemon: Server error" {
20
+		t.Fatalf("expected a Server Error, got %v", err)
21
+	}
22
+}
23
+
24
+func TestConfigRemove(t *testing.T) {
25
+	expectedURL := "/configs/config_id"
26
+
27
+	client := &Client{
28
+		client: newMockClient(func(req *http.Request) (*http.Response, error) {
29
+			if !strings.HasPrefix(req.URL.Path, expectedURL) {
30
+				return nil, fmt.Errorf("Expected URL '%s', got '%s'", expectedURL, req.URL)
31
+			}
32
+			if req.Method != "DELETE" {
33
+				return nil, fmt.Errorf("expected DELETE method, got %s", req.Method)
34
+			}
35
+			return &http.Response{
36
+				StatusCode: http.StatusOK,
37
+				Body:       ioutil.NopCloser(bytes.NewReader([]byte("body"))),
38
+			}, nil
39
+		}),
40
+	}
41
+
42
+	err := client.ConfigRemove(context.Background(), "config_id")
43
+	if err != nil {
44
+		t.Fatal(err)
45
+	}
46
+}
0 47
new file mode 100644
... ...
@@ -0,0 +1,18 @@
0
+package client
1
+
2
+import (
3
+	"net/url"
4
+	"strconv"
5
+
6
+	"github.com/docker/docker/api/types/swarm"
7
+	"golang.org/x/net/context"
8
+)
9
+
10
+// ConfigUpdate attempts to updates a Config
11
+func (cli *Client) ConfigUpdate(ctx context.Context, id string, version swarm.Version, config swarm.ConfigSpec) error {
12
+	query := url.Values{}
13
+	query.Set("version", strconv.FormatUint(version.Index, 10))
14
+	resp, err := cli.post(ctx, "/configs/"+id+"/update", query, config, nil)
15
+	ensureReaderClosed(resp)
16
+	return err
17
+}
0 18
new file mode 100644
... ...
@@ -0,0 +1,49 @@
0
+package client
1
+
2
+import (
3
+	"bytes"
4
+	"fmt"
5
+	"io/ioutil"
6
+	"net/http"
7
+	"strings"
8
+	"testing"
9
+
10
+	"golang.org/x/net/context"
11
+
12
+	"github.com/docker/docker/api/types/swarm"
13
+)
14
+
15
+func TestConfigUpdateError(t *testing.T) {
16
+	client := &Client{
17
+		client: newMockClient(errorMock(http.StatusInternalServerError, "Server error")),
18
+	}
19
+
20
+	err := client.ConfigUpdate(context.Background(), "config_id", swarm.Version{}, swarm.ConfigSpec{})
21
+	if err == nil || err.Error() != "Error response from daemon: Server error" {
22
+		t.Fatalf("expected a Server Error, got %v", err)
23
+	}
24
+}
25
+
26
+func TestConfigUpdate(t *testing.T) {
27
+	expectedURL := "/configs/config_id/update"
28
+
29
+	client := &Client{
30
+		client: newMockClient(func(req *http.Request) (*http.Response, error) {
31
+			if !strings.HasPrefix(req.URL.Path, expectedURL) {
32
+				return nil, fmt.Errorf("Expected URL '%s', got '%s'", expectedURL, req.URL)
33
+			}
34
+			if req.Method != "POST" {
35
+				return nil, fmt.Errorf("expected POST method, got %s", req.Method)
36
+			}
37
+			return &http.Response{
38
+				StatusCode: http.StatusOK,
39
+				Body:       ioutil.NopCloser(bytes.NewReader([]byte("body"))),
40
+			}, nil
41
+		}),
42
+	}
43
+
44
+	err := client.ConfigUpdate(context.Background(), "config_id", swarm.Version{}, swarm.ConfigSpec{})
45
+	if err != nil {
46
+		t.Fatal(err)
47
+	}
48
+}
... ...
@@ -256,6 +256,28 @@ func IsErrSecretNotFound(err error) bool {
256 256
 	return ok
257 257
 }
258 258
 
259
+// configNotFoundError implements an error returned when a config is not found.
260
+type configNotFoundError struct {
261
+	name string
262
+}
263
+
264
+// Error returns a string representation of a configNotFoundError
265
+func (e configNotFoundError) Error() string {
266
+	return fmt.Sprintf("Error: no such config: %s", e.name)
267
+}
268
+
269
+// NotFound indicates that this error type is of NotFound
270
+func (e configNotFoundError) NotFound() bool {
271
+	return true
272
+}
273
+
274
+// IsErrConfigNotFound returns true if the error is caused
275
+// when a config is not found.
276
+func IsErrConfigNotFound(err error) bool {
277
+	_, ok := err.(configNotFoundError)
278
+	return ok
279
+}
280
+
259 281
 // pluginNotFoundError implements an error returned when a plugin is not in the docker host.
260 282
 type pluginNotFoundError struct {
261 283
 	name string
... ...
@@ -18,6 +18,7 @@ import (
18 18
 
19 19
 // CommonAPIClient is the common methods between stable and experimental versions of APIClient.
20 20
 type CommonAPIClient interface {
21
+	ConfigAPIClient
21 22
 	ContainerAPIClient
22 23
 	ImageAPIClient
23 24
 	NodeAPIClient
... ...
@@ -171,3 +172,12 @@ type SecretAPIClient interface {
171 171
 	SecretInspectWithRaw(ctx context.Context, name string) (swarm.Secret, []byte, error)
172 172
 	SecretUpdate(ctx context.Context, id string, version swarm.Version, secret swarm.SecretSpec) error
173 173
 }
174
+
175
+// ConfigAPIClient defines API client methods for configs
176
+type ConfigAPIClient interface {
177
+	ConfigList(ctx context.Context, options types.ConfigListOptions) ([]swarm.Config, error)
178
+	ConfigCreate(ctx context.Context, config swarm.ConfigSpec) (types.ConfigCreateResponse, error)
179
+	ConfigRemove(ctx context.Context, id string) error
180
+	ConfigInspectWithRaw(ctx context.Context, name string) (swarm.Config, []byte, error)
181
+	ConfigUpdate(ctx context.Context, id string, version swarm.Version, config swarm.ConfigSpec) error
182
+}