Browse code

Move engine-api client package

This moves the engine-api client package to `/docker/docker/client`.

Signed-off-by: Michael Crosby <crosbymichael@gmail.com>

Michael Crosby authored on 2016/09/07 03:46:37
Showing 211 changed files
... ...
@@ -14,10 +14,10 @@ import (
14 14
 	"github.com/docker/docker/cliconfig"
15 15
 	"github.com/docker/docker/cliconfig/configfile"
16 16
 	"github.com/docker/docker/cliconfig/credentials"
17
+	"github.com/docker/docker/client"
17 18
 	"github.com/docker/docker/dockerversion"
18 19
 	dopts "github.com/docker/docker/opts"
19 20
 	"github.com/docker/docker/pkg/term"
20
-	"github.com/docker/engine-api/client"
21 21
 	"github.com/docker/go-connections/sockets"
22 22
 	"github.com/docker/go-connections/tlsconfig"
23 23
 )
... ...
@@ -14,10 +14,10 @@ import (
14 14
 	"github.com/docker/docker/api/types"
15 15
 	"github.com/docker/docker/api/types/container"
16 16
 	networktypes "github.com/docker/docker/api/types/network"
17
+	apiclient "github.com/docker/docker/client"
17 18
 	"github.com/docker/docker/reference"
18 19
 	"github.com/docker/docker/registry"
19 20
 	runconfigopts "github.com/docker/docker/runconfig/opts"
20
-	apiclient "github.com/docker/engine-api/client"
21 21
 	"github.com/spf13/cobra"
22 22
 	"github.com/spf13/pflag"
23 23
 )
... ...
@@ -11,7 +11,7 @@ import (
11 11
 
12 12
 	"github.com/Sirupsen/logrus"
13 13
 	"github.com/docker/docker/api/types"
14
-	"github.com/docker/engine-api/client"
14
+	"github.com/docker/docker/client"
15 15
 	"github.com/docker/go-units"
16 16
 	"golang.org/x/net/context"
17 17
 )
... ...
@@ -12,7 +12,7 @@ import (
12 12
 	"github.com/docker/docker/api/types"
13 13
 	"github.com/docker/docker/api/types/events"
14 14
 	"github.com/docker/docker/api/types/filters"
15
-	clientapi "github.com/docker/engine-api/client"
15
+	clientapi "github.com/docker/docker/client"
16 16
 )
17 17
 
18 18
 func waitExitOrRemoved(dockerCli *client.DockerCli, ctx context.Context, containerID string, waitRemove bool) (chan int, error) {
... ...
@@ -6,7 +6,7 @@ import (
6 6
 	"golang.org/x/net/context"
7 7
 
8 8
 	"github.com/docker/docker/api/types/swarm"
9
-	"github.com/docker/engine-api/client"
9
+	"github.com/docker/docker/client"
10 10
 )
11 11
 
12 12
 // IDResolver provides ID to Name resolution.
... ...
@@ -5,7 +5,7 @@ import (
5 5
 
6 6
 	"github.com/docker/docker/api/client"
7 7
 	"github.com/docker/docker/cli"
8
-	apiclient "github.com/docker/engine-api/client"
8
+	apiclient "github.com/docker/docker/client"
9 9
 	"github.com/spf13/cobra"
10 10
 	"golang.org/x/net/context"
11 11
 )
... ...
@@ -12,8 +12,8 @@ import (
12 12
 	"github.com/docker/docker/api/client/inspect"
13 13
 	"github.com/docker/docker/api/types/swarm"
14 14
 	"github.com/docker/docker/cli"
15
+	apiclient "github.com/docker/docker/client"
15 16
 	"github.com/docker/docker/pkg/ioutils"
16
-	apiclient "github.com/docker/engine-api/client"
17 17
 	"github.com/docker/go-units"
18 18
 	"github.com/spf13/cobra"
19 19
 )
... ...
@@ -8,7 +8,7 @@ import (
8 8
 	"github.com/docker/docker/api/types"
9 9
 	"github.com/docker/docker/api/types/filters"
10 10
 	"github.com/docker/docker/api/types/swarm"
11
-	"github.com/docker/engine-api/client"
11
+	"github.com/docker/docker/client"
12 12
 )
13 13
 
14 14
 const (
... ...
@@ -9,7 +9,7 @@ import (
9 9
 	"github.com/docker/docker/api/client"
10 10
 	"github.com/docker/docker/api/client/inspect"
11 11
 	"github.com/docker/docker/cli"
12
-	apiclient "github.com/docker/engine-api/client"
12
+	apiclient "github.com/docker/docker/client"
13 13
 	"github.com/spf13/cobra"
14 14
 )
15 15
 
... ...
@@ -15,9 +15,9 @@ import (
15 15
 
16 16
 	"github.com/Sirupsen/logrus"
17 17
 	"github.com/docker/docker/api/types"
18
+	"github.com/docker/docker/client"
18 19
 	"github.com/docker/docker/pkg/signal"
19 20
 	"github.com/docker/docker/pkg/term"
20
-	"github.com/docker/engine-api/client"
21 21
 )
22 22
 
23 23
 func (cli *DockerCli) resizeTty(ctx context.Context, id string, isExec bool) {
... ...
@@ -3,8 +3,8 @@ package build
3 3
 import (
4 4
 	"io"
5 5
 
6
+	"github.com/docker/docker/api/types"
6 7
 	"github.com/docker/docker/api/types/backend"
7
-	"github.com/docker/engine-api/types"
8 8
 	"golang.org/x/net/context"
9 9
 )
10 10
 
... ...
@@ -13,13 +13,13 @@ import (
13 13
 
14 14
 	"github.com/Sirupsen/logrus"
15 15
 	"github.com/docker/docker/api/server/httputils"
16
+	"github.com/docker/docker/api/types"
16 17
 	"github.com/docker/docker/api/types/backend"
18
+	"github.com/docker/docker/api/types/container"
19
+	"github.com/docker/docker/api/types/versions"
17 20
 	"github.com/docker/docker/pkg/ioutils"
18 21
 	"github.com/docker/docker/pkg/progress"
19 22
 	"github.com/docker/docker/pkg/streamformatter"
20
-	"github.com/docker/engine-api/types"
21
-	"github.com/docker/engine-api/types/container"
22
-	"github.com/docker/engine-api/types/versions"
23 23
 	"github.com/docker/go-units"
24 24
 	"golang.org/x/net/context"
25 25
 )
... ...
@@ -6,10 +6,10 @@ import (
6 6
 
7 7
 	"golang.org/x/net/context"
8 8
 
9
+	"github.com/docker/docker/api/types"
9 10
 	"github.com/docker/docker/api/types/backend"
11
+	"github.com/docker/docker/api/types/container"
10 12
 	"github.com/docker/docker/pkg/archive"
11
-	"github.com/docker/engine-api/types"
12
-	"github.com/docker/engine-api/types/container"
13 13
 )
14 14
 
15 15
 // execBackend includes functions to implement to provide exec functionality.
... ...
@@ -12,13 +12,13 @@ import (
12 12
 
13 13
 	"github.com/Sirupsen/logrus"
14 14
 	"github.com/docker/docker/api/server/httputils"
15
+	"github.com/docker/docker/api/types"
15 16
 	"github.com/docker/docker/api/types/backend"
17
+	"github.com/docker/docker/api/types/container"
18
+	"github.com/docker/docker/api/types/filters"
19
+	"github.com/docker/docker/api/types/versions"
16 20
 	"github.com/docker/docker/pkg/ioutils"
17 21
 	"github.com/docker/docker/pkg/signal"
18
-	"github.com/docker/engine-api/types"
19
-	"github.com/docker/engine-api/types/container"
20
-	"github.com/docker/engine-api/types/filters"
21
-	"github.com/docker/engine-api/types/versions"
22 22
 	"golang.org/x/net/context"
23 23
 	"golang.org/x/net/websocket"
24 24
 )
... ...
@@ -10,8 +10,8 @@ import (
10 10
 	"strings"
11 11
 
12 12
 	"github.com/docker/docker/api/server/httputils"
13
-	"github.com/docker/engine-api/types"
14
-	"github.com/docker/engine-api/types/versions"
13
+	"github.com/docker/docker/api/types"
14
+	"github.com/docker/docker/api/types/versions"
15 15
 	"golang.org/x/net/context"
16 16
 )
17 17
 
... ...
@@ -9,9 +9,9 @@ import (
9 9
 
10 10
 	"github.com/Sirupsen/logrus"
11 11
 	"github.com/docker/docker/api/server/httputils"
12
+	"github.com/docker/docker/api/types"
13
+	"github.com/docker/docker/api/types/versions"
12 14
 	"github.com/docker/docker/pkg/stdcopy"
13
-	"github.com/docker/engine-api/types"
14
-	"github.com/docker/engine-api/types/versions"
15 15
 	"golang.org/x/net/context"
16 16
 )
17 17
 
... ...
@@ -3,9 +3,9 @@ package image
3 3
 import (
4 4
 	"io"
5 5
 
6
+	"github.com/docker/docker/api/types"
6 7
 	"github.com/docker/docker/api/types/backend"
7
-	"github.com/docker/engine-api/types"
8
-	"github.com/docker/engine-api/types/registry"
8
+	"github.com/docker/docker/api/types/registry"
9 9
 	"golang.org/x/net/context"
10 10
 )
11 11
 
... ...
@@ -10,13 +10,13 @@ import (
10 10
 	"strings"
11 11
 
12 12
 	"github.com/docker/docker/api/server/httputils"
13
+	"github.com/docker/docker/api/types"
13 14
 	"github.com/docker/docker/api/types/backend"
15
+	"github.com/docker/docker/api/types/container"
16
+	"github.com/docker/docker/api/types/versions"
14 17
 	"github.com/docker/docker/pkg/ioutils"
15 18
 	"github.com/docker/docker/pkg/streamformatter"
16 19
 	"github.com/docker/docker/registry"
17
-	"github.com/docker/engine-api/types"
18
-	"github.com/docker/engine-api/types/container"
19
-	"github.com/docker/engine-api/types/versions"
20 20
 	"golang.org/x/net/context"
21 21
 )
22 22
 
... ...
@@ -1,8 +1,8 @@
1 1
 package network
2 2
 
3 3
 import (
4
-	"github.com/docker/engine-api/types"
5
-	"github.com/docker/engine-api/types/network"
4
+	"github.com/docker/docker/api/types"
5
+	"github.com/docker/docker/api/types/network"
6 6
 	"github.com/docker/libnetwork"
7 7
 )
8 8
 
... ...
@@ -3,9 +3,9 @@ package network
3 3
 import (
4 4
 	"fmt"
5 5
 
6
+	"github.com/docker/docker/api/types"
7
+	"github.com/docker/docker/api/types/filters"
6 8
 	"github.com/docker/docker/runconfig"
7
-	"github.com/docker/engine-api/types"
8
-	"github.com/docker/engine-api/types/filters"
9 9
 )
10 10
 
11 11
 var (
... ...
@@ -8,10 +8,10 @@ import (
8 8
 	"golang.org/x/net/context"
9 9
 
10 10
 	"github.com/docker/docker/api/server/httputils"
11
+	"github.com/docker/docker/api/types"
12
+	"github.com/docker/docker/api/types/filters"
13
+	"github.com/docker/docker/api/types/network"
11 14
 	"github.com/docker/docker/errors"
12
-	"github.com/docker/engine-api/types"
13
-	"github.com/docker/engine-api/types/filters"
14
-	"github.com/docker/engine-api/types/network"
15 15
 	"github.com/docker/libnetwork"
16 16
 )
17 17
 
... ...
@@ -5,7 +5,7 @@ package plugin
5 5
 import (
6 6
 	"net/http"
7 7
 
8
-	enginetypes "github.com/docker/engine-api/types"
8
+	enginetypes "github.com/docker/docker/api/types"
9 9
 )
10 10
 
11 11
 // Backend for Plugin
... ...
@@ -9,7 +9,7 @@ import (
9 9
 	"strings"
10 10
 
11 11
 	"github.com/docker/docker/api/server/httputils"
12
-	"github.com/docker/engine-api/types"
12
+	"github.com/docker/docker/api/types"
13 13
 	"golang.org/x/net/context"
14 14
 )
15 15
 
... ...
@@ -1,8 +1,8 @@
1 1
 package swarm
2 2
 
3 3
 import (
4
-	basictypes "github.com/docker/engine-api/types"
5
-	types "github.com/docker/engine-api/types/swarm"
4
+	basictypes "github.com/docker/docker/api/types"
5
+	types "github.com/docker/docker/api/types/swarm"
6 6
 )
7 7
 
8 8
 // Backend abstracts an swarm commands manager.
... ...
@@ -8,9 +8,9 @@ import (
8 8
 
9 9
 	"github.com/Sirupsen/logrus"
10 10
 	"github.com/docker/docker/api/server/httputils"
11
-	basictypes "github.com/docker/engine-api/types"
12
-	"github.com/docker/engine-api/types/filters"
13
-	types "github.com/docker/engine-api/types/swarm"
11
+	basictypes "github.com/docker/docker/api/types"
12
+	"github.com/docker/docker/api/types/filters"
13
+	types "github.com/docker/docker/api/types/swarm"
14 14
 	"golang.org/x/net/context"
15 15
 )
16 16
 
... ...
@@ -3,9 +3,9 @@ package system
3 3
 import (
4 4
 	"time"
5 5
 
6
-	"github.com/docker/engine-api/types"
7
-	"github.com/docker/engine-api/types/events"
8
-	"github.com/docker/engine-api/types/filters"
6
+	"github.com/docker/docker/api/types"
7
+	"github.com/docker/docker/api/types/events"
8
+	"github.com/docker/docker/api/types/filters"
9 9
 	"golang.org/x/net/context"
10 10
 )
11 11
 
... ...
@@ -9,13 +9,13 @@ import (
9 9
 	"github.com/Sirupsen/logrus"
10 10
 	"github.com/docker/docker/api"
11 11
 	"github.com/docker/docker/api/server/httputils"
12
+	"github.com/docker/docker/api/types"
13
+	"github.com/docker/docker/api/types/events"
14
+	"github.com/docker/docker/api/types/filters"
15
+	timetypes "github.com/docker/docker/api/types/time"
16
+	"github.com/docker/docker/api/types/versions"
12 17
 	"github.com/docker/docker/errors"
13 18
 	"github.com/docker/docker/pkg/ioutils"
14
-	"github.com/docker/engine-api/types"
15
-	"github.com/docker/engine-api/types/events"
16
-	"github.com/docker/engine-api/types/filters"
17
-	timetypes "github.com/docker/engine-api/types/time"
18
-	"github.com/docker/engine-api/types/versions"
19 19
 	"golang.org/x/net/context"
20 20
 )
21 21
 
... ...
@@ -2,7 +2,7 @@ package volume
2 2
 
3 3
 import (
4 4
 	// TODO return types need to be refactored into pkg
5
-	"github.com/docker/engine-api/types"
5
+	"github.com/docker/docker/api/types"
6 6
 )
7 7
 
8 8
 // Backend is the methods that need to be implemented to provide
... ...
@@ -5,7 +5,7 @@ import (
5 5
 	"net/http"
6 6
 
7 7
 	"github.com/docker/docker/api/server/httputils"
8
-	"github.com/docker/engine-api/types"
8
+	"github.com/docker/docker/api/types"
9 9
 	"golang.org/x/net/context"
10 10
 )
11 11
 
... ...
@@ -2,9 +2,9 @@
2 2
 package v1p19
3 3
 
4 4
 import (
5
-	"github.com/docker/engine-api/types"
6
-	"github.com/docker/engine-api/types/container"
7
-	"github.com/docker/engine-api/types/versions/v1p20"
5
+	"github.com/docker/docker/api/types"
6
+	"github.com/docker/docker/api/types/container"
7
+	"github.com/docker/docker/api/types/versions/v1p20"
8 8
 	"github.com/docker/go-connections/nat"
9 9
 )
10 10
 
... ...
@@ -2,8 +2,8 @@
2 2
 package v1p20
3 3
 
4 4
 import (
5
-	"github.com/docker/engine-api/types"
6
-	"github.com/docker/engine-api/types/container"
5
+	"github.com/docker/docker/api/types"
6
+	"github.com/docker/docker/api/types/container"
7 7
 	"github.com/docker/go-connections/nat"
8 8
 )
9 9
 
... ...
@@ -1,11 +1,9 @@
1 1
 package credentials
2 2
 
3 3
 import (
4
-	"github.com/docker/docker/cliconfig/configfile"
5
-	"github.com/docker/docker/registry"
6
-
7 4
 	"github.com/docker/docker/api/types"
8 5
 	"github.com/docker/docker/cliconfig/configfile"
6
+	"github.com/docker/docker/registry"
9 7
 )
10 8
 
11 9
 // fileStore implements a credentials store using
12 10
new file mode 100644
... ...
@@ -0,0 +1,13 @@
0
+package client
1
+
2
+import (
3
+	"github.com/docker/docker/api/types"
4
+	"golang.org/x/net/context"
5
+)
6
+
7
+// CheckpointCreate creates a checkpoint from the given container with the given name
8
+func (cli *Client) CheckpointCreate(ctx context.Context, container string, options types.CheckpointCreateOptions) error {
9
+	resp, err := cli.post(ctx, "/containers/"+container+"/checkpoints", nil, options, nil)
10
+	ensureReaderClosed(resp)
11
+	return err
12
+}
0 13
new file mode 100644
... ...
@@ -0,0 +1,73 @@
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
+	"golang.org/x/net/context"
13
+)
14
+
15
+func TestCheckpointCreateError(t *testing.T) {
16
+	client := &Client{
17
+		transport: newMockClient(nil, errorMock(http.StatusInternalServerError, "Server error")),
18
+	}
19
+	err := client.CheckpointCreate(context.Background(), "nothing", types.CheckpointCreateOptions{
20
+		CheckpointID: "noting",
21
+		Exit:         true,
22
+	})
23
+
24
+	if err == nil || err.Error() != "Error response from daemon: Server error" {
25
+		t.Fatalf("expected a Server Error, got %v", err)
26
+	}
27
+}
28
+
29
+func TestCheckpointCreate(t *testing.T) {
30
+	expectedContainerID := "container_id"
31
+	expectedCheckpointID := "checkpoint_id"
32
+	expectedURL := "/containers/container_id/checkpoints"
33
+
34
+	client := &Client{
35
+		transport: newMockClient(nil, func(req *http.Request) (*http.Response, error) {
36
+			if !strings.HasPrefix(req.URL.Path, expectedURL) {
37
+				return nil, fmt.Errorf("Expected URL '%s', got '%s'", expectedURL, req.URL)
38
+			}
39
+
40
+			if req.Method != "POST" {
41
+				return nil, fmt.Errorf("expected POST method, got %s", req.Method)
42
+			}
43
+
44
+			createOptions := &types.CheckpointCreateOptions{}
45
+			if err := json.NewDecoder(req.Body).Decode(createOptions); err != nil {
46
+				return nil, err
47
+			}
48
+
49
+			if createOptions.CheckpointID != expectedCheckpointID {
50
+				return nil, fmt.Errorf("expected CheckpointID to be 'checkpoint_id', got %v", createOptions.CheckpointID)
51
+			}
52
+
53
+			if !createOptions.Exit {
54
+				return nil, fmt.Errorf("expected Exit to be true")
55
+			}
56
+
57
+			return &http.Response{
58
+				StatusCode: http.StatusOK,
59
+				Body:       ioutil.NopCloser(bytes.NewReader([]byte(""))),
60
+			}, nil
61
+		}),
62
+	}
63
+
64
+	err := client.CheckpointCreate(context.Background(), expectedContainerID, types.CheckpointCreateOptions{
65
+		CheckpointID: expectedCheckpointID,
66
+		Exit:         true,
67
+	})
68
+
69
+	if err != nil {
70
+		t.Fatal(err)
71
+	}
72
+}
0 73
new file mode 100644
... ...
@@ -0,0 +1,12 @@
0
+package client
1
+
2
+import (
3
+	"golang.org/x/net/context"
4
+)
5
+
6
+// CheckpointDelete deletes the checkpoint with the given name from the given container
7
+func (cli *Client) CheckpointDelete(ctx context.Context, containerID string, checkpointID string) error {
8
+	resp, err := cli.delete(ctx, "/containers/"+containerID+"/checkpoints/"+checkpointID, nil, nil)
9
+	ensureReaderClosed(resp)
10
+	return err
11
+}
0 12
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 TestCheckpointDeleteError(t *testing.T) {
14
+	client := &Client{
15
+		transport: newMockClient(nil, errorMock(http.StatusInternalServerError, "Server error")),
16
+	}
17
+
18
+	err := client.CheckpointDelete(context.Background(), "container_id", "checkpoint_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 TestCheckpointDelete(t *testing.T) {
25
+	expectedURL := "/containers/container_id/checkpoints/checkpoint_id"
26
+
27
+	client := &Client{
28
+		transport: newMockClient(nil, 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(""))),
38
+			}, nil
39
+		}),
40
+	}
41
+
42
+	err := client.CheckpointDelete(context.Background(), "container_id", "checkpoint_id")
43
+	if err != nil {
44
+		t.Fatal(err)
45
+	}
46
+}
0 47
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
+	"golang.org/x/net/context"
7
+)
8
+
9
+// CheckpointList returns the volumes configured in the docker host.
10
+func (cli *Client) CheckpointList(ctx context.Context, container string) ([]types.Checkpoint, error) {
11
+	var checkpoints []types.Checkpoint
12
+
13
+	resp, err := cli.get(ctx, "/containers/"+container+"/checkpoints", nil, nil)
14
+	if err != nil {
15
+		return checkpoints, err
16
+	}
17
+
18
+	err = json.NewDecoder(resp.body).Decode(&checkpoints)
19
+	ensureReaderClosed(resp)
20
+	return checkpoints, 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
+	"golang.org/x/net/context"
13
+)
14
+
15
+func TestCheckpointListError(t *testing.T) {
16
+	client := &Client{
17
+		transport: newMockClient(nil, errorMock(http.StatusInternalServerError, "Server error")),
18
+	}
19
+
20
+	_, err := client.CheckpointList(context.Background(), "container_id")
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 TestCheckpointList(t *testing.T) {
27
+	expectedURL := "/containers/container_id/checkpoints"
28
+
29
+	client := &Client{
30
+		transport: newMockClient(nil, 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
+			content, err := json.Marshal([]types.Checkpoint{
35
+				{
36
+					Name: "checkpoint",
37
+				},
38
+			})
39
+			if err != nil {
40
+				return nil, err
41
+			}
42
+			return &http.Response{
43
+				StatusCode: http.StatusOK,
44
+				Body:       ioutil.NopCloser(bytes.NewReader(content)),
45
+			}, nil
46
+		}),
47
+	}
48
+
49
+	checkpoints, err := client.CheckpointList(context.Background(), "container_id")
50
+	if err != nil {
51
+		t.Fatal(err)
52
+	}
53
+	if len(checkpoints) != 1 {
54
+		t.Fatalf("expected 1 checkpoint, got %v", checkpoints)
55
+	}
56
+}
0 57
new file mode 100644
... ...
@@ -0,0 +1,156 @@
0
+package client
1
+
2
+import (
3
+	"fmt"
4
+	"net/http"
5
+	"net/url"
6
+	"os"
7
+	"path/filepath"
8
+	"strings"
9
+
10
+	"github.com/docker/docker/client/transport"
11
+	"github.com/docker/go-connections/tlsconfig"
12
+)
13
+
14
+// DefaultVersion is the version of the current stable API
15
+const DefaultVersion string = "1.23"
16
+
17
+// Client is the API client that performs all operations
18
+// against a docker server.
19
+type Client struct {
20
+	// host holds the server address to connect to
21
+	host string
22
+	// proto holds the client protocol i.e. unix.
23
+	proto string
24
+	// addr holds the client address.
25
+	addr string
26
+	// basePath holds the path to prepend to the requests.
27
+	basePath string
28
+	// transport is the interface to send request with, it implements transport.Client.
29
+	transport transport.Client
30
+	// version of the server to talk to.
31
+	version string
32
+	// custom http headers configured by users.
33
+	customHTTPHeaders map[string]string
34
+}
35
+
36
+// NewEnvClient initializes a new API client based on environment variables.
37
+// Use DOCKER_HOST to set the url to the docker server.
38
+// Use DOCKER_API_VERSION to set the version of the API to reach, leave empty for latest.
39
+// Use DOCKER_CERT_PATH to load the tls certificates from.
40
+// Use DOCKER_TLS_VERIFY to enable or disable TLS verification, off by default.
41
+func NewEnvClient() (*Client, error) {
42
+	var client *http.Client
43
+	if dockerCertPath := os.Getenv("DOCKER_CERT_PATH"); dockerCertPath != "" {
44
+		options := tlsconfig.Options{
45
+			CAFile:             filepath.Join(dockerCertPath, "ca.pem"),
46
+			CertFile:           filepath.Join(dockerCertPath, "cert.pem"),
47
+			KeyFile:            filepath.Join(dockerCertPath, "key.pem"),
48
+			InsecureSkipVerify: os.Getenv("DOCKER_TLS_VERIFY") == "",
49
+		}
50
+		tlsc, err := tlsconfig.Client(options)
51
+		if err != nil {
52
+			return nil, err
53
+		}
54
+
55
+		client = &http.Client{
56
+			Transport: &http.Transport{
57
+				TLSClientConfig: tlsc,
58
+			},
59
+		}
60
+	}
61
+
62
+	host := os.Getenv("DOCKER_HOST")
63
+	if host == "" {
64
+		host = DefaultDockerHost
65
+	}
66
+
67
+	version := os.Getenv("DOCKER_API_VERSION")
68
+	if version == "" {
69
+		version = DefaultVersion
70
+	}
71
+
72
+	return NewClient(host, version, client, nil)
73
+}
74
+
75
+// NewClient initializes a new API client for the given host and API version.
76
+// It uses the given http client as transport.
77
+// It also initializes the custom http headers to add to each request.
78
+//
79
+// It won't send any version information if the version number is empty. It is
80
+// highly recommended that you set a version or your client may break if the
81
+// server is upgraded.
82
+func NewClient(host string, version string, client *http.Client, httpHeaders map[string]string) (*Client, error) {
83
+	proto, addr, basePath, err := ParseHost(host)
84
+	if err != nil {
85
+		return nil, err
86
+	}
87
+
88
+	transport, err := transport.NewTransportWithHTTP(proto, addr, client)
89
+	if err != nil {
90
+		return nil, err
91
+	}
92
+
93
+	return &Client{
94
+		host:              host,
95
+		proto:             proto,
96
+		addr:              addr,
97
+		basePath:          basePath,
98
+		transport:         transport,
99
+		version:           version,
100
+		customHTTPHeaders: httpHeaders,
101
+	}, nil
102
+}
103
+
104
+// getAPIPath returns the versioned request path to call the api.
105
+// It appends the query parameters to the path if they are not empty.
106
+func (cli *Client) getAPIPath(p string, query url.Values) string {
107
+	var apiPath string
108
+	if cli.version != "" {
109
+		v := strings.TrimPrefix(cli.version, "v")
110
+		apiPath = fmt.Sprintf("%s/v%s%s", cli.basePath, v, p)
111
+	} else {
112
+		apiPath = fmt.Sprintf("%s%s", cli.basePath, p)
113
+	}
114
+
115
+	u := &url.URL{
116
+		Path: apiPath,
117
+	}
118
+	if len(query) > 0 {
119
+		u.RawQuery = query.Encode()
120
+	}
121
+	return u.String()
122
+}
123
+
124
+// ClientVersion returns the version string associated with this
125
+// instance of the Client. Note that this value can be changed
126
+// via the DOCKER_API_VERSION env var.
127
+func (cli *Client) ClientVersion() string {
128
+	return cli.version
129
+}
130
+
131
+// UpdateClientVersion updates the version string associated with this
132
+// instance of the Client.
133
+func (cli *Client) UpdateClientVersion(v string) {
134
+	cli.version = v
135
+}
136
+
137
+// ParseHost verifies that the given host strings is valid.
138
+func ParseHost(host string) (string, string, string, error) {
139
+	protoAddrParts := strings.SplitN(host, "://", 2)
140
+	if len(protoAddrParts) == 1 {
141
+		return "", "", "", fmt.Errorf("unable to parse docker host `%s`", host)
142
+	}
143
+
144
+	var basePath string
145
+	proto, addr := protoAddrParts[0], protoAddrParts[1]
146
+	if proto == "tcp" {
147
+		parsed, err := url.Parse("tcp://" + addr)
148
+		if err != nil {
149
+			return "", "", "", err
150
+		}
151
+		addr = parsed.Host
152
+		basePath = parsed.Path
153
+	}
154
+	return proto, addr, basePath, nil
155
+}
0 156
new file mode 100644
... ...
@@ -0,0 +1,76 @@
0
+package client
1
+
2
+import (
3
+	"bytes"
4
+	"crypto/tls"
5
+	"encoding/json"
6
+	"io/ioutil"
7
+	"net/http"
8
+
9
+	"github.com/docker/docker/api/types"
10
+	"github.com/docker/docker/client/transport"
11
+)
12
+
13
+type mockClient struct {
14
+	do func(*http.Request) (*http.Response, error)
15
+}
16
+
17
+// TLSConfig returns the TLS configuration.
18
+func (m *mockClient) TLSConfig() *tls.Config {
19
+	return &tls.Config{}
20
+}
21
+
22
+// Scheme returns protocol scheme to use.
23
+func (m *mockClient) Scheme() string {
24
+	return "http"
25
+}
26
+
27
+// Secure returns true if there is a TLS configuration.
28
+func (m *mockClient) Secure() bool {
29
+	return false
30
+}
31
+
32
+// NewMockClient returns a mocked client that runs the function supplied as `client.Do` call
33
+func newMockClient(tlsConfig *tls.Config, doer func(*http.Request) (*http.Response, error)) transport.Client {
34
+	if tlsConfig != nil {
35
+		panic("this actually gets set!")
36
+	}
37
+
38
+	return &mockClient{
39
+		do: doer,
40
+	}
41
+}
42
+
43
+// Do executes the supplied function for the mock.
44
+func (m mockClient) Do(req *http.Request) (*http.Response, error) {
45
+	return m.do(req)
46
+}
47
+
48
+func errorMock(statusCode int, message string) func(req *http.Request) (*http.Response, error) {
49
+	return func(req *http.Request) (*http.Response, error) {
50
+		header := http.Header{}
51
+		header.Set("Content-Type", "application/json")
52
+
53
+		body, err := json.Marshal(&types.ErrorResponse{
54
+			Message: message,
55
+		})
56
+		if err != nil {
57
+			return nil, err
58
+		}
59
+
60
+		return &http.Response{
61
+			StatusCode: statusCode,
62
+			Body:       ioutil.NopCloser(bytes.NewReader(body)),
63
+			Header:     header,
64
+		}, nil
65
+	}
66
+}
67
+
68
+func plainTextErrorMock(statusCode int, message string) func(req *http.Request) (*http.Response, error) {
69
+	return func(req *http.Request) (*http.Response, error) {
70
+		return &http.Response{
71
+			StatusCode: statusCode,
72
+			Body:       ioutil.NopCloser(bytes.NewReader([]byte(message))),
73
+		}, nil
74
+	}
75
+}
0 76
new file mode 100644
... ...
@@ -0,0 +1,249 @@
0
+package client
1
+
2
+import (
3
+	"bytes"
4
+	"encoding/json"
5
+	"io/ioutil"
6
+	"net/http"
7
+	"net/url"
8
+	"os"
9
+	"runtime"
10
+	"strings"
11
+	"testing"
12
+
13
+	"github.com/docker/docker/api/types"
14
+	"golang.org/x/net/context"
15
+)
16
+
17
+func TestNewEnvClient(t *testing.T) {
18
+	if runtime.GOOS == "windows" {
19
+		t.Skip("skipping unix only test for windows")
20
+	}
21
+	cases := []struct {
22
+		envs            map[string]string
23
+		expectedError   string
24
+		expectedVersion string
25
+	}{
26
+		{
27
+			envs:            map[string]string{},
28
+			expectedVersion: DefaultVersion,
29
+		},
30
+		{
31
+			envs: map[string]string{
32
+				"DOCKER_CERT_PATH": "invalid/path",
33
+			},
34
+			expectedError: "Could not load X509 key pair: open invalid/path/cert.pem: no such file or directory. Make sure the key is not encrypted",
35
+		},
36
+		{
37
+			envs: map[string]string{
38
+				"DOCKER_CERT_PATH": "testdata/",
39
+			},
40
+			expectedVersion: DefaultVersion,
41
+		},
42
+		{
43
+			envs: map[string]string{
44
+				"DOCKER_HOST": "host",
45
+			},
46
+			expectedError: "unable to parse docker host `host`",
47
+		},
48
+		{
49
+			envs: map[string]string{
50
+				"DOCKER_HOST": "invalid://url",
51
+			},
52
+			expectedVersion: DefaultVersion,
53
+		},
54
+		{
55
+			envs: map[string]string{
56
+				"DOCKER_API_VERSION": "anything",
57
+			},
58
+			expectedVersion: "anything",
59
+		},
60
+		{
61
+			envs: map[string]string{
62
+				"DOCKER_API_VERSION": "1.22",
63
+			},
64
+			expectedVersion: "1.22",
65
+		},
66
+	}
67
+	for _, c := range cases {
68
+		recoverEnvs := setupEnvs(t, c.envs)
69
+		apiclient, err := NewEnvClient()
70
+		if c.expectedError != "" {
71
+			if err == nil || err.Error() != c.expectedError {
72
+				t.Errorf("expected an error %s, got %s, for %v", c.expectedError, err.Error(), c)
73
+			}
74
+		} else {
75
+			if err != nil {
76
+				t.Error(err)
77
+			}
78
+			version := apiclient.ClientVersion()
79
+			if version != c.expectedVersion {
80
+				t.Errorf("expected %s, got %s, for %v", c.expectedVersion, version, c)
81
+			}
82
+		}
83
+		recoverEnvs(t)
84
+	}
85
+}
86
+
87
+func setupEnvs(t *testing.T, envs map[string]string) func(*testing.T) {
88
+	oldEnvs := map[string]string{}
89
+	for key, value := range envs {
90
+		oldEnv := os.Getenv(key)
91
+		oldEnvs[key] = oldEnv
92
+		err := os.Setenv(key, value)
93
+		if err != nil {
94
+			t.Error(err)
95
+		}
96
+	}
97
+	return func(t *testing.T) {
98
+		for key, value := range oldEnvs {
99
+			err := os.Setenv(key, value)
100
+			if err != nil {
101
+				t.Error(err)
102
+			}
103
+		}
104
+	}
105
+}
106
+
107
+func TestGetAPIPath(t *testing.T) {
108
+	cases := []struct {
109
+		v string
110
+		p string
111
+		q url.Values
112
+		e string
113
+	}{
114
+		{"", "/containers/json", nil, "/containers/json"},
115
+		{"", "/containers/json", url.Values{}, "/containers/json"},
116
+		{"", "/containers/json", url.Values{"s": []string{"c"}}, "/containers/json?s=c"},
117
+		{"1.22", "/containers/json", nil, "/v1.22/containers/json"},
118
+		{"1.22", "/containers/json", url.Values{}, "/v1.22/containers/json"},
119
+		{"1.22", "/containers/json", url.Values{"s": []string{"c"}}, "/v1.22/containers/json?s=c"},
120
+		{"v1.22", "/containers/json", nil, "/v1.22/containers/json"},
121
+		{"v1.22", "/containers/json", url.Values{}, "/v1.22/containers/json"},
122
+		{"v1.22", "/containers/json", url.Values{"s": []string{"c"}}, "/v1.22/containers/json?s=c"},
123
+		{"v1.22", "/networks/kiwl$%^", nil, "/v1.22/networks/kiwl$%25%5E"},
124
+	}
125
+
126
+	for _, cs := range cases {
127
+		c, err := NewClient("unix:///var/run/docker.sock", cs.v, nil, nil)
128
+		if err != nil {
129
+			t.Fatal(err)
130
+		}
131
+		g := c.getAPIPath(cs.p, cs.q)
132
+		if g != cs.e {
133
+			t.Fatalf("Expected %s, got %s", cs.e, g)
134
+		}
135
+	}
136
+}
137
+
138
+func TestParseHost(t *testing.T) {
139
+	cases := []struct {
140
+		host  string
141
+		proto string
142
+		addr  string
143
+		base  string
144
+		err   bool
145
+	}{
146
+		{"", "", "", "", true},
147
+		{"foobar", "", "", "", true},
148
+		{"foo://bar", "foo", "bar", "", false},
149
+		{"tcp://localhost:2476", "tcp", "localhost:2476", "", false},
150
+		{"tcp://localhost:2476/path", "tcp", "localhost:2476", "/path", false},
151
+	}
152
+
153
+	for _, cs := range cases {
154
+		p, a, b, e := ParseHost(cs.host)
155
+		if cs.err && e == nil {
156
+			t.Fatalf("expected error, got nil")
157
+		}
158
+		if !cs.err && e != nil {
159
+			t.Fatal(e)
160
+		}
161
+		if cs.proto != p {
162
+			t.Fatalf("expected proto %s, got %s", cs.proto, p)
163
+		}
164
+		if cs.addr != a {
165
+			t.Fatalf("expected addr %s, got %s", cs.addr, a)
166
+		}
167
+		if cs.base != b {
168
+			t.Fatalf("expected base %s, got %s", cs.base, b)
169
+		}
170
+	}
171
+}
172
+
173
+func TestUpdateClientVersion(t *testing.T) {
174
+	client := &Client{
175
+		transport: newMockClient(nil, func(req *http.Request) (*http.Response, error) {
176
+			splitQuery := strings.Split(req.URL.Path, "/")
177
+			queryVersion := splitQuery[1]
178
+			b, err := json.Marshal(types.Version{
179
+				APIVersion: queryVersion,
180
+			})
181
+			if err != nil {
182
+				return nil, err
183
+			}
184
+			return &http.Response{
185
+				StatusCode: http.StatusOK,
186
+				Body:       ioutil.NopCloser(bytes.NewReader(b)),
187
+			}, nil
188
+		}),
189
+	}
190
+
191
+	cases := []struct {
192
+		v string
193
+	}{
194
+		{"1.20"},
195
+		{"v1.21"},
196
+		{"1.22"},
197
+		{"v1.22"},
198
+	}
199
+
200
+	for _, cs := range cases {
201
+		client.UpdateClientVersion(cs.v)
202
+		r, err := client.ServerVersion(context.Background())
203
+		if err != nil {
204
+			t.Fatal(err)
205
+		}
206
+		if strings.TrimPrefix(r.APIVersion, "v") != strings.TrimPrefix(cs.v, "v") {
207
+			t.Fatalf("Expected %s, got %s", cs.v, r.APIVersion)
208
+		}
209
+	}
210
+}
211
+
212
+func TestNewEnvClientSetsDefaultVersion(t *testing.T) {
213
+	// Unset environment variables
214
+	envVarKeys := []string{
215
+		"DOCKER_HOST",
216
+		"DOCKER_API_VERSION",
217
+		"DOCKER_TLS_VERIFY",
218
+		"DOCKER_CERT_PATH",
219
+	}
220
+	envVarValues := make(map[string]string)
221
+	for _, key := range envVarKeys {
222
+		envVarValues[key] = os.Getenv(key)
223
+		os.Setenv(key, "")
224
+	}
225
+
226
+	client, err := NewEnvClient()
227
+	if err != nil {
228
+		t.Fatal(err)
229
+	}
230
+	if client.version != DefaultVersion {
231
+		t.Fatalf("Expected %s, got %s", DefaultVersion, client.version)
232
+	}
233
+
234
+	expected := "1.22"
235
+	os.Setenv("DOCKER_API_VERSION", expected)
236
+	client, err = NewEnvClient()
237
+	if err != nil {
238
+		t.Fatal(err)
239
+	}
240
+	if client.version != expected {
241
+		t.Fatalf("Expected %s, got %s", expected, client.version)
242
+	}
243
+
244
+	// Restore environment variables
245
+	for _, key := range envVarKeys {
246
+		os.Setenv(key, envVarValues[key])
247
+	}
248
+}
0 249
new file mode 100644
... ...
@@ -0,0 +1,6 @@
0
+// +build linux freebsd solaris openbsd darwin
1
+
2
+package client
3
+
4
+// DefaultDockerHost defines os specific default if DOCKER_HOST is unset
5
+const DefaultDockerHost = "unix:///var/run/docker.sock"
0 6
new file mode 100644
... ...
@@ -0,0 +1,4 @@
0
+package client
1
+
2
+// DefaultDockerHost defines os specific default if DOCKER_HOST is unset
3
+const DefaultDockerHost = "npipe:////./pipe/docker_engine"
0 4
new file mode 100644
... ...
@@ -0,0 +1,34 @@
0
+package client
1
+
2
+import (
3
+	"net/url"
4
+
5
+	"github.com/docker/docker/api/types"
6
+	"golang.org/x/net/context"
7
+)
8
+
9
+// ContainerAttach attaches a connection to a container in the server.
10
+// It returns a types.HijackedConnection with the hijacked connection
11
+// and the a reader to get output. It's up to the called to close
12
+// the hijacked connection by calling types.HijackedResponse.Close.
13
+func (cli *Client) ContainerAttach(ctx context.Context, container string, options types.ContainerAttachOptions) (types.HijackedResponse, error) {
14
+	query := url.Values{}
15
+	if options.Stream {
16
+		query.Set("stream", "1")
17
+	}
18
+	if options.Stdin {
19
+		query.Set("stdin", "1")
20
+	}
21
+	if options.Stdout {
22
+		query.Set("stdout", "1")
23
+	}
24
+	if options.Stderr {
25
+		query.Set("stderr", "1")
26
+	}
27
+	if options.DetachKeys != "" {
28
+		query.Set("detachKeys", options.DetachKeys)
29
+	}
30
+
31
+	headers := map[string][]string{"Content-Type": {"text/plain"}}
32
+	return cli.postHijacked(ctx, "/containers/"+container+"/attach", query, nil, headers)
33
+}
0 34
new file mode 100644
... ...
@@ -0,0 +1,53 @@
0
+package client
1
+
2
+import (
3
+	"encoding/json"
4
+	"errors"
5
+	"net/url"
6
+
7
+	distreference "github.com/docker/distribution/reference"
8
+	"github.com/docker/docker/api/types"
9
+	"github.com/docker/docker/api/types/reference"
10
+	"golang.org/x/net/context"
11
+)
12
+
13
+// ContainerCommit applies changes into a container and creates a new tagged image.
14
+func (cli *Client) ContainerCommit(ctx context.Context, container string, options types.ContainerCommitOptions) (types.ContainerCommitResponse, error) {
15
+	var repository, tag string
16
+	if options.Reference != "" {
17
+		distributionRef, err := distreference.ParseNamed(options.Reference)
18
+		if err != nil {
19
+			return types.ContainerCommitResponse{}, err
20
+		}
21
+
22
+		if _, isCanonical := distributionRef.(distreference.Canonical); isCanonical {
23
+			return types.ContainerCommitResponse{}, errors.New("refusing to create a tag with a digest reference")
24
+		}
25
+
26
+		tag = reference.GetTagFromNamedRef(distributionRef)
27
+		repository = distributionRef.Name()
28
+	}
29
+
30
+	query := url.Values{}
31
+	query.Set("container", container)
32
+	query.Set("repo", repository)
33
+	query.Set("tag", tag)
34
+	query.Set("comment", options.Comment)
35
+	query.Set("author", options.Author)
36
+	for _, change := range options.Changes {
37
+		query.Add("changes", change)
38
+	}
39
+	if options.Pause != true {
40
+		query.Set("pause", "0")
41
+	}
42
+
43
+	var response types.ContainerCommitResponse
44
+	resp, err := cli.post(ctx, "/commit", query, options.Config, nil)
45
+	if err != nil {
46
+		return response, err
47
+	}
48
+
49
+	err = json.NewDecoder(resp.body).Decode(&response)
50
+	ensureReaderClosed(resp)
51
+	return response, err
52
+}
0 53
new file mode 100644
... ...
@@ -0,0 +1,96 @@
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
+	"golang.org/x/net/context"
13
+)
14
+
15
+func TestContainerCommitError(t *testing.T) {
16
+	client := &Client{
17
+		transport: newMockClient(nil, errorMock(http.StatusInternalServerError, "Server error")),
18
+	}
19
+	_, err := client.ContainerCommit(context.Background(), "nothing", types.ContainerCommitOptions{})
20
+	if err == nil || err.Error() != "Error response from daemon: Server error" {
21
+		t.Fatalf("expected a Server Error, got %v", err)
22
+	}
23
+}
24
+
25
+func TestContainerCommit(t *testing.T) {
26
+	expectedURL := "/commit"
27
+	expectedContainerID := "container_id"
28
+	specifiedReference := "repository_name:tag"
29
+	expectedRepositoryName := "repository_name"
30
+	expectedTag := "tag"
31
+	expectedComment := "comment"
32
+	expectedAuthor := "author"
33
+	expectedChanges := []string{"change1", "change2"}
34
+
35
+	client := &Client{
36
+		transport: newMockClient(nil, func(req *http.Request) (*http.Response, error) {
37
+			if !strings.HasPrefix(req.URL.Path, expectedURL) {
38
+				return nil, fmt.Errorf("Expected URL '%s', got '%s'", expectedURL, req.URL)
39
+			}
40
+			query := req.URL.Query()
41
+			containerID := query.Get("container")
42
+			if containerID != expectedContainerID {
43
+				return nil, fmt.Errorf("container id not set in URL query properly. Expected '%s', got %s", expectedContainerID, containerID)
44
+			}
45
+			repo := query.Get("repo")
46
+			if repo != expectedRepositoryName {
47
+				return nil, fmt.Errorf("container repo not set in URL query properly. Expected '%s', got %s", expectedRepositoryName, repo)
48
+			}
49
+			tag := query.Get("tag")
50
+			if tag != expectedTag {
51
+				return nil, fmt.Errorf("container tag not set in URL query properly. Expected '%s', got %s'", expectedTag, tag)
52
+			}
53
+			comment := query.Get("comment")
54
+			if comment != expectedComment {
55
+				return nil, fmt.Errorf("container comment not set in URL query properly. Expected '%s', got %s'", expectedComment, comment)
56
+			}
57
+			author := query.Get("author")
58
+			if author != expectedAuthor {
59
+				return nil, fmt.Errorf("container author not set in URL query properly. Expected '%s', got %s'", expectedAuthor, author)
60
+			}
61
+			pause := query.Get("pause")
62
+			if pause != "0" {
63
+				return nil, fmt.Errorf("container pause not set in URL query properly. Expected 'true', got %v'", pause)
64
+			}
65
+			changes := query["changes"]
66
+			if len(changes) != len(expectedChanges) {
67
+				return nil, fmt.Errorf("expected container changes size to be '%d', got %d", len(expectedChanges), len(changes))
68
+			}
69
+			b, err := json.Marshal(types.ContainerCommitResponse{
70
+				ID: "new_container_id",
71
+			})
72
+			if err != nil {
73
+				return nil, err
74
+			}
75
+			return &http.Response{
76
+				StatusCode: http.StatusOK,
77
+				Body:       ioutil.NopCloser(bytes.NewReader(b)),
78
+			}, nil
79
+		}),
80
+	}
81
+
82
+	r, err := client.ContainerCommit(context.Background(), expectedContainerID, types.ContainerCommitOptions{
83
+		Reference: specifiedReference,
84
+		Comment:   expectedComment,
85
+		Author:    expectedAuthor,
86
+		Changes:   expectedChanges,
87
+		Pause:     false,
88
+	})
89
+	if err != nil {
90
+		t.Fatal(err)
91
+	}
92
+	if r.ID != "new_container_id" {
93
+		t.Fatalf("expected `container_id`, got %s", r.ID)
94
+	}
95
+}
0 96
new file mode 100644
... ...
@@ -0,0 +1,97 @@
0
+package client
1
+
2
+import (
3
+	"encoding/base64"
4
+	"encoding/json"
5
+	"fmt"
6
+	"io"
7
+	"net/http"
8
+	"net/url"
9
+	"path/filepath"
10
+	"strings"
11
+
12
+	"golang.org/x/net/context"
13
+
14
+	"github.com/docker/docker/api/types"
15
+)
16
+
17
+// ContainerStatPath returns Stat information about a path inside the container filesystem.
18
+func (cli *Client) ContainerStatPath(ctx context.Context, containerID, path string) (types.ContainerPathStat, error) {
19
+	query := url.Values{}
20
+	query.Set("path", filepath.ToSlash(path)) // Normalize the paths used in the API.
21
+
22
+	urlStr := fmt.Sprintf("/containers/%s/archive", containerID)
23
+	response, err := cli.head(ctx, urlStr, query, nil)
24
+	if err != nil {
25
+		return types.ContainerPathStat{}, err
26
+	}
27
+	defer ensureReaderClosed(response)
28
+	return getContainerPathStatFromHeader(response.header)
29
+}
30
+
31
+// CopyToContainer copies content into the container filesystem.
32
+func (cli *Client) CopyToContainer(ctx context.Context, container, path string, content io.Reader, options types.CopyToContainerOptions) error {
33
+	query := url.Values{}
34
+	query.Set("path", filepath.ToSlash(path)) // Normalize the paths used in the API.
35
+	// Do not allow for an existing directory to be overwritten by a non-directory and vice versa.
36
+	if !options.AllowOverwriteDirWithFile {
37
+		query.Set("noOverwriteDirNonDir", "true")
38
+	}
39
+
40
+	apiPath := fmt.Sprintf("/containers/%s/archive", container)
41
+
42
+	response, err := cli.putRaw(ctx, apiPath, query, content, nil)
43
+	if err != nil {
44
+		return err
45
+	}
46
+	defer ensureReaderClosed(response)
47
+
48
+	if response.statusCode != http.StatusOK {
49
+		return fmt.Errorf("unexpected status code from daemon: %d", response.statusCode)
50
+	}
51
+
52
+	return nil
53
+}
54
+
55
+// CopyFromContainer gets the content from the container and returns it as a Reader
56
+// to manipulate it in the host. It's up to the caller to close the reader.
57
+func (cli *Client) CopyFromContainer(ctx context.Context, container, srcPath string) (io.ReadCloser, types.ContainerPathStat, error) {
58
+	query := make(url.Values, 1)
59
+	query.Set("path", filepath.ToSlash(srcPath)) // Normalize the paths used in the API.
60
+
61
+	apiPath := fmt.Sprintf("/containers/%s/archive", container)
62
+	response, err := cli.get(ctx, apiPath, query, nil)
63
+	if err != nil {
64
+		return nil, types.ContainerPathStat{}, err
65
+	}
66
+
67
+	if response.statusCode != http.StatusOK {
68
+		return nil, types.ContainerPathStat{}, fmt.Errorf("unexpected status code from daemon: %d", response.statusCode)
69
+	}
70
+
71
+	// In order to get the copy behavior right, we need to know information
72
+	// about both the source and the destination. The response headers include
73
+	// stat info about the source that we can use in deciding exactly how to
74
+	// copy it locally. Along with the stat info about the local destination,
75
+	// we have everything we need to handle the multiple possibilities there
76
+	// can be when copying a file/dir from one location to another file/dir.
77
+	stat, err := getContainerPathStatFromHeader(response.header)
78
+	if err != nil {
79
+		return nil, stat, fmt.Errorf("unable to get resource stat from response: %s", err)
80
+	}
81
+	return response.body, stat, err
82
+}
83
+
84
+func getContainerPathStatFromHeader(header http.Header) (types.ContainerPathStat, error) {
85
+	var stat types.ContainerPathStat
86
+
87
+	encodedStat := header.Get("X-Docker-Container-Path-Stat")
88
+	statDecoder := base64.NewDecoder(base64.StdEncoding, strings.NewReader(encodedStat))
89
+
90
+	err := json.NewDecoder(statDecoder).Decode(&stat)
91
+	if err != nil {
92
+		err = fmt.Errorf("unable to decode container path stat header: %s", err)
93
+	}
94
+
95
+	return stat, err
96
+}
0 97
new file mode 100644
... ...
@@ -0,0 +1,244 @@
0
+package client
1
+
2
+import (
3
+	"bytes"
4
+	"encoding/base64"
5
+	"encoding/json"
6
+	"fmt"
7
+	"io/ioutil"
8
+	"net/http"
9
+	"strings"
10
+	"testing"
11
+
12
+	"golang.org/x/net/context"
13
+
14
+	"github.com/docker/docker/api/types"
15
+)
16
+
17
+func TestContainerStatPathError(t *testing.T) {
18
+	client := &Client{
19
+		transport: newMockClient(nil, errorMock(http.StatusInternalServerError, "Server error")),
20
+	}
21
+	_, err := client.ContainerStatPath(context.Background(), "container_id", "path")
22
+	if err == nil || err.Error() != "Error response from daemon: Server error" {
23
+		t.Fatalf("expected a Server error, got %v", err)
24
+	}
25
+}
26
+
27
+func TestContainerStatPathNoHeaderError(t *testing.T) {
28
+	client := &Client{
29
+		transport: newMockClient(nil, func(req *http.Request) (*http.Response, error) {
30
+			return &http.Response{
31
+				StatusCode: http.StatusOK,
32
+				Body:       ioutil.NopCloser(bytes.NewReader([]byte(""))),
33
+			}, nil
34
+		}),
35
+	}
36
+	_, err := client.ContainerStatPath(context.Background(), "container_id", "path/to/file")
37
+	if err == nil {
38
+		t.Fatalf("expected an error, got nothing")
39
+	}
40
+}
41
+
42
+func TestContainerStatPath(t *testing.T) {
43
+	expectedURL := "/containers/container_id/archive"
44
+	expectedPath := "path/to/file"
45
+	client := &Client{
46
+		transport: newMockClient(nil, func(req *http.Request) (*http.Response, error) {
47
+			if !strings.HasPrefix(req.URL.Path, expectedURL) {
48
+				return nil, fmt.Errorf("Expected URL '%s', got '%s'", expectedURL, req.URL)
49
+			}
50
+			if req.Method != "HEAD" {
51
+				return nil, fmt.Errorf("expected HEAD method, got %s", req.Method)
52
+			}
53
+			query := req.URL.Query()
54
+			path := query.Get("path")
55
+			if path != expectedPath {
56
+				return nil, fmt.Errorf("path not set in URL query properly")
57
+			}
58
+			content, err := json.Marshal(types.ContainerPathStat{
59
+				Name: "name",
60
+				Mode: 0700,
61
+			})
62
+			if err != nil {
63
+				return nil, err
64
+			}
65
+			base64PathStat := base64.StdEncoding.EncodeToString(content)
66
+			return &http.Response{
67
+				StatusCode: http.StatusOK,
68
+				Body:       ioutil.NopCloser(bytes.NewReader([]byte(""))),
69
+				Header: http.Header{
70
+					"X-Docker-Container-Path-Stat": []string{base64PathStat},
71
+				},
72
+			}, nil
73
+		}),
74
+	}
75
+	stat, err := client.ContainerStatPath(context.Background(), "container_id", expectedPath)
76
+	if err != nil {
77
+		t.Fatal(err)
78
+	}
79
+	if stat.Name != "name" {
80
+		t.Fatalf("expected container path stat name to be 'name', was '%s'", stat.Name)
81
+	}
82
+	if stat.Mode != 0700 {
83
+		t.Fatalf("expected container path stat mode to be 0700, was '%v'", stat.Mode)
84
+	}
85
+}
86
+
87
+func TestCopyToContainerError(t *testing.T) {
88
+	client := &Client{
89
+		transport: newMockClient(nil, errorMock(http.StatusInternalServerError, "Server error")),
90
+	}
91
+	err := client.CopyToContainer(context.Background(), "container_id", "path/to/file", bytes.NewReader([]byte("")), types.CopyToContainerOptions{})
92
+	if err == nil || err.Error() != "Error response from daemon: Server error" {
93
+		t.Fatalf("expected a Server error, got %v", err)
94
+	}
95
+}
96
+
97
+func TestCopyToContainerNotStatusOKError(t *testing.T) {
98
+	client := &Client{
99
+		transport: newMockClient(nil, errorMock(http.StatusNoContent, "No content")),
100
+	}
101
+	err := client.CopyToContainer(context.Background(), "container_id", "path/to/file", bytes.NewReader([]byte("")), types.CopyToContainerOptions{})
102
+	if err == nil || err.Error() != "unexpected status code from daemon: 204" {
103
+		t.Fatalf("expected an unexpected status code error, got %v", err)
104
+	}
105
+}
106
+
107
+func TestCopyToContainer(t *testing.T) {
108
+	expectedURL := "/containers/container_id/archive"
109
+	expectedPath := "path/to/file"
110
+	client := &Client{
111
+		transport: newMockClient(nil, func(req *http.Request) (*http.Response, error) {
112
+			if !strings.HasPrefix(req.URL.Path, expectedURL) {
113
+				return nil, fmt.Errorf("Expected URL '%s', got '%s'", expectedURL, req.URL)
114
+			}
115
+			if req.Method != "PUT" {
116
+				return nil, fmt.Errorf("expected PUT method, got %s", req.Method)
117
+			}
118
+			query := req.URL.Query()
119
+			path := query.Get("path")
120
+			if path != expectedPath {
121
+				return nil, fmt.Errorf("path not set in URL query properly, expected '%s', got %s", expectedPath, path)
122
+			}
123
+			noOverwriteDirNonDir := query.Get("noOverwriteDirNonDir")
124
+			if noOverwriteDirNonDir != "true" {
125
+				return nil, fmt.Errorf("noOverwriteDirNonDir not set in URL query properly, expected true, got %s", noOverwriteDirNonDir)
126
+			}
127
+
128
+			content, err := ioutil.ReadAll(req.Body)
129
+			if err != nil {
130
+				return nil, err
131
+			}
132
+			if err := req.Body.Close(); err != nil {
133
+				return nil, err
134
+			}
135
+			if string(content) != "content" {
136
+				return nil, fmt.Errorf("expected content to be 'content', got %s", string(content))
137
+			}
138
+
139
+			return &http.Response{
140
+				StatusCode: http.StatusOK,
141
+				Body:       ioutil.NopCloser(bytes.NewReader([]byte(""))),
142
+			}, nil
143
+		}),
144
+	}
145
+	err := client.CopyToContainer(context.Background(), "container_id", expectedPath, bytes.NewReader([]byte("content")), types.CopyToContainerOptions{
146
+		AllowOverwriteDirWithFile: false,
147
+	})
148
+	if err != nil {
149
+		t.Fatal(err)
150
+	}
151
+}
152
+
153
+func TestCopyFromContainerError(t *testing.T) {
154
+	client := &Client{
155
+		transport: newMockClient(nil, errorMock(http.StatusInternalServerError, "Server error")),
156
+	}
157
+	_, _, err := client.CopyFromContainer(context.Background(), "container_id", "path/to/file")
158
+	if err == nil || err.Error() != "Error response from daemon: Server error" {
159
+		t.Fatalf("expected a Server error, got %v", err)
160
+	}
161
+}
162
+
163
+func TestCopyFromContainerNotStatusOKError(t *testing.T) {
164
+	client := &Client{
165
+		transport: newMockClient(nil, errorMock(http.StatusNoContent, "No content")),
166
+	}
167
+	_, _, err := client.CopyFromContainer(context.Background(), "container_id", "path/to/file")
168
+	if err == nil || err.Error() != "unexpected status code from daemon: 204" {
169
+		t.Fatalf("expected an unexpected status code error, got %v", err)
170
+	}
171
+}
172
+
173
+func TestCopyFromContainerNoHeaderError(t *testing.T) {
174
+	client := &Client{
175
+		transport: newMockClient(nil, func(req *http.Request) (*http.Response, error) {
176
+			return &http.Response{
177
+				StatusCode: http.StatusOK,
178
+				Body:       ioutil.NopCloser(bytes.NewReader([]byte(""))),
179
+			}, nil
180
+		}),
181
+	}
182
+	_, _, err := client.CopyFromContainer(context.Background(), "container_id", "path/to/file")
183
+	if err == nil {
184
+		t.Fatalf("expected an error, got nothing")
185
+	}
186
+}
187
+
188
+func TestCopyFromContainer(t *testing.T) {
189
+	expectedURL := "/containers/container_id/archive"
190
+	expectedPath := "path/to/file"
191
+	client := &Client{
192
+		transport: newMockClient(nil, func(req *http.Request) (*http.Response, error) {
193
+			if !strings.HasPrefix(req.URL.Path, expectedURL) {
194
+				return nil, fmt.Errorf("Expected URL '%s', got '%s'", expectedURL, req.URL)
195
+			}
196
+			if req.Method != "GET" {
197
+				return nil, fmt.Errorf("expected PUT method, got %s", req.Method)
198
+			}
199
+			query := req.URL.Query()
200
+			path := query.Get("path")
201
+			if path != expectedPath {
202
+				return nil, fmt.Errorf("path not set in URL query properly, expected '%s', got %s", expectedPath, path)
203
+			}
204
+
205
+			headercontent, err := json.Marshal(types.ContainerPathStat{
206
+				Name: "name",
207
+				Mode: 0700,
208
+			})
209
+			if err != nil {
210
+				return nil, err
211
+			}
212
+			base64PathStat := base64.StdEncoding.EncodeToString(headercontent)
213
+
214
+			return &http.Response{
215
+				StatusCode: http.StatusOK,
216
+				Body:       ioutil.NopCloser(bytes.NewReader([]byte("content"))),
217
+				Header: http.Header{
218
+					"X-Docker-Container-Path-Stat": []string{base64PathStat},
219
+				},
220
+			}, nil
221
+		}),
222
+	}
223
+	r, stat, err := client.CopyFromContainer(context.Background(), "container_id", expectedPath)
224
+	if err != nil {
225
+		t.Fatal(err)
226
+	}
227
+	if stat.Name != "name" {
228
+		t.Fatalf("expected container path stat name to be 'name', was '%s'", stat.Name)
229
+	}
230
+	if stat.Mode != 0700 {
231
+		t.Fatalf("expected container path stat mode to be 0700, was '%v'", stat.Mode)
232
+	}
233
+	content, err := ioutil.ReadAll(r)
234
+	if err != nil {
235
+		t.Fatal(err)
236
+	}
237
+	if err := r.Close(); err != nil {
238
+		t.Fatal(err)
239
+	}
240
+	if string(content) != "content" {
241
+		t.Fatalf("expected content to be 'content', got %s", string(content))
242
+	}
243
+}
0 244
new file mode 100644
... ...
@@ -0,0 +1,46 @@
0
+package client
1
+
2
+import (
3
+	"encoding/json"
4
+	"net/url"
5
+	"strings"
6
+
7
+	"github.com/docker/docker/api/types"
8
+	"github.com/docker/docker/api/types/container"
9
+	"github.com/docker/docker/api/types/network"
10
+	"golang.org/x/net/context"
11
+)
12
+
13
+type configWrapper struct {
14
+	*container.Config
15
+	HostConfig       *container.HostConfig
16
+	NetworkingConfig *network.NetworkingConfig
17
+}
18
+
19
+// ContainerCreate creates a new container based in the given configuration.
20
+// It can be associated with a name, but it's not mandatory.
21
+func (cli *Client) ContainerCreate(ctx context.Context, config *container.Config, hostConfig *container.HostConfig, networkingConfig *network.NetworkingConfig, containerName string) (types.ContainerCreateResponse, error) {
22
+	var response types.ContainerCreateResponse
23
+	query := url.Values{}
24
+	if containerName != "" {
25
+		query.Set("name", containerName)
26
+	}
27
+
28
+	body := configWrapper{
29
+		Config:           config,
30
+		HostConfig:       hostConfig,
31
+		NetworkingConfig: networkingConfig,
32
+	}
33
+
34
+	serverResp, err := cli.post(ctx, "/containers/create", query, body, nil)
35
+	if err != nil {
36
+		if serverResp.statusCode == 404 && strings.Contains(err.Error(), "No such image") {
37
+			return response, imageNotFoundError{config.Image}
38
+		}
39
+		return response, err
40
+	}
41
+
42
+	err = json.NewDecoder(serverResp.body).Decode(&response)
43
+	ensureReaderClosed(serverResp)
44
+	return response, err
45
+}
0 46
new file mode 100644
... ...
@@ -0,0 +1,77 @@
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/container"
13
+	"golang.org/x/net/context"
14
+)
15
+
16
+func TestContainerCreateError(t *testing.T) {
17
+	client := &Client{
18
+		transport: newMockClient(nil, errorMock(http.StatusInternalServerError, "Server error")),
19
+	}
20
+	_, err := client.ContainerCreate(context.Background(), nil, nil, nil, "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
+	// 404 doesn't automagitally means an unknown image
26
+	client = &Client{
27
+		transport: newMockClient(nil, errorMock(http.StatusNotFound, "Server error")),
28
+	}
29
+	_, err = client.ContainerCreate(context.Background(), nil, nil, nil, "nothing")
30
+	if err == nil || err.Error() != "Error response from daemon: Server error" {
31
+		t.Fatalf("expected a Server Error, got %v", err)
32
+	}
33
+}
34
+
35
+func TestContainerCreateImageNotFound(t *testing.T) {
36
+	client := &Client{
37
+		transport: newMockClient(nil, errorMock(http.StatusNotFound, "No such image")),
38
+	}
39
+	_, err := client.ContainerCreate(context.Background(), &container.Config{Image: "unknown_image"}, nil, nil, "unknown")
40
+	if err == nil || !IsErrImageNotFound(err) {
41
+		t.Fatalf("expected an imageNotFound error, got %v", err)
42
+	}
43
+}
44
+
45
+func TestContainerCreateWithName(t *testing.T) {
46
+	expectedURL := "/containers/create"
47
+	client := &Client{
48
+		transport: newMockClient(nil, func(req *http.Request) (*http.Response, error) {
49
+			if !strings.HasPrefix(req.URL.Path, expectedURL) {
50
+				return nil, fmt.Errorf("Expected URL '%s', got '%s'", expectedURL, req.URL)
51
+			}
52
+			name := req.URL.Query().Get("name")
53
+			if name != "container_name" {
54
+				return nil, fmt.Errorf("container name not set in URL query properly. Expected `container_name`, got %s", name)
55
+			}
56
+			b, err := json.Marshal(types.ContainerCreateResponse{
57
+				ID: "container_id",
58
+			})
59
+			if err != nil {
60
+				return nil, err
61
+			}
62
+			return &http.Response{
63
+				StatusCode: http.StatusOK,
64
+				Body:       ioutil.NopCloser(bytes.NewReader(b)),
65
+			}, nil
66
+		}),
67
+	}
68
+
69
+	r, err := client.ContainerCreate(context.Background(), nil, nil, nil, "container_name")
70
+	if err != nil {
71
+		t.Fatal(err)
72
+	}
73
+	if r.ID != "container_id" {
74
+		t.Fatalf("expected `container_id`, got %s", r.ID)
75
+	}
76
+}
0 77
new file mode 100644
... ...
@@ -0,0 +1,23 @@
0
+package client
1
+
2
+import (
3
+	"encoding/json"
4
+	"net/url"
5
+
6
+	"github.com/docker/docker/api/types"
7
+	"golang.org/x/net/context"
8
+)
9
+
10
+// ContainerDiff shows differences in a container filesystem since it was started.
11
+func (cli *Client) ContainerDiff(ctx context.Context, containerID string) ([]types.ContainerChange, error) {
12
+	var changes []types.ContainerChange
13
+
14
+	serverResp, err := cli.get(ctx, "/containers/"+containerID+"/changes", url.Values{}, nil)
15
+	if err != nil {
16
+		return changes, err
17
+	}
18
+
19
+	err = json.NewDecoder(serverResp.body).Decode(&changes)
20
+	ensureReaderClosed(serverResp)
21
+	return changes, err
22
+}
0 23
new file mode 100644
... ...
@@ -0,0 +1,61 @@
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
+	"golang.org/x/net/context"
13
+)
14
+
15
+func TestContainerDiffError(t *testing.T) {
16
+	client := &Client{
17
+		transport: newMockClient(nil, errorMock(http.StatusInternalServerError, "Server error")),
18
+	}
19
+	_, err := client.ContainerDiff(context.Background(), "nothing")
20
+	if err == nil || err.Error() != "Error response from daemon: Server error" {
21
+		t.Fatalf("expected a Server Error, got %v", err)
22
+	}
23
+
24
+}
25
+
26
+func TestContainerDiff(t *testing.T) {
27
+	expectedURL := "/containers/container_id/changes"
28
+	client := &Client{
29
+		transport: newMockClient(nil, 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
+			b, err := json.Marshal([]types.ContainerChange{
34
+				{
35
+					Kind: 0,
36
+					Path: "/path/1",
37
+				},
38
+				{
39
+					Kind: 1,
40
+					Path: "/path/2",
41
+				},
42
+			})
43
+			if err != nil {
44
+				return nil, err
45
+			}
46
+			return &http.Response{
47
+				StatusCode: http.StatusOK,
48
+				Body:       ioutil.NopCloser(bytes.NewReader(b)),
49
+			}, nil
50
+		}),
51
+	}
52
+
53
+	changes, err := client.ContainerDiff(context.Background(), "container_id")
54
+	if err != nil {
55
+		t.Fatal(err)
56
+	}
57
+	if len(changes) != 2 {
58
+		t.Fatalf("expected an array of 2 changes, got %v", changes)
59
+	}
60
+}
0 61
new file mode 100644
... ...
@@ -0,0 +1,49 @@
0
+package client
1
+
2
+import (
3
+	"encoding/json"
4
+
5
+	"github.com/docker/docker/api/types"
6
+	"golang.org/x/net/context"
7
+)
8
+
9
+// ContainerExecCreate creates a new exec configuration to run an exec process.
10
+func (cli *Client) ContainerExecCreate(ctx context.Context, container string, config types.ExecConfig) (types.ContainerExecCreateResponse, error) {
11
+	var response types.ContainerExecCreateResponse
12
+	resp, err := cli.post(ctx, "/containers/"+container+"/exec", nil, config, nil)
13
+	if err != nil {
14
+		return response, err
15
+	}
16
+	err = json.NewDecoder(resp.body).Decode(&response)
17
+	ensureReaderClosed(resp)
18
+	return response, err
19
+}
20
+
21
+// ContainerExecStart starts an exec process already created in the docker host.
22
+func (cli *Client) ContainerExecStart(ctx context.Context, execID string, config types.ExecStartCheck) error {
23
+	resp, err := cli.post(ctx, "/exec/"+execID+"/start", nil, config, nil)
24
+	ensureReaderClosed(resp)
25
+	return err
26
+}
27
+
28
+// ContainerExecAttach attaches a connection to an exec process in the server.
29
+// It returns a types.HijackedConnection with the hijacked connection
30
+// and the a reader to get output. It's up to the called to close
31
+// the hijacked connection by calling types.HijackedResponse.Close.
32
+func (cli *Client) ContainerExecAttach(ctx context.Context, execID string, config types.ExecConfig) (types.HijackedResponse, error) {
33
+	headers := map[string][]string{"Content-Type": {"application/json"}}
34
+	return cli.postHijacked(ctx, "/exec/"+execID+"/start", nil, config, headers)
35
+}
36
+
37
+// ContainerExecInspect returns information about a specific exec process on the docker host.
38
+func (cli *Client) ContainerExecInspect(ctx context.Context, execID string) (types.ContainerExecInspect, error) {
39
+	var response types.ContainerExecInspect
40
+	resp, err := cli.get(ctx, "/exec/"+execID+"/json", nil, nil)
41
+	if err != nil {
42
+		return response, err
43
+	}
44
+
45
+	err = json.NewDecoder(resp.body).Decode(&response)
46
+	ensureReaderClosed(resp)
47
+	return response, err
48
+}
0 49
new file mode 100644
... ...
@@ -0,0 +1,157 @@
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
+	"golang.org/x/net/context"
12
+
13
+	"github.com/docker/docker/api/types"
14
+)
15
+
16
+func TestContainerExecCreateError(t *testing.T) {
17
+	client := &Client{
18
+		transport: newMockClient(nil, errorMock(http.StatusInternalServerError, "Server error")),
19
+	}
20
+	_, err := client.ContainerExecCreate(context.Background(), "container_id", types.ExecConfig{})
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 TestContainerExecCreate(t *testing.T) {
27
+	expectedURL := "/containers/container_id/exec"
28
+	client := &Client{
29
+		transport: newMockClient(nil, 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
+			// FIXME validate the content is the given ExecConfig ?
37
+			if err := req.ParseForm(); err != nil {
38
+				return nil, err
39
+			}
40
+			execConfig := &types.ExecConfig{}
41
+			if err := json.NewDecoder(req.Body).Decode(execConfig); err != nil {
42
+				return nil, err
43
+			}
44
+			if execConfig.User != "user" {
45
+				return nil, fmt.Errorf("expected an execConfig with User == 'user', got %v", execConfig)
46
+			}
47
+			b, err := json.Marshal(types.ContainerExecCreateResponse{
48
+				ID: "exec_id",
49
+			})
50
+			if err != nil {
51
+				return nil, err
52
+			}
53
+			return &http.Response{
54
+				StatusCode: http.StatusOK,
55
+				Body:       ioutil.NopCloser(bytes.NewReader(b)),
56
+			}, nil
57
+		}),
58
+	}
59
+
60
+	r, err := client.ContainerExecCreate(context.Background(), "container_id", types.ExecConfig{
61
+		User: "user",
62
+	})
63
+	if err != nil {
64
+		t.Fatal(err)
65
+	}
66
+	if r.ID != "exec_id" {
67
+		t.Fatalf("expected `exec_id`, got %s", r.ID)
68
+	}
69
+}
70
+
71
+func TestContainerExecStartError(t *testing.T) {
72
+	client := &Client{
73
+		transport: newMockClient(nil, errorMock(http.StatusInternalServerError, "Server error")),
74
+	}
75
+	err := client.ContainerExecStart(context.Background(), "nothing", types.ExecStartCheck{})
76
+	if err == nil || err.Error() != "Error response from daemon: Server error" {
77
+		t.Fatalf("expected a Server Error, got %v", err)
78
+	}
79
+}
80
+
81
+func TestContainerExecStart(t *testing.T) {
82
+	expectedURL := "/exec/exec_id/start"
83
+	client := &Client{
84
+		transport: newMockClient(nil, func(req *http.Request) (*http.Response, error) {
85
+			if !strings.HasPrefix(req.URL.Path, expectedURL) {
86
+				return nil, fmt.Errorf("Expected URL '%s', got '%s'", expectedURL, req.URL)
87
+			}
88
+			if err := req.ParseForm(); err != nil {
89
+				return nil, err
90
+			}
91
+			execStartCheck := &types.ExecStartCheck{}
92
+			if err := json.NewDecoder(req.Body).Decode(execStartCheck); err != nil {
93
+				return nil, err
94
+			}
95
+			if execStartCheck.Tty || !execStartCheck.Detach {
96
+				return nil, fmt.Errorf("expected execStartCheck{Detach:true,Tty:false}, got %v", execStartCheck)
97
+			}
98
+
99
+			return &http.Response{
100
+				StatusCode: http.StatusOK,
101
+				Body:       ioutil.NopCloser(bytes.NewReader([]byte(""))),
102
+			}, nil
103
+		}),
104
+	}
105
+
106
+	err := client.ContainerExecStart(context.Background(), "exec_id", types.ExecStartCheck{
107
+		Detach: true,
108
+		Tty:    false,
109
+	})
110
+	if err != nil {
111
+		t.Fatal(err)
112
+	}
113
+}
114
+
115
+func TestContainerExecInspectError(t *testing.T) {
116
+	client := &Client{
117
+		transport: newMockClient(nil, errorMock(http.StatusInternalServerError, "Server error")),
118
+	}
119
+	_, err := client.ContainerExecInspect(context.Background(), "nothing")
120
+	if err == nil || err.Error() != "Error response from daemon: Server error" {
121
+		t.Fatalf("expected a Server Error, got %v", err)
122
+	}
123
+}
124
+
125
+func TestContainerExecInspect(t *testing.T) {
126
+	expectedURL := "/exec/exec_id/json"
127
+	client := &Client{
128
+		transport: newMockClient(nil, func(req *http.Request) (*http.Response, error) {
129
+			if !strings.HasPrefix(req.URL.Path, expectedURL) {
130
+				return nil, fmt.Errorf("Expected URL '%s', got '%s'", expectedURL, req.URL)
131
+			}
132
+			b, err := json.Marshal(types.ContainerExecInspect{
133
+				ExecID:      "exec_id",
134
+				ContainerID: "container_id",
135
+			})
136
+			if err != nil {
137
+				return nil, err
138
+			}
139
+			return &http.Response{
140
+				StatusCode: http.StatusOK,
141
+				Body:       ioutil.NopCloser(bytes.NewReader(b)),
142
+			}, nil
143
+		}),
144
+	}
145
+
146
+	inspect, err := client.ContainerExecInspect(context.Background(), "exec_id")
147
+	if err != nil {
148
+		t.Fatal(err)
149
+	}
150
+	if inspect.ExecID != "exec_id" {
151
+		t.Fatalf("expected ExecID to be `exec_id`, got %s", inspect.ExecID)
152
+	}
153
+	if inspect.ContainerID != "container_id" {
154
+		t.Fatalf("expected ContainerID `container_id`, got %s", inspect.ContainerID)
155
+	}
156
+}
0 157
new file mode 100644
... ...
@@ -0,0 +1,20 @@
0
+package client
1
+
2
+import (
3
+	"io"
4
+	"net/url"
5
+
6
+	"golang.org/x/net/context"
7
+)
8
+
9
+// ContainerExport retrieves the raw contents of a container
10
+// and returns them as an io.ReadCloser. It's up to the caller
11
+// to close the stream.
12
+func (cli *Client) ContainerExport(ctx context.Context, containerID string) (io.ReadCloser, error) {
13
+	serverResp, err := cli.get(ctx, "/containers/"+containerID+"/export", url.Values{}, nil)
14
+	if err != nil {
15
+		return nil, err
16
+	}
17
+
18
+	return serverResp.body, nil
19
+}
0 20
new file mode 100644
... ...
@@ -0,0 +1,50 @@
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 TestContainerExportError(t *testing.T) {
14
+	client := &Client{
15
+		transport: newMockClient(nil, errorMock(http.StatusInternalServerError, "Server error")),
16
+	}
17
+	_, err := client.ContainerExport(context.Background(), "nothing")
18
+	if err == nil || err.Error() != "Error response from daemon: Server error" {
19
+		t.Fatalf("expected a Server Error, got %v", err)
20
+	}
21
+}
22
+
23
+func TestContainerExport(t *testing.T) {
24
+	expectedURL := "/containers/container_id/export"
25
+	client := &Client{
26
+		transport: newMockClient(nil, func(r *http.Request) (*http.Response, error) {
27
+			if !strings.HasPrefix(r.URL.Path, expectedURL) {
28
+				return nil, fmt.Errorf("Expected URL '%s', got '%s'", expectedURL, r.URL)
29
+			}
30
+
31
+			return &http.Response{
32
+				StatusCode: http.StatusOK,
33
+				Body:       ioutil.NopCloser(bytes.NewReader([]byte("response"))),
34
+			}, nil
35
+		}),
36
+	}
37
+	body, err := client.ContainerExport(context.Background(), "container_id")
38
+	if err != nil {
39
+		t.Fatal(err)
40
+	}
41
+	defer body.Close()
42
+	content, err := ioutil.ReadAll(body)
43
+	if err != nil {
44
+		t.Fatal(err)
45
+	}
46
+	if string(content) != "response" {
47
+		t.Fatalf("expected response to contain 'response', got %s", string(content))
48
+	}
49
+}
0 50
new file mode 100644
... ...
@@ -0,0 +1,54 @@
0
+package client
1
+
2
+import (
3
+	"bytes"
4
+	"encoding/json"
5
+	"io/ioutil"
6
+	"net/http"
7
+	"net/url"
8
+
9
+	"github.com/docker/docker/api/types"
10
+	"golang.org/x/net/context"
11
+)
12
+
13
+// ContainerInspect returns the container information.
14
+func (cli *Client) ContainerInspect(ctx context.Context, containerID string) (types.ContainerJSON, error) {
15
+	serverResp, err := cli.get(ctx, "/containers/"+containerID+"/json", nil, nil)
16
+	if err != nil {
17
+		if serverResp.statusCode == http.StatusNotFound {
18
+			return types.ContainerJSON{}, containerNotFoundError{containerID}
19
+		}
20
+		return types.ContainerJSON{}, err
21
+	}
22
+
23
+	var response types.ContainerJSON
24
+	err = json.NewDecoder(serverResp.body).Decode(&response)
25
+	ensureReaderClosed(serverResp)
26
+	return response, err
27
+}
28
+
29
+// ContainerInspectWithRaw returns the container information and its raw representation.
30
+func (cli *Client) ContainerInspectWithRaw(ctx context.Context, containerID string, getSize bool) (types.ContainerJSON, []byte, error) {
31
+	query := url.Values{}
32
+	if getSize {
33
+		query.Set("size", "1")
34
+	}
35
+	serverResp, err := cli.get(ctx, "/containers/"+containerID+"/json", query, nil)
36
+	if err != nil {
37
+		if serverResp.statusCode == http.StatusNotFound {
38
+			return types.ContainerJSON{}, nil, containerNotFoundError{containerID}
39
+		}
40
+		return types.ContainerJSON{}, nil, err
41
+	}
42
+	defer ensureReaderClosed(serverResp)
43
+
44
+	body, err := ioutil.ReadAll(serverResp.body)
45
+	if err != nil {
46
+		return types.ContainerJSON{}, nil, err
47
+	}
48
+
49
+	var response types.ContainerJSON
50
+	rdr := bytes.NewReader(body)
51
+	err = json.NewDecoder(rdr).Decode(&response)
52
+	return response, body, err
53
+}
0 54
new file mode 100644
... ...
@@ -0,0 +1,125 @@
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
+	"golang.org/x/net/context"
13
+)
14
+
15
+func TestContainerInspectError(t *testing.T) {
16
+	client := &Client{
17
+		transport: newMockClient(nil, errorMock(http.StatusInternalServerError, "Server error")),
18
+	}
19
+
20
+	_, err := client.ContainerInspect(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 TestContainerInspectContainerNotFound(t *testing.T) {
27
+	client := &Client{
28
+		transport: newMockClient(nil, errorMock(http.StatusNotFound, "Server error")),
29
+	}
30
+
31
+	_, err := client.ContainerInspect(context.Background(), "unknown")
32
+	if err == nil || !IsErrContainerNotFound(err) {
33
+		t.Fatalf("expected a containerNotFound error, got %v", err)
34
+	}
35
+}
36
+
37
+func TestContainerInspect(t *testing.T) {
38
+	expectedURL := "/containers/container_id/json"
39
+	client := &Client{
40
+		transport: newMockClient(nil, 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(types.ContainerJSON{
45
+				ContainerJSONBase: &types.ContainerJSONBase{
46
+					ID:    "container_id",
47
+					Image: "image",
48
+					Name:  "name",
49
+				},
50
+			})
51
+			if err != nil {
52
+				return nil, err
53
+			}
54
+			return &http.Response{
55
+				StatusCode: http.StatusOK,
56
+				Body:       ioutil.NopCloser(bytes.NewReader(content)),
57
+			}, nil
58
+		}),
59
+	}
60
+
61
+	r, err := client.ContainerInspect(context.Background(), "container_id")
62
+	if err != nil {
63
+		t.Fatal(err)
64
+	}
65
+	if r.ID != "container_id" {
66
+		t.Fatalf("expected `container_id`, got %s", r.ID)
67
+	}
68
+	if r.Image != "image" {
69
+		t.Fatalf("expected `image`, got %s", r.ID)
70
+	}
71
+	if r.Name != "name" {
72
+		t.Fatalf("expected `name`, got %s", r.ID)
73
+	}
74
+}
75
+
76
+func TestContainerInspectNode(t *testing.T) {
77
+	client := &Client{
78
+		transport: newMockClient(nil, func(req *http.Request) (*http.Response, error) {
79
+			content, err := json.Marshal(types.ContainerJSON{
80
+				ContainerJSONBase: &types.ContainerJSONBase{
81
+					ID:    "container_id",
82
+					Image: "image",
83
+					Name:  "name",
84
+					Node: &types.ContainerNode{
85
+						ID:     "container_node_id",
86
+						Addr:   "container_node",
87
+						Labels: map[string]string{"foo": "bar"},
88
+					},
89
+				},
90
+			})
91
+			if err != nil {
92
+				return nil, err
93
+			}
94
+			return &http.Response{
95
+				StatusCode: http.StatusOK,
96
+				Body:       ioutil.NopCloser(bytes.NewReader(content)),
97
+			}, nil
98
+		}),
99
+	}
100
+
101
+	r, err := client.ContainerInspect(context.Background(), "container_id")
102
+	if err != nil {
103
+		t.Fatal(err)
104
+	}
105
+	if r.ID != "container_id" {
106
+		t.Fatalf("expected `container_id`, got %s", r.ID)
107
+	}
108
+	if r.Image != "image" {
109
+		t.Fatalf("expected `image`, got %s", r.ID)
110
+	}
111
+	if r.Name != "name" {
112
+		t.Fatalf("expected `name`, got %s", r.ID)
113
+	}
114
+	if r.Node.ID != "container_node_id" {
115
+		t.Fatalf("expected `container_node_id`, got %s", r.Node.ID)
116
+	}
117
+	if r.Node.Addr != "container_node" {
118
+		t.Fatalf("expected `container_node`, got %s", r.Node.Addr)
119
+	}
120
+	foo, ok := r.Node.Labels["foo"]
121
+	if foo != "bar" || !ok {
122
+		t.Fatalf("expected `bar` for label `foo`")
123
+	}
124
+}
0 125
new file mode 100644
... ...
@@ -0,0 +1,17 @@
0
+package client
1
+
2
+import (
3
+	"net/url"
4
+
5
+	"golang.org/x/net/context"
6
+)
7
+
8
+// ContainerKill terminates the container process but does not remove the container from the docker host.
9
+func (cli *Client) ContainerKill(ctx context.Context, containerID, signal string) error {
10
+	query := url.Values{}
11
+	query.Set("signal", signal)
12
+
13
+	resp, err := cli.post(ctx, "/containers/"+containerID+"/kill", query, nil, nil)
14
+	ensureReaderClosed(resp)
15
+	return err
16
+}
0 17
new file mode 100644
... ...
@@ -0,0 +1,46 @@
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 TestContainerKillError(t *testing.T) {
14
+	client := &Client{
15
+		transport: newMockClient(nil, errorMock(http.StatusInternalServerError, "Server error")),
16
+	}
17
+	err := client.ContainerKill(context.Background(), "nothing", "SIGKILL")
18
+	if err == nil || err.Error() != "Error response from daemon: Server error" {
19
+		t.Fatalf("expected a Server Error, got %v", err)
20
+	}
21
+}
22
+
23
+func TestContainerKill(t *testing.T) {
24
+	expectedURL := "/containers/container_id/kill"
25
+	client := &Client{
26
+		transport: newMockClient(nil, func(req *http.Request) (*http.Response, error) {
27
+			if !strings.HasPrefix(req.URL.Path, expectedURL) {
28
+				return nil, fmt.Errorf("Expected URL '%s', got '%s'", expectedURL, req.URL)
29
+			}
30
+			signal := req.URL.Query().Get("signal")
31
+			if signal != "SIGKILL" {
32
+				return nil, fmt.Errorf("signal not set in URL query properly. Expected 'SIGKILL', got %s", signal)
33
+			}
34
+			return &http.Response{
35
+				StatusCode: http.StatusOK,
36
+				Body:       ioutil.NopCloser(bytes.NewReader([]byte(""))),
37
+			}, nil
38
+		}),
39
+	}
40
+
41
+	err := client.ContainerKill(context.Background(), "container_id", "SIGKILL")
42
+	if err != nil {
43
+		t.Fatal(err)
44
+	}
45
+}
0 46
new file mode 100644
... ...
@@ -0,0 +1,56 @@
0
+package client
1
+
2
+import (
3
+	"encoding/json"
4
+	"net/url"
5
+	"strconv"
6
+
7
+	"github.com/docker/docker/api/types"
8
+	"github.com/docker/docker/api/types/filters"
9
+	"golang.org/x/net/context"
10
+)
11
+
12
+// ContainerList returns the list of containers in the docker host.
13
+func (cli *Client) ContainerList(ctx context.Context, options types.ContainerListOptions) ([]types.Container, error) {
14
+	query := url.Values{}
15
+
16
+	if options.All {
17
+		query.Set("all", "1")
18
+	}
19
+
20
+	if options.Limit != -1 {
21
+		query.Set("limit", strconv.Itoa(options.Limit))
22
+	}
23
+
24
+	if options.Since != "" {
25
+		query.Set("since", options.Since)
26
+	}
27
+
28
+	if options.Before != "" {
29
+		query.Set("before", options.Before)
30
+	}
31
+
32
+	if options.Size {
33
+		query.Set("size", "1")
34
+	}
35
+
36
+	if options.Filter.Len() > 0 {
37
+		filterJSON, err := filters.ToParamWithVersion(cli.version, options.Filter)
38
+
39
+		if err != nil {
40
+			return nil, err
41
+		}
42
+
43
+		query.Set("filters", filterJSON)
44
+	}
45
+
46
+	resp, err := cli.get(ctx, "/containers/json", query, nil)
47
+	if err != nil {
48
+		return nil, err
49
+	}
50
+
51
+	var containers []types.Container
52
+	err = json.NewDecoder(resp.body).Decode(&containers)
53
+	ensureReaderClosed(resp)
54
+	return containers, err
55
+}
0 56
new file mode 100644
... ...
@@ -0,0 +1,96 @@
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
+	"golang.org/x/net/context"
14
+)
15
+
16
+func TestContainerListError(t *testing.T) {
17
+	client := &Client{
18
+		transport: newMockClient(nil, errorMock(http.StatusInternalServerError, "Server error")),
19
+	}
20
+	_, err := client.ContainerList(context.Background(), types.ContainerListOptions{})
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 TestContainerList(t *testing.T) {
27
+	expectedURL := "/containers/json"
28
+	expectedFilters := `{"before":{"container":true},"label":{"label1":true,"label2":true}}`
29
+	client := &Client{
30
+		transport: newMockClient(nil, 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
+			query := req.URL.Query()
35
+			all := query.Get("all")
36
+			if all != "1" {
37
+				return nil, fmt.Errorf("all not set in URL query properly. Expected '1', got %s", all)
38
+			}
39
+			limit := query.Get("limit")
40
+			if limit != "0" {
41
+				return nil, fmt.Errorf("limit should have not be present in query. Expected '0', got %s", limit)
42
+			}
43
+			since := query.Get("since")
44
+			if since != "container" {
45
+				return nil, fmt.Errorf("since not set in URL query properly. Expected 'container', got %s", since)
46
+			}
47
+			before := query.Get("before")
48
+			if before != "" {
49
+				return nil, fmt.Errorf("before should have not be present in query, go %s", before)
50
+			}
51
+			size := query.Get("size")
52
+			if size != "1" {
53
+				return nil, fmt.Errorf("size not set in URL query properly. Expected '1', got %s", size)
54
+			}
55
+			filters := query.Get("filters")
56
+			if filters != expectedFilters {
57
+				return nil, fmt.Errorf("expected filters incoherent '%v' with actual filters %v", expectedFilters, filters)
58
+			}
59
+
60
+			b, err := json.Marshal([]types.Container{
61
+				{
62
+					ID: "container_id1",
63
+				},
64
+				{
65
+					ID: "container_id2",
66
+				},
67
+			})
68
+			if err != nil {
69
+				return nil, err
70
+			}
71
+
72
+			return &http.Response{
73
+				StatusCode: http.StatusOK,
74
+				Body:       ioutil.NopCloser(bytes.NewReader(b)),
75
+			}, nil
76
+		}),
77
+	}
78
+
79
+	filters := filters.NewArgs()
80
+	filters.Add("label", "label1")
81
+	filters.Add("label", "label2")
82
+	filters.Add("before", "container")
83
+	containers, err := client.ContainerList(context.Background(), types.ContainerListOptions{
84
+		Size:   true,
85
+		All:    true,
86
+		Since:  "container",
87
+		Filter: filters,
88
+	})
89
+	if err != nil {
90
+		t.Fatal(err)
91
+	}
92
+	if len(containers) != 2 {
93
+		t.Fatalf("expected 2 containers, got %v", containers)
94
+	}
95
+}
0 96
new file mode 100644
... ...
@@ -0,0 +1,52 @@
0
+package client
1
+
2
+import (
3
+	"io"
4
+	"net/url"
5
+	"time"
6
+
7
+	"golang.org/x/net/context"
8
+
9
+	"github.com/docker/docker/api/types"
10
+	timetypes "github.com/docker/docker/api/types/time"
11
+)
12
+
13
+// ContainerLogs returns the logs generated by a container in an io.ReadCloser.
14
+// It's up to the caller to close the stream.
15
+func (cli *Client) ContainerLogs(ctx context.Context, container string, options types.ContainerLogsOptions) (io.ReadCloser, error) {
16
+	query := url.Values{}
17
+	if options.ShowStdout {
18
+		query.Set("stdout", "1")
19
+	}
20
+
21
+	if options.ShowStderr {
22
+		query.Set("stderr", "1")
23
+	}
24
+
25
+	if options.Since != "" {
26
+		ts, err := timetypes.GetTimestamp(options.Since, time.Now())
27
+		if err != nil {
28
+			return nil, err
29
+		}
30
+		query.Set("since", ts)
31
+	}
32
+
33
+	if options.Timestamps {
34
+		query.Set("timestamps", "1")
35
+	}
36
+
37
+	if options.Details {
38
+		query.Set("details", "1")
39
+	}
40
+
41
+	if options.Follow {
42
+		query.Set("follow", "1")
43
+	}
44
+	query.Set("tail", options.Tail)
45
+
46
+	resp, err := cli.get(ctx, "/containers/"+container+"/logs", query, nil)
47
+	if err != nil {
48
+		return nil, err
49
+	}
50
+	return resp.body, nil
51
+}
0 52
new file mode 100644
... ...
@@ -0,0 +1,133 @@
0
+package client
1
+
2
+import (
3
+	"bytes"
4
+	"fmt"
5
+	"io"
6
+	"io/ioutil"
7
+	"log"
8
+	"net/http"
9
+	"os"
10
+	"strings"
11
+	"testing"
12
+	"time"
13
+
14
+	"github.com/docker/docker/api/types"
15
+
16
+	"golang.org/x/net/context"
17
+)
18
+
19
+func TestContainerLogsError(t *testing.T) {
20
+	client := &Client{
21
+		transport: newMockClient(nil, errorMock(http.StatusInternalServerError, "Server error")),
22
+	}
23
+	_, err := client.ContainerLogs(context.Background(), "container_id", types.ContainerLogsOptions{})
24
+	if err == nil || err.Error() != "Error response from daemon: Server error" {
25
+		t.Fatalf("expected a Server Error, got %v", err)
26
+	}
27
+	_, err = client.ContainerLogs(context.Background(), "container_id", types.ContainerLogsOptions{
28
+		Since: "2006-01-02TZ",
29
+	})
30
+	if err == nil || !strings.Contains(err.Error(), `parsing time "2006-01-02TZ"`) {
31
+		t.Fatalf("expected a 'parsing time' error, got %v", err)
32
+	}
33
+}
34
+
35
+func TestContainerLogs(t *testing.T) {
36
+	expectedURL := "/containers/container_id/logs"
37
+	cases := []struct {
38
+		options             types.ContainerLogsOptions
39
+		expectedQueryParams map[string]string
40
+	}{
41
+		{
42
+			expectedQueryParams: map[string]string{
43
+				"tail": "",
44
+			},
45
+		},
46
+		{
47
+			options: types.ContainerLogsOptions{
48
+				Tail: "any",
49
+			},
50
+			expectedQueryParams: map[string]string{
51
+				"tail": "any",
52
+			},
53
+		},
54
+		{
55
+			options: types.ContainerLogsOptions{
56
+				ShowStdout: true,
57
+				ShowStderr: true,
58
+				Timestamps: true,
59
+				Details:    true,
60
+				Follow:     true,
61
+			},
62
+			expectedQueryParams: map[string]string{
63
+				"tail":       "",
64
+				"stdout":     "1",
65
+				"stderr":     "1",
66
+				"timestamps": "1",
67
+				"details":    "1",
68
+				"follow":     "1",
69
+			},
70
+		},
71
+		{
72
+			options: types.ContainerLogsOptions{
73
+				// An complete invalid date, timestamp or go duration will be
74
+				// passed as is
75
+				Since: "invalid but valid",
76
+			},
77
+			expectedQueryParams: map[string]string{
78
+				"tail":  "",
79
+				"since": "invalid but valid",
80
+			},
81
+		},
82
+	}
83
+	for _, logCase := range cases {
84
+		client := &Client{
85
+			transport: newMockClient(nil, func(r *http.Request) (*http.Response, error) {
86
+				if !strings.HasPrefix(r.URL.Path, expectedURL) {
87
+					return nil, fmt.Errorf("Expected URL '%s', got '%s'", expectedURL, r.URL)
88
+				}
89
+				// Check query parameters
90
+				query := r.URL.Query()
91
+				for key, expected := range logCase.expectedQueryParams {
92
+					actual := query.Get(key)
93
+					if actual != expected {
94
+						return nil, fmt.Errorf("%s not set in URL query properly. Expected '%s', got %s", key, expected, actual)
95
+					}
96
+				}
97
+				return &http.Response{
98
+					StatusCode: http.StatusOK,
99
+					Body:       ioutil.NopCloser(bytes.NewReader([]byte("response"))),
100
+				}, nil
101
+			}),
102
+		}
103
+		body, err := client.ContainerLogs(context.Background(), "container_id", logCase.options)
104
+		if err != nil {
105
+			t.Fatal(err)
106
+		}
107
+		defer body.Close()
108
+		content, err := ioutil.ReadAll(body)
109
+		if err != nil {
110
+			t.Fatal(err)
111
+		}
112
+		if string(content) != "response" {
113
+			t.Fatalf("expected response to contain 'response', got %s", string(content))
114
+		}
115
+	}
116
+}
117
+
118
+func ExampleClient_ContainerLogs_withTimeout() {
119
+	ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
120
+	defer cancel()
121
+
122
+	client, _ := NewEnvClient()
123
+	reader, err := client.ContainerLogs(ctx, "container_id", types.ContainerLogsOptions{})
124
+	if err != nil {
125
+		log.Fatal(err)
126
+	}
127
+
128
+	_, err = io.Copy(os.Stdout, reader)
129
+	if err != nil && err != io.EOF {
130
+		log.Fatal(err)
131
+	}
132
+}
0 133
new file mode 100644
... ...
@@ -0,0 +1,10 @@
0
+package client
1
+
2
+import "golang.org/x/net/context"
3
+
4
+// ContainerPause pauses the main process of a given container without terminating it.
5
+func (cli *Client) ContainerPause(ctx context.Context, containerID string) error {
6
+	resp, err := cli.post(ctx, "/containers/"+containerID+"/pause", nil, nil, nil)
7
+	ensureReaderClosed(resp)
8
+	return err
9
+}
0 10
new file mode 100644
... ...
@@ -0,0 +1,41 @@
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 TestContainerPauseError(t *testing.T) {
14
+	client := &Client{
15
+		transport: newMockClient(nil, errorMock(http.StatusInternalServerError, "Server error")),
16
+	}
17
+	err := client.ContainerPause(context.Background(), "nothing")
18
+	if err == nil || err.Error() != "Error response from daemon: Server error" {
19
+		t.Fatalf("expected a Server Error, got %v", err)
20
+	}
21
+}
22
+
23
+func TestContainerPause(t *testing.T) {
24
+	expectedURL := "/containers/container_id/pause"
25
+	client := &Client{
26
+		transport: newMockClient(nil, func(req *http.Request) (*http.Response, error) {
27
+			if !strings.HasPrefix(req.URL.Path, expectedURL) {
28
+				return nil, fmt.Errorf("Expected URL '%s', got '%s'", expectedURL, req.URL)
29
+			}
30
+			return &http.Response{
31
+				StatusCode: http.StatusOK,
32
+				Body:       ioutil.NopCloser(bytes.NewReader([]byte(""))),
33
+			}, nil
34
+		}),
35
+	}
36
+	err := client.ContainerPause(context.Background(), "container_id")
37
+	if err != nil {
38
+		t.Fatal(err)
39
+	}
40
+}
0 41
new file mode 100644
... ...
@@ -0,0 +1,27 @@
0
+package client
1
+
2
+import (
3
+	"net/url"
4
+
5
+	"github.com/docker/docker/api/types"
6
+	"golang.org/x/net/context"
7
+)
8
+
9
+// ContainerRemove kills and removes a container from the docker host.
10
+func (cli *Client) ContainerRemove(ctx context.Context, containerID string, options types.ContainerRemoveOptions) error {
11
+	query := url.Values{}
12
+	if options.RemoveVolumes {
13
+		query.Set("v", "1")
14
+	}
15
+	if options.RemoveLinks {
16
+		query.Set("link", "1")
17
+	}
18
+
19
+	if options.Force {
20
+		query.Set("force", "1")
21
+	}
22
+
23
+	resp, err := cli.delete(ctx, "/containers/"+containerID, query, nil)
24
+	ensureReaderClosed(resp)
25
+	return err
26
+}
0 27
new file mode 100644
... ...
@@ -0,0 +1,59 @@
0
+package client
1
+
2
+import (
3
+	"bytes"
4
+	"fmt"
5
+	"io/ioutil"
6
+	"net/http"
7
+	"strings"
8
+	"testing"
9
+
10
+	"github.com/docker/docker/api/types"
11
+	"golang.org/x/net/context"
12
+)
13
+
14
+func TestContainerRemoveError(t *testing.T) {
15
+	client := &Client{
16
+		transport: newMockClient(nil, errorMock(http.StatusInternalServerError, "Server error")),
17
+	}
18
+	err := client.ContainerRemove(context.Background(), "container_id", types.ContainerRemoveOptions{})
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 TestContainerRemove(t *testing.T) {
25
+	expectedURL := "/containers/container_id"
26
+	client := &Client{
27
+		transport: newMockClient(nil, func(req *http.Request) (*http.Response, error) {
28
+			if !strings.HasPrefix(req.URL.Path, expectedURL) {
29
+				return nil, fmt.Errorf("Expected URL '%s', got '%s'", expectedURL, req.URL)
30
+			}
31
+			query := req.URL.Query()
32
+			volume := query.Get("v")
33
+			if volume != "1" {
34
+				return nil, fmt.Errorf("v (volume) not set in URL query properly. Expected '1', got %s", volume)
35
+			}
36
+			force := query.Get("force")
37
+			if force != "1" {
38
+				return nil, fmt.Errorf("force not set in URL query properly. Expected '1', got %s", force)
39
+			}
40
+			link := query.Get("link")
41
+			if link != "" {
42
+				return nil, fmt.Errorf("link should have not be present in query, go %s", link)
43
+			}
44
+			return &http.Response{
45
+				StatusCode: http.StatusOK,
46
+				Body:       ioutil.NopCloser(bytes.NewReader([]byte(""))),
47
+			}, nil
48
+		}),
49
+	}
50
+
51
+	err := client.ContainerRemove(context.Background(), "container_id", types.ContainerRemoveOptions{
52
+		RemoveVolumes: true,
53
+		Force:         true,
54
+	})
55
+	if err != nil {
56
+		t.Fatal(err)
57
+	}
58
+}
0 59
new file mode 100644
... ...
@@ -0,0 +1,16 @@
0
+package client
1
+
2
+import (
3
+	"net/url"
4
+
5
+	"golang.org/x/net/context"
6
+)
7
+
8
+// ContainerRename changes the name of a given container.
9
+func (cli *Client) ContainerRename(ctx context.Context, containerID, newContainerName string) error {
10
+	query := url.Values{}
11
+	query.Set("name", newContainerName)
12
+	resp, err := cli.post(ctx, "/containers/"+containerID+"/rename", query, nil, nil)
13
+	ensureReaderClosed(resp)
14
+	return err
15
+}
0 16
new file mode 100644
... ...
@@ -0,0 +1,46 @@
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 TestContainerRenameError(t *testing.T) {
14
+	client := &Client{
15
+		transport: newMockClient(nil, errorMock(http.StatusInternalServerError, "Server error")),
16
+	}
17
+	err := client.ContainerRename(context.Background(), "nothing", "newNothing")
18
+	if err == nil || err.Error() != "Error response from daemon: Server error" {
19
+		t.Fatalf("expected a Server Error, got %v", err)
20
+	}
21
+}
22
+
23
+func TestContainerRename(t *testing.T) {
24
+	expectedURL := "/containers/container_id/rename"
25
+	client := &Client{
26
+		transport: newMockClient(nil, func(req *http.Request) (*http.Response, error) {
27
+			if !strings.HasPrefix(req.URL.Path, expectedURL) {
28
+				return nil, fmt.Errorf("Expected URL '%s', got '%s'", expectedURL, req.URL)
29
+			}
30
+			name := req.URL.Query().Get("name")
31
+			if name != "newName" {
32
+				return nil, fmt.Errorf("name not set in URL query properly. Expected 'newName', got %s", name)
33
+			}
34
+			return &http.Response{
35
+				StatusCode: http.StatusOK,
36
+				Body:       ioutil.NopCloser(bytes.NewReader([]byte(""))),
37
+			}, nil
38
+		}),
39
+	}
40
+
41
+	err := client.ContainerRename(context.Background(), "container_id", "newName")
42
+	if err != nil {
43
+		t.Fatal(err)
44
+	}
45
+}
0 46
new file mode 100644
... ...
@@ -0,0 +1,29 @@
0
+package client
1
+
2
+import (
3
+	"net/url"
4
+	"strconv"
5
+
6
+	"github.com/docker/docker/api/types"
7
+	"golang.org/x/net/context"
8
+)
9
+
10
+// ContainerResize changes the size of the tty for a container.
11
+func (cli *Client) ContainerResize(ctx context.Context, containerID string, options types.ResizeOptions) error {
12
+	return cli.resize(ctx, "/containers/"+containerID, options.Height, options.Width)
13
+}
14
+
15
+// ContainerExecResize changes the size of the tty for an exec process running inside a container.
16
+func (cli *Client) ContainerExecResize(ctx context.Context, execID string, options types.ResizeOptions) error {
17
+	return cli.resize(ctx, "/exec/"+execID, options.Height, options.Width)
18
+}
19
+
20
+func (cli *Client) resize(ctx context.Context, basePath string, height, width int) error {
21
+	query := url.Values{}
22
+	query.Set("h", strconv.Itoa(height))
23
+	query.Set("w", strconv.Itoa(width))
24
+
25
+	resp, err := cli.post(ctx, basePath+"/resize", query, nil, nil)
26
+	ensureReaderClosed(resp)
27
+	return err
28
+}
0 29
new file mode 100644
... ...
@@ -0,0 +1,82 @@
0
+package client
1
+
2
+import (
3
+	"bytes"
4
+	"fmt"
5
+	"io/ioutil"
6
+	"net/http"
7
+	"strings"
8
+	"testing"
9
+
10
+	"github.com/docker/docker/api/types"
11
+	"golang.org/x/net/context"
12
+)
13
+
14
+func TestContainerResizeError(t *testing.T) {
15
+	client := &Client{
16
+		transport: newMockClient(nil, errorMock(http.StatusInternalServerError, "Server error")),
17
+	}
18
+	err := client.ContainerResize(context.Background(), "container_id", types.ResizeOptions{})
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 TestContainerExecResizeError(t *testing.T) {
25
+	client := &Client{
26
+		transport: newMockClient(nil, errorMock(http.StatusInternalServerError, "Server error")),
27
+	}
28
+	err := client.ContainerExecResize(context.Background(), "exec_id", types.ResizeOptions{})
29
+	if err == nil || err.Error() != "Error response from daemon: Server error" {
30
+		t.Fatalf("expected a Server Error, got %v", err)
31
+	}
32
+}
33
+
34
+func TestContainerResize(t *testing.T) {
35
+	client := &Client{
36
+		transport: newMockClient(nil, resizeTransport("/containers/container_id/resize")),
37
+	}
38
+
39
+	err := client.ContainerResize(context.Background(), "container_id", types.ResizeOptions{
40
+		Height: 500,
41
+		Width:  600,
42
+	})
43
+	if err != nil {
44
+		t.Fatal(err)
45
+	}
46
+}
47
+
48
+func TestContainerExecResize(t *testing.T) {
49
+	client := &Client{
50
+		transport: newMockClient(nil, resizeTransport("/exec/exec_id/resize")),
51
+	}
52
+
53
+	err := client.ContainerExecResize(context.Background(), "exec_id", types.ResizeOptions{
54
+		Height: 500,
55
+		Width:  600,
56
+	})
57
+	if err != nil {
58
+		t.Fatal(err)
59
+	}
60
+}
61
+
62
+func resizeTransport(expectedURL string) func(req *http.Request) (*http.Response, error) {
63
+	return func(req *http.Request) (*http.Response, error) {
64
+		if !strings.HasPrefix(req.URL.Path, expectedURL) {
65
+			return nil, fmt.Errorf("Expected URL '%s', got '%s'", expectedURL, req.URL)
66
+		}
67
+		query := req.URL.Query()
68
+		h := query.Get("h")
69
+		if h != "500" {
70
+			return nil, fmt.Errorf("h not set in URL query properly. Expected '500', got %s", h)
71
+		}
72
+		w := query.Get("w")
73
+		if w != "600" {
74
+			return nil, fmt.Errorf("w not set in URL query properly. Expected '600', got %s", w)
75
+		}
76
+		return &http.Response{
77
+			StatusCode: http.StatusOK,
78
+			Body:       ioutil.NopCloser(bytes.NewReader([]byte(""))),
79
+		}, nil
80
+	}
81
+}
0 82
new file mode 100644
... ...
@@ -0,0 +1,22 @@
0
+package client
1
+
2
+import (
3
+	"net/url"
4
+	"time"
5
+
6
+	timetypes "github.com/docker/docker/api/types/time"
7
+	"golang.org/x/net/context"
8
+)
9
+
10
+// ContainerRestart stops and starts a container again.
11
+// It makes the daemon to wait for the container to be up again for
12
+// a specific amount of time, given the timeout.
13
+func (cli *Client) ContainerRestart(ctx context.Context, containerID string, timeout *time.Duration) error {
14
+	query := url.Values{}
15
+	if timeout != nil {
16
+		query.Set("t", timetypes.DurationToSecondsString(*timeout))
17
+	}
18
+	resp, err := cli.post(ctx, "/containers/"+containerID+"/restart", query, nil, nil)
19
+	ensureReaderClosed(resp)
20
+	return err
21
+}
0 22
new file mode 100644
... ...
@@ -0,0 +1,48 @@
0
+package client
1
+
2
+import (
3
+	"bytes"
4
+	"fmt"
5
+	"io/ioutil"
6
+	"net/http"
7
+	"strings"
8
+	"testing"
9
+	"time"
10
+
11
+	"golang.org/x/net/context"
12
+)
13
+
14
+func TestContainerRestartError(t *testing.T) {
15
+	client := &Client{
16
+		transport: newMockClient(nil, errorMock(http.StatusInternalServerError, "Server error")),
17
+	}
18
+	timeout := 0 * time.Second
19
+	err := client.ContainerRestart(context.Background(), "nothing", &timeout)
20
+	if err == nil || err.Error() != "Error response from daemon: Server error" {
21
+		t.Fatalf("expected a Server Error, got %v", err)
22
+	}
23
+}
24
+
25
+func TestContainerRestart(t *testing.T) {
26
+	expectedURL := "/containers/container_id/restart"
27
+	client := &Client{
28
+		transport: newMockClient(nil, 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
+			t := req.URL.Query().Get("t")
33
+			if t != "100" {
34
+				return nil, fmt.Errorf("t (timeout) not set in URL query properly. Expected '100', got %s", t)
35
+			}
36
+			return &http.Response{
37
+				StatusCode: http.StatusOK,
38
+				Body:       ioutil.NopCloser(bytes.NewReader([]byte(""))),
39
+			}, nil
40
+		}),
41
+	}
42
+	timeout := 100 * time.Second
43
+	err := client.ContainerRestart(context.Background(), "container_id", &timeout)
44
+	if err != nil {
45
+		t.Fatal(err)
46
+	}
47
+}
0 48
new file mode 100644
... ...
@@ -0,0 +1,21 @@
0
+package client
1
+
2
+import (
3
+	"net/url"
4
+
5
+	"golang.org/x/net/context"
6
+
7
+	"github.com/docker/docker/api/types"
8
+)
9
+
10
+// ContainerStart sends a request to the docker daemon to start a container.
11
+func (cli *Client) ContainerStart(ctx context.Context, containerID string, options types.ContainerStartOptions) error {
12
+	query := url.Values{}
13
+	if len(options.CheckpointID) != 0 {
14
+		query.Set("checkpoint", options.CheckpointID)
15
+	}
16
+
17
+	resp, err := cli.post(ctx, "/containers/"+containerID+"/start", query, nil, nil)
18
+	ensureReaderClosed(resp)
19
+	return err
20
+}
0 21
new file mode 100644
... ...
@@ -0,0 +1,58 @@
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
+	"golang.org/x/net/context"
12
+
13
+	"github.com/docker/docker/api/types"
14
+)
15
+
16
+func TestContainerStartError(t *testing.T) {
17
+	client := &Client{
18
+		transport: newMockClient(nil, errorMock(http.StatusInternalServerError, "Server error")),
19
+	}
20
+	err := client.ContainerStart(context.Background(), "nothing", types.ContainerStartOptions{})
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 TestContainerStart(t *testing.T) {
27
+	expectedURL := "/containers/container_id/start"
28
+	client := &Client{
29
+		transport: newMockClient(nil, 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
+			// we're not expecting any payload, but if one is supplied, check it is valid.
34
+			if req.Header.Get("Content-Type") == "application/json" {
35
+				var startConfig interface{}
36
+				if err := json.NewDecoder(req.Body).Decode(&startConfig); err != nil {
37
+					return nil, fmt.Errorf("Unable to parse json: %s", err)
38
+				}
39
+			}
40
+
41
+			checkpoint := req.URL.Query().Get("checkpoint")
42
+			if checkpoint != "checkpoint_id" {
43
+				return nil, fmt.Errorf("checkpoint not set in URL query properly. Expected 'checkpoint_id', got %s", checkpoint)
44
+			}
45
+
46
+			return &http.Response{
47
+				StatusCode: http.StatusOK,
48
+				Body:       ioutil.NopCloser(bytes.NewReader([]byte(""))),
49
+			}, nil
50
+		}),
51
+	}
52
+
53
+	err := client.ContainerStart(context.Background(), "container_id", types.ContainerStartOptions{CheckpointID: "checkpoint_id"})
54
+	if err != nil {
55
+		t.Fatal(err)
56
+	}
57
+}
0 58
new file mode 100644
... ...
@@ -0,0 +1,24 @@
0
+package client
1
+
2
+import (
3
+	"io"
4
+	"net/url"
5
+
6
+	"golang.org/x/net/context"
7
+)
8
+
9
+// ContainerStats returns near realtime stats for a given container.
10
+// It's up to the caller to close the io.ReadCloser returned.
11
+func (cli *Client) ContainerStats(ctx context.Context, containerID string, stream bool) (io.ReadCloser, error) {
12
+	query := url.Values{}
13
+	query.Set("stream", "0")
14
+	if stream {
15
+		query.Set("stream", "1")
16
+	}
17
+
18
+	resp, err := cli.get(ctx, "/containers/"+containerID+"/stats", query, nil)
19
+	if err != nil {
20
+		return nil, err
21
+	}
22
+	return resp.body, err
23
+}
0 24
new file mode 100644
... ...
@@ -0,0 +1,70 @@
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 TestContainerStatsError(t *testing.T) {
14
+	client := &Client{
15
+		transport: newMockClient(nil, errorMock(http.StatusInternalServerError, "Server error")),
16
+	}
17
+	_, err := client.ContainerStats(context.Background(), "nothing", false)
18
+	if err == nil || err.Error() != "Error response from daemon: Server error" {
19
+		t.Fatalf("expected a Server Error, got %v", err)
20
+	}
21
+}
22
+
23
+func TestContainerStats(t *testing.T) {
24
+	expectedURL := "/containers/container_id/stats"
25
+	cases := []struct {
26
+		stream         bool
27
+		expectedStream string
28
+	}{
29
+		{
30
+			expectedStream: "0",
31
+		},
32
+		{
33
+			stream:         true,
34
+			expectedStream: "1",
35
+		},
36
+	}
37
+	for _, c := range cases {
38
+		client := &Client{
39
+			transport: newMockClient(nil, func(r *http.Request) (*http.Response, error) {
40
+				if !strings.HasPrefix(r.URL.Path, expectedURL) {
41
+					return nil, fmt.Errorf("Expected URL '%s', got '%s'", expectedURL, r.URL)
42
+				}
43
+
44
+				query := r.URL.Query()
45
+				stream := query.Get("stream")
46
+				if stream != c.expectedStream {
47
+					return nil, fmt.Errorf("stream not set in URL query properly. Expected '%s', got %s", c.expectedStream, stream)
48
+				}
49
+
50
+				return &http.Response{
51
+					StatusCode: http.StatusOK,
52
+					Body:       ioutil.NopCloser(bytes.NewReader([]byte("response"))),
53
+				}, nil
54
+			}),
55
+		}
56
+		body, err := client.ContainerStats(context.Background(), "container_id", c.stream)
57
+		if err != nil {
58
+			t.Fatal(err)
59
+		}
60
+		defer body.Close()
61
+		content, err := ioutil.ReadAll(body)
62
+		if err != nil {
63
+			t.Fatal(err)
64
+		}
65
+		if string(content) != "response" {
66
+			t.Fatalf("expected response to contain 'response', got %s", string(content))
67
+		}
68
+	}
69
+}
0 70
new file mode 100644
... ...
@@ -0,0 +1,21 @@
0
+package client
1
+
2
+import (
3
+	"net/url"
4
+	"time"
5
+
6
+	timetypes "github.com/docker/docker/api/types/time"
7
+	"golang.org/x/net/context"
8
+)
9
+
10
+// ContainerStop stops a container without terminating the process.
11
+// The process is blocked until the container stops or the timeout expires.
12
+func (cli *Client) ContainerStop(ctx context.Context, containerID string, timeout *time.Duration) error {
13
+	query := url.Values{}
14
+	if timeout != nil {
15
+		query.Set("t", timetypes.DurationToSecondsString(*timeout))
16
+	}
17
+	resp, err := cli.post(ctx, "/containers/"+containerID+"/stop", query, nil, nil)
18
+	ensureReaderClosed(resp)
19
+	return err
20
+}
0 21
new file mode 100644
... ...
@@ -0,0 +1,48 @@
0
+package client
1
+
2
+import (
3
+	"bytes"
4
+	"fmt"
5
+	"io/ioutil"
6
+	"net/http"
7
+	"strings"
8
+	"testing"
9
+	"time"
10
+
11
+	"golang.org/x/net/context"
12
+)
13
+
14
+func TestContainerStopError(t *testing.T) {
15
+	client := &Client{
16
+		transport: newMockClient(nil, errorMock(http.StatusInternalServerError, "Server error")),
17
+	}
18
+	timeout := 0 * time.Second
19
+	err := client.ContainerStop(context.Background(), "nothing", &timeout)
20
+	if err == nil || err.Error() != "Error response from daemon: Server error" {
21
+		t.Fatalf("expected a Server Error, got %v", err)
22
+	}
23
+}
24
+
25
+func TestContainerStop(t *testing.T) {
26
+	expectedURL := "/containers/container_id/stop"
27
+	client := &Client{
28
+		transport: newMockClient(nil, 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
+			t := req.URL.Query().Get("t")
33
+			if t != "100" {
34
+				return nil, fmt.Errorf("t (timeout) not set in URL query properly. Expected '100', got %s", t)
35
+			}
36
+			return &http.Response{
37
+				StatusCode: http.StatusOK,
38
+				Body:       ioutil.NopCloser(bytes.NewReader([]byte(""))),
39
+			}, nil
40
+		}),
41
+	}
42
+	timeout := 100 * time.Second
43
+	err := client.ContainerStop(context.Background(), "container_id", &timeout)
44
+	if err != nil {
45
+		t.Fatal(err)
46
+	}
47
+}
0 48
new file mode 100644
... ...
@@ -0,0 +1,28 @@
0
+package client
1
+
2
+import (
3
+	"encoding/json"
4
+	"net/url"
5
+	"strings"
6
+
7
+	"github.com/docker/docker/api/types"
8
+	"golang.org/x/net/context"
9
+)
10
+
11
+// ContainerTop shows process information from within a container.
12
+func (cli *Client) ContainerTop(ctx context.Context, containerID string, arguments []string) (types.ContainerProcessList, error) {
13
+	var response types.ContainerProcessList
14
+	query := url.Values{}
15
+	if len(arguments) > 0 {
16
+		query.Set("ps_args", strings.Join(arguments, " "))
17
+	}
18
+
19
+	resp, err := cli.get(ctx, "/containers/"+containerID+"/top", query, nil)
20
+	if err != nil {
21
+		return response, err
22
+	}
23
+
24
+	err = json.NewDecoder(resp.body).Decode(&response)
25
+	ensureReaderClosed(resp)
26
+	return response, err
27
+}
0 28
new file mode 100644
... ...
@@ -0,0 +1,74 @@
0
+package client
1
+
2
+import (
3
+	"bytes"
4
+	"encoding/json"
5
+	"fmt"
6
+	"io/ioutil"
7
+	"net/http"
8
+	"reflect"
9
+	"strings"
10
+	"testing"
11
+
12
+	"github.com/docker/docker/api/types"
13
+	"golang.org/x/net/context"
14
+)
15
+
16
+func TestContainerTopError(t *testing.T) {
17
+	client := &Client{
18
+		transport: newMockClient(nil, errorMock(http.StatusInternalServerError, "Server error")),
19
+	}
20
+	_, err := client.ContainerTop(context.Background(), "nothing", []string{})
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 TestContainerTop(t *testing.T) {
27
+	expectedURL := "/containers/container_id/top"
28
+	expectedProcesses := [][]string{
29
+		{"p1", "p2"},
30
+		{"p3"},
31
+	}
32
+	expectedTitles := []string{"title1", "title2"}
33
+
34
+	client := &Client{
35
+		transport: newMockClient(nil, func(req *http.Request) (*http.Response, error) {
36
+			if !strings.HasPrefix(req.URL.Path, expectedURL) {
37
+				return nil, fmt.Errorf("Expected URL '%s', got '%s'", expectedURL, req.URL)
38
+			}
39
+			query := req.URL.Query()
40
+			args := query.Get("ps_args")
41
+			if args != "arg1 arg2" {
42
+				return nil, fmt.Errorf("args not set in URL query properly. Expected 'arg1 arg2', got %v", args)
43
+			}
44
+
45
+			b, err := json.Marshal(types.ContainerProcessList{
46
+				Processes: [][]string{
47
+					{"p1", "p2"},
48
+					{"p3"},
49
+				},
50
+				Titles: []string{"title1", "title2"},
51
+			})
52
+			if err != nil {
53
+				return nil, err
54
+			}
55
+
56
+			return &http.Response{
57
+				StatusCode: http.StatusOK,
58
+				Body:       ioutil.NopCloser(bytes.NewReader(b)),
59
+			}, nil
60
+		}),
61
+	}
62
+
63
+	processList, err := client.ContainerTop(context.Background(), "container_id", []string{"arg1", "arg2"})
64
+	if err != nil {
65
+		t.Fatal(err)
66
+	}
67
+	if !reflect.DeepEqual(expectedProcesses, processList.Processes) {
68
+		t.Fatalf("Processes: expected %v, got %v", expectedProcesses, processList.Processes)
69
+	}
70
+	if !reflect.DeepEqual(expectedTitles, processList.Titles) {
71
+		t.Fatalf("Titles: expected %v, got %v", expectedTitles, processList.Titles)
72
+	}
73
+}
0 74
new file mode 100644
... ...
@@ -0,0 +1,10 @@
0
+package client
1
+
2
+import "golang.org/x/net/context"
3
+
4
+// ContainerUnpause resumes the process execution within a container
5
+func (cli *Client) ContainerUnpause(ctx context.Context, containerID string) error {
6
+	resp, err := cli.post(ctx, "/containers/"+containerID+"/unpause", nil, nil, nil)
7
+	ensureReaderClosed(resp)
8
+	return err
9
+}
0 10
new file mode 100644
... ...
@@ -0,0 +1,41 @@
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 TestContainerUnpauseError(t *testing.T) {
14
+	client := &Client{
15
+		transport: newMockClient(nil, errorMock(http.StatusInternalServerError, "Server error")),
16
+	}
17
+	err := client.ContainerUnpause(context.Background(), "nothing")
18
+	if err == nil || err.Error() != "Error response from daemon: Server error" {
19
+		t.Fatalf("expected a Server Error, got %v", err)
20
+	}
21
+}
22
+
23
+func TestContainerUnpause(t *testing.T) {
24
+	expectedURL := "/containers/container_id/unpause"
25
+	client := &Client{
26
+		transport: newMockClient(nil, func(req *http.Request) (*http.Response, error) {
27
+			if !strings.HasPrefix(req.URL.Path, expectedURL) {
28
+				return nil, fmt.Errorf("Expected URL '%s', got '%s'", expectedURL, req.URL)
29
+			}
30
+			return &http.Response{
31
+				StatusCode: http.StatusOK,
32
+				Body:       ioutil.NopCloser(bytes.NewReader([]byte(""))),
33
+			}, nil
34
+		}),
35
+	}
36
+	err := client.ContainerUnpause(context.Background(), "container_id")
37
+	if err != nil {
38
+		t.Fatal(err)
39
+	}
40
+}
0 41
new file mode 100644
... ...
@@ -0,0 +1,23 @@
0
+package client
1
+
2
+import (
3
+	"encoding/json"
4
+
5
+	"github.com/docker/docker/api/types"
6
+	"github.com/docker/docker/api/types/container"
7
+	"golang.org/x/net/context"
8
+)
9
+
10
+// ContainerUpdate updates resources of a container
11
+func (cli *Client) ContainerUpdate(ctx context.Context, containerID string, updateConfig container.UpdateConfig) (types.ContainerUpdateResponse, error) {
12
+	var response types.ContainerUpdateResponse
13
+	serverResp, err := cli.post(ctx, "/containers/"+containerID+"/update", nil, updateConfig, nil)
14
+	if err != nil {
15
+		return response, err
16
+	}
17
+
18
+	err = json.NewDecoder(serverResp.body).Decode(&response)
19
+
20
+	ensureReaderClosed(serverResp)
21
+	return response, err
22
+}
0 23
new file mode 100644
... ...
@@ -0,0 +1,59 @@
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/container"
13
+	"golang.org/x/net/context"
14
+)
15
+
16
+func TestContainerUpdateError(t *testing.T) {
17
+	client := &Client{
18
+		transport: newMockClient(nil, errorMock(http.StatusInternalServerError, "Server error")),
19
+	}
20
+	_, err := client.ContainerUpdate(context.Background(), "nothing", container.UpdateConfig{})
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 TestContainerUpdate(t *testing.T) {
27
+	expectedURL := "/containers/container_id/update"
28
+
29
+	client := &Client{
30
+		transport: newMockClient(nil, 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
+
35
+			b, err := json.Marshal(types.ContainerUpdateResponse{})
36
+			if err != nil {
37
+				return nil, err
38
+			}
39
+
40
+			return &http.Response{
41
+				StatusCode: http.StatusOK,
42
+				Body:       ioutil.NopCloser(bytes.NewReader(b)),
43
+			}, nil
44
+		}),
45
+	}
46
+
47
+	_, err := client.ContainerUpdate(context.Background(), "container_id", container.UpdateConfig{
48
+		Resources: container.Resources{
49
+			CPUPeriod: 1,
50
+		},
51
+		RestartPolicy: container.RestartPolicy{
52
+			Name: "always",
53
+		},
54
+	})
55
+	if err != nil {
56
+		t.Fatal(err)
57
+	}
58
+}
0 59
new file mode 100644
... ...
@@ -0,0 +1,26 @@
0
+package client
1
+
2
+import (
3
+	"encoding/json"
4
+
5
+	"golang.org/x/net/context"
6
+
7
+	"github.com/docker/docker/api/types"
8
+)
9
+
10
+// ContainerWait pauses execution until a container exits.
11
+// It returns the API status code as response of its readiness.
12
+func (cli *Client) ContainerWait(ctx context.Context, containerID string) (int, error) {
13
+	resp, err := cli.post(ctx, "/containers/"+containerID+"/wait", nil, nil, nil)
14
+	if err != nil {
15
+		return -1, err
16
+	}
17
+	defer ensureReaderClosed(resp)
18
+
19
+	var res types.ContainerWaitResponse
20
+	if err := json.NewDecoder(resp.body).Decode(&res); err != nil {
21
+		return -1, err
22
+	}
23
+
24
+	return res.StatusCode, nil
25
+}
0 26
new file mode 100644
... ...
@@ -0,0 +1,70 @@
0
+package client
1
+
2
+import (
3
+	"bytes"
4
+	"encoding/json"
5
+	"fmt"
6
+	"io/ioutil"
7
+	"log"
8
+	"net/http"
9
+	"strings"
10
+	"testing"
11
+	"time"
12
+
13
+	"github.com/docker/docker/api/types"
14
+
15
+	"golang.org/x/net/context"
16
+)
17
+
18
+func TestContainerWaitError(t *testing.T) {
19
+	client := &Client{
20
+		transport: newMockClient(nil, errorMock(http.StatusInternalServerError, "Server error")),
21
+	}
22
+	code, err := client.ContainerWait(context.Background(), "nothing")
23
+	if err == nil || err.Error() != "Error response from daemon: Server error" {
24
+		t.Fatalf("expected a Server Error, got %v", err)
25
+	}
26
+	if code != -1 {
27
+		t.Fatalf("expected a status code equal to '-1', got %d", code)
28
+	}
29
+}
30
+
31
+func TestContainerWait(t *testing.T) {
32
+	expectedURL := "/containers/container_id/wait"
33
+	client := &Client{
34
+		transport: newMockClient(nil, func(req *http.Request) (*http.Response, error) {
35
+			if !strings.HasPrefix(req.URL.Path, expectedURL) {
36
+				return nil, fmt.Errorf("Expected URL '%s', got '%s'", expectedURL, req.URL)
37
+			}
38
+			b, err := json.Marshal(types.ContainerWaitResponse{
39
+				StatusCode: 15,
40
+			})
41
+			if err != nil {
42
+				return nil, err
43
+			}
44
+			return &http.Response{
45
+				StatusCode: http.StatusOK,
46
+				Body:       ioutil.NopCloser(bytes.NewReader(b)),
47
+			}, nil
48
+		}),
49
+	}
50
+
51
+	code, err := client.ContainerWait(context.Background(), "container_id")
52
+	if err != nil {
53
+		t.Fatal(err)
54
+	}
55
+	if code != 15 {
56
+		t.Fatalf("expected a status code equal to '15', got %d", code)
57
+	}
58
+}
59
+
60
+func ExampleClient_ContainerWait_withTimeout() {
61
+	ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
62
+	defer cancel()
63
+
64
+	client, _ := NewEnvClient()
65
+	_, err := client.ContainerWait(ctx, "container_id")
66
+	if err != nil {
67
+		log.Fatal(err)
68
+	}
69
+}
0 70
new file mode 100644
... ...
@@ -0,0 +1,208 @@
0
+package client
1
+
2
+import (
3
+	"errors"
4
+	"fmt"
5
+)
6
+
7
+// ErrConnectionFailed is an error raised when the connection between the client and the server failed.
8
+var ErrConnectionFailed = errors.New("Cannot connect to the Docker daemon. Is the docker daemon running on this host?")
9
+
10
+// ErrorConnectionFailed returns an error with host in the error message when connection to docker daemon failed.
11
+func ErrorConnectionFailed(host string) error {
12
+	return fmt.Errorf("Cannot connect to the Docker daemon at %s. Is the docker daemon running?", host)
13
+}
14
+
15
+type notFound interface {
16
+	error
17
+	NotFound() bool // Is the error a NotFound error
18
+}
19
+
20
+// IsErrNotFound returns true if the error is caused with an
21
+// object (image, container, network, volume, …) is not found in the docker host.
22
+func IsErrNotFound(err error) bool {
23
+	te, ok := err.(notFound)
24
+	return ok && te.NotFound()
25
+}
26
+
27
+// imageNotFoundError implements an error returned when an image is not in the docker host.
28
+type imageNotFoundError struct {
29
+	imageID string
30
+}
31
+
32
+// NoFound indicates that this error type is of NotFound
33
+func (e imageNotFoundError) NotFound() bool {
34
+	return true
35
+}
36
+
37
+// Error returns a string representation of an imageNotFoundError
38
+func (e imageNotFoundError) Error() string {
39
+	return fmt.Sprintf("Error: No such image: %s", e.imageID)
40
+}
41
+
42
+// IsErrImageNotFound returns true if the error is caused
43
+// when an image is not found in the docker host.
44
+func IsErrImageNotFound(err error) bool {
45
+	return IsErrNotFound(err)
46
+}
47
+
48
+// containerNotFoundError implements an error returned when a container is not in the docker host.
49
+type containerNotFoundError struct {
50
+	containerID string
51
+}
52
+
53
+// NoFound indicates that this error type is of NotFound
54
+func (e containerNotFoundError) NotFound() bool {
55
+	return true
56
+}
57
+
58
+// Error returns a string representation of a containerNotFoundError
59
+func (e containerNotFoundError) Error() string {
60
+	return fmt.Sprintf("Error: No such container: %s", e.containerID)
61
+}
62
+
63
+// IsErrContainerNotFound returns true if the error is caused
64
+// when a container is not found in the docker host.
65
+func IsErrContainerNotFound(err error) bool {
66
+	return IsErrNotFound(err)
67
+}
68
+
69
+// networkNotFoundError implements an error returned when a network is not in the docker host.
70
+type networkNotFoundError struct {
71
+	networkID string
72
+}
73
+
74
+// NoFound indicates that this error type is of NotFound
75
+func (e networkNotFoundError) NotFound() bool {
76
+	return true
77
+}
78
+
79
+// Error returns a string representation of a networkNotFoundError
80
+func (e networkNotFoundError) Error() string {
81
+	return fmt.Sprintf("Error: No such network: %s", e.networkID)
82
+}
83
+
84
+// IsErrNetworkNotFound returns true if the error is caused
85
+// when a network is not found in the docker host.
86
+func IsErrNetworkNotFound(err error) bool {
87
+	return IsErrNotFound(err)
88
+}
89
+
90
+// volumeNotFoundError implements an error returned when a volume is not in the docker host.
91
+type volumeNotFoundError struct {
92
+	volumeID string
93
+}
94
+
95
+// NoFound indicates that this error type is of NotFound
96
+func (e volumeNotFoundError) NotFound() bool {
97
+	return true
98
+}
99
+
100
+// Error returns a string representation of a networkNotFoundError
101
+func (e volumeNotFoundError) Error() string {
102
+	return fmt.Sprintf("Error: No such volume: %s", e.volumeID)
103
+}
104
+
105
+// IsErrVolumeNotFound returns true if the error is caused
106
+// when a volume is not found in the docker host.
107
+func IsErrVolumeNotFound(err error) bool {
108
+	return IsErrNotFound(err)
109
+}
110
+
111
+// unauthorizedError represents an authorization error in a remote registry.
112
+type unauthorizedError struct {
113
+	cause error
114
+}
115
+
116
+// Error returns a string representation of an unauthorizedError
117
+func (u unauthorizedError) Error() string {
118
+	return u.cause.Error()
119
+}
120
+
121
+// IsErrUnauthorized returns true if the error is caused
122
+// when a remote registry authentication fails
123
+func IsErrUnauthorized(err error) bool {
124
+	_, ok := err.(unauthorizedError)
125
+	return ok
126
+}
127
+
128
+// nodeNotFoundError implements an error returned when a node is not found.
129
+type nodeNotFoundError struct {
130
+	nodeID string
131
+}
132
+
133
+// Error returns a string representation of a nodeNotFoundError
134
+func (e nodeNotFoundError) Error() string {
135
+	return fmt.Sprintf("Error: No such node: %s", e.nodeID)
136
+}
137
+
138
+// NoFound indicates that this error type is of NotFound
139
+func (e nodeNotFoundError) NotFound() bool {
140
+	return true
141
+}
142
+
143
+// IsErrNodeNotFound returns true if the error is caused
144
+// when a node is not found.
145
+func IsErrNodeNotFound(err error) bool {
146
+	_, ok := err.(nodeNotFoundError)
147
+	return ok
148
+}
149
+
150
+// serviceNotFoundError implements an error returned when a service is not found.
151
+type serviceNotFoundError struct {
152
+	serviceID string
153
+}
154
+
155
+// Error returns a string representation of a serviceNotFoundError
156
+func (e serviceNotFoundError) Error() string {
157
+	return fmt.Sprintf("Error: No such service: %s", e.serviceID)
158
+}
159
+
160
+// NoFound indicates that this error type is of NotFound
161
+func (e serviceNotFoundError) NotFound() bool {
162
+	return true
163
+}
164
+
165
+// IsErrServiceNotFound returns true if the error is caused
166
+// when a service is not found.
167
+func IsErrServiceNotFound(err error) bool {
168
+	_, ok := err.(serviceNotFoundError)
169
+	return ok
170
+}
171
+
172
+// taskNotFoundError implements an error returned when a task is not found.
173
+type taskNotFoundError struct {
174
+	taskID string
175
+}
176
+
177
+// Error returns a string representation of a taskNotFoundError
178
+func (e taskNotFoundError) Error() string {
179
+	return fmt.Sprintf("Error: No such task: %s", e.taskID)
180
+}
181
+
182
+// NoFound indicates that this error type is of NotFound
183
+func (e taskNotFoundError) NotFound() bool {
184
+	return true
185
+}
186
+
187
+// IsErrTaskNotFound returns true if the error is caused
188
+// when a task is not found.
189
+func IsErrTaskNotFound(err error) bool {
190
+	_, ok := err.(taskNotFoundError)
191
+	return ok
192
+}
193
+
194
+type pluginPermissionDenied struct {
195
+	name string
196
+}
197
+
198
+func (e pluginPermissionDenied) Error() string {
199
+	return "Permission denied while installing plugin " + e.name
200
+}
201
+
202
+// IsErrPluginPermissionDenied returns true if the error is caused
203
+// when a user denies a plugin's permissions
204
+func IsErrPluginPermissionDenied(err error) bool {
205
+	_, ok := err.(pluginPermissionDenied)
206
+	return ok
207
+}
0 208
new file mode 100644
... ...
@@ -0,0 +1,48 @@
0
+package client
1
+
2
+import (
3
+	"io"
4
+	"net/url"
5
+	"time"
6
+
7
+	"golang.org/x/net/context"
8
+
9
+	"github.com/docker/docker/api/types"
10
+	"github.com/docker/docker/api/types/filters"
11
+	timetypes "github.com/docker/docker/api/types/time"
12
+)
13
+
14
+// Events returns a stream of events in the daemon in a ReadCloser.
15
+// It's up to the caller to close the stream.
16
+func (cli *Client) Events(ctx context.Context, options types.EventsOptions) (io.ReadCloser, error) {
17
+	query := url.Values{}
18
+	ref := time.Now()
19
+
20
+	if options.Since != "" {
21
+		ts, err := timetypes.GetTimestamp(options.Since, ref)
22
+		if err != nil {
23
+			return nil, err
24
+		}
25
+		query.Set("since", ts)
26
+	}
27
+	if options.Until != "" {
28
+		ts, err := timetypes.GetTimestamp(options.Until, ref)
29
+		if err != nil {
30
+			return nil, err
31
+		}
32
+		query.Set("until", ts)
33
+	}
34
+	if options.Filters.Len() > 0 {
35
+		filterJSON, err := filters.ToParamWithVersion(cli.version, options.Filters)
36
+		if err != nil {
37
+			return nil, err
38
+		}
39
+		query.Set("filters", filterJSON)
40
+	}
41
+
42
+	serverResponse, err := cli.get(ctx, "/events", query, nil)
43
+	if err != nil {
44
+		return nil, err
45
+	}
46
+	return serverResponse.body, nil
47
+}
0 48
new file mode 100644
... ...
@@ -0,0 +1,126 @@
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"
13
+	"github.com/docker/docker/api/types/filters"
14
+)
15
+
16
+func TestEventsErrorInOptions(t *testing.T) {
17
+	errorCases := []struct {
18
+		options       types.EventsOptions
19
+		expectedError string
20
+	}{
21
+		{
22
+			options: types.EventsOptions{
23
+				Since: "2006-01-02TZ",
24
+			},
25
+			expectedError: `parsing time "2006-01-02TZ"`,
26
+		},
27
+		{
28
+			options: types.EventsOptions{
29
+				Until: "2006-01-02TZ",
30
+			},
31
+			expectedError: `parsing time "2006-01-02TZ"`,
32
+		},
33
+	}
34
+	for _, e := range errorCases {
35
+		client := &Client{
36
+			transport: newMockClient(nil, errorMock(http.StatusInternalServerError, "Server error")),
37
+		}
38
+		_, err := client.Events(context.Background(), e.options)
39
+		if err == nil || !strings.Contains(err.Error(), e.expectedError) {
40
+			t.Fatalf("expected a error %q, got %v", e.expectedError, err)
41
+		}
42
+	}
43
+}
44
+
45
+func TestEventsErrorFromServer(t *testing.T) {
46
+	client := &Client{
47
+		transport: newMockClient(nil, errorMock(http.StatusInternalServerError, "Server error")),
48
+	}
49
+	_, err := client.Events(context.Background(), types.EventsOptions{})
50
+	if err == nil || err.Error() != "Error response from daemon: Server error" {
51
+		t.Fatalf("expected a Server Error, got %v", err)
52
+	}
53
+}
54
+
55
+func TestEvents(t *testing.T) {
56
+	expectedURL := "/events"
57
+
58
+	filters := filters.NewArgs()
59
+	filters.Add("label", "label1")
60
+	filters.Add("label", "label2")
61
+	expectedFiltersJSON := `{"label":{"label1":true,"label2":true}}`
62
+
63
+	eventsCases := []struct {
64
+		options             types.EventsOptions
65
+		expectedQueryParams map[string]string
66
+	}{
67
+		{
68
+			options: types.EventsOptions{
69
+				Since: "invalid but valid",
70
+			},
71
+			expectedQueryParams: map[string]string{
72
+				"since": "invalid but valid",
73
+			},
74
+		},
75
+		{
76
+			options: types.EventsOptions{
77
+				Until: "invalid but valid",
78
+			},
79
+			expectedQueryParams: map[string]string{
80
+				"until": "invalid but valid",
81
+			},
82
+		},
83
+		{
84
+			options: types.EventsOptions{
85
+				Filters: filters,
86
+			},
87
+			expectedQueryParams: map[string]string{
88
+				"filters": expectedFiltersJSON,
89
+			},
90
+		},
91
+	}
92
+
93
+	for _, eventsCase := range eventsCases {
94
+		client := &Client{
95
+			transport: newMockClient(nil, func(req *http.Request) (*http.Response, error) {
96
+				if !strings.HasPrefix(req.URL.Path, expectedURL) {
97
+					return nil, fmt.Errorf("Expected URL '%s', got '%s'", expectedURL, req.URL)
98
+				}
99
+				query := req.URL.Query()
100
+				for key, expected := range eventsCase.expectedQueryParams {
101
+					actual := query.Get(key)
102
+					if actual != expected {
103
+						return nil, fmt.Errorf("%s not set in URL query properly. Expected '%s', got %s", key, expected, actual)
104
+					}
105
+				}
106
+				return &http.Response{
107
+					StatusCode: http.StatusOK,
108
+					Body:       ioutil.NopCloser(bytes.NewReader([]byte("response"))),
109
+				}, nil
110
+			}),
111
+		}
112
+		body, err := client.Events(context.Background(), eventsCase.options)
113
+		if err != nil {
114
+			t.Fatal(err)
115
+		}
116
+		defer body.Close()
117
+		content, err := ioutil.ReadAll(body)
118
+		if err != nil {
119
+			t.Fatal(err)
120
+		}
121
+		if string(content) != "response" {
122
+			t.Fatalf("expected response to contain 'response', got %s", string(content))
123
+		}
124
+	}
125
+}
0 126
new file mode 100644
... ...
@@ -0,0 +1,174 @@
0
+package client
1
+
2
+import (
3
+	"crypto/tls"
4
+	"errors"
5
+	"fmt"
6
+	"net"
7
+	"net/http/httputil"
8
+	"net/url"
9
+	"strings"
10
+	"time"
11
+
12
+	"github.com/docker/docker/api/types"
13
+	"github.com/docker/docker/client/transport"
14
+	"github.com/docker/go-connections/sockets"
15
+	"golang.org/x/net/context"
16
+)
17
+
18
+// tlsClientCon holds tls information and a dialed connection.
19
+type tlsClientCon struct {
20
+	*tls.Conn
21
+	rawConn net.Conn
22
+}
23
+
24
+func (c *tlsClientCon) CloseWrite() error {
25
+	// Go standard tls.Conn doesn't provide the CloseWrite() method so we do it
26
+	// on its underlying connection.
27
+	if conn, ok := c.rawConn.(types.CloseWriter); ok {
28
+		return conn.CloseWrite()
29
+	}
30
+	return nil
31
+}
32
+
33
+// postHijacked sends a POST request and hijacks the connection.
34
+func (cli *Client) postHijacked(ctx context.Context, path string, query url.Values, body interface{}, headers map[string][]string) (types.HijackedResponse, error) {
35
+	bodyEncoded, err := encodeData(body)
36
+	if err != nil {
37
+		return types.HijackedResponse{}, err
38
+	}
39
+
40
+	req, err := cli.newRequest("POST", path, query, bodyEncoded, headers)
41
+	if err != nil {
42
+		return types.HijackedResponse{}, err
43
+	}
44
+	req.Host = cli.addr
45
+
46
+	req.Header.Set("Connection", "Upgrade")
47
+	req.Header.Set("Upgrade", "tcp")
48
+
49
+	conn, err := dial(cli.proto, cli.addr, cli.transport.TLSConfig())
50
+	if err != nil {
51
+		if strings.Contains(err.Error(), "connection refused") {
52
+			return types.HijackedResponse{}, fmt.Errorf("Cannot connect to the Docker daemon. Is 'docker daemon' running on this host?")
53
+		}
54
+		return types.HijackedResponse{}, err
55
+	}
56
+
57
+	// When we set up a TCP connection for hijack, there could be long periods
58
+	// of inactivity (a long running command with no output) that in certain
59
+	// network setups may cause ECONNTIMEOUT, leaving the client in an unknown
60
+	// state. Setting TCP KeepAlive on the socket connection will prohibit
61
+	// ECONNTIMEOUT unless the socket connection truly is broken
62
+	if tcpConn, ok := conn.(*net.TCPConn); ok {
63
+		tcpConn.SetKeepAlive(true)
64
+		tcpConn.SetKeepAlivePeriod(30 * time.Second)
65
+	}
66
+
67
+	clientconn := httputil.NewClientConn(conn, nil)
68
+	defer clientconn.Close()
69
+
70
+	// Server hijacks the connection, error 'connection closed' expected
71
+	_, err = clientconn.Do(req)
72
+
73
+	rwc, br := clientconn.Hijack()
74
+
75
+	return types.HijackedResponse{Conn: rwc, Reader: br}, err
76
+}
77
+
78
+func tlsDial(network, addr string, config *tls.Config) (net.Conn, error) {
79
+	return tlsDialWithDialer(new(net.Dialer), network, addr, config)
80
+}
81
+
82
+// We need to copy Go's implementation of tls.Dial (pkg/cryptor/tls/tls.go) in
83
+// order to return our custom tlsClientCon struct which holds both the tls.Conn
84
+// object _and_ its underlying raw connection. The rationale for this is that
85
+// we need to be able to close the write end of the connection when attaching,
86
+// which tls.Conn does not provide.
87
+func tlsDialWithDialer(dialer *net.Dialer, network, addr string, config *tls.Config) (net.Conn, error) {
88
+	// We want the Timeout and Deadline values from dialer to cover the
89
+	// whole process: TCP connection and TLS handshake. This means that we
90
+	// also need to start our own timers now.
91
+	timeout := dialer.Timeout
92
+
93
+	if !dialer.Deadline.IsZero() {
94
+		deadlineTimeout := dialer.Deadline.Sub(time.Now())
95
+		if timeout == 0 || deadlineTimeout < timeout {
96
+			timeout = deadlineTimeout
97
+		}
98
+	}
99
+
100
+	var errChannel chan error
101
+
102
+	if timeout != 0 {
103
+		errChannel = make(chan error, 2)
104
+		time.AfterFunc(timeout, func() {
105
+			errChannel <- errors.New("")
106
+		})
107
+	}
108
+
109
+	proxyDialer, err := sockets.DialerFromEnvironment(dialer)
110
+	if err != nil {
111
+		return nil, err
112
+	}
113
+
114
+	rawConn, err := proxyDialer.Dial(network, addr)
115
+	if err != nil {
116
+		return nil, err
117
+	}
118
+	// When we set up a TCP connection for hijack, there could be long periods
119
+	// of inactivity (a long running command with no output) that in certain
120
+	// network setups may cause ECONNTIMEOUT, leaving the client in an unknown
121
+	// state. Setting TCP KeepAlive on the socket connection will prohibit
122
+	// ECONNTIMEOUT unless the socket connection truly is broken
123
+	if tcpConn, ok := rawConn.(*net.TCPConn); ok {
124
+		tcpConn.SetKeepAlive(true)
125
+		tcpConn.SetKeepAlivePeriod(30 * time.Second)
126
+	}
127
+
128
+	colonPos := strings.LastIndex(addr, ":")
129
+	if colonPos == -1 {
130
+		colonPos = len(addr)
131
+	}
132
+	hostname := addr[:colonPos]
133
+
134
+	// If no ServerName is set, infer the ServerName
135
+	// from the hostname we're connecting to.
136
+	if config.ServerName == "" {
137
+		// Make a copy to avoid polluting argument or default.
138
+		config = transport.TLSConfigClone(config)
139
+		config.ServerName = hostname
140
+	}
141
+
142
+	conn := tls.Client(rawConn, config)
143
+
144
+	if timeout == 0 {
145
+		err = conn.Handshake()
146
+	} else {
147
+		go func() {
148
+			errChannel <- conn.Handshake()
149
+		}()
150
+
151
+		err = <-errChannel
152
+	}
153
+
154
+	if err != nil {
155
+		rawConn.Close()
156
+		return nil, err
157
+	}
158
+
159
+	// This is Docker difference with standard's crypto/tls package: returned a
160
+	// wrapper which holds both the TLS and raw connections.
161
+	return &tlsClientCon{conn, rawConn}, nil
162
+}
163
+
164
+func dial(proto, addr string, tlsConfig *tls.Config) (net.Conn, error) {
165
+	if tlsConfig != nil && proto != "unix" && proto != "npipe" {
166
+		// Notice this isn't Go standard's tls.Dial function
167
+		return tlsDial(proto, addr, tlsConfig)
168
+	}
169
+	if proto == "npipe" {
170
+		return sockets.DialPipe(addr, 32*time.Second)
171
+	}
172
+	return net.Dial(proto, addr)
173
+}
0 174
new file mode 100644
... ...
@@ -0,0 +1,123 @@
0
+package client
1
+
2
+import (
3
+	"encoding/base64"
4
+	"encoding/json"
5
+	"io"
6
+	"net/http"
7
+	"net/url"
8
+	"regexp"
9
+	"strconv"
10
+
11
+	"golang.org/x/net/context"
12
+
13
+	"github.com/docker/docker/api/types"
14
+	"github.com/docker/docker/api/types/container"
15
+)
16
+
17
+var headerRegexp = regexp.MustCompile(`\ADocker/.+\s\((.+)\)\z`)
18
+
19
+// ImageBuild sends request to the daemon to build images.
20
+// The Body in the response implement an io.ReadCloser and it's up to the caller to
21
+// close it.
22
+func (cli *Client) ImageBuild(ctx context.Context, buildContext io.Reader, options types.ImageBuildOptions) (types.ImageBuildResponse, error) {
23
+	query, err := imageBuildOptionsToQuery(options)
24
+	if err != nil {
25
+		return types.ImageBuildResponse{}, err
26
+	}
27
+
28
+	headers := http.Header(make(map[string][]string))
29
+	buf, err := json.Marshal(options.AuthConfigs)
30
+	if err != nil {
31
+		return types.ImageBuildResponse{}, err
32
+	}
33
+	headers.Add("X-Registry-Config", base64.URLEncoding.EncodeToString(buf))
34
+	headers.Set("Content-Type", "application/tar")
35
+
36
+	serverResp, err := cli.postRaw(ctx, "/build", query, buildContext, headers)
37
+	if err != nil {
38
+		return types.ImageBuildResponse{}, err
39
+	}
40
+
41
+	osType := getDockerOS(serverResp.header.Get("Server"))
42
+
43
+	return types.ImageBuildResponse{
44
+		Body:   serverResp.body,
45
+		OSType: osType,
46
+	}, nil
47
+}
48
+
49
+func imageBuildOptionsToQuery(options types.ImageBuildOptions) (url.Values, error) {
50
+	query := url.Values{
51
+		"t": options.Tags,
52
+	}
53
+	if options.SuppressOutput {
54
+		query.Set("q", "1")
55
+	}
56
+	if options.RemoteContext != "" {
57
+		query.Set("remote", options.RemoteContext)
58
+	}
59
+	if options.NoCache {
60
+		query.Set("nocache", "1")
61
+	}
62
+	if options.Remove {
63
+		query.Set("rm", "1")
64
+	} else {
65
+		query.Set("rm", "0")
66
+	}
67
+
68
+	if options.ForceRemove {
69
+		query.Set("forcerm", "1")
70
+	}
71
+
72
+	if options.PullParent {
73
+		query.Set("pull", "1")
74
+	}
75
+
76
+	if options.Squash {
77
+		query.Set("squash", "1")
78
+	}
79
+
80
+	if !container.Isolation.IsDefault(options.Isolation) {
81
+		query.Set("isolation", string(options.Isolation))
82
+	}
83
+
84
+	query.Set("cpusetcpus", options.CPUSetCPUs)
85
+	query.Set("cpusetmems", options.CPUSetMems)
86
+	query.Set("cpushares", strconv.FormatInt(options.CPUShares, 10))
87
+	query.Set("cpuquota", strconv.FormatInt(options.CPUQuota, 10))
88
+	query.Set("cpuperiod", strconv.FormatInt(options.CPUPeriod, 10))
89
+	query.Set("memory", strconv.FormatInt(options.Memory, 10))
90
+	query.Set("memswap", strconv.FormatInt(options.MemorySwap, 10))
91
+	query.Set("cgroupparent", options.CgroupParent)
92
+	query.Set("shmsize", strconv.FormatInt(options.ShmSize, 10))
93
+	query.Set("dockerfile", options.Dockerfile)
94
+
95
+	ulimitsJSON, err := json.Marshal(options.Ulimits)
96
+	if err != nil {
97
+		return query, err
98
+	}
99
+	query.Set("ulimits", string(ulimitsJSON))
100
+
101
+	buildArgsJSON, err := json.Marshal(options.BuildArgs)
102
+	if err != nil {
103
+		return query, err
104
+	}
105
+	query.Set("buildargs", string(buildArgsJSON))
106
+
107
+	labelsJSON, err := json.Marshal(options.Labels)
108
+	if err != nil {
109
+		return query, err
110
+	}
111
+	query.Set("labels", string(labelsJSON))
112
+	return query, nil
113
+}
114
+
115
+func getDockerOS(serverHeader string) string {
116
+	var osType string
117
+	matches := headerRegexp.FindStringSubmatch(serverHeader)
118
+	if len(matches) > 0 {
119
+		osType = matches[1]
120
+	}
121
+	return osType
122
+}
0 123
new file mode 100644
... ...
@@ -0,0 +1,230 @@
0
+package client
1
+
2
+import (
3
+	"bytes"
4
+	"fmt"
5
+	"io/ioutil"
6
+	"net/http"
7
+	"reflect"
8
+	"strings"
9
+	"testing"
10
+
11
+	"golang.org/x/net/context"
12
+
13
+	"github.com/docker/docker/api/types"
14
+	"github.com/docker/docker/api/types/container"
15
+	"github.com/docker/go-units"
16
+)
17
+
18
+func TestImageBuildError(t *testing.T) {
19
+	client := &Client{
20
+		transport: newMockClient(nil, errorMock(http.StatusInternalServerError, "Server error")),
21
+	}
22
+	_, err := client.ImageBuild(context.Background(), nil, types.ImageBuildOptions{})
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 TestImageBuild(t *testing.T) {
29
+	emptyRegistryConfig := "bnVsbA=="
30
+	buildCases := []struct {
31
+		buildOptions           types.ImageBuildOptions
32
+		expectedQueryParams    map[string]string
33
+		expectedTags           []string
34
+		expectedRegistryConfig string
35
+	}{
36
+		{
37
+			buildOptions: types.ImageBuildOptions{
38
+				SuppressOutput: true,
39
+				NoCache:        true,
40
+				Remove:         true,
41
+				ForceRemove:    true,
42
+				PullParent:     true,
43
+			},
44
+			expectedQueryParams: map[string]string{
45
+				"q":       "1",
46
+				"nocache": "1",
47
+				"rm":      "1",
48
+				"forcerm": "1",
49
+				"pull":    "1",
50
+			},
51
+			expectedTags:           []string{},
52
+			expectedRegistryConfig: emptyRegistryConfig,
53
+		},
54
+		{
55
+			buildOptions: types.ImageBuildOptions{
56
+				SuppressOutput: false,
57
+				NoCache:        false,
58
+				Remove:         false,
59
+				ForceRemove:    false,
60
+				PullParent:     false,
61
+			},
62
+			expectedQueryParams: map[string]string{
63
+				"q":       "",
64
+				"nocache": "",
65
+				"rm":      "0",
66
+				"forcerm": "",
67
+				"pull":    "",
68
+			},
69
+			expectedTags:           []string{},
70
+			expectedRegistryConfig: emptyRegistryConfig,
71
+		},
72
+		{
73
+			buildOptions: types.ImageBuildOptions{
74
+				RemoteContext: "remoteContext",
75
+				Isolation:     container.Isolation("isolation"),
76
+				CPUSetCPUs:    "2",
77
+				CPUSetMems:    "12",
78
+				CPUShares:     20,
79
+				CPUQuota:      10,
80
+				CPUPeriod:     30,
81
+				Memory:        256,
82
+				MemorySwap:    512,
83
+				ShmSize:       10,
84
+				CgroupParent:  "cgroup_parent",
85
+				Dockerfile:    "Dockerfile",
86
+			},
87
+			expectedQueryParams: map[string]string{
88
+				"remote":       "remoteContext",
89
+				"isolation":    "isolation",
90
+				"cpusetcpus":   "2",
91
+				"cpusetmems":   "12",
92
+				"cpushares":    "20",
93
+				"cpuquota":     "10",
94
+				"cpuperiod":    "30",
95
+				"memory":       "256",
96
+				"memswap":      "512",
97
+				"shmsize":      "10",
98
+				"cgroupparent": "cgroup_parent",
99
+				"dockerfile":   "Dockerfile",
100
+				"rm":           "0",
101
+			},
102
+			expectedTags:           []string{},
103
+			expectedRegistryConfig: emptyRegistryConfig,
104
+		},
105
+		{
106
+			buildOptions: types.ImageBuildOptions{
107
+				BuildArgs: map[string]string{
108
+					"ARG1": "value1",
109
+					"ARG2": "value2",
110
+				},
111
+			},
112
+			expectedQueryParams: map[string]string{
113
+				"buildargs": `{"ARG1":"value1","ARG2":"value2"}`,
114
+				"rm":        "0",
115
+			},
116
+			expectedTags:           []string{},
117
+			expectedRegistryConfig: emptyRegistryConfig,
118
+		},
119
+		{
120
+			buildOptions: types.ImageBuildOptions{
121
+				Ulimits: []*units.Ulimit{
122
+					{
123
+						Name: "nproc",
124
+						Hard: 65557,
125
+						Soft: 65557,
126
+					},
127
+					{
128
+						Name: "nofile",
129
+						Hard: 20000,
130
+						Soft: 40000,
131
+					},
132
+				},
133
+			},
134
+			expectedQueryParams: map[string]string{
135
+				"ulimits": `[{"Name":"nproc","Hard":65557,"Soft":65557},{"Name":"nofile","Hard":20000,"Soft":40000}]`,
136
+				"rm":      "0",
137
+			},
138
+			expectedTags:           []string{},
139
+			expectedRegistryConfig: emptyRegistryConfig,
140
+		},
141
+		{
142
+			buildOptions: types.ImageBuildOptions{
143
+				AuthConfigs: map[string]types.AuthConfig{
144
+					"https://index.docker.io/v1/": {
145
+						Auth: "dG90bwo=",
146
+					},
147
+				},
148
+			},
149
+			expectedQueryParams: map[string]string{
150
+				"rm": "0",
151
+			},
152
+			expectedTags:           []string{},
153
+			expectedRegistryConfig: "eyJodHRwczovL2luZGV4LmRvY2tlci5pby92MS8iOnsiYXV0aCI6ImRHOTBid289In19",
154
+		},
155
+	}
156
+	for _, buildCase := range buildCases {
157
+		expectedURL := "/build"
158
+		client := &Client{
159
+			transport: newMockClient(nil, func(r *http.Request) (*http.Response, error) {
160
+				if !strings.HasPrefix(r.URL.Path, expectedURL) {
161
+					return nil, fmt.Errorf("Expected URL '%s', got '%s'", expectedURL, r.URL)
162
+				}
163
+				// Check request headers
164
+				registryConfig := r.Header.Get("X-Registry-Config")
165
+				if registryConfig != buildCase.expectedRegistryConfig {
166
+					return nil, fmt.Errorf("X-Registry-Config header not properly set in the request. Expected '%s', got %s", buildCase.expectedRegistryConfig, registryConfig)
167
+				}
168
+				contentType := r.Header.Get("Content-Type")
169
+				if contentType != "application/tar" {
170
+					return nil, fmt.Errorf("Content-type header not properly set in the request. Expected 'application/tar', got %s", contentType)
171
+				}
172
+
173
+				// Check query parameters
174
+				query := r.URL.Query()
175
+				for key, expected := range buildCase.expectedQueryParams {
176
+					actual := query.Get(key)
177
+					if actual != expected {
178
+						return nil, fmt.Errorf("%s not set in URL query properly. Expected '%s', got %s", key, expected, actual)
179
+					}
180
+				}
181
+
182
+				// Check tags
183
+				if len(buildCase.expectedTags) > 0 {
184
+					tags := query["t"]
185
+					if !reflect.DeepEqual(tags, buildCase.expectedTags) {
186
+						return nil, fmt.Errorf("t (tags) not set in URL query properly. Expected '%s', got %s", buildCase.expectedTags, tags)
187
+					}
188
+				}
189
+
190
+				headers := http.Header{}
191
+				headers.Add("Server", "Docker/v1.23 (MyOS)")
192
+				return &http.Response{
193
+					StatusCode: http.StatusOK,
194
+					Body:       ioutil.NopCloser(bytes.NewReader([]byte("body"))),
195
+					Header:     headers,
196
+				}, nil
197
+			}),
198
+		}
199
+		buildResponse, err := client.ImageBuild(context.Background(), nil, buildCase.buildOptions)
200
+		if err != nil {
201
+			t.Fatal(err)
202
+		}
203
+		if buildResponse.OSType != "MyOS" {
204
+			t.Fatalf("expected OSType to be 'MyOS', got %s", buildResponse.OSType)
205
+		}
206
+		response, err := ioutil.ReadAll(buildResponse.Body)
207
+		if err != nil {
208
+			t.Fatal(err)
209
+		}
210
+		buildResponse.Body.Close()
211
+		if string(response) != "body" {
212
+			t.Fatalf("expected Body to contain 'body' string, got %s", response)
213
+		}
214
+	}
215
+}
216
+
217
+func TestGetDockerOS(t *testing.T) {
218
+	cases := map[string]string{
219
+		"Docker/v1.22 (linux)":   "linux",
220
+		"Docker/v1.22 (windows)": "windows",
221
+		"Foo/v1.22 (bar)":        "",
222
+	}
223
+	for header, os := range cases {
224
+		g := getDockerOS(header)
225
+		if g != os {
226
+			t.Fatalf("Expected %s, got %s", os, g)
227
+		}
228
+	}
229
+}
0 230
new file mode 100644
... ...
@@ -0,0 +1,34 @@
0
+package client
1
+
2
+import (
3
+	"io"
4
+	"net/url"
5
+
6
+	"golang.org/x/net/context"
7
+
8
+	"github.com/docker/docker/api/types"
9
+	"github.com/docker/docker/api/types/reference"
10
+)
11
+
12
+// ImageCreate creates a new image based in the parent options.
13
+// It returns the JSON content in the response body.
14
+func (cli *Client) ImageCreate(ctx context.Context, parentReference string, options types.ImageCreateOptions) (io.ReadCloser, error) {
15
+	repository, tag, err := reference.Parse(parentReference)
16
+	if err != nil {
17
+		return nil, err
18
+	}
19
+
20
+	query := url.Values{}
21
+	query.Set("fromImage", repository)
22
+	query.Set("tag", tag)
23
+	resp, err := cli.tryImageCreate(ctx, query, options.RegistryAuth)
24
+	if err != nil {
25
+		return nil, err
26
+	}
27
+	return resp.body, nil
28
+}
29
+
30
+func (cli *Client) tryImageCreate(ctx context.Context, query url.Values, registryAuth string) (serverResponse, error) {
31
+	headers := map[string][]string{"X-Registry-Auth": {registryAuth}}
32
+	return cli.post(ctx, "/images/create", query, nil, headers)
33
+}
0 34
new file mode 100644
... ...
@@ -0,0 +1,76 @@
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"
13
+)
14
+
15
+func TestImageCreateError(t *testing.T) {
16
+	client := &Client{
17
+		transport: newMockClient(nil, errorMock(http.StatusInternalServerError, "Server error")),
18
+	}
19
+	_, err := client.ImageCreate(context.Background(), "reference", types.ImageCreateOptions{})
20
+	if err == nil || err.Error() != "Error response from daemon: Server error" {
21
+		t.Fatalf("expected a Server error, got %v", err)
22
+	}
23
+}
24
+
25
+func TestImageCreate(t *testing.T) {
26
+	expectedURL := "/images/create"
27
+	expectedImage := "test:5000/my_image"
28
+	expectedTag := "sha256:ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff"
29
+	expectedReference := fmt.Sprintf("%s@%s", expectedImage, expectedTag)
30
+	expectedRegistryAuth := "eyJodHRwczovL2luZGV4LmRvY2tlci5pby92MS8iOnsiYXV0aCI6ImRHOTBid289IiwiZW1haWwiOiJqb2huQGRvZS5jb20ifX0="
31
+	client := &Client{
32
+		transport: newMockClient(nil, func(r *http.Request) (*http.Response, error) {
33
+			if !strings.HasPrefix(r.URL.Path, expectedURL) {
34
+				return nil, fmt.Errorf("Expected URL '%s', got '%s'", expectedURL, r.URL)
35
+			}
36
+			registryAuth := r.Header.Get("X-Registry-Auth")
37
+			if registryAuth != expectedRegistryAuth {
38
+				return nil, fmt.Errorf("X-Registry-Auth header not properly set in the request. Expected '%s', got %s", expectedRegistryAuth, registryAuth)
39
+			}
40
+
41
+			query := r.URL.Query()
42
+			fromImage := query.Get("fromImage")
43
+			if fromImage != expectedImage {
44
+				return nil, fmt.Errorf("fromImage not set in URL query properly. Expected '%s', got %s", expectedImage, fromImage)
45
+			}
46
+
47
+			tag := query.Get("tag")
48
+			if tag != expectedTag {
49
+				return nil, fmt.Errorf("tag not set in URL query properly. Expected '%s', got %s", expectedTag, tag)
50
+			}
51
+
52
+			return &http.Response{
53
+				StatusCode: http.StatusOK,
54
+				Body:       ioutil.NopCloser(bytes.NewReader([]byte("body"))),
55
+			}, nil
56
+		}),
57
+	}
58
+
59
+	createResponse, err := client.ImageCreate(context.Background(), expectedReference, types.ImageCreateOptions{
60
+		RegistryAuth: expectedRegistryAuth,
61
+	})
62
+	if err != nil {
63
+		t.Fatal(err)
64
+	}
65
+	response, err := ioutil.ReadAll(createResponse)
66
+	if err != nil {
67
+		t.Fatal(err)
68
+	}
69
+	if err = createResponse.Close(); err != nil {
70
+		t.Fatal(err)
71
+	}
72
+	if string(response) != "body" {
73
+		t.Fatalf("expected Body to contain 'body' string, got %s", response)
74
+	}
75
+}
0 76
new file mode 100644
... ...
@@ -0,0 +1,22 @@
0
+package client
1
+
2
+import (
3
+	"encoding/json"
4
+	"net/url"
5
+
6
+	"github.com/docker/docker/api/types"
7
+	"golang.org/x/net/context"
8
+)
9
+
10
+// ImageHistory returns the changes in an image in history format.
11
+func (cli *Client) ImageHistory(ctx context.Context, imageID string) ([]types.ImageHistory, error) {
12
+	var history []types.ImageHistory
13
+	serverResp, err := cli.get(ctx, "/images/"+imageID+"/history", url.Values{}, nil)
14
+	if err != nil {
15
+		return history, err
16
+	}
17
+
18
+	err = json.NewDecoder(serverResp.body).Decode(&history)
19
+	ensureReaderClosed(serverResp)
20
+	return history, err
21
+}
0 22
new file mode 100644
... ...
@@ -0,0 +1,60 @@
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
+	"golang.org/x/net/context"
13
+)
14
+
15
+func TestImageHistoryError(t *testing.T) {
16
+	client := &Client{
17
+		transport: newMockClient(nil, errorMock(http.StatusInternalServerError, "Server error")),
18
+	}
19
+	_, err := client.ImageHistory(context.Background(), "nothing")
20
+	if err == nil || err.Error() != "Error response from daemon: Server error" {
21
+		t.Fatalf("expected a Server error, got %v", err)
22
+	}
23
+}
24
+
25
+func TestImageHistory(t *testing.T) {
26
+	expectedURL := "/images/image_id/history"
27
+	client := &Client{
28
+		transport: newMockClient(nil, func(r *http.Request) (*http.Response, error) {
29
+			if !strings.HasPrefix(r.URL.Path, expectedURL) {
30
+				return nil, fmt.Errorf("Expected URL '%s', got '%s'", expectedURL, r.URL)
31
+			}
32
+			b, err := json.Marshal([]types.ImageHistory{
33
+				{
34
+					ID:   "image_id1",
35
+					Tags: []string{"tag1", "tag2"},
36
+				},
37
+				{
38
+					ID:   "image_id2",
39
+					Tags: []string{"tag1", "tag2"},
40
+				},
41
+			})
42
+			if err != nil {
43
+				return nil, err
44
+			}
45
+
46
+			return &http.Response{
47
+				StatusCode: http.StatusOK,
48
+				Body:       ioutil.NopCloser(bytes.NewReader(b)),
49
+			}, nil
50
+		}),
51
+	}
52
+	imageHistories, err := client.ImageHistory(context.Background(), "image_id")
53
+	if err != nil {
54
+		t.Fatal(err)
55
+	}
56
+	if len(imageHistories) != 2 {
57
+		t.Fatalf("expected 2 containers, got %v", imageHistories)
58
+	}
59
+}
0 60
new file mode 100644
... ...
@@ -0,0 +1,37 @@
0
+package client
1
+
2
+import (
3
+	"io"
4
+	"net/url"
5
+
6
+	"golang.org/x/net/context"
7
+
8
+	"github.com/docker/distribution/reference"
9
+	"github.com/docker/docker/api/types"
10
+)
11
+
12
+// ImageImport creates a new image based in the source options.
13
+// It returns the JSON content in the response body.
14
+func (cli *Client) ImageImport(ctx context.Context, source types.ImageImportSource, ref string, options types.ImageImportOptions) (io.ReadCloser, error) {
15
+	if ref != "" {
16
+		//Check if the given image name can be resolved
17
+		if _, err := reference.ParseNamed(ref); err != nil {
18
+			return nil, err
19
+		}
20
+	}
21
+
22
+	query := url.Values{}
23
+	query.Set("fromSrc", source.SourceName)
24
+	query.Set("repo", ref)
25
+	query.Set("tag", options.Tag)
26
+	query.Set("message", options.Message)
27
+	for _, change := range options.Changes {
28
+		query.Add("changes", change)
29
+	}
30
+
31
+	resp, err := cli.postRaw(ctx, "/images/create", query, source.Source, nil)
32
+	if err != nil {
33
+		return nil, err
34
+	}
35
+	return resp.body, nil
36
+}
0 37
new file mode 100644
... ...
@@ -0,0 +1,81 @@
0
+package client
1
+
2
+import (
3
+	"bytes"
4
+	"fmt"
5
+	"io/ioutil"
6
+	"net/http"
7
+	"reflect"
8
+	"strings"
9
+	"testing"
10
+
11
+	"github.com/docker/docker/api/types"
12
+	"golang.org/x/net/context"
13
+)
14
+
15
+func TestImageImportError(t *testing.T) {
16
+	client := &Client{
17
+		transport: newMockClient(nil, errorMock(http.StatusInternalServerError, "Server error")),
18
+	}
19
+	_, err := client.ImageImport(context.Background(), types.ImageImportSource{}, "image:tag", types.ImageImportOptions{})
20
+	if err == nil || err.Error() != "Error response from daemon: Server error" {
21
+		t.Fatalf("expected a Server error, got %v", err)
22
+	}
23
+}
24
+
25
+func TestImageImport(t *testing.T) {
26
+	expectedURL := "/images/create"
27
+	client := &Client{
28
+		transport: newMockClient(nil, func(r *http.Request) (*http.Response, error) {
29
+			if !strings.HasPrefix(r.URL.Path, expectedURL) {
30
+				return nil, fmt.Errorf("Expected URL '%s', got '%s'", expectedURL, r.URL)
31
+			}
32
+			query := r.URL.Query()
33
+			fromSrc := query.Get("fromSrc")
34
+			if fromSrc != "image_source" {
35
+				return nil, fmt.Errorf("fromSrc not set in URL query properly. Expected 'image_source', got %s", fromSrc)
36
+			}
37
+			repo := query.Get("repo")
38
+			if repo != "repository_name:imported" {
39
+				return nil, fmt.Errorf("repo not set in URL query properly. Expected 'repository_name', got %s", repo)
40
+			}
41
+			tag := query.Get("tag")
42
+			if tag != "imported" {
43
+				return nil, fmt.Errorf("tag not set in URL query properly. Expected 'imported', got %s", tag)
44
+			}
45
+			message := query.Get("message")
46
+			if message != "A message" {
47
+				return nil, fmt.Errorf("message not set in URL query properly. Expected 'A message', got %s", message)
48
+			}
49
+			changes := query["changes"]
50
+			expectedChanges := []string{"change1", "change2"}
51
+			if !reflect.DeepEqual(expectedChanges, changes) {
52
+				return nil, fmt.Errorf("changes not set in URL query properly. Expected %v, got %v", expectedChanges, changes)
53
+			}
54
+
55
+			return &http.Response{
56
+				StatusCode: http.StatusOK,
57
+				Body:       ioutil.NopCloser(bytes.NewReader([]byte("response"))),
58
+			}, nil
59
+		}),
60
+	}
61
+	importResponse, err := client.ImageImport(context.Background(), types.ImageImportSource{
62
+		Source:     strings.NewReader("source"),
63
+		SourceName: "image_source",
64
+	}, "repository_name:imported", types.ImageImportOptions{
65
+		Tag:     "imported",
66
+		Message: "A message",
67
+		Changes: []string{"change1", "change2"},
68
+	})
69
+	if err != nil {
70
+		t.Fatal(err)
71
+	}
72
+	response, err := ioutil.ReadAll(importResponse)
73
+	if err != nil {
74
+		t.Fatal(err)
75
+	}
76
+	importResponse.Close()
77
+	if string(response) != "response" {
78
+		t.Fatalf("expected response to contain 'response', got %s", string(response))
79
+	}
80
+}
0 81
new file mode 100644
... ...
@@ -0,0 +1,33 @@
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"
9
+	"golang.org/x/net/context"
10
+)
11
+
12
+// ImageInspectWithRaw returns the image information and its raw representation.
13
+func (cli *Client) ImageInspectWithRaw(ctx context.Context, imageID string) (types.ImageInspect, []byte, error) {
14
+	serverResp, err := cli.get(ctx, "/images/"+imageID+"/json", nil, nil)
15
+	if err != nil {
16
+		if serverResp.statusCode == http.StatusNotFound {
17
+			return types.ImageInspect{}, nil, imageNotFoundError{imageID}
18
+		}
19
+		return types.ImageInspect{}, nil, err
20
+	}
21
+	defer ensureReaderClosed(serverResp)
22
+
23
+	body, err := ioutil.ReadAll(serverResp.body)
24
+	if err != nil {
25
+		return types.ImageInspect{}, nil, err
26
+	}
27
+
28
+	var response types.ImageInspect
29
+	rdr := bytes.NewReader(body)
30
+	err = json.NewDecoder(rdr).Decode(&response)
31
+	return response, body, err
32
+}
0 33
new file mode 100644
... ...
@@ -0,0 +1,71 @@
0
+package client
1
+
2
+import (
3
+	"bytes"
4
+	"encoding/json"
5
+	"fmt"
6
+	"io/ioutil"
7
+	"net/http"
8
+	"reflect"
9
+	"strings"
10
+	"testing"
11
+
12
+	"github.com/docker/docker/api/types"
13
+	"golang.org/x/net/context"
14
+)
15
+
16
+func TestImageInspectError(t *testing.T) {
17
+	client := &Client{
18
+		transport: newMockClient(nil, errorMock(http.StatusInternalServerError, "Server error")),
19
+	}
20
+
21
+	_, _, err := client.ImageInspectWithRaw(context.Background(), "nothing")
22
+	if err == nil || err.Error() != "Error response from daemon: Server error" {
23
+		t.Fatalf("expected a Server Error, got %v", err)
24
+	}
25
+}
26
+
27
+func TestImageInspectImageNotFound(t *testing.T) {
28
+	client := &Client{
29
+		transport: newMockClient(nil, errorMock(http.StatusNotFound, "Server error")),
30
+	}
31
+
32
+	_, _, err := client.ImageInspectWithRaw(context.Background(), "unknown")
33
+	if err == nil || !IsErrImageNotFound(err) {
34
+		t.Fatalf("expected an imageNotFound error, got %v", err)
35
+	}
36
+}
37
+
38
+func TestImageInspect(t *testing.T) {
39
+	expectedURL := "/images/image_id/json"
40
+	expectedTags := []string{"tag1", "tag2"}
41
+	client := &Client{
42
+		transport: newMockClient(nil, func(req *http.Request) (*http.Response, error) {
43
+			if !strings.HasPrefix(req.URL.Path, expectedURL) {
44
+				return nil, fmt.Errorf("Expected URL '%s', got '%s'", expectedURL, req.URL)
45
+			}
46
+			content, err := json.Marshal(types.ImageInspect{
47
+				ID:       "image_id",
48
+				RepoTags: expectedTags,
49
+			})
50
+			if err != nil {
51
+				return nil, err
52
+			}
53
+			return &http.Response{
54
+				StatusCode: http.StatusOK,
55
+				Body:       ioutil.NopCloser(bytes.NewReader(content)),
56
+			}, nil
57
+		}),
58
+	}
59
+
60
+	imageInspect, _, err := client.ImageInspectWithRaw(context.Background(), "image_id")
61
+	if err != nil {
62
+		t.Fatal(err)
63
+	}
64
+	if imageInspect.ID != "image_id" {
65
+		t.Fatalf("expected `image_id`, got %s", imageInspect.ID)
66
+	}
67
+	if !reflect.DeepEqual(imageInspect.RepoTags, expectedTags) {
68
+		t.Fatalf("expected `%v`, got %v", expectedTags, imageInspect.RepoTags)
69
+	}
70
+}
0 71
new file mode 100644
... ...
@@ -0,0 +1,40 @@
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
+	"golang.org/x/net/context"
9
+)
10
+
11
+// ImageList returns a list of images in the docker host.
12
+func (cli *Client) ImageList(ctx context.Context, options types.ImageListOptions) ([]types.Image, error) {
13
+	var images []types.Image
14
+	query := url.Values{}
15
+
16
+	if options.Filters.Len() > 0 {
17
+		filterJSON, err := filters.ToParamWithVersion(cli.version, options.Filters)
18
+		if err != nil {
19
+			return images, err
20
+		}
21
+		query.Set("filters", filterJSON)
22
+	}
23
+	if options.MatchName != "" {
24
+		// FIXME rename this parameter, to not be confused with the filters flag
25
+		query.Set("filter", options.MatchName)
26
+	}
27
+	if options.All {
28
+		query.Set("all", "1")
29
+	}
30
+
31
+	serverResp, err := cli.get(ctx, "/images/json", query, nil)
32
+	if err != nil {
33
+		return images, err
34
+	}
35
+
36
+	err = json.NewDecoder(serverResp.body).Decode(&images)
37
+	ensureReaderClosed(serverResp)
38
+	return images, err
39
+}
0 40
new file mode 100644
... ...
@@ -0,0 +1,122 @@
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
+	"golang.org/x/net/context"
14
+)
15
+
16
+func TestImageListError(t *testing.T) {
17
+	client := &Client{
18
+		transport: newMockClient(nil, errorMock(http.StatusInternalServerError, "Server error")),
19
+	}
20
+
21
+	_, err := client.ImageList(context.Background(), types.ImageListOptions{})
22
+	if err == nil || err.Error() != "Error response from daemon: Server error" {
23
+		t.Fatalf("expected a Server Error, got %v", err)
24
+	}
25
+}
26
+
27
+func TestImageList(t *testing.T) {
28
+	expectedURL := "/images/json"
29
+
30
+	noDanglingfilters := filters.NewArgs()
31
+	noDanglingfilters.Add("dangling", "false")
32
+
33
+	filters := filters.NewArgs()
34
+	filters.Add("label", "label1")
35
+	filters.Add("label", "label2")
36
+	filters.Add("dangling", "true")
37
+
38
+	listCases := []struct {
39
+		options             types.ImageListOptions
40
+		expectedQueryParams map[string]string
41
+	}{
42
+		{
43
+			options: types.ImageListOptions{},
44
+			expectedQueryParams: map[string]string{
45
+				"all":     "",
46
+				"filter":  "",
47
+				"filters": "",
48
+			},
49
+		},
50
+		{
51
+			options: types.ImageListOptions{
52
+				All:       true,
53
+				MatchName: "image_name",
54
+			},
55
+			expectedQueryParams: map[string]string{
56
+				"all":     "1",
57
+				"filter":  "image_name",
58
+				"filters": "",
59
+			},
60
+		},
61
+		{
62
+			options: types.ImageListOptions{
63
+				Filters: filters,
64
+			},
65
+			expectedQueryParams: map[string]string{
66
+				"all":     "",
67
+				"filter":  "",
68
+				"filters": `{"dangling":{"true":true},"label":{"label1":true,"label2":true}}`,
69
+			},
70
+		},
71
+		{
72
+			options: types.ImageListOptions{
73
+				Filters: noDanglingfilters,
74
+			},
75
+			expectedQueryParams: map[string]string{
76
+				"all":     "",
77
+				"filter":  "",
78
+				"filters": `{"dangling":{"false":true}}`,
79
+			},
80
+		},
81
+	}
82
+	for _, listCase := range listCases {
83
+		client := &Client{
84
+			transport: newMockClient(nil, func(req *http.Request) (*http.Response, error) {
85
+				if !strings.HasPrefix(req.URL.Path, expectedURL) {
86
+					return nil, fmt.Errorf("Expected URL '%s', got '%s'", expectedURL, req.URL)
87
+				}
88
+				query := req.URL.Query()
89
+				for key, expected := range listCase.expectedQueryParams {
90
+					actual := query.Get(key)
91
+					if actual != expected {
92
+						return nil, fmt.Errorf("%s not set in URL query properly. Expected '%s', got %s", key, expected, actual)
93
+					}
94
+				}
95
+				content, err := json.Marshal([]types.Image{
96
+					{
97
+						ID: "image_id2",
98
+					},
99
+					{
100
+						ID: "image_id2",
101
+					},
102
+				})
103
+				if err != nil {
104
+					return nil, err
105
+				}
106
+				return &http.Response{
107
+					StatusCode: http.StatusOK,
108
+					Body:       ioutil.NopCloser(bytes.NewReader(content)),
109
+				}, nil
110
+			}),
111
+		}
112
+
113
+		images, err := client.ImageList(context.Background(), listCase.options)
114
+		if err != nil {
115
+			t.Fatal(err)
116
+		}
117
+		if len(images) != 2 {
118
+			t.Fatalf("expected 2 images, got %v", images)
119
+		}
120
+	}
121
+}
0 122
new file mode 100644
... ...
@@ -0,0 +1,30 @@
0
+package client
1
+
2
+import (
3
+	"io"
4
+	"net/url"
5
+
6
+	"golang.org/x/net/context"
7
+
8
+	"github.com/docker/docker/api/types"
9
+)
10
+
11
+// ImageLoad loads an image in the docker host from the client host.
12
+// It's up to the caller to close the io.ReadCloser in the
13
+// ImageLoadResponse returned by this function.
14
+func (cli *Client) ImageLoad(ctx context.Context, input io.Reader, quiet bool) (types.ImageLoadResponse, error) {
15
+	v := url.Values{}
16
+	v.Set("quiet", "0")
17
+	if quiet {
18
+		v.Set("quiet", "1")
19
+	}
20
+	headers := map[string][]string{"Content-Type": {"application/x-tar"}}
21
+	resp, err := cli.postRaw(ctx, "/images/load", v, input, headers)
22
+	if err != nil {
23
+		return types.ImageLoadResponse{}, err
24
+	}
25
+	return types.ImageLoadResponse{
26
+		Body: resp.body,
27
+		JSON: resp.header.Get("Content-Type") == "application/json",
28
+	}, nil
29
+}
0 30
new file mode 100644
... ...
@@ -0,0 +1,95 @@
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 TestImageLoadError(t *testing.T) {
14
+	client := &Client{
15
+		transport: newMockClient(nil, errorMock(http.StatusInternalServerError, "Server error")),
16
+	}
17
+
18
+	_, err := client.ImageLoad(context.Background(), nil, true)
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 TestImageLoad(t *testing.T) {
25
+	expectedURL := "/images/load"
26
+	expectedInput := "inputBody"
27
+	expectedOutput := "outputBody"
28
+	loadCases := []struct {
29
+		quiet                bool
30
+		responseContentType  string
31
+		expectedResponseJSON bool
32
+		expectedQueryParams  map[string]string
33
+	}{
34
+		{
35
+			quiet:                false,
36
+			responseContentType:  "text/plain",
37
+			expectedResponseJSON: false,
38
+			expectedQueryParams: map[string]string{
39
+				"quiet": "0",
40
+			},
41
+		},
42
+		{
43
+			quiet:                true,
44
+			responseContentType:  "application/json",
45
+			expectedResponseJSON: true,
46
+			expectedQueryParams: map[string]string{
47
+				"quiet": "1",
48
+			},
49
+		},
50
+	}
51
+	for _, loadCase := range loadCases {
52
+		client := &Client{
53
+			transport: newMockClient(nil, func(req *http.Request) (*http.Response, error) {
54
+				if !strings.HasPrefix(req.URL.Path, expectedURL) {
55
+					return nil, fmt.Errorf("Expected URL '%s', got '%s'", expectedURL, req.URL)
56
+				}
57
+				contentType := req.Header.Get("Content-Type")
58
+				if contentType != "application/x-tar" {
59
+					return nil, fmt.Errorf("content-type not set in URL headers properly. Expected 'application/x-tar', got %s", contentType)
60
+				}
61
+				query := req.URL.Query()
62
+				for key, expected := range loadCase.expectedQueryParams {
63
+					actual := query.Get(key)
64
+					if actual != expected {
65
+						return nil, fmt.Errorf("%s not set in URL query properly. Expected '%s', got %s", key, expected, actual)
66
+					}
67
+				}
68
+				headers := http.Header{}
69
+				headers.Add("Content-Type", loadCase.responseContentType)
70
+				return &http.Response{
71
+					StatusCode: http.StatusOK,
72
+					Body:       ioutil.NopCloser(bytes.NewReader([]byte(expectedOutput))),
73
+					Header:     headers,
74
+				}, nil
75
+			}),
76
+		}
77
+
78
+		input := bytes.NewReader([]byte(expectedInput))
79
+		imageLoadResponse, err := client.ImageLoad(context.Background(), input, loadCase.quiet)
80
+		if err != nil {
81
+			t.Fatal(err)
82
+		}
83
+		if imageLoadResponse.JSON != loadCase.expectedResponseJSON {
84
+			t.Fatalf("expected a JSON response, was not.")
85
+		}
86
+		body, err := ioutil.ReadAll(imageLoadResponse.Body)
87
+		if err != nil {
88
+			t.Fatal(err)
89
+		}
90
+		if string(body) != expectedOutput {
91
+			t.Fatalf("expected %s, got %s", expectedOutput, string(body))
92
+		}
93
+	}
94
+}
0 95
new file mode 100644
... ...
@@ -0,0 +1,46 @@
0
+package client
1
+
2
+import (
3
+	"io"
4
+	"net/http"
5
+	"net/url"
6
+
7
+	"golang.org/x/net/context"
8
+
9
+	"github.com/docker/docker/api/types"
10
+	"github.com/docker/docker/api/types/reference"
11
+)
12
+
13
+// ImagePull requests the docker host to pull an image from a remote registry.
14
+// It executes the privileged function if the operation is unauthorized
15
+// and it tries one more time.
16
+// It's up to the caller to handle the io.ReadCloser and close it properly.
17
+//
18
+// FIXME(vdemeester): there is currently used in a few way in docker/docker
19
+// - if not in trusted content, ref is used to pass the whole reference, and tag is empty
20
+// - if in trusted content, ref is used to pass the reference name, and tag for the digest
21
+func (cli *Client) ImagePull(ctx context.Context, ref string, options types.ImagePullOptions) (io.ReadCloser, error) {
22
+	repository, tag, err := reference.Parse(ref)
23
+	if err != nil {
24
+		return nil, err
25
+	}
26
+
27
+	query := url.Values{}
28
+	query.Set("fromImage", repository)
29
+	if tag != "" && !options.All {
30
+		query.Set("tag", tag)
31
+	}
32
+
33
+	resp, err := cli.tryImageCreate(ctx, query, options.RegistryAuth)
34
+	if resp.statusCode == http.StatusUnauthorized && options.PrivilegeFunc != nil {
35
+		newAuthHeader, privilegeErr := options.PrivilegeFunc()
36
+		if privilegeErr != nil {
37
+			return nil, privilegeErr
38
+		}
39
+		resp, err = cli.tryImageCreate(ctx, query, newAuthHeader)
40
+	}
41
+	if err != nil {
42
+		return nil, err
43
+	}
44
+	return resp.body, nil
45
+}
0 46
new file mode 100644
... ...
@@ -0,0 +1,199 @@
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"
13
+)
14
+
15
+func TestImagePullReferenceParseError(t *testing.T) {
16
+	client := &Client{
17
+		transport: newMockClient(nil, func(req *http.Request) (*http.Response, error) {
18
+			return nil, nil
19
+		}),
20
+	}
21
+	// An empty reference is an invalid reference
22
+	_, err := client.ImagePull(context.Background(), "", types.ImagePullOptions{})
23
+	if err == nil || err.Error() != "repository name must have at least one component" {
24
+		t.Fatalf("expected an error, got %v", err)
25
+	}
26
+}
27
+
28
+func TestImagePullAnyError(t *testing.T) {
29
+	client := &Client{
30
+		transport: newMockClient(nil, errorMock(http.StatusInternalServerError, "Server error")),
31
+	}
32
+	_, err := client.ImagePull(context.Background(), "myimage", types.ImagePullOptions{})
33
+	if err == nil || err.Error() != "Error response from daemon: Server error" {
34
+		t.Fatalf("expected a Server Error, got %v", err)
35
+	}
36
+}
37
+
38
+func TestImagePullStatusUnauthorizedError(t *testing.T) {
39
+	client := &Client{
40
+		transport: newMockClient(nil, errorMock(http.StatusUnauthorized, "Unauthorized error")),
41
+	}
42
+	_, err := client.ImagePull(context.Background(), "myimage", types.ImagePullOptions{})
43
+	if err == nil || err.Error() != "Error response from daemon: Unauthorized error" {
44
+		t.Fatalf("expected an Unauthorized Error, got %v", err)
45
+	}
46
+}
47
+
48
+func TestImagePullWithUnauthorizedErrorAndPrivilegeFuncError(t *testing.T) {
49
+	client := &Client{
50
+		transport: newMockClient(nil, errorMock(http.StatusUnauthorized, "Unauthorized error")),
51
+	}
52
+	privilegeFunc := func() (string, error) {
53
+		return "", fmt.Errorf("Error requesting privilege")
54
+	}
55
+	_, err := client.ImagePull(context.Background(), "myimage", types.ImagePullOptions{
56
+		PrivilegeFunc: privilegeFunc,
57
+	})
58
+	if err == nil || err.Error() != "Error requesting privilege" {
59
+		t.Fatalf("expected an error requesting privilege, got %v", err)
60
+	}
61
+}
62
+
63
+func TestImagePullWithUnauthorizedErrorAndAnotherUnauthorizedError(t *testing.T) {
64
+	client := &Client{
65
+		transport: newMockClient(nil, errorMock(http.StatusUnauthorized, "Unauthorized error")),
66
+	}
67
+	privilegeFunc := func() (string, error) {
68
+		return "a-auth-header", nil
69
+	}
70
+	_, err := client.ImagePull(context.Background(), "myimage", types.ImagePullOptions{
71
+		PrivilegeFunc: privilegeFunc,
72
+	})
73
+	if err == nil || err.Error() != "Error response from daemon: Unauthorized error" {
74
+		t.Fatalf("expected an Unauthorized Error, got %v", err)
75
+	}
76
+}
77
+
78
+func TestImagePullWithPrivilegedFuncNoError(t *testing.T) {
79
+	expectedURL := "/images/create"
80
+	client := &Client{
81
+		transport: newMockClient(nil, func(req *http.Request) (*http.Response, error) {
82
+			if !strings.HasPrefix(req.URL.Path, expectedURL) {
83
+				return nil, fmt.Errorf("Expected URL '%s', got '%s'", expectedURL, req.URL)
84
+			}
85
+			auth := req.Header.Get("X-Registry-Auth")
86
+			if auth == "NotValid" {
87
+				return &http.Response{
88
+					StatusCode: http.StatusUnauthorized,
89
+					Body:       ioutil.NopCloser(bytes.NewReader([]byte("Invalid credentials"))),
90
+				}, nil
91
+			}
92
+			if auth != "IAmValid" {
93
+				return nil, fmt.Errorf("Invalid auth header : expected %s, got %s", "IAmValid", auth)
94
+			}
95
+			query := req.URL.Query()
96
+			fromImage := query.Get("fromImage")
97
+			if fromImage != "myimage" {
98
+				return nil, fmt.Errorf("fromimage not set in URL query properly. Expected '%s', got %s", "myimage", fromImage)
99
+			}
100
+			tag := query.Get("tag")
101
+			if tag != "latest" {
102
+				return nil, fmt.Errorf("tag not set in URL query properly. Expected '%s', got %s", "latest", tag)
103
+			}
104
+			return &http.Response{
105
+				StatusCode: http.StatusOK,
106
+				Body:       ioutil.NopCloser(bytes.NewReader([]byte("hello world"))),
107
+			}, nil
108
+		}),
109
+	}
110
+	privilegeFunc := func() (string, error) {
111
+		return "IAmValid", nil
112
+	}
113
+	resp, err := client.ImagePull(context.Background(), "myimage", types.ImagePullOptions{
114
+		RegistryAuth:  "NotValid",
115
+		PrivilegeFunc: privilegeFunc,
116
+	})
117
+	if err != nil {
118
+		t.Fatal(err)
119
+	}
120
+	body, err := ioutil.ReadAll(resp)
121
+	if err != nil {
122
+		t.Fatal(err)
123
+	}
124
+	if string(body) != "hello world" {
125
+		t.Fatalf("expected 'hello world', got %s", string(body))
126
+	}
127
+}
128
+
129
+func TestImagePullWithoutErrors(t *testing.T) {
130
+	expectedURL := "/images/create"
131
+	expectedOutput := "hello world"
132
+	pullCases := []struct {
133
+		all           bool
134
+		reference     string
135
+		expectedImage string
136
+		expectedTag   string
137
+	}{
138
+		{
139
+			all:           false,
140
+			reference:     "myimage",
141
+			expectedImage: "myimage",
142
+			expectedTag:   "latest",
143
+		},
144
+		{
145
+			all:           false,
146
+			reference:     "myimage:tag",
147
+			expectedImage: "myimage",
148
+			expectedTag:   "tag",
149
+		},
150
+		{
151
+			all:           true,
152
+			reference:     "myimage",
153
+			expectedImage: "myimage",
154
+			expectedTag:   "",
155
+		},
156
+		{
157
+			all:           true,
158
+			reference:     "myimage:anything",
159
+			expectedImage: "myimage",
160
+			expectedTag:   "",
161
+		},
162
+	}
163
+	for _, pullCase := range pullCases {
164
+		client := &Client{
165
+			transport: newMockClient(nil, func(req *http.Request) (*http.Response, error) {
166
+				if !strings.HasPrefix(req.URL.Path, expectedURL) {
167
+					return nil, fmt.Errorf("Expected URL '%s', got '%s'", expectedURL, req.URL)
168
+				}
169
+				query := req.URL.Query()
170
+				fromImage := query.Get("fromImage")
171
+				if fromImage != pullCase.expectedImage {
172
+					return nil, fmt.Errorf("fromimage not set in URL query properly. Expected '%s', got %s", pullCase.expectedImage, fromImage)
173
+				}
174
+				tag := query.Get("tag")
175
+				if tag != pullCase.expectedTag {
176
+					return nil, fmt.Errorf("tag not set in URL query properly. Expected '%s', got %s", pullCase.expectedTag, tag)
177
+				}
178
+				return &http.Response{
179
+					StatusCode: http.StatusOK,
180
+					Body:       ioutil.NopCloser(bytes.NewReader([]byte(expectedOutput))),
181
+				}, nil
182
+			}),
183
+		}
184
+		resp, err := client.ImagePull(context.Background(), pullCase.reference, types.ImagePullOptions{
185
+			All: pullCase.all,
186
+		})
187
+		if err != nil {
188
+			t.Fatal(err)
189
+		}
190
+		body, err := ioutil.ReadAll(resp)
191
+		if err != nil {
192
+			t.Fatal(err)
193
+		}
194
+		if string(body) != expectedOutput {
195
+			t.Fatalf("expected '%s', got %s", expectedOutput, string(body))
196
+		}
197
+	}
198
+}
0 199
new file mode 100644
... ...
@@ -0,0 +1,54 @@
0
+package client
1
+
2
+import (
3
+	"errors"
4
+	"io"
5
+	"net/http"
6
+	"net/url"
7
+
8
+	"golang.org/x/net/context"
9
+
10
+	distreference "github.com/docker/distribution/reference"
11
+	"github.com/docker/docker/api/types"
12
+)
13
+
14
+// ImagePush requests the docker host to push an image to a remote registry.
15
+// It executes the privileged function if the operation is unauthorized
16
+// and it tries one more time.
17
+// It's up to the caller to handle the io.ReadCloser and close it properly.
18
+func (cli *Client) ImagePush(ctx context.Context, ref string, options types.ImagePushOptions) (io.ReadCloser, error) {
19
+	distributionRef, err := distreference.ParseNamed(ref)
20
+	if err != nil {
21
+		return nil, err
22
+	}
23
+
24
+	if _, isCanonical := distributionRef.(distreference.Canonical); isCanonical {
25
+		return nil, errors.New("cannot push a digest reference")
26
+	}
27
+
28
+	var tag = ""
29
+	if nameTaggedRef, isNamedTagged := distributionRef.(distreference.NamedTagged); isNamedTagged {
30
+		tag = nameTaggedRef.Tag()
31
+	}
32
+
33
+	query := url.Values{}
34
+	query.Set("tag", tag)
35
+
36
+	resp, err := cli.tryImagePush(ctx, distributionRef.Name(), query, options.RegistryAuth)
37
+	if resp.statusCode == http.StatusUnauthorized && options.PrivilegeFunc != nil {
38
+		newAuthHeader, privilegeErr := options.PrivilegeFunc()
39
+		if privilegeErr != nil {
40
+			return nil, privilegeErr
41
+		}
42
+		resp, err = cli.tryImagePush(ctx, distributionRef.Name(), query, newAuthHeader)
43
+	}
44
+	if err != nil {
45
+		return nil, err
46
+	}
47
+	return resp.body, nil
48
+}
49
+
50
+func (cli *Client) tryImagePush(ctx context.Context, imageID string, query url.Values, registryAuth string) (serverResponse, error) {
51
+	headers := map[string][]string{"X-Registry-Auth": {registryAuth}}
52
+	return cli.post(ctx, "/images/"+imageID+"/push", query, nil, headers)
53
+}
0 54
new file mode 100644
... ...
@@ -0,0 +1,180 @@
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"
13
+)
14
+
15
+func TestImagePushReferenceError(t *testing.T) {
16
+	client := &Client{
17
+		transport: newMockClient(nil, func(req *http.Request) (*http.Response, error) {
18
+			return nil, nil
19
+		}),
20
+	}
21
+	// An empty reference is an invalid reference
22
+	_, err := client.ImagePush(context.Background(), "", types.ImagePushOptions{})
23
+	if err == nil || err.Error() != "repository name must have at least one component" {
24
+		t.Fatalf("expected an error, got %v", err)
25
+	}
26
+	// An canonical reference cannot be pushed
27
+	_, err = client.ImagePush(context.Background(), "repo@sha256:ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff", types.ImagePushOptions{})
28
+	if err == nil || err.Error() != "cannot push a digest reference" {
29
+		t.Fatalf("expected an error, got %v", err)
30
+	}
31
+}
32
+
33
+func TestImagePushAnyError(t *testing.T) {
34
+	client := &Client{
35
+		transport: newMockClient(nil, errorMock(http.StatusInternalServerError, "Server error")),
36
+	}
37
+	_, err := client.ImagePush(context.Background(), "myimage", types.ImagePushOptions{})
38
+	if err == nil || err.Error() != "Error response from daemon: Server error" {
39
+		t.Fatalf("expected a Server Error, got %v", err)
40
+	}
41
+}
42
+
43
+func TestImagePushStatusUnauthorizedError(t *testing.T) {
44
+	client := &Client{
45
+		transport: newMockClient(nil, errorMock(http.StatusUnauthorized, "Unauthorized error")),
46
+	}
47
+	_, err := client.ImagePush(context.Background(), "myimage", types.ImagePushOptions{})
48
+	if err == nil || err.Error() != "Error response from daemon: Unauthorized error" {
49
+		t.Fatalf("expected an Unauthorized Error, got %v", err)
50
+	}
51
+}
52
+
53
+func TestImagePushWithUnauthorizedErrorAndPrivilegeFuncError(t *testing.T) {
54
+	client := &Client{
55
+		transport: newMockClient(nil, errorMock(http.StatusUnauthorized, "Unauthorized error")),
56
+	}
57
+	privilegeFunc := func() (string, error) {
58
+		return "", fmt.Errorf("Error requesting privilege")
59
+	}
60
+	_, err := client.ImagePush(context.Background(), "myimage", types.ImagePushOptions{
61
+		PrivilegeFunc: privilegeFunc,
62
+	})
63
+	if err == nil || err.Error() != "Error requesting privilege" {
64
+		t.Fatalf("expected an error requesting privilege, got %v", err)
65
+	}
66
+}
67
+
68
+func TestImagePushWithUnauthorizedErrorAndAnotherUnauthorizedError(t *testing.T) {
69
+	client := &Client{
70
+		transport: newMockClient(nil, errorMock(http.StatusUnauthorized, "Unauthorized error")),
71
+	}
72
+	privilegeFunc := func() (string, error) {
73
+		return "a-auth-header", nil
74
+	}
75
+	_, err := client.ImagePush(context.Background(), "myimage", types.ImagePushOptions{
76
+		PrivilegeFunc: privilegeFunc,
77
+	})
78
+	if err == nil || err.Error() != "Error response from daemon: Unauthorized error" {
79
+		t.Fatalf("expected an Unauthorized Error, got %v", err)
80
+	}
81
+}
82
+
83
+func TestImagePushWithPrivilegedFuncNoError(t *testing.T) {
84
+	expectedURL := "/images/myimage/push"
85
+	client := &Client{
86
+		transport: newMockClient(nil, func(req *http.Request) (*http.Response, error) {
87
+			if !strings.HasPrefix(req.URL.Path, expectedURL) {
88
+				return nil, fmt.Errorf("Expected URL '%s', got '%s'", expectedURL, req.URL)
89
+			}
90
+			auth := req.Header.Get("X-Registry-Auth")
91
+			if auth == "NotValid" {
92
+				return &http.Response{
93
+					StatusCode: http.StatusUnauthorized,
94
+					Body:       ioutil.NopCloser(bytes.NewReader([]byte("Invalid credentials"))),
95
+				}, nil
96
+			}
97
+			if auth != "IAmValid" {
98
+				return nil, fmt.Errorf("Invalid auth header : expected %s, got %s", "IAmValid", auth)
99
+			}
100
+			query := req.URL.Query()
101
+			tag := query.Get("tag")
102
+			if tag != "tag" {
103
+				return nil, fmt.Errorf("tag not set in URL query properly. Expected '%s', got %s", "tag", tag)
104
+			}
105
+			return &http.Response{
106
+				StatusCode: http.StatusOK,
107
+				Body:       ioutil.NopCloser(bytes.NewReader([]byte("hello world"))),
108
+			}, nil
109
+		}),
110
+	}
111
+	privilegeFunc := func() (string, error) {
112
+		return "IAmValid", nil
113
+	}
114
+	resp, err := client.ImagePush(context.Background(), "myimage:tag", types.ImagePushOptions{
115
+		RegistryAuth:  "NotValid",
116
+		PrivilegeFunc: privilegeFunc,
117
+	})
118
+	if err != nil {
119
+		t.Fatal(err)
120
+	}
121
+	body, err := ioutil.ReadAll(resp)
122
+	if err != nil {
123
+		t.Fatal(err)
124
+	}
125
+	if string(body) != "hello world" {
126
+		t.Fatalf("expected 'hello world', got %s", string(body))
127
+	}
128
+}
129
+
130
+func TestImagePushWithoutErrors(t *testing.T) {
131
+	expectedOutput := "hello world"
132
+	expectedURLFormat := "/images/%s/push"
133
+	pullCases := []struct {
134
+		reference     string
135
+		expectedImage string
136
+		expectedTag   string
137
+	}{
138
+		{
139
+			reference:     "myimage",
140
+			expectedImage: "myimage",
141
+			expectedTag:   "",
142
+		},
143
+		{
144
+			reference:     "myimage:tag",
145
+			expectedImage: "myimage",
146
+			expectedTag:   "tag",
147
+		},
148
+	}
149
+	for _, pullCase := range pullCases {
150
+		client := &Client{
151
+			transport: newMockClient(nil, func(req *http.Request) (*http.Response, error) {
152
+				expectedURL := fmt.Sprintf(expectedURLFormat, pullCase.expectedImage)
153
+				if !strings.HasPrefix(req.URL.Path, expectedURL) {
154
+					return nil, fmt.Errorf("Expected URL '%s', got '%s'", expectedURL, req.URL)
155
+				}
156
+				query := req.URL.Query()
157
+				tag := query.Get("tag")
158
+				if tag != pullCase.expectedTag {
159
+					return nil, fmt.Errorf("tag not set in URL query properly. Expected '%s', got %s", pullCase.expectedTag, tag)
160
+				}
161
+				return &http.Response{
162
+					StatusCode: http.StatusOK,
163
+					Body:       ioutil.NopCloser(bytes.NewReader([]byte(expectedOutput))),
164
+				}, nil
165
+			}),
166
+		}
167
+		resp, err := client.ImagePush(context.Background(), pullCase.reference, types.ImagePushOptions{})
168
+		if err != nil {
169
+			t.Fatal(err)
170
+		}
171
+		body, err := ioutil.ReadAll(resp)
172
+		if err != nil {
173
+			t.Fatal(err)
174
+		}
175
+		if string(body) != expectedOutput {
176
+			t.Fatalf("expected '%s', got %s", expectedOutput, string(body))
177
+		}
178
+	}
179
+}
0 180
new file mode 100644
... ...
@@ -0,0 +1,31 @@
0
+package client
1
+
2
+import (
3
+	"encoding/json"
4
+	"net/url"
5
+
6
+	"github.com/docker/docker/api/types"
7
+	"golang.org/x/net/context"
8
+)
9
+
10
+// ImageRemove removes an image from the docker host.
11
+func (cli *Client) ImageRemove(ctx context.Context, imageID string, options types.ImageRemoveOptions) ([]types.ImageDelete, error) {
12
+	query := url.Values{}
13
+
14
+	if options.Force {
15
+		query.Set("force", "1")
16
+	}
17
+	if !options.PruneChildren {
18
+		query.Set("noprune", "1")
19
+	}
20
+
21
+	resp, err := cli.delete(ctx, "/images/"+imageID, query, nil)
22
+	if err != nil {
23
+		return nil, err
24
+	}
25
+
26
+	var dels []types.ImageDelete
27
+	err = json.NewDecoder(resp.body).Decode(&dels)
28
+	ensureReaderClosed(resp)
29
+	return dels, err
30
+}
0 31
new file mode 100644
... ...
@@ -0,0 +1,95 @@
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
+	"golang.org/x/net/context"
13
+)
14
+
15
+func TestImageRemoveError(t *testing.T) {
16
+	client := &Client{
17
+		transport: newMockClient(nil, errorMock(http.StatusInternalServerError, "Server error")),
18
+	}
19
+
20
+	_, err := client.ImageRemove(context.Background(), "image_id", types.ImageRemoveOptions{})
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 TestImageRemove(t *testing.T) {
27
+	expectedURL := "/images/image_id"
28
+	removeCases := []struct {
29
+		force               bool
30
+		pruneChildren       bool
31
+		expectedQueryParams map[string]string
32
+	}{
33
+		{
34
+			force:         false,
35
+			pruneChildren: false,
36
+			expectedQueryParams: map[string]string{
37
+				"force":   "",
38
+				"noprune": "1",
39
+			},
40
+		}, {
41
+			force:         true,
42
+			pruneChildren: true,
43
+			expectedQueryParams: map[string]string{
44
+				"force":   "1",
45
+				"noprune": "",
46
+			},
47
+		},
48
+	}
49
+	for _, removeCase := range removeCases {
50
+		client := &Client{
51
+			transport: newMockClient(nil, func(req *http.Request) (*http.Response, error) {
52
+				if !strings.HasPrefix(req.URL.Path, expectedURL) {
53
+					return nil, fmt.Errorf("expected URL '%s', got '%s'", expectedURL, req.URL)
54
+				}
55
+				if req.Method != "DELETE" {
56
+					return nil, fmt.Errorf("expected DELETE method, got %s", req.Method)
57
+				}
58
+				query := req.URL.Query()
59
+				for key, expected := range removeCase.expectedQueryParams {
60
+					actual := query.Get(key)
61
+					if actual != expected {
62
+						return nil, fmt.Errorf("%s not set in URL query properly. Expected '%s', got %s", key, expected, actual)
63
+					}
64
+				}
65
+				b, err := json.Marshal([]types.ImageDelete{
66
+					{
67
+						Untagged: "image_id1",
68
+					},
69
+					{
70
+						Deleted: "image_id",
71
+					},
72
+				})
73
+				if err != nil {
74
+					return nil, err
75
+				}
76
+
77
+				return &http.Response{
78
+					StatusCode: http.StatusOK,
79
+					Body:       ioutil.NopCloser(bytes.NewReader(b)),
80
+				}, nil
81
+			}),
82
+		}
83
+		imageDeletes, err := client.ImageRemove(context.Background(), "image_id", types.ImageRemoveOptions{
84
+			Force:         removeCase.force,
85
+			PruneChildren: removeCase.pruneChildren,
86
+		})
87
+		if err != nil {
88
+			t.Fatal(err)
89
+		}
90
+		if len(imageDeletes) != 2 {
91
+			t.Fatalf("expected 2 deleted images, got %v", imageDeletes)
92
+		}
93
+	}
94
+}
0 95
new file mode 100644
... ...
@@ -0,0 +1,22 @@
0
+package client
1
+
2
+import (
3
+	"io"
4
+	"net/url"
5
+
6
+	"golang.org/x/net/context"
7
+)
8
+
9
+// ImageSave retrieves one or more images from the docker host as an io.ReadCloser.
10
+// It's up to the caller to store the images and close the stream.
11
+func (cli *Client) ImageSave(ctx context.Context, imageIDs []string) (io.ReadCloser, error) {
12
+	query := url.Values{
13
+		"names": imageIDs,
14
+	}
15
+
16
+	resp, err := cli.get(ctx, "/images/get", query, nil)
17
+	if err != nil {
18
+		return nil, err
19
+	}
20
+	return resp.body, nil
21
+}
0 22
new file mode 100644
... ...
@@ -0,0 +1,58 @@
0
+package client
1
+
2
+import (
3
+	"bytes"
4
+	"fmt"
5
+	"io/ioutil"
6
+	"net/http"
7
+	"reflect"
8
+	"testing"
9
+
10
+	"golang.org/x/net/context"
11
+
12
+	"strings"
13
+)
14
+
15
+func TestImageSaveError(t *testing.T) {
16
+	client := &Client{
17
+		transport: newMockClient(nil, errorMock(http.StatusInternalServerError, "Server error")),
18
+	}
19
+	_, err := client.ImageSave(context.Background(), []string{"nothing"})
20
+	if err == nil || err.Error() != "Error response from daemon: Server error" {
21
+		t.Fatalf("expected a Server error, got %v", err)
22
+	}
23
+}
24
+
25
+func TestImageSave(t *testing.T) {
26
+	expectedURL := "/images/get"
27
+	client := &Client{
28
+		transport: newMockClient(nil, func(r *http.Request) (*http.Response, error) {
29
+			if !strings.HasPrefix(r.URL.Path, expectedURL) {
30
+				return nil, fmt.Errorf("Expected URL '%s', got '%s'", expectedURL, r.URL)
31
+			}
32
+			query := r.URL.Query()
33
+			names := query["names"]
34
+			expectedNames := []string{"image_id1", "image_id2"}
35
+			if !reflect.DeepEqual(names, expectedNames) {
36
+				return nil, fmt.Errorf("names not set in URL query properly. Expected %v, got %v", names, expectedNames)
37
+			}
38
+
39
+			return &http.Response{
40
+				StatusCode: http.StatusOK,
41
+				Body:       ioutil.NopCloser(bytes.NewReader([]byte("response"))),
42
+			}, nil
43
+		}),
44
+	}
45
+	saveResponse, err := client.ImageSave(context.Background(), []string{"image_id1", "image_id2"})
46
+	if err != nil {
47
+		t.Fatal(err)
48
+	}
49
+	response, err := ioutil.ReadAll(saveResponse)
50
+	if err != nil {
51
+		t.Fatal(err)
52
+	}
53
+	saveResponse.Close()
54
+	if string(response) != "response" {
55
+		t.Fatalf("expected response to contain 'response', got %s", string(response))
56
+	}
57
+}
0 58
new file mode 100644
... ...
@@ -0,0 +1,51 @@
0
+package client
1
+
2
+import (
3
+	"encoding/json"
4
+	"fmt"
5
+	"net/http"
6
+	"net/url"
7
+
8
+	"github.com/docker/docker/api/types"
9
+	"github.com/docker/docker/api/types/filters"
10
+	"github.com/docker/docker/api/types/registry"
11
+	"golang.org/x/net/context"
12
+)
13
+
14
+// ImageSearch makes the docker host to search by a term in a remote registry.
15
+// The list of results is not sorted in any fashion.
16
+func (cli *Client) ImageSearch(ctx context.Context, term string, options types.ImageSearchOptions) ([]registry.SearchResult, error) {
17
+	var results []registry.SearchResult
18
+	query := url.Values{}
19
+	query.Set("term", term)
20
+	query.Set("limit", fmt.Sprintf("%d", options.Limit))
21
+
22
+	if options.Filters.Len() > 0 {
23
+		filterJSON, err := filters.ToParam(options.Filters)
24
+		if err != nil {
25
+			return results, err
26
+		}
27
+		query.Set("filters", filterJSON)
28
+	}
29
+
30
+	resp, err := cli.tryImageSearch(ctx, query, options.RegistryAuth)
31
+	if resp.statusCode == http.StatusUnauthorized && options.PrivilegeFunc != nil {
32
+		newAuthHeader, privilegeErr := options.PrivilegeFunc()
33
+		if privilegeErr != nil {
34
+			return results, privilegeErr
35
+		}
36
+		resp, err = cli.tryImageSearch(ctx, query, newAuthHeader)
37
+	}
38
+	if err != nil {
39
+		return results, err
40
+	}
41
+
42
+	err = json.NewDecoder(resp.body).Decode(&results)
43
+	ensureReaderClosed(resp)
44
+	return results, err
45
+}
46
+
47
+func (cli *Client) tryImageSearch(ctx context.Context, query url.Values, registryAuth string) (serverResponse, error) {
48
+	headers := map[string][]string{"X-Registry-Auth": {registryAuth}}
49
+	return cli.get(ctx, "/images/search", query, headers)
50
+}
0 51
new file mode 100644
... ...
@@ -0,0 +1,165 @@
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
+	"encoding/json"
13
+	"github.com/docker/docker/api/types"
14
+	"github.com/docker/docker/api/types/filters"
15
+	"github.com/docker/docker/api/types/registry"
16
+)
17
+
18
+func TestImageSearchAnyError(t *testing.T) {
19
+	client := &Client{
20
+		transport: newMockClient(nil, errorMock(http.StatusInternalServerError, "Server error")),
21
+	}
22
+	_, err := client.ImageSearch(context.Background(), "some-image", types.ImageSearchOptions{})
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 TestImageSearchStatusUnauthorizedError(t *testing.T) {
29
+	client := &Client{
30
+		transport: newMockClient(nil, errorMock(http.StatusUnauthorized, "Unauthorized error")),
31
+	}
32
+	_, err := client.ImageSearch(context.Background(), "some-image", types.ImageSearchOptions{})
33
+	if err == nil || err.Error() != "Error response from daemon: Unauthorized error" {
34
+		t.Fatalf("expected an Unauthorized Error, got %v", err)
35
+	}
36
+}
37
+
38
+func TestImageSearchWithUnauthorizedErrorAndPrivilegeFuncError(t *testing.T) {
39
+	client := &Client{
40
+		transport: newMockClient(nil, errorMock(http.StatusUnauthorized, "Unauthorized error")),
41
+	}
42
+	privilegeFunc := func() (string, error) {
43
+		return "", fmt.Errorf("Error requesting privilege")
44
+	}
45
+	_, err := client.ImageSearch(context.Background(), "some-image", types.ImageSearchOptions{
46
+		PrivilegeFunc: privilegeFunc,
47
+	})
48
+	if err == nil || err.Error() != "Error requesting privilege" {
49
+		t.Fatalf("expected an error requesting privilege, got %v", err)
50
+	}
51
+}
52
+
53
+func TestImageSearchWithUnauthorizedErrorAndAnotherUnauthorizedError(t *testing.T) {
54
+	client := &Client{
55
+		transport: newMockClient(nil, errorMock(http.StatusUnauthorized, "Unauthorized error")),
56
+	}
57
+	privilegeFunc := func() (string, error) {
58
+		return "a-auth-header", nil
59
+	}
60
+	_, err := client.ImageSearch(context.Background(), "some-image", types.ImageSearchOptions{
61
+		PrivilegeFunc: privilegeFunc,
62
+	})
63
+	if err == nil || err.Error() != "Error response from daemon: Unauthorized error" {
64
+		t.Fatalf("expected an Unauthorized Error, got %v", err)
65
+	}
66
+}
67
+
68
+func TestImageSearchWithPrivilegedFuncNoError(t *testing.T) {
69
+	expectedURL := "/images/search"
70
+	client := &Client{
71
+		transport: newMockClient(nil, func(req *http.Request) (*http.Response, error) {
72
+			if !strings.HasPrefix(req.URL.Path, expectedURL) {
73
+				return nil, fmt.Errorf("Expected URL '%s', got '%s'", expectedURL, req.URL)
74
+			}
75
+			auth := req.Header.Get("X-Registry-Auth")
76
+			if auth == "NotValid" {
77
+				return &http.Response{
78
+					StatusCode: http.StatusUnauthorized,
79
+					Body:       ioutil.NopCloser(bytes.NewReader([]byte("Invalid credentials"))),
80
+				}, nil
81
+			}
82
+			if auth != "IAmValid" {
83
+				return nil, fmt.Errorf("Invalid auth header : expected %s, got %s", "IAmValid", auth)
84
+			}
85
+			query := req.URL.Query()
86
+			term := query.Get("term")
87
+			if term != "some-image" {
88
+				return nil, fmt.Errorf("tag not set in URL query properly. Expected '%s', got %s", "some-image", term)
89
+			}
90
+			content, err := json.Marshal([]registry.SearchResult{
91
+				{
92
+					Name: "anything",
93
+				},
94
+			})
95
+			if err != nil {
96
+				return nil, err
97
+			}
98
+			return &http.Response{
99
+				StatusCode: http.StatusOK,
100
+				Body:       ioutil.NopCloser(bytes.NewReader(content)),
101
+			}, nil
102
+		}),
103
+	}
104
+	privilegeFunc := func() (string, error) {
105
+		return "IAmValid", nil
106
+	}
107
+	results, err := client.ImageSearch(context.Background(), "some-image", types.ImageSearchOptions{
108
+		RegistryAuth:  "NotValid",
109
+		PrivilegeFunc: privilegeFunc,
110
+	})
111
+	if err != nil {
112
+		t.Fatal(err)
113
+	}
114
+	if len(results) != 1 {
115
+		t.Fatalf("expected a result, got %v", results)
116
+	}
117
+}
118
+
119
+func TestImageSearchWithoutErrors(t *testing.T) {
120
+	expectedURL := "/images/search"
121
+	filterArgs := filters.NewArgs()
122
+	filterArgs.Add("is-automated", "true")
123
+	filterArgs.Add("stars", "3")
124
+
125
+	expectedFilters := `{"is-automated":{"true":true},"stars":{"3":true}}`
126
+
127
+	client := &Client{
128
+		transport: newMockClient(nil, func(req *http.Request) (*http.Response, error) {
129
+			if !strings.HasPrefix(req.URL.Path, expectedURL) {
130
+				return nil, fmt.Errorf("Expected URL '%s', got '%s'", expectedURL, req.URL)
131
+			}
132
+			query := req.URL.Query()
133
+			term := query.Get("term")
134
+			if term != "some-image" {
135
+				return nil, fmt.Errorf("tag not set in URL query properly. Expected '%s', got %s", "some-image", term)
136
+			}
137
+			filters := query.Get("filters")
138
+			if filters != expectedFilters {
139
+				return nil, fmt.Errorf("filters not set in URL query properly. Expected '%s', got %s", expectedFilters, filters)
140
+			}
141
+			content, err := json.Marshal([]registry.SearchResult{
142
+				{
143
+					Name: "anything",
144
+				},
145
+			})
146
+			if err != nil {
147
+				return nil, err
148
+			}
149
+			return &http.Response{
150
+				StatusCode: http.StatusOK,
151
+				Body:       ioutil.NopCloser(bytes.NewReader(content)),
152
+			}, nil
153
+		}),
154
+	}
155
+	results, err := client.ImageSearch(context.Background(), "some-image", types.ImageSearchOptions{
156
+		Filters: filterArgs,
157
+	})
158
+	if err != nil {
159
+		t.Fatal(err)
160
+	}
161
+	if len(results) != 1 {
162
+		t.Fatalf("expected a result, got %v", results)
163
+	}
164
+}
0 165
new file mode 100644
... ...
@@ -0,0 +1,34 @@
0
+package client
1
+
2
+import (
3
+	"errors"
4
+	"fmt"
5
+	"net/url"
6
+
7
+	"golang.org/x/net/context"
8
+
9
+	distreference "github.com/docker/distribution/reference"
10
+	"github.com/docker/docker/api/types/reference"
11
+)
12
+
13
+// ImageTag tags an image in the docker host
14
+func (cli *Client) ImageTag(ctx context.Context, imageID, ref string) error {
15
+	distributionRef, err := distreference.ParseNamed(ref)
16
+	if err != nil {
17
+		return fmt.Errorf("Error parsing reference: %q is not a valid repository/tag", ref)
18
+	}
19
+
20
+	if _, isCanonical := distributionRef.(distreference.Canonical); isCanonical {
21
+		return errors.New("refusing to create a tag with a digest reference")
22
+	}
23
+
24
+	tag := reference.GetTagFromNamedRef(distributionRef)
25
+
26
+	query := url.Values{}
27
+	query.Set("repo", distributionRef.Name())
28
+	query.Set("tag", tag)
29
+
30
+	resp, err := cli.post(ctx, "/images/"+imageID+"/tag", query, nil, nil)
31
+	ensureReaderClosed(resp)
32
+	return err
33
+}
0 34
new file mode 100644
... ...
@@ -0,0 +1,121 @@
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 TestImageTagError(t *testing.T) {
14
+	client := &Client{
15
+		transport: newMockClient(nil, errorMock(http.StatusInternalServerError, "Server error")),
16
+	}
17
+
18
+	err := client.ImageTag(context.Background(), "image_id", "repo:tag")
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
+// Note: this is not testing all the InvalidReference as it's the reponsability
25
+// of distribution/reference package.
26
+func TestImageTagInvalidReference(t *testing.T) {
27
+	client := &Client{
28
+		transport: newMockClient(nil, errorMock(http.StatusInternalServerError, "Server error")),
29
+	}
30
+
31
+	err := client.ImageTag(context.Background(), "image_id", "aa/asdf$$^/aa")
32
+	if err == nil || err.Error() != `Error parsing reference: "aa/asdf$$^/aa" is not a valid repository/tag` {
33
+		t.Fatalf("expected ErrReferenceInvalidFormat, got %v", err)
34
+	}
35
+}
36
+
37
+func TestImageTag(t *testing.T) {
38
+	expectedURL := "/images/image_id/tag"
39
+	tagCases := []struct {
40
+		reference           string
41
+		expectedQueryParams map[string]string
42
+	}{
43
+		{
44
+			reference: "repository:tag1",
45
+			expectedQueryParams: map[string]string{
46
+				"repo": "repository",
47
+				"tag":  "tag1",
48
+			},
49
+		}, {
50
+			reference: "another_repository:latest",
51
+			expectedQueryParams: map[string]string{
52
+				"repo": "another_repository",
53
+				"tag":  "latest",
54
+			},
55
+		}, {
56
+			reference: "another_repository",
57
+			expectedQueryParams: map[string]string{
58
+				"repo": "another_repository",
59
+				"tag":  "latest",
60
+			},
61
+		}, {
62
+			reference: "test/another_repository",
63
+			expectedQueryParams: map[string]string{
64
+				"repo": "test/another_repository",
65
+				"tag":  "latest",
66
+			},
67
+		}, {
68
+			reference: "test/another_repository:tag1",
69
+			expectedQueryParams: map[string]string{
70
+				"repo": "test/another_repository",
71
+				"tag":  "tag1",
72
+			},
73
+		}, {
74
+			reference: "test/test/another_repository:tag1",
75
+			expectedQueryParams: map[string]string{
76
+				"repo": "test/test/another_repository",
77
+				"tag":  "tag1",
78
+			},
79
+		}, {
80
+			reference: "test:5000/test/another_repository:tag1",
81
+			expectedQueryParams: map[string]string{
82
+				"repo": "test:5000/test/another_repository",
83
+				"tag":  "tag1",
84
+			},
85
+		}, {
86
+			reference: "test:5000/test/another_repository",
87
+			expectedQueryParams: map[string]string{
88
+				"repo": "test:5000/test/another_repository",
89
+				"tag":  "latest",
90
+			},
91
+		},
92
+	}
93
+	for _, tagCase := range tagCases {
94
+		client := &Client{
95
+			transport: newMockClient(nil, func(req *http.Request) (*http.Response, error) {
96
+				if !strings.HasPrefix(req.URL.Path, expectedURL) {
97
+					return nil, fmt.Errorf("expected URL '%s', got '%s'", expectedURL, req.URL)
98
+				}
99
+				if req.Method != "POST" {
100
+					return nil, fmt.Errorf("expected POST method, got %s", req.Method)
101
+				}
102
+				query := req.URL.Query()
103
+				for key, expected := range tagCase.expectedQueryParams {
104
+					actual := query.Get(key)
105
+					if actual != expected {
106
+						return nil, fmt.Errorf("%s not set in URL query properly. Expected '%s', got %s", key, expected, actual)
107
+					}
108
+				}
109
+				return &http.Response{
110
+					StatusCode: http.StatusOK,
111
+					Body:       ioutil.NopCloser(bytes.NewReader([]byte(""))),
112
+				}, nil
113
+			}),
114
+		}
115
+		err := client.ImageTag(context.Background(), "image_id", tagCase.reference)
116
+		if err != nil {
117
+			t.Fatal(err)
118
+		}
119
+	}
120
+}
0 121
new file mode 100644
... ...
@@ -0,0 +1,26 @@
0
+package client
1
+
2
+import (
3
+	"encoding/json"
4
+	"fmt"
5
+	"net/url"
6
+
7
+	"github.com/docker/docker/api/types"
8
+	"golang.org/x/net/context"
9
+)
10
+
11
+// Info returns information about the docker server.
12
+func (cli *Client) Info(ctx context.Context) (types.Info, error) {
13
+	var info types.Info
14
+	serverResp, err := cli.get(ctx, "/info", url.Values{}, nil)
15
+	if err != nil {
16
+		return info, err
17
+	}
18
+	defer ensureReaderClosed(serverResp)
19
+
20
+	if err := json.NewDecoder(serverResp.body).Decode(&info); err != nil {
21
+		return info, fmt.Errorf("Error reading remote info: %v", err)
22
+	}
23
+
24
+	return info, nil
25
+}
0 26
new file mode 100644
... ...
@@ -0,0 +1,76 @@
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
+	"golang.org/x/net/context"
13
+)
14
+
15
+func TestInfoServerError(t *testing.T) {
16
+	client := &Client{
17
+		transport: newMockClient(nil, errorMock(http.StatusInternalServerError, "Server error")),
18
+	}
19
+	_, err := client.Info(context.Background())
20
+	if err == nil || err.Error() != "Error response from daemon: Server error" {
21
+		t.Fatalf("expected a Server Error, got %v", err)
22
+	}
23
+}
24
+
25
+func TestInfoInvalidResponseJSONError(t *testing.T) {
26
+	client := &Client{
27
+		transport: newMockClient(nil, func(req *http.Request) (*http.Response, error) {
28
+			return &http.Response{
29
+				StatusCode: http.StatusOK,
30
+				Body:       ioutil.NopCloser(bytes.NewReader([]byte("invalid json"))),
31
+			}, nil
32
+		}),
33
+	}
34
+	_, err := client.Info(context.Background())
35
+	if err == nil || !strings.Contains(err.Error(), "invalid character") {
36
+		t.Fatalf("expected a 'invalid character' error, got %v", err)
37
+	}
38
+}
39
+
40
+func TestInfo(t *testing.T) {
41
+	expectedURL := "/info"
42
+	client := &Client{
43
+		transport: newMockClient(nil, func(req *http.Request) (*http.Response, error) {
44
+			if !strings.HasPrefix(req.URL.Path, expectedURL) {
45
+				return nil, fmt.Errorf("Expected URL '%s', got '%s'", expectedURL, req.URL)
46
+			}
47
+			info := &types.Info{
48
+				ID:         "daemonID",
49
+				Containers: 3,
50
+			}
51
+			b, err := json.Marshal(info)
52
+			if err != nil {
53
+				return nil, err
54
+			}
55
+
56
+			return &http.Response{
57
+				StatusCode: http.StatusOK,
58
+				Body:       ioutil.NopCloser(bytes.NewReader(b)),
59
+			}, nil
60
+		}),
61
+	}
62
+
63
+	info, err := client.Info(context.Background())
64
+	if err != nil {
65
+		t.Fatal(err)
66
+	}
67
+
68
+	if info.ID != "daemonID" {
69
+		t.Fatalf("expected daemonID, got %s", info.ID)
70
+	}
71
+
72
+	if info.Containers != 3 {
73
+		t.Fatalf("expected 3 containers, got %d", info.Containers)
74
+	}
75
+}
0 76
new file mode 100644
... ...
@@ -0,0 +1,135 @@
0
+package client
1
+
2
+import (
3
+	"io"
4
+	"time"
5
+
6
+	"github.com/docker/docker/api/types"
7
+	"github.com/docker/docker/api/types/container"
8
+	"github.com/docker/docker/api/types/filters"
9
+	"github.com/docker/docker/api/types/network"
10
+	"github.com/docker/docker/api/types/registry"
11
+	"github.com/docker/docker/api/types/swarm"
12
+	"golang.org/x/net/context"
13
+)
14
+
15
+// CommonAPIClient is the common methods between stable and experimental versions of APIClient.
16
+type CommonAPIClient interface {
17
+	ContainerAPIClient
18
+	ImageAPIClient
19
+	NodeAPIClient
20
+	NetworkAPIClient
21
+	ServiceAPIClient
22
+	SwarmAPIClient
23
+	SystemAPIClient
24
+	VolumeAPIClient
25
+	ClientVersion() string
26
+	ServerVersion(ctx context.Context) (types.Version, error)
27
+	UpdateClientVersion(v string)
28
+}
29
+
30
+// ContainerAPIClient defines API client methods for the containers
31
+type ContainerAPIClient interface {
32
+	ContainerAttach(ctx context.Context, container string, options types.ContainerAttachOptions) (types.HijackedResponse, error)
33
+	ContainerCommit(ctx context.Context, container string, options types.ContainerCommitOptions) (types.ContainerCommitResponse, error)
34
+	ContainerCreate(ctx context.Context, config *container.Config, hostConfig *container.HostConfig, networkingConfig *network.NetworkingConfig, containerName string) (types.ContainerCreateResponse, error)
35
+	ContainerDiff(ctx context.Context, container string) ([]types.ContainerChange, error)
36
+	ContainerExecAttach(ctx context.Context, execID string, config types.ExecConfig) (types.HijackedResponse, error)
37
+	ContainerExecCreate(ctx context.Context, container string, config types.ExecConfig) (types.ContainerExecCreateResponse, error)
38
+	ContainerExecInspect(ctx context.Context, execID string) (types.ContainerExecInspect, error)
39
+	ContainerExecResize(ctx context.Context, execID string, options types.ResizeOptions) error
40
+	ContainerExecStart(ctx context.Context, execID string, config types.ExecStartCheck) error
41
+	ContainerExport(ctx context.Context, container string) (io.ReadCloser, error)
42
+	ContainerInspect(ctx context.Context, container string) (types.ContainerJSON, error)
43
+	ContainerInspectWithRaw(ctx context.Context, container string, getSize bool) (types.ContainerJSON, []byte, error)
44
+	ContainerKill(ctx context.Context, container, signal string) error
45
+	ContainerList(ctx context.Context, options types.ContainerListOptions) ([]types.Container, error)
46
+	ContainerLogs(ctx context.Context, container string, options types.ContainerLogsOptions) (io.ReadCloser, error)
47
+	ContainerPause(ctx context.Context, container string) error
48
+	ContainerRemove(ctx context.Context, container string, options types.ContainerRemoveOptions) error
49
+	ContainerRename(ctx context.Context, container, newContainerName string) error
50
+	ContainerResize(ctx context.Context, container string, options types.ResizeOptions) error
51
+	ContainerRestart(ctx context.Context, container string, timeout *time.Duration) error
52
+	ContainerStatPath(ctx context.Context, container, path string) (types.ContainerPathStat, error)
53
+	ContainerStats(ctx context.Context, container string, stream bool) (io.ReadCloser, error)
54
+	ContainerStart(ctx context.Context, container string, options types.ContainerStartOptions) error
55
+	ContainerStop(ctx context.Context, container string, timeout *time.Duration) error
56
+	ContainerTop(ctx context.Context, container string, arguments []string) (types.ContainerProcessList, error)
57
+	ContainerUnpause(ctx context.Context, container string) error
58
+	ContainerUpdate(ctx context.Context, container string, updateConfig container.UpdateConfig) (types.ContainerUpdateResponse, error)
59
+	ContainerWait(ctx context.Context, container string) (int, error)
60
+	CopyFromContainer(ctx context.Context, container, srcPath string) (io.ReadCloser, types.ContainerPathStat, error)
61
+	CopyToContainer(ctx context.Context, container, path string, content io.Reader, options types.CopyToContainerOptions) error
62
+}
63
+
64
+// ImageAPIClient defines API client methods for the images
65
+type ImageAPIClient interface {
66
+	ImageBuild(ctx context.Context, context io.Reader, options types.ImageBuildOptions) (types.ImageBuildResponse, error)
67
+	ImageCreate(ctx context.Context, parentReference string, options types.ImageCreateOptions) (io.ReadCloser, error)
68
+	ImageHistory(ctx context.Context, image string) ([]types.ImageHistory, error)
69
+	ImageImport(ctx context.Context, source types.ImageImportSource, ref string, options types.ImageImportOptions) (io.ReadCloser, error)
70
+	ImageInspectWithRaw(ctx context.Context, image string) (types.ImageInspect, []byte, error)
71
+	ImageList(ctx context.Context, options types.ImageListOptions) ([]types.Image, error)
72
+	ImageLoad(ctx context.Context, input io.Reader, quiet bool) (types.ImageLoadResponse, error)
73
+	ImagePull(ctx context.Context, ref string, options types.ImagePullOptions) (io.ReadCloser, error)
74
+	ImagePush(ctx context.Context, ref string, options types.ImagePushOptions) (io.ReadCloser, error)
75
+	ImageRemove(ctx context.Context, image string, options types.ImageRemoveOptions) ([]types.ImageDelete, error)
76
+	ImageSearch(ctx context.Context, term string, options types.ImageSearchOptions) ([]registry.SearchResult, error)
77
+	ImageSave(ctx context.Context, images []string) (io.ReadCloser, error)
78
+	ImageTag(ctx context.Context, image, ref string) error
79
+}
80
+
81
+// NetworkAPIClient defines API client methods for the networks
82
+type NetworkAPIClient interface {
83
+	NetworkConnect(ctx context.Context, networkID, container string, config *network.EndpointSettings) error
84
+	NetworkCreate(ctx context.Context, name string, options types.NetworkCreate) (types.NetworkCreateResponse, error)
85
+	NetworkDisconnect(ctx context.Context, networkID, container string, force bool) error
86
+	NetworkInspect(ctx context.Context, networkID string) (types.NetworkResource, error)
87
+	NetworkInspectWithRaw(ctx context.Context, networkID string) (types.NetworkResource, []byte, error)
88
+	NetworkList(ctx context.Context, options types.NetworkListOptions) ([]types.NetworkResource, error)
89
+	NetworkRemove(ctx context.Context, networkID string) error
90
+}
91
+
92
+// NodeAPIClient defines API client methods for the nodes
93
+type NodeAPIClient interface {
94
+	NodeInspectWithRaw(ctx context.Context, nodeID string) (swarm.Node, []byte, error)
95
+	NodeList(ctx context.Context, options types.NodeListOptions) ([]swarm.Node, error)
96
+	NodeRemove(ctx context.Context, nodeID string, options types.NodeRemoveOptions) error
97
+	NodeUpdate(ctx context.Context, nodeID string, version swarm.Version, node swarm.NodeSpec) error
98
+}
99
+
100
+// ServiceAPIClient defines API client methods for the services
101
+type ServiceAPIClient interface {
102
+	ServiceCreate(ctx context.Context, service swarm.ServiceSpec, options types.ServiceCreateOptions) (types.ServiceCreateResponse, error)
103
+	ServiceInspectWithRaw(ctx context.Context, serviceID string) (swarm.Service, []byte, error)
104
+	ServiceList(ctx context.Context, options types.ServiceListOptions) ([]swarm.Service, error)
105
+	ServiceRemove(ctx context.Context, serviceID string) error
106
+	ServiceUpdate(ctx context.Context, serviceID string, version swarm.Version, service swarm.ServiceSpec, options types.ServiceUpdateOptions) error
107
+	TaskInspectWithRaw(ctx context.Context, taskID string) (swarm.Task, []byte, error)
108
+	TaskList(ctx context.Context, options types.TaskListOptions) ([]swarm.Task, error)
109
+}
110
+
111
+// SwarmAPIClient defines API client methods for the swarm
112
+type SwarmAPIClient interface {
113
+	SwarmInit(ctx context.Context, req swarm.InitRequest) (string, error)
114
+	SwarmJoin(ctx context.Context, req swarm.JoinRequest) error
115
+	SwarmLeave(ctx context.Context, force bool) error
116
+	SwarmInspect(ctx context.Context) (swarm.Swarm, error)
117
+	SwarmUpdate(ctx context.Context, version swarm.Version, swarm swarm.Spec, flags swarm.UpdateFlags) error
118
+}
119
+
120
+// SystemAPIClient defines API client methods for the system
121
+type SystemAPIClient interface {
122
+	Events(ctx context.Context, options types.EventsOptions) (io.ReadCloser, error)
123
+	Info(ctx context.Context) (types.Info, error)
124
+	RegistryLogin(ctx context.Context, auth types.AuthConfig) (types.AuthResponse, error)
125
+}
126
+
127
+// VolumeAPIClient defines API client methods for the volumes
128
+type VolumeAPIClient interface {
129
+	VolumeCreate(ctx context.Context, options types.VolumeCreateRequest) (types.Volume, error)
130
+	VolumeInspect(ctx context.Context, volumeID string) (types.Volume, error)
131
+	VolumeInspectWithRaw(ctx context.Context, volumeID string) (types.Volume, []byte, error)
132
+	VolumeList(ctx context.Context, filter filters.Args) (types.VolumesListResponse, error)
133
+	VolumeRemove(ctx context.Context, volumeID string, force bool) error
134
+}
0 135
new file mode 100644
... ...
@@ -0,0 +1,37 @@
0
+// +build experimental
1
+
2
+package client
3
+
4
+import (
5
+	"github.com/docker/docker/api/types"
6
+	"golang.org/x/net/context"
7
+)
8
+
9
+// APIClient is an interface that clients that talk with a docker server must implement.
10
+type APIClient interface {
11
+	CommonAPIClient
12
+	CheckpointAPIClient
13
+	PluginAPIClient
14
+}
15
+
16
+// CheckpointAPIClient defines API client methods for the checkpoints
17
+type CheckpointAPIClient interface {
18
+	CheckpointCreate(ctx context.Context, container string, options types.CheckpointCreateOptions) error
19
+	CheckpointDelete(ctx context.Context, container string, checkpointID string) error
20
+	CheckpointList(ctx context.Context, container string) ([]types.Checkpoint, error)
21
+}
22
+
23
+// PluginAPIClient defines API client methods for the plugins
24
+type PluginAPIClient interface {
25
+	PluginList(ctx context.Context) (types.PluginsListResponse, error)
26
+	PluginRemove(ctx context.Context, name string, options types.PluginRemoveOptions) error
27
+	PluginEnable(ctx context.Context, name string) error
28
+	PluginDisable(ctx context.Context, name string) error
29
+	PluginInstall(ctx context.Context, name string, options types.PluginInstallOptions) error
30
+	PluginPush(ctx context.Context, name string, registryAuth string) error
31
+	PluginSet(ctx context.Context, name string, args []string) error
32
+	PluginInspectWithRaw(ctx context.Context, name string) (*types.Plugin, []byte, error)
33
+}
34
+
35
+// Ensure that Client always implements APIClient.
36
+var _ APIClient = &Client{}
0 37
new file mode 100644
... ...
@@ -0,0 +1,11 @@
0
+// +build !experimental
1
+
2
+package client
3
+
4
+// APIClient is an interface that clients that talk with a docker server must implement.
5
+type APIClient interface {
6
+	CommonAPIClient
7
+}
8
+
9
+// Ensure that Client always implements APIClient.
10
+var _ APIClient = &Client{}
0 11
new file mode 100644
... ...
@@ -0,0 +1,28 @@
0
+package client
1
+
2
+import (
3
+	"encoding/json"
4
+	"net/http"
5
+	"net/url"
6
+
7
+	"github.com/docker/docker/api/types"
8
+	"golang.org/x/net/context"
9
+)
10
+
11
+// RegistryLogin authenticates the docker server with a given docker registry.
12
+// It returns UnauthorizerError when the authentication fails.
13
+func (cli *Client) RegistryLogin(ctx context.Context, auth types.AuthConfig) (types.AuthResponse, error) {
14
+	resp, err := cli.post(ctx, "/auth", url.Values{}, auth, nil)
15
+
16
+	if resp.statusCode == http.StatusUnauthorized {
17
+		return types.AuthResponse{}, unauthorizedError{err}
18
+	}
19
+	if err != nil {
20
+		return types.AuthResponse{}, err
21
+	}
22
+
23
+	var response types.AuthResponse
24
+	err = json.NewDecoder(resp.body).Decode(&response)
25
+	ensureReaderClosed(resp)
26
+	return response, err
27
+}
0 28
new file mode 100644
... ...
@@ -0,0 +1,18 @@
0
+package client
1
+
2
+import (
3
+	"github.com/docker/docker/api/types"
4
+	"github.com/docker/docker/api/types/network"
5
+	"golang.org/x/net/context"
6
+)
7
+
8
+// NetworkConnect connects a container to an existent network in the docker host.
9
+func (cli *Client) NetworkConnect(ctx context.Context, networkID, containerID string, config *network.EndpointSettings) error {
10
+	nc := types.NetworkConnect{
11
+		Container:      containerID,
12
+		EndpointConfig: config,
13
+	}
14
+	resp, err := cli.post(ctx, "/networks/"+networkID+"/connect", nil, nc, nil)
15
+	ensureReaderClosed(resp)
16
+	return err
17
+}
0 18
new file mode 100644
... ...
@@ -0,0 +1,107 @@
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
+	"golang.org/x/net/context"
12
+
13
+	"github.com/docker/docker/api/types"
14
+	"github.com/docker/docker/api/types/network"
15
+)
16
+
17
+func TestNetworkConnectError(t *testing.T) {
18
+	client := &Client{
19
+		transport: newMockClient(nil, errorMock(http.StatusInternalServerError, "Server error")),
20
+	}
21
+
22
+	err := client.NetworkConnect(context.Background(), "network_id", "container_id", nil)
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 TestNetworkConnectEmptyNilEndpointSettings(t *testing.T) {
29
+	expectedURL := "/networks/network_id/connect"
30
+
31
+	client := &Client{
32
+		transport: newMockClient(nil, func(req *http.Request) (*http.Response, error) {
33
+			if !strings.HasPrefix(req.URL.Path, expectedURL) {
34
+				return nil, fmt.Errorf("Expected URL '%s', got '%s'", expectedURL, req.URL)
35
+			}
36
+
37
+			if req.Method != "POST" {
38
+				return nil, fmt.Errorf("expected POST method, got %s", req.Method)
39
+			}
40
+
41
+			var connect types.NetworkConnect
42
+			if err := json.NewDecoder(req.Body).Decode(&connect); err != nil {
43
+				return nil, err
44
+			}
45
+
46
+			if connect.Container != "container_id" {
47
+				return nil, fmt.Errorf("expected 'container_id', got %s", connect.Container)
48
+			}
49
+
50
+			if connect.EndpointConfig != nil {
51
+				return nil, fmt.Errorf("expected connect.EndpointConfig to be nil, got %v", connect.EndpointConfig)
52
+			}
53
+
54
+			return &http.Response{
55
+				StatusCode: http.StatusOK,
56
+				Body:       ioutil.NopCloser(bytes.NewReader([]byte(""))),
57
+			}, nil
58
+		}),
59
+	}
60
+
61
+	err := client.NetworkConnect(context.Background(), "network_id", "container_id", nil)
62
+	if err != nil {
63
+		t.Fatal(err)
64
+	}
65
+}
66
+
67
+func TestNetworkConnect(t *testing.T) {
68
+	expectedURL := "/networks/network_id/connect"
69
+
70
+	client := &Client{
71
+		transport: newMockClient(nil, func(req *http.Request) (*http.Response, error) {
72
+			if !strings.HasPrefix(req.URL.Path, expectedURL) {
73
+				return nil, fmt.Errorf("Expected URL '%s', got '%s'", expectedURL, req.URL)
74
+			}
75
+
76
+			if req.Method != "POST" {
77
+				return nil, fmt.Errorf("expected POST method, got %s", req.Method)
78
+			}
79
+
80
+			var connect types.NetworkConnect
81
+			if err := json.NewDecoder(req.Body).Decode(&connect); err != nil {
82
+				return nil, err
83
+			}
84
+
85
+			if connect.Container != "container_id" {
86
+				return nil, fmt.Errorf("expected 'container_id', got %s", connect.Container)
87
+			}
88
+
89
+			if connect.EndpointConfig.NetworkID != "NetworkID" {
90
+				return nil, fmt.Errorf("expected 'NetworkID', got %s", connect.EndpointConfig.NetworkID)
91
+			}
92
+
93
+			return &http.Response{
94
+				StatusCode: http.StatusOK,
95
+				Body:       ioutil.NopCloser(bytes.NewReader([]byte(""))),
96
+			}, nil
97
+		}),
98
+	}
99
+
100
+	err := client.NetworkConnect(context.Background(), "network_id", "container_id", &network.EndpointSettings{
101
+		NetworkID: "NetworkID",
102
+	})
103
+	if err != nil {
104
+		t.Fatal(err)
105
+	}
106
+}
0 107
new file mode 100644
... ...
@@ -0,0 +1,25 @@
0
+package client
1
+
2
+import (
3
+	"encoding/json"
4
+
5
+	"github.com/docker/docker/api/types"
6
+	"golang.org/x/net/context"
7
+)
8
+
9
+// NetworkCreate creates a new network in the docker host.
10
+func (cli *Client) NetworkCreate(ctx context.Context, name string, options types.NetworkCreate) (types.NetworkCreateResponse, error) {
11
+	networkCreateRequest := types.NetworkCreateRequest{
12
+		NetworkCreate: options,
13
+		Name:          name,
14
+	}
15
+	var response types.NetworkCreateResponse
16
+	serverResp, err := cli.post(ctx, "/networks/create", nil, networkCreateRequest, nil)
17
+	if err != nil {
18
+		return response, err
19
+	}
20
+
21
+	json.NewDecoder(serverResp.body).Decode(&response)
22
+	ensureReaderClosed(serverResp)
23
+	return response, err
24
+}
0 25
new file mode 100644
... ...
@@ -0,0 +1,72 @@
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
+	"golang.org/x/net/context"
13
+)
14
+
15
+func TestNetworkCreateError(t *testing.T) {
16
+	client := &Client{
17
+		transport: newMockClient(nil, errorMock(http.StatusInternalServerError, "Server error")),
18
+	}
19
+
20
+	_, err := client.NetworkCreate(context.Background(), "mynetwork", types.NetworkCreate{})
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 TestNetworkCreate(t *testing.T) {
27
+	expectedURL := "/networks/create"
28
+
29
+	client := &Client{
30
+		transport: newMockClient(nil, 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
+
35
+			if req.Method != "POST" {
36
+				return nil, fmt.Errorf("expected POST method, got %s", req.Method)
37
+			}
38
+
39
+			content, err := json.Marshal(types.NetworkCreateResponse{
40
+				ID:      "network_id",
41
+				Warning: "warning",
42
+			})
43
+			if err != nil {
44
+				return nil, err
45
+			}
46
+			return &http.Response{
47
+				StatusCode: http.StatusOK,
48
+				Body:       ioutil.NopCloser(bytes.NewReader(content)),
49
+			}, nil
50
+		}),
51
+	}
52
+
53
+	networkResponse, err := client.NetworkCreate(context.Background(), "mynetwork", types.NetworkCreate{
54
+		CheckDuplicate: true,
55
+		Driver:         "mydriver",
56
+		EnableIPv6:     true,
57
+		Internal:       true,
58
+		Options: map[string]string{
59
+			"opt-key": "opt-value",
60
+		},
61
+	})
62
+	if err != nil {
63
+		t.Fatal(err)
64
+	}
65
+	if networkResponse.ID != "network_id" {
66
+		t.Fatalf("expected networkResponse.ID to be 'network_id', got %s", networkResponse.ID)
67
+	}
68
+	if networkResponse.Warning != "warning" {
69
+		t.Fatalf("expected networkResponse.Warning to be 'warning', got %s", networkResponse.Warning)
70
+	}
71
+}
0 72
new file mode 100644
... ...
@@ -0,0 +1,14 @@
0
+package client
1
+
2
+import (
3
+	"github.com/docker/docker/api/types"
4
+	"golang.org/x/net/context"
5
+)
6
+
7
+// NetworkDisconnect disconnects a container from an existent network in the docker host.
8
+func (cli *Client) NetworkDisconnect(ctx context.Context, networkID, containerID string, force bool) error {
9
+	nd := types.NetworkDisconnect{Container: containerID, Force: force}
10
+	resp, err := cli.post(ctx, "/networks/"+networkID+"/disconnect", nil, nd, nil)
11
+	ensureReaderClosed(resp)
12
+	return err
13
+}
0 14
new file mode 100644
... ...
@@ -0,0 +1,64 @@
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
+	"golang.org/x/net/context"
13
+)
14
+
15
+func TestNetworkDisconnectError(t *testing.T) {
16
+	client := &Client{
17
+		transport: newMockClient(nil, errorMock(http.StatusInternalServerError, "Server error")),
18
+	}
19
+
20
+	err := client.NetworkDisconnect(context.Background(), "network_id", "container_id", false)
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 TestNetworkDisconnect(t *testing.T) {
27
+	expectedURL := "/networks/network_id/disconnect"
28
+
29
+	client := &Client{
30
+		transport: newMockClient(nil, 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
+
35
+			if req.Method != "POST" {
36
+				return nil, fmt.Errorf("expected POST method, got %s", req.Method)
37
+			}
38
+
39
+			var disconnect types.NetworkDisconnect
40
+			if err := json.NewDecoder(req.Body).Decode(&disconnect); err != nil {
41
+				return nil, err
42
+			}
43
+
44
+			if disconnect.Container != "container_id" {
45
+				return nil, fmt.Errorf("expected 'container_id', got %s", disconnect.Container)
46
+			}
47
+
48
+			if !disconnect.Force {
49
+				return nil, fmt.Errorf("expected Force to be true, got %v", disconnect.Force)
50
+			}
51
+
52
+			return &http.Response{
53
+				StatusCode: http.StatusOK,
54
+				Body:       ioutil.NopCloser(bytes.NewReader([]byte(""))),
55
+			}, nil
56
+		}),
57
+	}
58
+
59
+	err := client.NetworkDisconnect(context.Background(), "network_id", "container_id", true)
60
+	if err != nil {
61
+		t.Fatal(err)
62
+	}
63
+}
0 64
new file mode 100644
... ...
@@ -0,0 +1,38 @@
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"
9
+	"golang.org/x/net/context"
10
+)
11
+
12
+// NetworkInspect returns the information for a specific network configured in the docker host.
13
+func (cli *Client) NetworkInspect(ctx context.Context, networkID string) (types.NetworkResource, error) {
14
+	networkResource, _, err := cli.NetworkInspectWithRaw(ctx, networkID)
15
+	return networkResource, err
16
+}
17
+
18
+// NetworkInspectWithRaw returns the information for a specific network configured in the docker host and its raw representation.
19
+func (cli *Client) NetworkInspectWithRaw(ctx context.Context, networkID string) (types.NetworkResource, []byte, error) {
20
+	var networkResource types.NetworkResource
21
+	resp, err := cli.get(ctx, "/networks/"+networkID, nil, nil)
22
+	if err != nil {
23
+		if resp.statusCode == http.StatusNotFound {
24
+			return networkResource, nil, networkNotFoundError{networkID}
25
+		}
26
+		return networkResource, nil, err
27
+	}
28
+	defer ensureReaderClosed(resp)
29
+
30
+	body, err := ioutil.ReadAll(resp.body)
31
+	if err != nil {
32
+		return networkResource, nil, err
33
+	}
34
+	rdr := bytes.NewReader(body)
35
+	err = json.NewDecoder(rdr).Decode(&networkResource)
36
+	return networkResource, body, err
37
+}
0 38
new file mode 100644
... ...
@@ -0,0 +1,69 @@
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
+	"golang.org/x/net/context"
13
+)
14
+
15
+func TestNetworkInspectError(t *testing.T) {
16
+	client := &Client{
17
+		transport: newMockClient(nil, errorMock(http.StatusInternalServerError, "Server error")),
18
+	}
19
+
20
+	_, err := client.NetworkInspect(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 TestNetworkInspectContainerNotFound(t *testing.T) {
27
+	client := &Client{
28
+		transport: newMockClient(nil, errorMock(http.StatusNotFound, "Server error")),
29
+	}
30
+
31
+	_, err := client.NetworkInspect(context.Background(), "unknown")
32
+	if err == nil || !IsErrNetworkNotFound(err) {
33
+		t.Fatalf("expected a containerNotFound error, got %v", err)
34
+	}
35
+}
36
+
37
+func TestNetworkInspect(t *testing.T) {
38
+	expectedURL := "/networks/network_id"
39
+	client := &Client{
40
+		transport: newMockClient(nil, 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
+			if req.Method != "GET" {
45
+				return nil, fmt.Errorf("expected GET method, got %s", req.Method)
46
+			}
47
+
48
+			content, err := json.Marshal(types.NetworkResource{
49
+				Name: "mynetwork",
50
+			})
51
+			if err != nil {
52
+				return nil, err
53
+			}
54
+			return &http.Response{
55
+				StatusCode: http.StatusOK,
56
+				Body:       ioutil.NopCloser(bytes.NewReader(content)),
57
+			}, nil
58
+		}),
59
+	}
60
+
61
+	r, err := client.NetworkInspect(context.Background(), "network_id")
62
+	if err != nil {
63
+		t.Fatal(err)
64
+	}
65
+	if r.Name != "mynetwork" {
66
+		t.Fatalf("expected `mynetwork`, got %s", r.Name)
67
+	}
68
+}
0 69
new file mode 100644
... ...
@@ -0,0 +1,31 @@
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
+	"golang.org/x/net/context"
9
+)
10
+
11
+// NetworkList returns the list of networks configured in the docker host.
12
+func (cli *Client) NetworkList(ctx context.Context, options types.NetworkListOptions) ([]types.NetworkResource, error) {
13
+	query := url.Values{}
14
+	if options.Filters.Len() > 0 {
15
+		filterJSON, err := filters.ToParamWithVersion(cli.version, options.Filters)
16
+		if err != nil {
17
+			return nil, err
18
+		}
19
+
20
+		query.Set("filters", filterJSON)
21
+	}
22
+	var networkResources []types.NetworkResource
23
+	resp, err := cli.get(ctx, "/networks", query, nil)
24
+	if err != nil {
25
+		return networkResources, err
26
+	}
27
+	err = json.NewDecoder(resp.body).Decode(&networkResources)
28
+	ensureReaderClosed(resp)
29
+	return networkResources, err
30
+}
0 31
new file mode 100644
... ...
@@ -0,0 +1,108 @@
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
+	"golang.org/x/net/context"
14
+)
15
+
16
+func TestNetworkListError(t *testing.T) {
17
+	client := &Client{
18
+		transport: newMockClient(nil, errorMock(http.StatusInternalServerError, "Server error")),
19
+	}
20
+
21
+	_, err := client.NetworkList(context.Background(), types.NetworkListOptions{
22
+		Filters: filters.NewArgs(),
23
+	})
24
+	if err == nil || err.Error() != "Error response from daemon: Server error" {
25
+		t.Fatalf("expected a Server Error, got %v", err)
26
+	}
27
+}
28
+
29
+func TestNetworkList(t *testing.T) {
30
+	expectedURL := "/networks"
31
+
32
+	noDanglingFilters := filters.NewArgs()
33
+	noDanglingFilters.Add("dangling", "false")
34
+
35
+	danglingFilters := filters.NewArgs()
36
+	danglingFilters.Add("dangling", "true")
37
+
38
+	labelFilters := filters.NewArgs()
39
+	labelFilters.Add("label", "label1")
40
+	labelFilters.Add("label", "label2")
41
+
42
+	listCases := []struct {
43
+		options         types.NetworkListOptions
44
+		expectedFilters string
45
+	}{
46
+		{
47
+			options: types.NetworkListOptions{
48
+				Filters: filters.NewArgs(),
49
+			},
50
+			expectedFilters: "",
51
+		}, {
52
+			options: types.NetworkListOptions{
53
+				Filters: noDanglingFilters,
54
+			},
55
+			expectedFilters: `{"dangling":{"false":true}}`,
56
+		}, {
57
+			options: types.NetworkListOptions{
58
+				Filters: danglingFilters,
59
+			},
60
+			expectedFilters: `{"dangling":{"true":true}}`,
61
+		}, {
62
+			options: types.NetworkListOptions{
63
+				Filters: labelFilters,
64
+			},
65
+			expectedFilters: `{"label":{"label1":true,"label2":true}}`,
66
+		},
67
+	}
68
+
69
+	for _, listCase := range listCases {
70
+		client := &Client{
71
+			transport: newMockClient(nil, func(req *http.Request) (*http.Response, error) {
72
+				if !strings.HasPrefix(req.URL.Path, expectedURL) {
73
+					return nil, fmt.Errorf("Expected URL '%s', got '%s'", expectedURL, req.URL)
74
+				}
75
+				if req.Method != "GET" {
76
+					return nil, fmt.Errorf("expected GET method, got %s", req.Method)
77
+				}
78
+				query := req.URL.Query()
79
+				actualFilters := query.Get("filters")
80
+				if actualFilters != listCase.expectedFilters {
81
+					return nil, fmt.Errorf("filters not set in URL query properly. Expected '%s', got %s", listCase.expectedFilters, actualFilters)
82
+				}
83
+				content, err := json.Marshal([]types.NetworkResource{
84
+					{
85
+						Name:   "network",
86
+						Driver: "bridge",
87
+					},
88
+				})
89
+				if err != nil {
90
+					return nil, err
91
+				}
92
+				return &http.Response{
93
+					StatusCode: http.StatusOK,
94
+					Body:       ioutil.NopCloser(bytes.NewReader(content)),
95
+				}, nil
96
+			}),
97
+		}
98
+
99
+		networkResources, err := client.NetworkList(context.Background(), listCase.options)
100
+		if err != nil {
101
+			t.Fatal(err)
102
+		}
103
+		if len(networkResources) != 1 {
104
+			t.Fatalf("expected 1 network resource, got %v", networkResources)
105
+		}
106
+	}
107
+}
0 108
new file mode 100644
... ...
@@ -0,0 +1,10 @@
0
+package client
1
+
2
+import "golang.org/x/net/context"
3
+
4
+// NetworkRemove removes an existent network from the docker host.
5
+func (cli *Client) NetworkRemove(ctx context.Context, networkID string) error {
6
+	resp, err := cli.delete(ctx, "/networks/"+networkID, 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 TestNetworkRemoveError(t *testing.T) {
14
+	client := &Client{
15
+		transport: newMockClient(nil, errorMock(http.StatusInternalServerError, "Server error")),
16
+	}
17
+
18
+	err := client.NetworkRemove(context.Background(), "network_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 TestNetworkRemove(t *testing.T) {
25
+	expectedURL := "/networks/network_id"
26
+
27
+	client := &Client{
28
+		transport: newMockClient(nil, 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.NetworkRemove(context.Background(), "network_id")
43
+	if err != nil {
44
+		t.Fatal(err)
45
+	}
46
+}
0 47
new file mode 100644
... ...
@@ -0,0 +1,33 @@
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
+// NodeInspectWithRaw returns the node information.
13
+func (cli *Client) NodeInspectWithRaw(ctx context.Context, nodeID string) (swarm.Node, []byte, error) {
14
+	serverResp, err := cli.get(ctx, "/nodes/"+nodeID, nil, nil)
15
+	if err != nil {
16
+		if serverResp.statusCode == http.StatusNotFound {
17
+			return swarm.Node{}, nil, nodeNotFoundError{nodeID}
18
+		}
19
+		return swarm.Node{}, nil, err
20
+	}
21
+	defer ensureReaderClosed(serverResp)
22
+
23
+	body, err := ioutil.ReadAll(serverResp.body)
24
+	if err != nil {
25
+		return swarm.Node{}, nil, err
26
+	}
27
+
28
+	var response swarm.Node
29
+	rdr := bytes.NewReader(body)
30
+	err = json.NewDecoder(rdr).Decode(&response)
31
+	return response, body, err
32
+}
0 33
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 TestNodeInspectError(t *testing.T) {
16
+	client := &Client{
17
+		transport: newMockClient(nil, errorMock(http.StatusInternalServerError, "Server error")),
18
+	}
19
+
20
+	_, _, err := client.NodeInspectWithRaw(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 TestNodeInspectNodeNotFound(t *testing.T) {
27
+	client := &Client{
28
+		transport: newMockClient(nil, errorMock(http.StatusNotFound, "Server error")),
29
+	}
30
+
31
+	_, _, err := client.NodeInspectWithRaw(context.Background(), "unknown")
32
+	if err == nil || !IsErrNodeNotFound(err) {
33
+		t.Fatalf("expected an nodeNotFoundError error, got %v", err)
34
+	}
35
+}
36
+
37
+func TestNodeInspect(t *testing.T) {
38
+	expectedURL := "/nodes/node_id"
39
+	client := &Client{
40
+		transport: newMockClient(nil, 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.Node{
45
+				ID: "node_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
+	nodeInspect, _, err := client.NodeInspectWithRaw(context.Background(), "node_id")
58
+	if err != nil {
59
+		t.Fatal(err)
60
+	}
61
+	if nodeInspect.ID != "node_id" {
62
+		t.Fatalf("expected `node_id`, got %s", nodeInspect.ID)
63
+	}
64
+}
0 65
new file mode 100644
... ...
@@ -0,0 +1,36 @@
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
+// NodeList returns the list of nodes.
13
+func (cli *Client) NodeList(ctx context.Context, options types.NodeListOptions) ([]swarm.Node, error) {
14
+	query := url.Values{}
15
+
16
+	if options.Filter.Len() > 0 {
17
+		filterJSON, err := filters.ToParam(options.Filter)
18
+
19
+		if err != nil {
20
+			return nil, err
21
+		}
22
+
23
+		query.Set("filters", filterJSON)
24
+	}
25
+
26
+	resp, err := cli.get(ctx, "/nodes", query, nil)
27
+	if err != nil {
28
+		return nil, err
29
+	}
30
+
31
+	var nodes []swarm.Node
32
+	err = json.NewDecoder(resp.body).Decode(&nodes)
33
+	ensureReaderClosed(resp)
34
+	return nodes, err
35
+}
0 36
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 TestNodeListError(t *testing.T) {
18
+	client := &Client{
19
+		transport: newMockClient(nil, errorMock(http.StatusInternalServerError, "Server error")),
20
+	}
21
+
22
+	_, err := client.NodeList(context.Background(), types.NodeListOptions{})
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 TestNodeList(t *testing.T) {
29
+	expectedURL := "/nodes"
30
+
31
+	filters := filters.NewArgs()
32
+	filters.Add("label", "label1")
33
+	filters.Add("label", "label2")
34
+
35
+	listCases := []struct {
36
+		options             types.NodeListOptions
37
+		expectedQueryParams map[string]string
38
+	}{
39
+		{
40
+			options: types.NodeListOptions{},
41
+			expectedQueryParams: map[string]string{
42
+				"filters": "",
43
+			},
44
+		},
45
+		{
46
+			options: types.NodeListOptions{
47
+				Filter: 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
+			transport: newMockClient(nil, 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.Node{
68
+					{
69
+						ID: "node_id1",
70
+					},
71
+					{
72
+						ID: "node_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
+		nodes, err := client.NodeList(context.Background(), listCase.options)
86
+		if err != nil {
87
+			t.Fatal(err)
88
+		}
89
+		if len(nodes) != 2 {
90
+			t.Fatalf("expected 2 nodes, got %v", nodes)
91
+		}
92
+	}
93
+}
0 94
new file mode 100644
... ...
@@ -0,0 +1,21 @@
0
+package client
1
+
2
+import (
3
+	"net/url"
4
+
5
+	"github.com/docker/docker/api/types"
6
+
7
+	"golang.org/x/net/context"
8
+)
9
+
10
+// NodeRemove removes a Node.
11
+func (cli *Client) NodeRemove(ctx context.Context, nodeID string, options types.NodeRemoveOptions) error {
12
+	query := url.Values{}
13
+	if options.Force {
14
+		query.Set("force", "1")
15
+	}
16
+
17
+	resp, err := cli.delete(ctx, "/nodes/"+nodeID, query, nil)
18
+	ensureReaderClosed(resp)
19
+	return err
20
+}
0 21
new file mode 100644
... ...
@@ -0,0 +1,69 @@
0
+package client
1
+
2
+import (
3
+	"bytes"
4
+	"fmt"
5
+	"io/ioutil"
6
+	"net/http"
7
+	"strings"
8
+	"testing"
9
+
10
+	"github.com/docker/docker/api/types"
11
+
12
+	"golang.org/x/net/context"
13
+)
14
+
15
+func TestNodeRemoveError(t *testing.T) {
16
+	client := &Client{
17
+		transport: newMockClient(nil, errorMock(http.StatusInternalServerError, "Server error")),
18
+	}
19
+
20
+	err := client.NodeRemove(context.Background(), "node_id", types.NodeRemoveOptions{Force: false})
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 TestNodeRemove(t *testing.T) {
27
+	expectedURL := "/nodes/node_id"
28
+
29
+	removeCases := []struct {
30
+		force         bool
31
+		expectedForce string
32
+	}{
33
+		{
34
+			expectedForce: "",
35
+		},
36
+		{
37
+			force:         true,
38
+			expectedForce: "1",
39
+		},
40
+	}
41
+
42
+	for _, removeCase := range removeCases {
43
+		client := &Client{
44
+			transport: newMockClient(nil, func(req *http.Request) (*http.Response, error) {
45
+				if !strings.HasPrefix(req.URL.Path, expectedURL) {
46
+					return nil, fmt.Errorf("Expected URL '%s', got '%s'", expectedURL, req.URL)
47
+				}
48
+				if req.Method != "DELETE" {
49
+					return nil, fmt.Errorf("expected DELETE method, got %s", req.Method)
50
+				}
51
+				force := req.URL.Query().Get("force")
52
+				if force != removeCase.expectedForce {
53
+					return nil, fmt.Errorf("force not set in URL query properly. expected '%s', got %s", removeCase.expectedForce, force)
54
+				}
55
+
56
+				return &http.Response{
57
+					StatusCode: http.StatusOK,
58
+					Body:       ioutil.NopCloser(bytes.NewReader([]byte("body"))),
59
+				}, nil
60
+			}),
61
+		}
62
+
63
+		err := client.NodeRemove(context.Background(), "node_id", types.NodeRemoveOptions{Force: removeCase.force})
64
+		if err != nil {
65
+			t.Fatal(err)
66
+		}
67
+	}
68
+}
0 69
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
+// NodeUpdate updates a Node.
11
+func (cli *Client) NodeUpdate(ctx context.Context, nodeID string, version swarm.Version, node swarm.NodeSpec) error {
12
+	query := url.Values{}
13
+	query.Set("version", strconv.FormatUint(version.Index, 10))
14
+	resp, err := cli.post(ctx, "/nodes/"+nodeID+"/update", query, node, 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 TestNodeUpdateError(t *testing.T) {
16
+	client := &Client{
17
+		transport: newMockClient(nil, errorMock(http.StatusInternalServerError, "Server error")),
18
+	}
19
+
20
+	err := client.NodeUpdate(context.Background(), "node_id", swarm.Version{}, swarm.NodeSpec{})
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 TestNodeUpdate(t *testing.T) {
27
+	expectedURL := "/nodes/node_id/update"
28
+
29
+	client := &Client{
30
+		transport: newMockClient(nil, 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.NodeUpdate(context.Background(), "node_id", swarm.Version{}, swarm.NodeSpec{})
45
+	if err != nil {
46
+		t.Fatal(err)
47
+	}
48
+}
0 49
new file mode 100644
... ...
@@ -0,0 +1,14 @@
0
+// +build experimental
1
+
2
+package client
3
+
4
+import (
5
+	"golang.org/x/net/context"
6
+)
7
+
8
+// PluginDisable disables a plugin
9
+func (cli *Client) PluginDisable(ctx context.Context, name string) error {
10
+	resp, err := cli.post(ctx, "/plugins/"+name+"/disable", nil, nil, nil)
11
+	ensureReaderClosed(resp)
12
+	return err
13
+}
0 14
new file mode 100644
... ...
@@ -0,0 +1,49 @@
0
+// +build experimental
1
+
2
+package client
3
+
4
+import (
5
+	"bytes"
6
+	"fmt"
7
+	"io/ioutil"
8
+	"net/http"
9
+	"strings"
10
+	"testing"
11
+
12
+	"golang.org/x/net/context"
13
+)
14
+
15
+func TestPluginDisableError(t *testing.T) {
16
+	client := &Client{
17
+		transport: newMockClient(nil, errorMock(http.StatusInternalServerError, "Server error")),
18
+	}
19
+
20
+	err := client.PluginDisable(context.Background(), "plugin_name")
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 TestPluginDisable(t *testing.T) {
27
+	expectedURL := "/plugins/plugin_name/disable"
28
+
29
+	client := &Client{
30
+		transport: newMockClient(nil, 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(""))),
40
+			}, nil
41
+		}),
42
+	}
43
+
44
+	err := client.PluginDisable(context.Background(), "plugin_name")
45
+	if err != nil {
46
+		t.Fatal(err)
47
+	}
48
+}
0 49
new file mode 100644
... ...
@@ -0,0 +1,14 @@
0
+// +build experimental
1
+
2
+package client
3
+
4
+import (
5
+	"golang.org/x/net/context"
6
+)
7
+
8
+// PluginEnable enables a plugin
9
+func (cli *Client) PluginEnable(ctx context.Context, name string) error {
10
+	resp, err := cli.post(ctx, "/plugins/"+name+"/enable", nil, nil, nil)
11
+	ensureReaderClosed(resp)
12
+	return err
13
+}
0 14
new file mode 100644
... ...
@@ -0,0 +1,49 @@
0
+// +build experimental
1
+
2
+package client
3
+
4
+import (
5
+	"bytes"
6
+	"fmt"
7
+	"io/ioutil"
8
+	"net/http"
9
+	"strings"
10
+	"testing"
11
+
12
+	"golang.org/x/net/context"
13
+)
14
+
15
+func TestPluginEnableError(t *testing.T) {
16
+	client := &Client{
17
+		transport: newMockClient(nil, errorMock(http.StatusInternalServerError, "Server error")),
18
+	}
19
+
20
+	err := client.PluginEnable(context.Background(), "plugin_name")
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 TestPluginEnable(t *testing.T) {
27
+	expectedURL := "/plugins/plugin_name/enable"
28
+
29
+	client := &Client{
30
+		transport: newMockClient(nil, 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(""))),
40
+			}, nil
41
+		}),
42
+	}
43
+
44
+	err := client.PluginEnable(context.Background(), "plugin_name")
45
+	if err != nil {
46
+		t.Fatal(err)
47
+	}
48
+}
0 49
new file mode 100644
... ...
@@ -0,0 +1,30 @@
0
+// +build experimental
1
+
2
+package client
3
+
4
+import (
5
+	"bytes"
6
+	"encoding/json"
7
+	"io/ioutil"
8
+
9
+	"github.com/docker/docker/api/types"
10
+	"golang.org/x/net/context"
11
+)
12
+
13
+// PluginInspectWithRaw inspects an existing plugin
14
+func (cli *Client) PluginInspectWithRaw(ctx context.Context, name string) (*types.Plugin, []byte, error) {
15
+	resp, err := cli.get(ctx, "/plugins/"+name, nil, nil)
16
+	if err != nil {
17
+		return nil, nil, err
18
+	}
19
+
20
+	defer ensureReaderClosed(resp)
21
+	body, err := ioutil.ReadAll(resp.body)
22
+	if err != nil {
23
+		return nil, nil, err
24
+	}
25
+	var p types.Plugin
26
+	rdr := bytes.NewReader(body)
27
+	err = json.NewDecoder(rdr).Decode(&p)
28
+	return &p, body, err
29
+}
0 30
new file mode 100644
... ...
@@ -0,0 +1,56 @@
0
+// +build experimental
1
+
2
+package client
3
+
4
+import (
5
+	"bytes"
6
+	"encoding/json"
7
+	"fmt"
8
+	"io/ioutil"
9
+	"net/http"
10
+	"strings"
11
+	"testing"
12
+
13
+	"github.com/docker/docker/api/types"
14
+	"golang.org/x/net/context"
15
+)
16
+
17
+func TestPluginInspectError(t *testing.T) {
18
+	client := &Client{
19
+		transport: newMockClient(nil, errorMock(http.StatusInternalServerError, "Server error")),
20
+	}
21
+
22
+	_, _, err := client.PluginInspectWithRaw(context.Background(), "nothing")
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 TestPluginInspect(t *testing.T) {
29
+	expectedURL := "/plugins/plugin_name"
30
+	client := &Client{
31
+		transport: newMockClient(nil, func(req *http.Request) (*http.Response, error) {
32
+			if !strings.HasPrefix(req.URL.Path, expectedURL) {
33
+				return nil, fmt.Errorf("Expected URL '%s', got '%s'", expectedURL, req.URL)
34
+			}
35
+			content, err := json.Marshal(types.Plugin{
36
+				ID: "plugin_id",
37
+			})
38
+			if err != nil {
39
+				return nil, err
40
+			}
41
+			return &http.Response{
42
+				StatusCode: http.StatusOK,
43
+				Body:       ioutil.NopCloser(bytes.NewReader(content)),
44
+			}, nil
45
+		}),
46
+	}
47
+
48
+	pluginInspect, _, err := client.PluginInspectWithRaw(context.Background(), "plugin_name")
49
+	if err != nil {
50
+		t.Fatal(err)
51
+	}
52
+	if pluginInspect.ID != "plugin_id" {
53
+		t.Fatalf("expected `plugin_id`, got %s", pluginInspect.ID)
54
+	}
55
+}
0 56
new file mode 100644
... ...
@@ -0,0 +1,59 @@
0
+// +build experimental
1
+
2
+package client
3
+
4
+import (
5
+	"encoding/json"
6
+	"net/http"
7
+	"net/url"
8
+
9
+	"github.com/docker/docker/api/types"
10
+	"golang.org/x/net/context"
11
+)
12
+
13
+// PluginInstall installs a plugin
14
+func (cli *Client) PluginInstall(ctx context.Context, name string, options types.PluginInstallOptions) error {
15
+	// FIXME(vdemeester) name is a ref, we might want to parse/validate it here.
16
+	query := url.Values{}
17
+	query.Set("name", name)
18
+	resp, err := cli.tryPluginPull(ctx, query, options.RegistryAuth)
19
+	if resp.statusCode == http.StatusUnauthorized && options.PrivilegeFunc != nil {
20
+		newAuthHeader, privilegeErr := options.PrivilegeFunc()
21
+		if privilegeErr != nil {
22
+			ensureReaderClosed(resp)
23
+			return privilegeErr
24
+		}
25
+		resp, err = cli.tryPluginPull(ctx, query, newAuthHeader)
26
+	}
27
+	if err != nil {
28
+		ensureReaderClosed(resp)
29
+		return err
30
+	}
31
+	var privileges types.PluginPrivileges
32
+	if err := json.NewDecoder(resp.body).Decode(&privileges); err != nil {
33
+		ensureReaderClosed(resp)
34
+		return err
35
+	}
36
+	ensureReaderClosed(resp)
37
+
38
+	if !options.AcceptAllPermissions && options.AcceptPermissionsFunc != nil && len(privileges) > 0 {
39
+		accept, err := options.AcceptPermissionsFunc(privileges)
40
+		if err != nil {
41
+			return err
42
+		}
43
+		if !accept {
44
+			resp, _ := cli.delete(ctx, "/plugins/"+name, nil, nil)
45
+			ensureReaderClosed(resp)
46
+			return pluginPermissionDenied{name}
47
+		}
48
+	}
49
+	if options.Disabled {
50
+		return nil
51
+	}
52
+	return cli.PluginEnable(ctx, name)
53
+}
54
+
55
+func (cli *Client) tryPluginPull(ctx context.Context, query url.Values, registryAuth string) (serverResponse, error) {
56
+	headers := map[string][]string{"X-Registry-Auth": {registryAuth}}
57
+	return cli.post(ctx, "/plugins/pull", query, nil, headers)
58
+}
0 59
new file mode 100644
... ...
@@ -0,0 +1,23 @@
0
+// +build experimental
1
+
2
+package client
3
+
4
+import (
5
+	"encoding/json"
6
+
7
+	"github.com/docker/docker/api/types"
8
+	"golang.org/x/net/context"
9
+)
10
+
11
+// PluginList returns the installed plugins
12
+func (cli *Client) PluginList(ctx context.Context) (types.PluginsListResponse, error) {
13
+	var plugins types.PluginsListResponse
14
+	resp, err := cli.get(ctx, "/plugins", nil, nil)
15
+	if err != nil {
16
+		return plugins, err
17
+	}
18
+
19
+	err = json.NewDecoder(resp.body).Decode(&plugins)
20
+	ensureReaderClosed(resp)
21
+	return plugins, err
22
+}
0 23
new file mode 100644
... ...
@@ -0,0 +1,61 @@
0
+// +build experimental
1
+
2
+package client
3
+
4
+import (
5
+	"bytes"
6
+	"encoding/json"
7
+	"fmt"
8
+	"io/ioutil"
9
+	"net/http"
10
+	"strings"
11
+	"testing"
12
+
13
+	"github.com/docker/docker/api/types"
14
+	"golang.org/x/net/context"
15
+)
16
+
17
+func TestPluginListError(t *testing.T) {
18
+	client := &Client{
19
+		transport: newMockClient(nil, errorMock(http.StatusInternalServerError, "Server error")),
20
+	}
21
+
22
+	_, err := client.PluginList(context.Background())
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 TestPluginList(t *testing.T) {
29
+	expectedURL := "/plugins"
30
+	client := &Client{
31
+		transport: newMockClient(nil, func(req *http.Request) (*http.Response, error) {
32
+			if !strings.HasPrefix(req.URL.Path, expectedURL) {
33
+				return nil, fmt.Errorf("Expected URL '%s', got '%s'", expectedURL, req.URL)
34
+			}
35
+			content, err := json.Marshal([]*types.Plugin{
36
+				{
37
+					ID: "plugin_id1",
38
+				},
39
+				{
40
+					ID: "plugin_id2",
41
+				},
42
+			})
43
+			if err != nil {
44
+				return nil, err
45
+			}
46
+			return &http.Response{
47
+				StatusCode: http.StatusOK,
48
+				Body:       ioutil.NopCloser(bytes.NewReader(content)),
49
+			}, nil
50
+		}),
51
+	}
52
+
53
+	plugins, err := client.PluginList(context.Background())
54
+	if err != nil {
55
+		t.Fatal(err)
56
+	}
57
+	if len(plugins) != 2 {
58
+		t.Fatalf("expected 2 plugins, got %v", plugins)
59
+	}
60
+}
0 61
new file mode 100644
... ...
@@ -0,0 +1,15 @@
0
+// +build experimental
1
+
2
+package client
3
+
4
+import (
5
+	"golang.org/x/net/context"
6
+)
7
+
8
+// PluginPush pushes a plugin to a registry
9
+func (cli *Client) PluginPush(ctx context.Context, name string, registryAuth string) error {
10
+	headers := map[string][]string{"X-Registry-Auth": {registryAuth}}
11
+	resp, err := cli.post(ctx, "/plugins/"+name+"/push", nil, nil, headers)
12
+	ensureReaderClosed(resp)
13
+	return err
14
+}
0 15
new file mode 100644
... ...
@@ -0,0 +1,53 @@
0
+// +build experimental
1
+
2
+package client
3
+
4
+import (
5
+	"bytes"
6
+	"fmt"
7
+	"io/ioutil"
8
+	"net/http"
9
+	"strings"
10
+	"testing"
11
+
12
+	"golang.org/x/net/context"
13
+)
14
+
15
+func TestPluginPushError(t *testing.T) {
16
+	client := &Client{
17
+		transport: newMockClient(nil, errorMock(http.StatusInternalServerError, "Server error")),
18
+	}
19
+
20
+	err := client.PluginPush(context.Background(), "plugin_name", "")
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 TestPluginPush(t *testing.T) {
27
+	expectedURL := "/plugins/plugin_name"
28
+
29
+	client := &Client{
30
+		transport: newMockClient(nil, 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
+			auth := req.Header.Get("X-Registry-Auth")
38
+			if auth != "authtoken" {
39
+				return nil, fmt.Errorf("Invalid auth header : expected %s, got %s", "authtoken", auth)
40
+			}
41
+			return &http.Response{
42
+				StatusCode: http.StatusOK,
43
+				Body:       ioutil.NopCloser(bytes.NewReader([]byte(""))),
44
+			}, nil
45
+		}),
46
+	}
47
+
48
+	err := client.PluginPush(context.Background(), "plugin_name", "authtoken")
49
+	if err != nil {
50
+		t.Fatal(err)
51
+	}
52
+}
0 53
new file mode 100644
... ...
@@ -0,0 +1,22 @@
0
+// +build experimental
1
+
2
+package client
3
+
4
+import (
5
+	"net/url"
6
+
7
+	"github.com/docker/docker/api/types"
8
+	"golang.org/x/net/context"
9
+)
10
+
11
+// PluginRemove removes a plugin
12
+func (cli *Client) PluginRemove(ctx context.Context, name string, options types.PluginRemoveOptions) error {
13
+	query := url.Values{}
14
+	if options.Force {
15
+		query.Set("force", "1")
16
+	}
17
+
18
+	resp, err := cli.delete(ctx, "/plugins/"+name, query, nil)
19
+	ensureReaderClosed(resp)
20
+	return err
21
+}
0 22
new file mode 100644
... ...
@@ -0,0 +1,51 @@
0
+// +build experimental
1
+
2
+package client
3
+
4
+import (
5
+	"bytes"
6
+	"fmt"
7
+	"io/ioutil"
8
+	"net/http"
9
+	"strings"
10
+	"testing"
11
+
12
+	"github.com/docker/docker/api/types"
13
+
14
+	"golang.org/x/net/context"
15
+)
16
+
17
+func TestPluginRemoveError(t *testing.T) {
18
+	client := &Client{
19
+		transport: newMockClient(nil, errorMock(http.StatusInternalServerError, "Server error")),
20
+	}
21
+
22
+	err := client.PluginRemove(context.Background(), "plugin_name", types.PluginRemoveOptions{})
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 TestPluginRemove(t *testing.T) {
29
+	expectedURL := "/plugins/plugin_name"
30
+
31
+	client := &Client{
32
+		transport: newMockClient(nil, func(req *http.Request) (*http.Response, error) {
33
+			if !strings.HasPrefix(req.URL.Path, expectedURL) {
34
+				return nil, fmt.Errorf("Expected URL '%s', got '%s'", expectedURL, req.URL)
35
+			}
36
+			if req.Method != "DELETE" {
37
+				return nil, fmt.Errorf("expected POST method, got %s", req.Method)
38
+			}
39
+			return &http.Response{
40
+				StatusCode: http.StatusOK,
41
+				Body:       ioutil.NopCloser(bytes.NewReader([]byte(""))),
42
+			}, nil
43
+		}),
44
+	}
45
+
46
+	err := client.PluginRemove(context.Background(), "plugin_name", types.PluginRemoveOptions{})
47
+	if err != nil {
48
+		t.Fatal(err)
49
+	}
50
+}
0 51
new file mode 100644
... ...
@@ -0,0 +1,14 @@
0
+// +build experimental
1
+
2
+package client
3
+
4
+import (
5
+	"golang.org/x/net/context"
6
+)
7
+
8
+// PluginSet modifies settings for an existing plugin
9
+func (cli *Client) PluginSet(ctx context.Context, name string, args []string) error {
10
+	resp, err := cli.post(ctx, "/plugins/"+name+"/set", nil, args, nil)
11
+	ensureReaderClosed(resp)
12
+	return err
13
+}
0 14
new file mode 100644
... ...
@@ -0,0 +1,49 @@
0
+// +build experimental
1
+
2
+package client
3
+
4
+import (
5
+	"bytes"
6
+	"fmt"
7
+	"io/ioutil"
8
+	"net/http"
9
+	"strings"
10
+	"testing"
11
+
12
+	"golang.org/x/net/context"
13
+)
14
+
15
+func TestPluginSetError(t *testing.T) {
16
+	client := &Client{
17
+		transport: newMockClient(nil, errorMock(http.StatusInternalServerError, "Server error")),
18
+	}
19
+
20
+	err := client.PluginSet(context.Background(), "plugin_name", []string{})
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 TestPluginSet(t *testing.T) {
27
+	expectedURL := "/plugins/plugin_name/set"
28
+
29
+	client := &Client{
30
+		transport: newMockClient(nil, 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(""))),
40
+			}, nil
41
+		}),
42
+	}
43
+
44
+	err := client.PluginSet(context.Background(), "plugin_name", []string{"arg1"})
45
+	if err != nil {
46
+		t.Fatal(err)
47
+	}
48
+}
0 49
new file mode 100644
... ...
@@ -0,0 +1,208 @@
0
+package client
1
+
2
+import (
3
+	"bytes"
4
+	"encoding/json"
5
+	"fmt"
6
+	"io"
7
+	"io/ioutil"
8
+	"net"
9
+	"net/http"
10
+	"net/url"
11
+	"strings"
12
+
13
+	"github.com/docker/docker/api/types"
14
+	"github.com/docker/docker/api/types/versions"
15
+	"github.com/docker/docker/client/transport/cancellable"
16
+	"golang.org/x/net/context"
17
+)
18
+
19
+// serverResponse is a wrapper for http API responses.
20
+type serverResponse struct {
21
+	body       io.ReadCloser
22
+	header     http.Header
23
+	statusCode int
24
+}
25
+
26
+// head sends an http request to the docker API using the method HEAD.
27
+func (cli *Client) head(ctx context.Context, path string, query url.Values, headers map[string][]string) (serverResponse, error) {
28
+	return cli.sendRequest(ctx, "HEAD", path, query, nil, headers)
29
+}
30
+
31
+// getWithContext sends an http request to the docker API using the method GET with a specific go context.
32
+func (cli *Client) get(ctx context.Context, path string, query url.Values, headers map[string][]string) (serverResponse, error) {
33
+	return cli.sendRequest(ctx, "GET", path, query, nil, headers)
34
+}
35
+
36
+// postWithContext sends an http request to the docker API using the method POST with a specific go context.
37
+func (cli *Client) post(ctx context.Context, path string, query url.Values, obj interface{}, headers map[string][]string) (serverResponse, error) {
38
+	return cli.sendRequest(ctx, "POST", path, query, obj, headers)
39
+}
40
+
41
+func (cli *Client) postRaw(ctx context.Context, path string, query url.Values, body io.Reader, headers map[string][]string) (serverResponse, error) {
42
+	return cli.sendClientRequest(ctx, "POST", path, query, body, headers)
43
+}
44
+
45
+// put sends an http request to the docker API using the method PUT.
46
+func (cli *Client) put(ctx context.Context, path string, query url.Values, obj interface{}, headers map[string][]string) (serverResponse, error) {
47
+	return cli.sendRequest(ctx, "PUT", path, query, obj, headers)
48
+}
49
+
50
+// put sends an http request to the docker API using the method PUT.
51
+func (cli *Client) putRaw(ctx context.Context, path string, query url.Values, body io.Reader, headers map[string][]string) (serverResponse, error) {
52
+	return cli.sendClientRequest(ctx, "PUT", path, query, body, headers)
53
+}
54
+
55
+// delete sends an http request to the docker API using the method DELETE.
56
+func (cli *Client) delete(ctx context.Context, path string, query url.Values, headers map[string][]string) (serverResponse, error) {
57
+	return cli.sendRequest(ctx, "DELETE", path, query, nil, headers)
58
+}
59
+
60
+func (cli *Client) sendRequest(ctx context.Context, method, path string, query url.Values, obj interface{}, headers map[string][]string) (serverResponse, error) {
61
+	var body io.Reader
62
+
63
+	if obj != nil {
64
+		var err error
65
+		body, err = encodeData(obj)
66
+		if err != nil {
67
+			return serverResponse{}, err
68
+		}
69
+		if headers == nil {
70
+			headers = make(map[string][]string)
71
+		}
72
+		headers["Content-Type"] = []string{"application/json"}
73
+	}
74
+
75
+	return cli.sendClientRequest(ctx, method, path, query, body, headers)
76
+}
77
+
78
+func (cli *Client) sendClientRequest(ctx context.Context, method, path string, query url.Values, body io.Reader, headers map[string][]string) (serverResponse, error) {
79
+	serverResp := serverResponse{
80
+		body:       nil,
81
+		statusCode: -1,
82
+	}
83
+
84
+	expectedPayload := (method == "POST" || method == "PUT")
85
+	if expectedPayload && body == nil {
86
+		body = bytes.NewReader([]byte{})
87
+	}
88
+
89
+	req, err := cli.newRequest(method, path, query, body, headers)
90
+	if err != nil {
91
+		return serverResp, err
92
+	}
93
+
94
+	if cli.proto == "unix" || cli.proto == "npipe" {
95
+		// For local communications, it doesn't matter what the host is. We just
96
+		// need a valid and meaningful host name. (See #189)
97
+		req.Host = "docker"
98
+	}
99
+	req.URL.Host = cli.addr
100
+	req.URL.Scheme = cli.transport.Scheme()
101
+
102
+	if expectedPayload && req.Header.Get("Content-Type") == "" {
103
+		req.Header.Set("Content-Type", "text/plain")
104
+	}
105
+
106
+	resp, err := cancellable.Do(ctx, cli.transport, req)
107
+	if err != nil {
108
+		if !cli.transport.Secure() && strings.Contains(err.Error(), "malformed HTTP response") {
109
+			return serverResp, fmt.Errorf("%v.\n* Are you trying to connect to a TLS-enabled daemon without TLS?", err)
110
+		}
111
+
112
+		if cli.transport.Secure() && strings.Contains(err.Error(), "bad certificate") {
113
+			return serverResp, fmt.Errorf("The server probably has client authentication (--tlsverify) enabled. Please check your TLS client certification settings: %v", err)
114
+		}
115
+
116
+		// Don't decorate context sentinel errors; users may be comparing to
117
+		// them directly.
118
+		switch err {
119
+		case context.Canceled, context.DeadlineExceeded:
120
+			return serverResp, err
121
+		}
122
+
123
+		if err, ok := err.(net.Error); ok {
124
+			if err.Timeout() {
125
+				return serverResp, ErrorConnectionFailed(cli.host)
126
+			}
127
+			if !err.Temporary() {
128
+				if strings.Contains(err.Error(), "connection refused") || strings.Contains(err.Error(), "dial unix") {
129
+					return serverResp, ErrorConnectionFailed(cli.host)
130
+				}
131
+			}
132
+		}
133
+		return serverResp, fmt.Errorf("An error occurred trying to connect: %v", err)
134
+	}
135
+
136
+	if resp != nil {
137
+		serverResp.statusCode = resp.StatusCode
138
+	}
139
+
140
+	if serverResp.statusCode < 200 || serverResp.statusCode >= 400 {
141
+		body, err := ioutil.ReadAll(resp.Body)
142
+		if err != nil {
143
+			return serverResp, err
144
+		}
145
+		if len(body) == 0 {
146
+			return serverResp, fmt.Errorf("Error: request returned %s for API route and version %s, check if the server supports the requested API version", http.StatusText(serverResp.statusCode), req.URL)
147
+		}
148
+
149
+		var errorMessage string
150
+		if (cli.version == "" || versions.GreaterThan(cli.version, "1.23")) &&
151
+			resp.Header.Get("Content-Type") == "application/json" {
152
+			var errorResponse types.ErrorResponse
153
+			if err := json.Unmarshal(body, &errorResponse); err != nil {
154
+				return serverResp, fmt.Errorf("Error reading JSON: %v", err)
155
+			}
156
+			errorMessage = errorResponse.Message
157
+		} else {
158
+			errorMessage = string(body)
159
+		}
160
+
161
+		return serverResp, fmt.Errorf("Error response from daemon: %s", strings.TrimSpace(errorMessage))
162
+	}
163
+
164
+	serverResp.body = resp.Body
165
+	serverResp.header = resp.Header
166
+	return serverResp, nil
167
+}
168
+
169
+func (cli *Client) newRequest(method, path string, query url.Values, body io.Reader, headers map[string][]string) (*http.Request, error) {
170
+	apiPath := cli.getAPIPath(path, query)
171
+	req, err := http.NewRequest(method, apiPath, body)
172
+	if err != nil {
173
+		return nil, err
174
+	}
175
+
176
+	// Add CLI Config's HTTP Headers BEFORE we set the Docker headers
177
+	// then the user can't change OUR headers
178
+	for k, v := range cli.customHTTPHeaders {
179
+		req.Header.Set(k, v)
180
+	}
181
+
182
+	if headers != nil {
183
+		for k, v := range headers {
184
+			req.Header[k] = v
185
+		}
186
+	}
187
+
188
+	return req, nil
189
+}
190
+
191
+func encodeData(data interface{}) (*bytes.Buffer, error) {
192
+	params := bytes.NewBuffer(nil)
193
+	if data != nil {
194
+		if err := json.NewEncoder(params).Encode(data); err != nil {
195
+			return nil, err
196
+		}
197
+	}
198
+	return params, nil
199
+}
200
+
201
+func ensureReaderClosed(response serverResponse) {
202
+	if body := response.body; body != nil {
203
+		// Drain up to 512 bytes and close the body to let the Transport reuse the connection
204
+		io.CopyN(ioutil.Discard, body, 512)
205
+		response.body.Close()
206
+	}
207
+}
0 208
new file mode 100644
... ...
@@ -0,0 +1,91 @@
0
+package client
1
+
2
+import (
3
+	"bytes"
4
+	"fmt"
5
+	"io/ioutil"
6
+	"net/http"
7
+	"strings"
8
+	"testing"
9
+
10
+	"github.com/docker/docker/api/types"
11
+	"golang.org/x/net/context"
12
+)
13
+
14
+// TestSetHostHeader should set fake host for local communications, set real host
15
+// for normal communications.
16
+func TestSetHostHeader(t *testing.T) {
17
+	testURL := "/test"
18
+	testCases := []struct {
19
+		host            string
20
+		expectedHost    string
21
+		expectedURLHost string
22
+	}{
23
+		{
24
+			"unix:///var/run/docker.sock",
25
+			"docker",
26
+			"/var/run/docker.sock",
27
+		},
28
+		{
29
+			"npipe:////./pipe/docker_engine",
30
+			"docker",
31
+			"//./pipe/docker_engine",
32
+		},
33
+		{
34
+			"tcp://0.0.0.0:4243",
35
+			"",
36
+			"0.0.0.0:4243",
37
+		},
38
+		{
39
+			"tcp://localhost:4243",
40
+			"",
41
+			"localhost:4243",
42
+		},
43
+	}
44
+
45
+	for c, test := range testCases {
46
+		proto, addr, basePath, err := ParseHost(test.host)
47
+		if err != nil {
48
+			t.Fatal(err)
49
+		}
50
+
51
+		client := &Client{
52
+			transport: newMockClient(nil, func(req *http.Request) (*http.Response, error) {
53
+				if !strings.HasPrefix(req.URL.Path, testURL) {
54
+					return nil, fmt.Errorf("Test Case #%d: Expected URL %q, got %q", c, testURL, req.URL)
55
+				}
56
+				if req.Host != test.expectedHost {
57
+					return nil, fmt.Errorf("Test Case #%d: Expected host %q, got %q", c, test.expectedHost, req.Host)
58
+				}
59
+				if req.URL.Host != test.expectedURLHost {
60
+					return nil, fmt.Errorf("Test Case #%d: Expected URL host %q, got %q", c, test.expectedURLHost, req.URL.Host)
61
+				}
62
+				return &http.Response{
63
+					StatusCode: http.StatusOK,
64
+					Body:       ioutil.NopCloser(bytes.NewReader(([]byte("")))),
65
+				}, nil
66
+			}),
67
+			proto:    proto,
68
+			addr:     addr,
69
+			basePath: basePath,
70
+		}
71
+
72
+		_, err = client.sendRequest(context.Background(), "GET", testURL, nil, nil, nil)
73
+		if err != nil {
74
+			t.Fatal(err)
75
+		}
76
+	}
77
+}
78
+
79
+// TestPlainTextError tests the server returning an error in plain text for
80
+// backwards compatibility with API versions <1.24. All other tests use
81
+// errors returned as JSON
82
+func TestPlainTextError(t *testing.T) {
83
+	client := &Client{
84
+		transport: newMockClient(nil, plainTextErrorMock(http.StatusInternalServerError, "Server error")),
85
+	}
86
+	_, err := client.ContainerList(context.Background(), types.ContainerListOptions{})
87
+	if err == nil || err.Error() != "Error response from daemon: Server error" {
88
+		t.Fatalf("expected a Server Error, got %v", err)
89
+	}
90
+}
0 91
new file mode 100644
... ...
@@ -0,0 +1,30 @@
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
+// ServiceCreate creates a new Service.
11
+func (cli *Client) ServiceCreate(ctx context.Context, service swarm.ServiceSpec, options types.ServiceCreateOptions) (types.ServiceCreateResponse, error) {
12
+	var headers map[string][]string
13
+
14
+	if options.EncodedRegistryAuth != "" {
15
+		headers = map[string][]string{
16
+			"X-Registry-Auth": {options.EncodedRegistryAuth},
17
+		}
18
+	}
19
+
20
+	var response types.ServiceCreateResponse
21
+	resp, err := cli.post(ctx, "/services/create", nil, service, headers)
22
+	if err != nil {
23
+		return response, err
24
+	}
25
+
26
+	err = json.NewDecoder(resp.body).Decode(&response)
27
+	ensureReaderClosed(resp)
28
+	return response, err
29
+}
0 30
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 TestServiceCreateError(t *testing.T) {
17
+	client := &Client{
18
+		transport: newMockClient(nil, errorMock(http.StatusInternalServerError, "Server error")),
19
+	}
20
+	_, err := client.ServiceCreate(context.Background(), swarm.ServiceSpec{}, types.ServiceCreateOptions{})
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 TestServiceCreate(t *testing.T) {
27
+	expectedURL := "/services/create"
28
+	client := &Client{
29
+		transport: newMockClient(nil, 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.ServiceCreateResponse{
37
+				ID: "service_id",
38
+			})
39
+			if err != nil {
40
+				return nil, err
41
+			}
42
+			return &http.Response{
43
+				StatusCode: http.StatusOK,
44
+				Body:       ioutil.NopCloser(bytes.NewReader(b)),
45
+			}, nil
46
+		}),
47
+	}
48
+
49
+	r, err := client.ServiceCreate(context.Background(), swarm.ServiceSpec{}, types.ServiceCreateOptions{})
50
+	if err != nil {
51
+		t.Fatal(err)
52
+	}
53
+	if r.ID != "service_id" {
54
+		t.Fatalf("expected `service_id`, got %s", r.ID)
55
+	}
56
+}
0 57
new file mode 100644
... ...
@@ -0,0 +1,33 @@
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
+// ServiceInspectWithRaw returns the service information and the raw data.
13
+func (cli *Client) ServiceInspectWithRaw(ctx context.Context, serviceID string) (swarm.Service, []byte, error) {
14
+	serverResp, err := cli.get(ctx, "/services/"+serviceID, nil, nil)
15
+	if err != nil {
16
+		if serverResp.statusCode == http.StatusNotFound {
17
+			return swarm.Service{}, nil, serviceNotFoundError{serviceID}
18
+		}
19
+		return swarm.Service{}, nil, err
20
+	}
21
+	defer ensureReaderClosed(serverResp)
22
+
23
+	body, err := ioutil.ReadAll(serverResp.body)
24
+	if err != nil {
25
+		return swarm.Service{}, nil, err
26
+	}
27
+
28
+	var response swarm.Service
29
+	rdr := bytes.NewReader(body)
30
+	err = json.NewDecoder(rdr).Decode(&response)
31
+	return response, body, err
32
+}
0 33
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 TestServiceInspectError(t *testing.T) {
16
+	client := &Client{
17
+		transport: newMockClient(nil, errorMock(http.StatusInternalServerError, "Server error")),
18
+	}
19
+
20
+	_, _, err := client.ServiceInspectWithRaw(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 TestServiceInspectServiceNotFound(t *testing.T) {
27
+	client := &Client{
28
+		transport: newMockClient(nil, errorMock(http.StatusNotFound, "Server error")),
29
+	}
30
+
31
+	_, _, err := client.ServiceInspectWithRaw(context.Background(), "unknown")
32
+	if err == nil || !IsErrServiceNotFound(err) {
33
+		t.Fatalf("expected an serviceNotFoundError error, got %v", err)
34
+	}
35
+}
36
+
37
+func TestServiceInspect(t *testing.T) {
38
+	expectedURL := "/services/service_id"
39
+	client := &Client{
40
+		transport: newMockClient(nil, 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.Service{
45
+				ID: "service_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
+	serviceInspect, _, err := client.ServiceInspectWithRaw(context.Background(), "service_id")
58
+	if err != nil {
59
+		t.Fatal(err)
60
+	}
61
+	if serviceInspect.ID != "service_id" {
62
+		t.Fatalf("expected `service_id`, got %s", serviceInspect.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
+// ServiceList returns the list of services.
13
+func (cli *Client) ServiceList(ctx context.Context, options types.ServiceListOptions) ([]swarm.Service, error) {
14
+	query := url.Values{}
15
+
16
+	if options.Filter.Len() > 0 {
17
+		filterJSON, err := filters.ToParam(options.Filter)
18
+		if err != nil {
19
+			return nil, err
20
+		}
21
+
22
+		query.Set("filters", filterJSON)
23
+	}
24
+
25
+	resp, err := cli.get(ctx, "/services", query, nil)
26
+	if err != nil {
27
+		return nil, err
28
+	}
29
+
30
+	var services []swarm.Service
31
+	err = json.NewDecoder(resp.body).Decode(&services)
32
+	ensureReaderClosed(resp)
33
+	return services, 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 TestServiceListError(t *testing.T) {
18
+	client := &Client{
19
+		transport: newMockClient(nil, errorMock(http.StatusInternalServerError, "Server error")),
20
+	}
21
+
22
+	_, err := client.ServiceList(context.Background(), types.ServiceListOptions{})
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 TestServiceList(t *testing.T) {
29
+	expectedURL := "/services"
30
+
31
+	filters := filters.NewArgs()
32
+	filters.Add("label", "label1")
33
+	filters.Add("label", "label2")
34
+
35
+	listCases := []struct {
36
+		options             types.ServiceListOptions
37
+		expectedQueryParams map[string]string
38
+	}{
39
+		{
40
+			options: types.ServiceListOptions{},
41
+			expectedQueryParams: map[string]string{
42
+				"filters": "",
43
+			},
44
+		},
45
+		{
46
+			options: types.ServiceListOptions{
47
+				Filter: 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
+			transport: newMockClient(nil, 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.Service{
68
+					{
69
+						ID: "service_id1",
70
+					},
71
+					{
72
+						ID: "service_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
+		services, err := client.ServiceList(context.Background(), listCase.options)
86
+		if err != nil {
87
+			t.Fatal(err)
88
+		}
89
+		if len(services) != 2 {
90
+			t.Fatalf("expected 2 services, got %v", services)
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
+// ServiceRemove kills and removes a service.
5
+func (cli *Client) ServiceRemove(ctx context.Context, serviceID string) error {
6
+	resp, err := cli.delete(ctx, "/services/"+serviceID, 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 TestServiceRemoveError(t *testing.T) {
14
+	client := &Client{
15
+		transport: newMockClient(nil, errorMock(http.StatusInternalServerError, "Server error")),
16
+	}
17
+
18
+	err := client.ServiceRemove(context.Background(), "service_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 TestServiceRemove(t *testing.T) {
25
+	expectedURL := "/services/service_id"
26
+
27
+	client := &Client{
28
+		transport: newMockClient(nil, 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.ServiceRemove(context.Background(), "service_id")
43
+	if err != nil {
44
+		t.Fatal(err)
45
+	}
46
+}
0 47
new file mode 100644
... ...
@@ -0,0 +1,30 @@
0
+package client
1
+
2
+import (
3
+	"net/url"
4
+	"strconv"
5
+
6
+	"github.com/docker/docker/api/types"
7
+	"github.com/docker/docker/api/types/swarm"
8
+	"golang.org/x/net/context"
9
+)
10
+
11
+// ServiceUpdate updates a Service.
12
+func (cli *Client) ServiceUpdate(ctx context.Context, serviceID string, version swarm.Version, service swarm.ServiceSpec, options types.ServiceUpdateOptions) error {
13
+	var (
14
+		headers map[string][]string
15
+		query   = url.Values{}
16
+	)
17
+
18
+	if options.EncodedRegistryAuth != "" {
19
+		headers = map[string][]string{
20
+			"X-Registry-Auth": {options.EncodedRegistryAuth},
21
+		}
22
+	}
23
+
24
+	query.Set("version", strconv.FormatUint(version.Index, 10))
25
+
26
+	resp, err := cli.post(ctx, "/services/"+serviceID+"/update", query, service, headers)
27
+	ensureReaderClosed(resp)
28
+	return err
29
+}
0 30
new file mode 100644
... ...
@@ -0,0 +1,77 @@
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"
13
+	"github.com/docker/docker/api/types/swarm"
14
+)
15
+
16
+func TestServiceUpdateError(t *testing.T) {
17
+	client := &Client{
18
+		transport: newMockClient(nil, errorMock(http.StatusInternalServerError, "Server error")),
19
+	}
20
+
21
+	err := client.ServiceUpdate(context.Background(), "service_id", swarm.Version{}, swarm.ServiceSpec{}, types.ServiceUpdateOptions{})
22
+	if err == nil || err.Error() != "Error response from daemon: Server error" {
23
+		t.Fatalf("expected a Server Error, got %v", err)
24
+	}
25
+}
26
+
27
+func TestServiceUpdate(t *testing.T) {
28
+	expectedURL := "/services/service_id/update"
29
+
30
+	updateCases := []struct {
31
+		swarmVersion    swarm.Version
32
+		expectedVersion string
33
+	}{
34
+		{
35
+			expectedVersion: "0",
36
+		},
37
+		{
38
+			swarmVersion: swarm.Version{
39
+				Index: 0,
40
+			},
41
+			expectedVersion: "0",
42
+		},
43
+		{
44
+			swarmVersion: swarm.Version{
45
+				Index: 10,
46
+			},
47
+			expectedVersion: "10",
48
+		},
49
+	}
50
+
51
+	for _, updateCase := range updateCases {
52
+		client := &Client{
53
+			transport: newMockClient(nil, func(req *http.Request) (*http.Response, error) {
54
+				if !strings.HasPrefix(req.URL.Path, expectedURL) {
55
+					return nil, fmt.Errorf("Expected URL '%s', got '%s'", expectedURL, req.URL)
56
+				}
57
+				if req.Method != "POST" {
58
+					return nil, fmt.Errorf("expected POST method, got %s", req.Method)
59
+				}
60
+				version := req.URL.Query().Get("version")
61
+				if version != updateCase.expectedVersion {
62
+					return nil, fmt.Errorf("version not set in URL query properly, expected '%s', got %s", updateCase.expectedVersion, version)
63
+				}
64
+				return &http.Response{
65
+					StatusCode: http.StatusOK,
66
+					Body:       ioutil.NopCloser(bytes.NewReader([]byte("body"))),
67
+				}, nil
68
+			}),
69
+		}
70
+
71
+		err := client.ServiceUpdate(context.Background(), "service_id", updateCase.swarmVersion, swarm.ServiceSpec{}, types.ServiceUpdateOptions{})
72
+		if err != nil {
73
+			t.Fatal(err)
74
+		}
75
+	}
76
+}
0 77
new file mode 100644
... ...
@@ -0,0 +1,21 @@
0
+package client
1
+
2
+import (
3
+	"encoding/json"
4
+
5
+	"github.com/docker/docker/api/types/swarm"
6
+	"golang.org/x/net/context"
7
+)
8
+
9
+// SwarmInit initializes the Swarm.
10
+func (cli *Client) SwarmInit(ctx context.Context, req swarm.InitRequest) (string, error) {
11
+	serverResp, err := cli.post(ctx, "/swarm/init", nil, req, nil)
12
+	if err != nil {
13
+		return "", err
14
+	}
15
+
16
+	var response string
17
+	err = json.NewDecoder(serverResp.body).Decode(&response)
18
+	ensureReaderClosed(serverResp)
19
+	return response, err
20
+}
0 21
new file mode 100644
... ...
@@ -0,0 +1,54 @@
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 TestSwarmInitError(t *testing.T) {
16
+	client := &Client{
17
+		transport: newMockClient(nil, errorMock(http.StatusInternalServerError, "Server error")),
18
+	}
19
+
20
+	_, err := client.SwarmInit(context.Background(), swarm.InitRequest{})
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 TestSwarmInit(t *testing.T) {
27
+	expectedURL := "/swarm/init"
28
+
29
+	client := &Client{
30
+		transport: newMockClient(nil, 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
+	resp, err := client.SwarmInit(context.Background(), swarm.InitRequest{
45
+		ListenAddr: "0.0.0.0:2377",
46
+	})
47
+	if err != nil {
48
+		t.Fatal(err)
49
+	}
50
+	if resp != "body" {
51
+		t.Fatalf("Expected 'body', got %s", resp)
52
+	}
53
+}
0 54
new file mode 100644
... ...
@@ -0,0 +1,21 @@
0
+package client
1
+
2
+import (
3
+	"encoding/json"
4
+
5
+	"github.com/docker/docker/api/types/swarm"
6
+	"golang.org/x/net/context"
7
+)
8
+
9
+// SwarmInspect inspects the Swarm.
10
+func (cli *Client) SwarmInspect(ctx context.Context) (swarm.Swarm, error) {
11
+	serverResp, err := cli.get(ctx, "/swarm", nil, nil)
12
+	if err != nil {
13
+		return swarm.Swarm{}, err
14
+	}
15
+
16
+	var response swarm.Swarm
17
+	err = json.NewDecoder(serverResp.body).Decode(&response)
18
+	ensureReaderClosed(serverResp)
19
+	return response, err
20
+}
0 21
new file mode 100644
... ...
@@ -0,0 +1,56 @@
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 TestSwarmInspectError(t *testing.T) {
16
+	client := &Client{
17
+		transport: newMockClient(nil, errorMock(http.StatusInternalServerError, "Server error")),
18
+	}
19
+
20
+	_, err := client.SwarmInspect(context.Background())
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 TestSwarmInspect(t *testing.T) {
27
+	expectedURL := "/swarm"
28
+	client := &Client{
29
+		transport: newMockClient(nil, 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
+			content, err := json.Marshal(swarm.Swarm{
34
+				ClusterInfo: swarm.ClusterInfo{
35
+					ID: "swarm_id",
36
+				},
37
+			})
38
+			if err != nil {
39
+				return nil, err
40
+			}
41
+			return &http.Response{
42
+				StatusCode: http.StatusOK,
43
+				Body:       ioutil.NopCloser(bytes.NewReader(content)),
44
+			}, nil
45
+		}),
46
+	}
47
+
48
+	swarmInspect, err := client.SwarmInspect(context.Background())
49
+	if err != nil {
50
+		t.Fatal(err)
51
+	}
52
+	if swarmInspect.ID != "swarm_id" {
53
+		t.Fatalf("expected `swarm_id`, got %s", swarmInspect.ID)
54
+	}
55
+}
0 56
new file mode 100644
... ...
@@ -0,0 +1,13 @@
0
+package client
1
+
2
+import (
3
+	"github.com/docker/docker/api/types/swarm"
4
+	"golang.org/x/net/context"
5
+)
6
+
7
+// SwarmJoin joins the Swarm.
8
+func (cli *Client) SwarmJoin(ctx context.Context, req swarm.JoinRequest) error {
9
+	resp, err := cli.post(ctx, "/swarm/join", nil, req, nil)
10
+	ensureReaderClosed(resp)
11
+	return err
12
+}
0 13
new file mode 100644
... ...
@@ -0,0 +1,51 @@
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 TestSwarmJoinError(t *testing.T) {
16
+	client := &Client{
17
+		transport: newMockClient(nil, errorMock(http.StatusInternalServerError, "Server error")),
18
+	}
19
+
20
+	err := client.SwarmJoin(context.Background(), swarm.JoinRequest{})
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 TestSwarmJoin(t *testing.T) {
27
+	expectedURL := "/swarm/join"
28
+
29
+	client := &Client{
30
+		transport: newMockClient(nil, 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(""))),
40
+			}, nil
41
+		}),
42
+	}
43
+
44
+	err := client.SwarmJoin(context.Background(), swarm.JoinRequest{
45
+		ListenAddr: "0.0.0.0:2377",
46
+	})
47
+	if err != nil {
48
+		t.Fatal(err)
49
+	}
50
+}
0 51
new file mode 100644
... ...
@@ -0,0 +1,18 @@
0
+package client
1
+
2
+import (
3
+	"net/url"
4
+
5
+	"golang.org/x/net/context"
6
+)
7
+
8
+// SwarmLeave leaves the Swarm.
9
+func (cli *Client) SwarmLeave(ctx context.Context, force bool) error {
10
+	query := url.Values{}
11
+	if force {
12
+		query.Set("force", "1")
13
+	}
14
+	resp, err := cli.post(ctx, "/swarm/leave", query, nil, nil)
15
+	ensureReaderClosed(resp)
16
+	return err
17
+}
0 18
new file mode 100644
... ...
@@ -0,0 +1,66 @@
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 TestSwarmLeaveError(t *testing.T) {
14
+	client := &Client{
15
+		transport: newMockClient(nil, errorMock(http.StatusInternalServerError, "Server error")),
16
+	}
17
+
18
+	err := client.SwarmLeave(context.Background(), false)
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 TestSwarmLeave(t *testing.T) {
25
+	expectedURL := "/swarm/leave"
26
+
27
+	leaveCases := []struct {
28
+		force         bool
29
+		expectedForce string
30
+	}{
31
+		{
32
+			expectedForce: "",
33
+		},
34
+		{
35
+			force:         true,
36
+			expectedForce: "1",
37
+		},
38
+	}
39
+
40
+	for _, leaveCase := range leaveCases {
41
+		client := &Client{
42
+			transport: newMockClient(nil, func(req *http.Request) (*http.Response, error) {
43
+				if !strings.HasPrefix(req.URL.Path, expectedURL) {
44
+					return nil, fmt.Errorf("Expected URL '%s', got '%s'", expectedURL, req.URL)
45
+				}
46
+				if req.Method != "POST" {
47
+					return nil, fmt.Errorf("expected POST method, got %s", req.Method)
48
+				}
49
+				force := req.URL.Query().Get("force")
50
+				if force != leaveCase.expectedForce {
51
+					return nil, fmt.Errorf("force not set in URL query properly. expected '%s', got %s", leaveCase.expectedForce, force)
52
+				}
53
+				return &http.Response{
54
+					StatusCode: http.StatusOK,
55
+					Body:       ioutil.NopCloser(bytes.NewReader([]byte(""))),
56
+				}, nil
57
+			}),
58
+		}
59
+
60
+		err := client.SwarmLeave(context.Background(), leaveCase.force)
61
+		if err != nil {
62
+			t.Fatal(err)
63
+		}
64
+	}
65
+}
0 66
new file mode 100644
... ...
@@ -0,0 +1,21 @@
0
+package client
1
+
2
+import (
3
+	"fmt"
4
+	"net/url"
5
+	"strconv"
6
+
7
+	"github.com/docker/docker/api/types/swarm"
8
+	"golang.org/x/net/context"
9
+)
10
+
11
+// SwarmUpdate updates the Swarm.
12
+func (cli *Client) SwarmUpdate(ctx context.Context, version swarm.Version, swarm swarm.Spec, flags swarm.UpdateFlags) error {
13
+	query := url.Values{}
14
+	query.Set("version", strconv.FormatUint(version.Index, 10))
15
+	query.Set("rotateWorkerToken", fmt.Sprintf("%v", flags.RotateWorkerToken))
16
+	query.Set("rotateManagerToken", fmt.Sprintf("%v", flags.RotateManagerToken))
17
+	resp, err := cli.post(ctx, "/swarm/update", query, swarm, nil)
18
+	ensureReaderClosed(resp)
19
+	return err
20
+}
0 21
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 TestSwarmUpdateError(t *testing.T) {
16
+	client := &Client{
17
+		transport: newMockClient(nil, errorMock(http.StatusInternalServerError, "Server error")),
18
+	}
19
+
20
+	err := client.SwarmUpdate(context.Background(), swarm.Version{}, swarm.Spec{}, swarm.UpdateFlags{})
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 TestSwarmUpdate(t *testing.T) {
27
+	expectedURL := "/swarm/update"
28
+
29
+	client := &Client{
30
+		transport: newMockClient(nil, 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(""))),
40
+			}, nil
41
+		}),
42
+	}
43
+
44
+	err := client.SwarmUpdate(context.Background(), swarm.Version{}, swarm.Spec{}, swarm.UpdateFlags{})
45
+	if err != nil {
46
+		t.Fatal(err)
47
+	}
48
+}
0 49
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
+
10
+	"golang.org/x/net/context"
11
+)
12
+
13
+// TaskInspectWithRaw returns the task information and its raw representation..
14
+func (cli *Client) TaskInspectWithRaw(ctx context.Context, taskID string) (swarm.Task, []byte, error) {
15
+	serverResp, err := cli.get(ctx, "/tasks/"+taskID, nil, nil)
16
+	if err != nil {
17
+		if serverResp.statusCode == http.StatusNotFound {
18
+			return swarm.Task{}, nil, taskNotFoundError{taskID}
19
+		}
20
+		return swarm.Task{}, nil, err
21
+	}
22
+	defer ensureReaderClosed(serverResp)
23
+
24
+	body, err := ioutil.ReadAll(serverResp.body)
25
+	if err != nil {
26
+		return swarm.Task{}, nil, err
27
+	}
28
+
29
+	var response swarm.Task
30
+	rdr := bytes.NewReader(body)
31
+	err = json.NewDecoder(rdr).Decode(&response)
32
+	return response, body, err
33
+}
0 34
new file mode 100644
... ...
@@ -0,0 +1,54 @@
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 TestTaskInspectError(t *testing.T) {
16
+	client := &Client{
17
+		transport: newMockClient(nil, errorMock(http.StatusInternalServerError, "Server error")),
18
+	}
19
+
20
+	_, _, err := client.TaskInspectWithRaw(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 TestTaskInspect(t *testing.T) {
27
+	expectedURL := "/tasks/task_id"
28
+	client := &Client{
29
+		transport: newMockClient(nil, 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
+			content, err := json.Marshal(swarm.Task{
34
+				ID: "task_id",
35
+			})
36
+			if err != nil {
37
+				return nil, err
38
+			}
39
+			return &http.Response{
40
+				StatusCode: http.StatusOK,
41
+				Body:       ioutil.NopCloser(bytes.NewReader(content)),
42
+			}, nil
43
+		}),
44
+	}
45
+
46
+	taskInspect, _, err := client.TaskInspectWithRaw(context.Background(), "task_id")
47
+	if err != nil {
48
+		t.Fatal(err)
49
+	}
50
+	if taskInspect.ID != "task_id" {
51
+		t.Fatalf("expected `task_id`, got %s", taskInspect.ID)
52
+	}
53
+}
0 54
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
+// TaskList returns the list of tasks.
13
+func (cli *Client) TaskList(ctx context.Context, options types.TaskListOptions) ([]swarm.Task, error) {
14
+	query := url.Values{}
15
+
16
+	if options.Filter.Len() > 0 {
17
+		filterJSON, err := filters.ToParam(options.Filter)
18
+		if err != nil {
19
+			return nil, err
20
+		}
21
+
22
+		query.Set("filters", filterJSON)
23
+	}
24
+
25
+	resp, err := cli.get(ctx, "/tasks", query, nil)
26
+	if err != nil {
27
+		return nil, err
28
+	}
29
+
30
+	var tasks []swarm.Task
31
+	err = json.NewDecoder(resp.body).Decode(&tasks)
32
+	ensureReaderClosed(resp)
33
+	return tasks, 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 TestTaskListError(t *testing.T) {
18
+	client := &Client{
19
+		transport: newMockClient(nil, errorMock(http.StatusInternalServerError, "Server error")),
20
+	}
21
+
22
+	_, err := client.TaskList(context.Background(), types.TaskListOptions{})
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 TestTaskList(t *testing.T) {
29
+	expectedURL := "/tasks"
30
+
31
+	filters := filters.NewArgs()
32
+	filters.Add("label", "label1")
33
+	filters.Add("label", "label2")
34
+
35
+	listCases := []struct {
36
+		options             types.TaskListOptions
37
+		expectedQueryParams map[string]string
38
+	}{
39
+		{
40
+			options: types.TaskListOptions{},
41
+			expectedQueryParams: map[string]string{
42
+				"filters": "",
43
+			},
44
+		},
45
+		{
46
+			options: types.TaskListOptions{
47
+				Filter: 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
+			transport: newMockClient(nil, 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.Task{
68
+					{
69
+						ID: "task_id1",
70
+					},
71
+					{
72
+						ID: "task_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
+		tasks, err := client.TaskList(context.Background(), listCase.options)
86
+		if err != nil {
87
+			t.Fatal(err)
88
+		}
89
+		if len(tasks) != 2 {
90
+			t.Fatalf("expected 2 tasks, got %v", tasks)
91
+		}
92
+	}
93
+}
0 94
new file mode 100644
... ...
@@ -0,0 +1,18 @@
0
+-----BEGIN CERTIFICATE-----
1
+MIIC0jCCAbqgAwIBAgIRAILlP5WWLaHkQ/m2ASHP7SowDQYJKoZIhvcNAQELBQAw
2
+EjEQMA4GA1UEChMHdmluY2VudDAeFw0xNjAzMjQxMDE5MDBaFw0xOTAzMDkxMDE5
3
+MDBaMBIxEDAOBgNVBAoTB3ZpbmNlbnQwggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAw
4
+ggEKAoIBAQD0yZPKAGncoaxaU/QW9tWEHbrvDoGVF/65L8Si/jBrlAgLjhmmV1di
5
+vKG9QPzuU8snxHro3/uCwyA6kTqw0U8bGwHxJq2Bpa6JBYj8N2jMJ+M+sjXgSo2t
6
+E0zIzjTW2Pir3C8qwfrVL6NFp9xClwMD23SFZ0UsEH36NkfyrKBVeM8IOjJd4Wjs
7
+xIcuvF3BTVkji84IJBW2JIKf9ZrzJwUlSCPgptRp4Evdbyp5d+UPxtwxD7qjW4lM
8
+yQQ8vfcC4lKkVx5s/RNJ4fzd5uEgLdEbZ20qt7Zt/bLcxFHpUhH2teA0QjmrOWFh
9
+gbL83s95/+hbSVhsO4hoFW7vTeiCCY4xAgMBAAGjIzAhMA4GA1UdDwEB/wQEAwIC
10
+rDAPBgNVHRMBAf8EBTADAQH/MA0GCSqGSIb3DQEBCwUAA4IBAQBY51RHajuDuhO2
11
+tcm26jeNROzfffnjhvbOVPjSEdo9vI3JpMU/RuQw+nbNcLwJrdjL6UH7tD/36Y+q
12
+NXH+xSIjWFH0zXGxrIUsVrvt6f8CbOvw7vD+gygOG+849PDQMbL6czP8rvXY7vZV
13
+9pdpQfrENk4b5kePRW/6HaGSTvtgN7XOrYD9fp3pm/G534T2e3IxgYMRNwdB9Ul9
14
+bLwMqQqf4eiqqMs6x4IVmZUkGVMKiFKcvkNg9a+Ozx5pMizHeAezWMcZ5V+QJZVT
15
+8lElSCKZ2Yy2xkcl7aeQMLwcAeZwfTp+Yu9dVzlqXiiBTLd1+LtAQCuKHzmw4Q8k
16
+EvD5m49l
17
+-----END CERTIFICATE-----
0 18
new file mode 100644
... ...
@@ -0,0 +1,18 @@
0
+-----BEGIN CERTIFICATE-----
1
+MIIC8DCCAdigAwIBAgIRAJAS1glgcke4q7eCaretwgUwDQYJKoZIhvcNAQELBQAw
2
+EjEQMA4GA1UEChMHdmluY2VudDAeFw0xNjAzMjQxMDE5MDBaFw0xOTAzMDkxMDE5
3
+MDBaMB4xHDAaBgNVBAoME3ZpbmNlbnQuPGJvb3RzdHJhcD4wggEiMA0GCSqGSIb3
4
+DQEBAQUAA4IBDwAwggEKAoIBAQClpvG442dGEvrRgmCrqY4kBml1LVlw2Y7ZDn6B
5
+TKa52+MuGDmfXbO1UhclNqTXjLgAwKjPz/OvnPRxNEUoQEDbBd+Xev7rxTY5TvYI
6
+27YH3fMH2LL2j62jum649abfhZ6ekD5eD8tCn3mnrEOgqRIlK7efPIVixq/ZqU1H
7
+7ez0ggB7dmWHlhnUaxyQOCSnAX/7nKYQXqZgVvGhDeR2jp7GcnhbK/qPrZ/mOm83
8
+2IjCeYN145opYlzTSp64GYIZz7uqMNcnDKK37ZbS8MYcTjrRaHEiqZVVdIC+ghbx
9
+qYqzbZRVfgztI9jwmifn0mYrN4yt+nhNYwBcRJ4Pv3uLFbo7AgMBAAGjNTAzMA4G
10
+A1UdDwEB/wQEAwIHgDATBgNVHSUEDDAKBggrBgEFBQcDAjAMBgNVHRMBAf8EAjAA
11
+MA0GCSqGSIb3DQEBCwUAA4IBAQDg1r7nksjYgDFYEcBbrRrRHddIoK+RVmSBTTrq
12
+8giC77m0srKdh9XTVWK1PUbGfODV1oD8m9QhPE8zPDyYQ8jeXNRSU5wXdkrTRmmY
13
+w/T3SREqmE7CObMtusokHidjYFuqqCR07sJzqBKRlzr3o0EGe3tuEhUlF5ARY028
14
+eipaDcVlT5ChGcDa6LeJ4e05u4cVap0dd6Rp1w3Rx1AYAecdgtgBMnw1iWdl/nrC
15
+sp26ZXNaAhFOUovlY9VY257AMd9hQV7WvAK4yNEHcckVu3uXTBmDgNSOPtl0QLsL
16
+Kjlj75ksCx8nCln/hCut/0+kGTsGZqdV5c6ktgcGYRir/5Hs
17
+-----END CERTIFICATE-----
0 18
new file mode 100644
... ...
@@ -0,0 +1,27 @@
0
+-----BEGIN RSA PRIVATE KEY-----
1
+MIIEowIBAAKCAQEApabxuONnRhL60YJgq6mOJAZpdS1ZcNmO2Q5+gUymudvjLhg5
2
+n12ztVIXJTak14y4AMCoz8/zr5z0cTRFKEBA2wXfl3r+68U2OU72CNu2B93zB9iy
3
+9o+to7puuPWm34WenpA+Xg/LQp95p6xDoKkSJSu3nzyFYsav2alNR+3s9IIAe3Zl
4
+h5YZ1GsckDgkpwF/+5ymEF6mYFbxoQ3kdo6exnJ4Wyv6j62f5jpvN9iIwnmDdeOa
5
+KWJc00qeuBmCGc+7qjDXJwyit+2W0vDGHE460WhxIqmVVXSAvoIW8amKs22UVX4M
6
+7SPY8Jon59JmKzeMrfp4TWMAXESeD797ixW6OwIDAQABAoIBAHfyAAleL8NfrtnR
7
+S+pApbmUIvxD0AWUooispBE/zWG6xC72P5MTqDJctIGvpYCmVf3Fgvamns7EGYN2
8
+07Sngc6V3Ca1WqyhaffpIuGbJZ1gqr89u6gotRRexBmNVj13ZTlvPJmjWgxtqQsu
9
+AvHsOkVL+HOGwRaaw24Z1umEcBVCepl7PGTqsLeJUtBUZBiqdJTu4JYLAB6BggBI
10
+OxhHoTWvlNWwzezo2C/IXkXcXD/tp3i5vTn5rAXHSMQkdMAUh7/xJ73Fl36gxZhp
11
+W7NoPKaS9qNh8jhs6p54S7tInb6+mrKtvRFKl5XAR3istXrXteT5UaukpuBbQ/5d
12
+qf4BXuECgYEAzoOKxMee5tG/G9iC6ImNq5xGAZm0OnmteNgIEQj49If1Q68av525
13
+FioqdC9zV+blfHQqXEIUeum4JAou4xqmB8Lw2H0lYwOJ1IkpUy3QJjU1IrI+U5Qy
14
+ryZuA9cxSTLf1AJFbROsoZDpjaBh0uUQkD/4PHpwXMgHu/3CaJ4nTEkCgYEAzVjE
15
+VWgczWJGyRxmHSeR51ft1jrlChZHEd3HwgLfo854JIj+MGUH4KPLSMIkYNuyiwNQ
16
+W7zdXCB47U8afSL/lPTv1M5+ZsWY6sZAT6gtp/IeU0Va943h9cj10fAOBJaz1H6M
17
+jnZS4jjWhVInE7wpCDVCwDRoHHJ84kb6JeflamMCgYBDQDcKie9HP3q6uLE4xMKr
18
+5gIuNz2n5UQGnGNUGNXp2/SVDArr55MEksqsd19aesi01KeOz74XoNDke6R1NJJo
19
+6KTB+08XhWl3GwuoGL02FBGvsNf3I8W1oBAnlAZqzfRx+CNfuA55ttU318jDgvD3
20
+6L0QBNdef411PNf4dbhacQKBgAd/e0PHFm4lbYJAaDYeUMSKwGN3KQ/SOmwblgSu
21
+iC36BwcGfYmU1tHMCUsx05Q50W4kA9Ylskt/4AqCPexdz8lHnE4/7/uesXO5I3YF
22
+JQ2h2Jufx6+MXbjUyq0Mv+ZI/m3+5PD6vxIFk0ew9T5SO4lSMIrGHxsSzx6QCuhB
23
+bG4TAoGBAJ5PWG7d2CyCjLtfF8J4NxykRvIQ8l/3kDvDdNrXiXbgonojo2lgRYaM
24
+5LoK9ApN8KHdedpTRipBaDA22Sp5SjMcUE7A6q42PJCL9r+BRYF0foFQx/rqpCff
25
+pVWKgwIPoKnfxDqN1RUgyFcx1jbA3XVJZCuT+wbMuDQ9nlvulD1W
26
+-----END RSA PRIVATE KEY-----
0 27
new file mode 100644
... ...
@@ -0,0 +1,27 @@
0
+Copyright (c) 2009 The Go Authors. All rights reserved.
1
+
2
+Redistribution and use in source and binary forms, with or without
3
+modification, are permitted provided that the following conditions are
4
+met:
5
+
6
+   * Redistributions of source code must retain the above copyright
7
+notice, this list of conditions and the following disclaimer.
8
+   * Redistributions in binary form must reproduce the above
9
+copyright notice, this list of conditions and the following disclaimer
10
+in the documentation and/or other materials provided with the
11
+distribution.
12
+   * Neither the name of Google Inc. nor the names of its
13
+contributors may be used to endorse or promote products derived from
14
+this software without specific prior written permission.
15
+
16
+THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
17
+"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
18
+LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
19
+A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
20
+OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
21
+SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
22
+LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
23
+DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
24
+THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
25
+(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
26
+OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
0 27
new file mode 100644
... ...
@@ -0,0 +1,23 @@
0
+// Copyright 2015 The Go Authors. All rights reserved.
1
+// Use of this source code is governed by a BSD-style
2
+// license that can be found in the LICENSE file.
3
+
4
+// +build go1.5
5
+
6
+package cancellable
7
+
8
+import (
9
+	"net/http"
10
+
11
+	"github.com/docker/docker/client/transport"
12
+)
13
+
14
+func canceler(client transport.Sender, req *http.Request) func() {
15
+	// TODO(djd): Respect any existing value of req.Cancel.
16
+	ch := make(chan struct{})
17
+	req.Cancel = ch
18
+
19
+	return func() {
20
+		close(ch)
21
+	}
22
+}
0 23
new file mode 100644
... ...
@@ -0,0 +1,27 @@
0
+// Copyright 2015 The Go Authors. All rights reserved.
1
+// Use of this source code is governed by a BSD-style
2
+// license that can be found in the LICENSE file.
3
+
4
+// +build !go1.5
5
+
6
+package cancellable
7
+
8
+import (
9
+	"net/http"
10
+
11
+	"github.com/docker/docker/client/transport"
12
+)
13
+
14
+type requestCanceler interface {
15
+	CancelRequest(*http.Request)
16
+}
17
+
18
+func canceler(client transport.Sender, req *http.Request) func() {
19
+	rc, ok := client.(requestCanceler)
20
+	if !ok {
21
+		return func() {}
22
+	}
23
+	return func() {
24
+		rc.CancelRequest(req)
25
+	}
26
+}
0 27
new file mode 100644
... ...
@@ -0,0 +1,115 @@
0
+// Copyright 2015 The Go Authors. All rights reserved.
1
+// Use of this source code is governed by a BSD-style
2
+// license that can be found in the LICENSE file.
3
+
4
+// Package cancellable provides helper function to cancel http requests.
5
+package cancellable
6
+
7
+import (
8
+	"io"
9
+	"net/http"
10
+	"sync"
11
+
12
+	"github.com/docker/docker/client/transport"
13
+
14
+	"golang.org/x/net/context"
15
+)
16
+
17
+func nop() {}
18
+
19
+var (
20
+	testHookContextDoneBeforeHeaders = nop
21
+	testHookDoReturned               = nop
22
+	testHookDidBodyClose             = nop
23
+)
24
+
25
+// Do sends an HTTP request with the provided transport.Sender and returns an HTTP response.
26
+// If the client is nil, http.DefaultClient is used.
27
+// If the context is canceled or times out, ctx.Err() will be returned.
28
+//
29
+// FORK INFORMATION:
30
+//
31
+// This function deviates from the upstream version in golang.org/x/net/context/ctxhttp by
32
+// taking a Sender interface rather than a *http.Client directly. That allow us to use
33
+// this function with mocked clients and hijacked connections.
34
+func Do(ctx context.Context, client transport.Sender, req *http.Request) (*http.Response, error) {
35
+	if client == nil {
36
+		client = http.DefaultClient
37
+	}
38
+
39
+	// Request cancelation changed in Go 1.5, see canceler.go and canceler_go14.go.
40
+	cancel := canceler(client, req)
41
+
42
+	type responseAndError struct {
43
+		resp *http.Response
44
+		err  error
45
+	}
46
+	result := make(chan responseAndError, 1)
47
+
48
+	go func() {
49
+		resp, err := client.Do(req)
50
+		testHookDoReturned()
51
+		result <- responseAndError{resp, err}
52
+	}()
53
+
54
+	var resp *http.Response
55
+
56
+	select {
57
+	case <-ctx.Done():
58
+		testHookContextDoneBeforeHeaders()
59
+		cancel()
60
+		// Clean up after the goroutine calling client.Do:
61
+		go func() {
62
+			if r := <-result; r.resp != nil && r.resp.Body != nil {
63
+				testHookDidBodyClose()
64
+				r.resp.Body.Close()
65
+			}
66
+		}()
67
+		return nil, ctx.Err()
68
+	case r := <-result:
69
+		var err error
70
+		resp, err = r.resp, r.err
71
+		if err != nil {
72
+			return resp, err
73
+		}
74
+	}
75
+
76
+	c := make(chan struct{})
77
+	go func() {
78
+		select {
79
+		case <-ctx.Done():
80
+			cancel()
81
+		case <-c:
82
+			// The response's Body is closed.
83
+		}
84
+	}()
85
+	resp.Body = &notifyingReader{ReadCloser: resp.Body, notify: c}
86
+
87
+	return resp, nil
88
+}
89
+
90
+// notifyingReader is an io.ReadCloser that closes the notify channel after
91
+// Close is called or a Read fails on the underlying ReadCloser.
92
+type notifyingReader struct {
93
+	io.ReadCloser
94
+	notify     chan<- struct{}
95
+	notifyOnce sync.Once
96
+}
97
+
98
+func (r *notifyingReader) Read(p []byte) (int, error) {
99
+	n, err := r.ReadCloser.Read(p)
100
+	if err != nil {
101
+		r.notifyOnce.Do(func() {
102
+			close(r.notify)
103
+		})
104
+	}
105
+	return n, err
106
+}
107
+
108
+func (r *notifyingReader) Close() error {
109
+	err := r.ReadCloser.Close()
110
+	r.notifyOnce.Do(func() {
111
+		close(r.notify)
112
+	})
113
+	return err
114
+}
0 115
new file mode 100644
... ...
@@ -0,0 +1,47 @@
0
+package transport
1
+
2
+import (
3
+	"crypto/tls"
4
+	"net/http"
5
+)
6
+
7
+// Sender is an interface that clients must implement
8
+// to be able to send requests to a remote connection.
9
+type Sender interface {
10
+	// Do sends request to a remote endpoint.
11
+	Do(*http.Request) (*http.Response, error)
12
+}
13
+
14
+// Client is an interface that abstracts all remote connections.
15
+type Client interface {
16
+	Sender
17
+	// Secure tells whether the connection is secure or not.
18
+	Secure() bool
19
+	// Scheme returns the connection protocol the client uses.
20
+	Scheme() string
21
+	// TLSConfig returns any TLS configuration the client uses.
22
+	TLSConfig() *tls.Config
23
+}
24
+
25
+// tlsInfo returns information about the TLS configuration.
26
+type tlsInfo struct {
27
+	tlsConfig *tls.Config
28
+}
29
+
30
+// TLSConfig returns the TLS configuration.
31
+func (t *tlsInfo) TLSConfig() *tls.Config {
32
+	return t.tlsConfig
33
+}
34
+
35
+// Scheme returns protocol scheme to use.
36
+func (t *tlsInfo) Scheme() string {
37
+	if t.tlsConfig != nil {
38
+		return "https"
39
+	}
40
+	return "http"
41
+}
42
+
43
+// Secure returns true if there is a TLS configuration.
44
+func (t *tlsInfo) Secure() bool {
45
+	return t.tlsConfig != nil
46
+}
0 47
new file mode 100644
... ...
@@ -0,0 +1,11 @@
0
+// +build !go1.7,!windows
1
+
2
+package transport
3
+
4
+import "crypto/tls"
5
+
6
+// TLSConfigClone returns a clone of tls.Config. This function is provided for
7
+// compatibility for go1.7 that doesn't include this method in stdlib.
8
+func TLSConfigClone(c *tls.Config) *tls.Config {
9
+	return c.Clone()
10
+}
0 11
new file mode 100644
... ...
@@ -0,0 +1,33 @@
0
+// +build go1.7
1
+
2
+package transport
3
+
4
+import "crypto/tls"
5
+
6
+// TLSConfigClone returns a clone of tls.Config. This function is provided for
7
+// compatibility for go1.7 that doesn't include this method in stdlib.
8
+func TLSConfigClone(c *tls.Config) *tls.Config {
9
+	return &tls.Config{
10
+		Rand:                        c.Rand,
11
+		Time:                        c.Time,
12
+		Certificates:                c.Certificates,
13
+		NameToCertificate:           c.NameToCertificate,
14
+		GetCertificate:              c.GetCertificate,
15
+		RootCAs:                     c.RootCAs,
16
+		NextProtos:                  c.NextProtos,
17
+		ServerName:                  c.ServerName,
18
+		ClientAuth:                  c.ClientAuth,
19
+		ClientCAs:                   c.ClientCAs,
20
+		InsecureSkipVerify:          c.InsecureSkipVerify,
21
+		CipherSuites:                c.CipherSuites,
22
+		PreferServerCipherSuites:    c.PreferServerCipherSuites,
23
+		SessionTicketsDisabled:      c.SessionTicketsDisabled,
24
+		SessionTicketKey:            c.SessionTicketKey,
25
+		ClientSessionCache:          c.ClientSessionCache,
26
+		MinVersion:                  c.MinVersion,
27
+		MaxVersion:                  c.MaxVersion,
28
+		CurvePreferences:            c.CurvePreferences,
29
+		DynamicRecordSizingDisabled: c.DynamicRecordSizingDisabled,
30
+		Renegotiation:               c.Renegotiation,
31
+	}
32
+}
0 33
new file mode 100644
... ...
@@ -0,0 +1,57 @@
0
+// Package transport provides function to send request to remote endpoints.
1
+package transport
2
+
3
+import (
4
+	"fmt"
5
+	"net/http"
6
+
7
+	"github.com/docker/go-connections/sockets"
8
+)
9
+
10
+// apiTransport holds information about the http transport to connect with the API.
11
+type apiTransport struct {
12
+	*http.Client
13
+	*tlsInfo
14
+	transport *http.Transport
15
+}
16
+
17
+// NewTransportWithHTTP creates a new transport based on the provided proto, address and http client.
18
+// It uses Docker's default http transport configuration if the client is nil.
19
+// It does not modify the client's transport if it's not nil.
20
+func NewTransportWithHTTP(proto, addr string, client *http.Client) (Client, error) {
21
+	var transport *http.Transport
22
+
23
+	if client != nil {
24
+		tr, ok := client.Transport.(*http.Transport)
25
+		if !ok {
26
+			return nil, fmt.Errorf("unable to verify TLS configuration, invalid transport %v", client.Transport)
27
+		}
28
+		transport = tr
29
+	} else {
30
+		transport = defaultTransport(proto, addr)
31
+		client = &http.Client{
32
+			Transport: transport,
33
+		}
34
+	}
35
+
36
+	return &apiTransport{
37
+		Client:    client,
38
+		tlsInfo:   &tlsInfo{transport.TLSClientConfig},
39
+		transport: transport,
40
+	}, nil
41
+}
42
+
43
+// CancelRequest stops a request execution.
44
+func (a *apiTransport) CancelRequest(req *http.Request) {
45
+	a.transport.CancelRequest(req)
46
+}
47
+
48
+// defaultTransport creates a new http.Transport with Docker's
49
+// default transport configuration.
50
+func defaultTransport(proto, addr string) *http.Transport {
51
+	tr := new(http.Transport)
52
+	sockets.ConfigureTransport(tr, proto, addr)
53
+	return tr
54
+}
55
+
56
+var _ Client = &apiTransport{}
0 57
new file mode 100644
... ...
@@ -0,0 +1,21 @@
0
+package client
1
+
2
+import (
3
+	"encoding/json"
4
+
5
+	"github.com/docker/docker/api/types"
6
+	"golang.org/x/net/context"
7
+)
8
+
9
+// ServerVersion returns information of the docker client and server host.
10
+func (cli *Client) ServerVersion(ctx context.Context) (types.Version, error) {
11
+	resp, err := cli.get(ctx, "/version", nil, nil)
12
+	if err != nil {
13
+		return types.Version{}, err
14
+	}
15
+
16
+	var server types.Version
17
+	err = json.NewDecoder(resp.body).Decode(&server)
18
+	ensureReaderClosed(resp)
19
+	return server, err
20
+}
0 21
new file mode 100644
... ...
@@ -0,0 +1,20 @@
0
+package client
1
+
2
+import (
3
+	"encoding/json"
4
+
5
+	"github.com/docker/docker/api/types"
6
+	"golang.org/x/net/context"
7
+)
8
+
9
+// VolumeCreate creates a volume in the docker host.
10
+func (cli *Client) VolumeCreate(ctx context.Context, options types.VolumeCreateRequest) (types.Volume, error) {
11
+	var volume types.Volume
12
+	resp, err := cli.post(ctx, "/volumes/create", nil, options, nil)
13
+	if err != nil {
14
+		return volume, err
15
+	}
16
+	err = json.NewDecoder(resp.body).Decode(&volume)
17
+	ensureReaderClosed(resp)
18
+	return volume, err
19
+}
0 20
new file mode 100644
... ...
@@ -0,0 +1,74 @@
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
+	"golang.org/x/net/context"
13
+)
14
+
15
+func TestVolumeCreateError(t *testing.T) {
16
+	client := &Client{
17
+		transport: newMockClient(nil, errorMock(http.StatusInternalServerError, "Server error")),
18
+	}
19
+
20
+	_, err := client.VolumeCreate(context.Background(), types.VolumeCreateRequest{})
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 TestVolumeCreate(t *testing.T) {
27
+	expectedURL := "/volumes/create"
28
+
29
+	client := &Client{
30
+		transport: newMockClient(nil, 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
+
35
+			if req.Method != "POST" {
36
+				return nil, fmt.Errorf("expected POST method, got %s", req.Method)
37
+			}
38
+
39
+			content, err := json.Marshal(types.Volume{
40
+				Name:       "volume",
41
+				Driver:     "local",
42
+				Mountpoint: "mountpoint",
43
+			})
44
+			if err != nil {
45
+				return nil, err
46
+			}
47
+			return &http.Response{
48
+				StatusCode: http.StatusOK,
49
+				Body:       ioutil.NopCloser(bytes.NewReader(content)),
50
+			}, nil
51
+		}),
52
+	}
53
+
54
+	volume, err := client.VolumeCreate(context.Background(), types.VolumeCreateRequest{
55
+		Name:   "myvolume",
56
+		Driver: "mydriver",
57
+		DriverOpts: map[string]string{
58
+			"opt-key": "opt-value",
59
+		},
60
+	})
61
+	if err != nil {
62
+		t.Fatal(err)
63
+	}
64
+	if volume.Name != "volume" {
65
+		t.Fatalf("expected volume.Name to be 'volume', got %s", volume.Name)
66
+	}
67
+	if volume.Driver != "local" {
68
+		t.Fatalf("expected volume.Driver to be 'local', got %s", volume.Driver)
69
+	}
70
+	if volume.Mountpoint != "mountpoint" {
71
+		t.Fatalf("expected volume.Mountpoint to be 'mountpoint', got %s", volume.Mountpoint)
72
+	}
73
+}
0 74
new file mode 100644
... ...
@@ -0,0 +1,38 @@
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"
9
+	"golang.org/x/net/context"
10
+)
11
+
12
+// VolumeInspect returns the information about a specific volume in the docker host.
13
+func (cli *Client) VolumeInspect(ctx context.Context, volumeID string) (types.Volume, error) {
14
+	volume, _, err := cli.VolumeInspectWithRaw(ctx, volumeID)
15
+	return volume, err
16
+}
17
+
18
+// VolumeInspectWithRaw returns the information about a specific volume in the docker host and its raw representation
19
+func (cli *Client) VolumeInspectWithRaw(ctx context.Context, volumeID string) (types.Volume, []byte, error) {
20
+	var volume types.Volume
21
+	resp, err := cli.get(ctx, "/volumes/"+volumeID, nil, nil)
22
+	if err != nil {
23
+		if resp.statusCode == http.StatusNotFound {
24
+			return volume, nil, volumeNotFoundError{volumeID}
25
+		}
26
+		return volume, nil, err
27
+	}
28
+	defer ensureReaderClosed(resp)
29
+
30
+	body, err := ioutil.ReadAll(resp.body)
31
+	if err != nil {
32
+		return volume, nil, err
33
+	}
34
+	rdr := bytes.NewReader(body)
35
+	err = json.NewDecoder(rdr).Decode(&volume)
36
+	return volume, body, err
37
+}
0 38
new file mode 100644
... ...
@@ -0,0 +1,76 @@
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
+	"golang.org/x/net/context"
13
+)
14
+
15
+func TestVolumeInspectError(t *testing.T) {
16
+	client := &Client{
17
+		transport: newMockClient(nil, errorMock(http.StatusInternalServerError, "Server error")),
18
+	}
19
+
20
+	_, err := client.VolumeInspect(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 TestVolumeInspectNotFound(t *testing.T) {
27
+	client := &Client{
28
+		transport: newMockClient(nil, errorMock(http.StatusNotFound, "Server error")),
29
+	}
30
+
31
+	_, err := client.VolumeInspect(context.Background(), "unknown")
32
+	if err == nil || !IsErrVolumeNotFound(err) {
33
+		t.Fatalf("expected a volumeNotFound error, got %v", err)
34
+	}
35
+}
36
+
37
+func TestVolumeInspect(t *testing.T) {
38
+	expectedURL := "/volumes/volume_id"
39
+	client := &Client{
40
+		transport: newMockClient(nil, 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
+			if req.Method != "GET" {
45
+				return nil, fmt.Errorf("expected GET method, got %s", req.Method)
46
+			}
47
+			content, err := json.Marshal(types.Volume{
48
+				Name:       "name",
49
+				Driver:     "driver",
50
+				Mountpoint: "mountpoint",
51
+			})
52
+			if err != nil {
53
+				return nil, err
54
+			}
55
+			return &http.Response{
56
+				StatusCode: http.StatusOK,
57
+				Body:       ioutil.NopCloser(bytes.NewReader(content)),
58
+			}, nil
59
+		}),
60
+	}
61
+
62
+	v, err := client.VolumeInspect(context.Background(), "volume_id")
63
+	if err != nil {
64
+		t.Fatal(err)
65
+	}
66
+	if v.Name != "name" {
67
+		t.Fatalf("expected `name`, got %s", v.Name)
68
+	}
69
+	if v.Driver != "driver" {
70
+		t.Fatalf("expected `driver`, got %s", v.Driver)
71
+	}
72
+	if v.Mountpoint != "mountpoint" {
73
+		t.Fatalf("expected `mountpoint`, got %s", v.Mountpoint)
74
+	}
75
+}
0 76
new file mode 100644
... ...
@@ -0,0 +1,32 @@
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
+	"golang.org/x/net/context"
9
+)
10
+
11
+// VolumeList returns the volumes configured in the docker host.
12
+func (cli *Client) VolumeList(ctx context.Context, filter filters.Args) (types.VolumesListResponse, error) {
13
+	var volumes types.VolumesListResponse
14
+	query := url.Values{}
15
+
16
+	if filter.Len() > 0 {
17
+		filterJSON, err := filters.ToParamWithVersion(cli.version, filter)
18
+		if err != nil {
19
+			return volumes, err
20
+		}
21
+		query.Set("filters", filterJSON)
22
+	}
23
+	resp, err := cli.get(ctx, "/volumes", query, nil)
24
+	if err != nil {
25
+		return volumes, err
26
+	}
27
+
28
+	err = json.NewDecoder(resp.body).Decode(&volumes)
29
+	ensureReaderClosed(resp)
30
+	return volumes, err
31
+}
0 32
new file mode 100644
... ...
@@ -0,0 +1,97 @@
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
+	"golang.org/x/net/context"
14
+)
15
+
16
+func TestVolumeListError(t *testing.T) {
17
+	client := &Client{
18
+		transport: newMockClient(nil, errorMock(http.StatusInternalServerError, "Server error")),
19
+	}
20
+
21
+	_, err := client.VolumeList(context.Background(), filters.NewArgs())
22
+	if err == nil || err.Error() != "Error response from daemon: Server error" {
23
+		t.Fatalf("expected a Server Error, got %v", err)
24
+	}
25
+}
26
+
27
+func TestVolumeList(t *testing.T) {
28
+	expectedURL := "/volumes"
29
+
30
+	noDanglingFilters := filters.NewArgs()
31
+	noDanglingFilters.Add("dangling", "false")
32
+
33
+	danglingFilters := filters.NewArgs()
34
+	danglingFilters.Add("dangling", "true")
35
+
36
+	labelFilters := filters.NewArgs()
37
+	labelFilters.Add("label", "label1")
38
+	labelFilters.Add("label", "label2")
39
+
40
+	listCases := []struct {
41
+		filters         filters.Args
42
+		expectedFilters string
43
+	}{
44
+		{
45
+			filters:         filters.NewArgs(),
46
+			expectedFilters: "",
47
+		}, {
48
+			filters:         noDanglingFilters,
49
+			expectedFilters: `{"dangling":{"false":true}}`,
50
+		}, {
51
+			filters:         danglingFilters,
52
+			expectedFilters: `{"dangling":{"true":true}}`,
53
+		}, {
54
+			filters:         labelFilters,
55
+			expectedFilters: `{"label":{"label1":true,"label2":true}}`,
56
+		},
57
+	}
58
+
59
+	for _, listCase := range listCases {
60
+		client := &Client{
61
+			transport: newMockClient(nil, func(req *http.Request) (*http.Response, error) {
62
+				if !strings.HasPrefix(req.URL.Path, expectedURL) {
63
+					return nil, fmt.Errorf("Expected URL '%s', got '%s'", expectedURL, req.URL)
64
+				}
65
+				query := req.URL.Query()
66
+				actualFilters := query.Get("filters")
67
+				if actualFilters != listCase.expectedFilters {
68
+					return nil, fmt.Errorf("filters not set in URL query properly. Expected '%s', got %s", listCase.expectedFilters, actualFilters)
69
+				}
70
+				content, err := json.Marshal(types.VolumesListResponse{
71
+					Volumes: []*types.Volume{
72
+						{
73
+							Name:   "volume",
74
+							Driver: "local",
75
+						},
76
+					},
77
+				})
78
+				if err != nil {
79
+					return nil, err
80
+				}
81
+				return &http.Response{
82
+					StatusCode: http.StatusOK,
83
+					Body:       ioutil.NopCloser(bytes.NewReader(content)),
84
+				}, nil
85
+			}),
86
+		}
87
+
88
+		volumeResponse, err := client.VolumeList(context.Background(), listCase.filters)
89
+		if err != nil {
90
+			t.Fatal(err)
91
+		}
92
+		if len(volumeResponse.Volumes) != 1 {
93
+			t.Fatalf("expected 1 volume, got %v", volumeResponse.Volumes)
94
+		}
95
+	}
96
+}
0 97
new file mode 100644
... ...
@@ -0,0 +1,18 @@
0
+package client
1
+
2
+import (
3
+	"net/url"
4
+
5
+	"golang.org/x/net/context"
6
+)
7
+
8
+// VolumeRemove removes a volume from the docker host.
9
+func (cli *Client) VolumeRemove(ctx context.Context, volumeID string, force bool) error {
10
+	query := url.Values{}
11
+	if force {
12
+		query.Set("force", "1")
13
+	}
14
+	resp, err := cli.delete(ctx, "/volumes/"+volumeID, query, nil)
15
+	ensureReaderClosed(resp)
16
+	return err
17
+}
0 18
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 TestVolumeRemoveError(t *testing.T) {
14
+	client := &Client{
15
+		transport: newMockClient(nil, errorMock(http.StatusInternalServerError, "Server error")),
16
+	}
17
+
18
+	err := client.VolumeRemove(context.Background(), "volume_id", false)
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 TestVolumeRemove(t *testing.T) {
25
+	expectedURL := "/volumes/volume_id"
26
+
27
+	client := &Client{
28
+		transport: newMockClient(nil, 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.VolumeRemove(context.Background(), "volume_id", false)
43
+	if err != nil {
44
+		t.Fatal(err)
45
+	}
46
+}
... ...
@@ -11,10 +11,10 @@ import (
11 11
 
12 12
 	"github.com/Sirupsen/logrus"
13 13
 	"github.com/docker/docker/api/server/httputils"
14
+	"github.com/docker/docker/api/types"
15
+	"github.com/docker/docker/api/types/events"
16
+	"github.com/docker/docker/api/types/versions"
14 17
 	executorpkg "github.com/docker/docker/daemon/cluster/executor"
15
-	"github.com/docker/engine-api/types"
16
-	"github.com/docker/engine-api/types/events"
17
-	"github.com/docker/engine-api/types/versions"
18 18
 	"github.com/docker/libnetwork"
19 19
 	"github.com/docker/swarmkit/api"
20 20
 	"github.com/docker/swarmkit/log"
... ...
@@ -9,13 +9,13 @@ import (
9 9
 
10 10
 	"github.com/Sirupsen/logrus"
11 11
 
12
+	"github.com/docker/docker/api/types"
13
+	enginecontainer "github.com/docker/docker/api/types/container"
14
+	"github.com/docker/docker/api/types/events"
15
+	"github.com/docker/docker/api/types/filters"
16
+	"github.com/docker/docker/api/types/network"
12 17
 	clustertypes "github.com/docker/docker/daemon/cluster/provider"
13 18
 	"github.com/docker/docker/reference"
14
-	"github.com/docker/engine-api/types"
15
-	enginecontainer "github.com/docker/engine-api/types/container"
16
-	"github.com/docker/engine-api/types/events"
17
-	"github.com/docker/engine-api/types/filters"
18
-	"github.com/docker/engine-api/types/network"
19 19
 	"github.com/docker/swarmkit/agent/exec"
20 20
 	"github.com/docker/swarmkit/api"
21 21
 )
... ...
@@ -4,9 +4,9 @@ import (
4 4
 	"fmt"
5 5
 	"os"
6 6
 
7
+	"github.com/docker/docker/api/types"
8
+	"github.com/docker/docker/api/types/events"
7 9
 	executorpkg "github.com/docker/docker/daemon/cluster/executor"
8
-	"github.com/docker/engine-api/types"
9
-	"github.com/docker/engine-api/types/events"
10 10
 	"github.com/docker/libnetwork"
11 11
 	"github.com/docker/swarmkit/agent/exec"
12 12
 	"github.com/docker/swarmkit/api"
... ...
@@ -4,10 +4,10 @@ import (
4 4
 	"sort"
5 5
 	"strings"
6 6
 
7
+	"github.com/docker/docker/api/types"
8
+	"github.com/docker/docker/api/types/network"
7 9
 	executorpkg "github.com/docker/docker/daemon/cluster/executor"
8 10
 	clustertypes "github.com/docker/docker/daemon/cluster/provider"
9
-	"github.com/docker/engine-api/types"
10
-	"github.com/docker/engine-api/types/network"
11 11
 	networktypes "github.com/docker/libnetwork/types"
12 12
 	"github.com/docker/swarmkit/agent/exec"
13 13
 	"github.com/docker/swarmkit/api"
... ...
@@ -6,10 +6,10 @@ import (
6 6
 	"testing"
7 7
 	"time"
8 8
 
9
+	containertypes "github.com/docker/docker/api/types/container"
9 10
 	"github.com/docker/docker/container"
10 11
 	"github.com/docker/docker/daemon"
11 12
 	"github.com/docker/docker/daemon/events"
12
-	containertypes "github.com/docker/engine-api/types/container"
13 13
 	"github.com/docker/swarmkit/api"
14 14
 	"golang.org/x/net/context"
15 15
 )
... ...
@@ -68,7 +68,8 @@ func (daemon *Daemon) ContainerLogs(ctx context.Context, containerName string, c
68 68
 	close(started)
69 69
 	wf.Flush()
70 70
 
71
-	var outStream io.Writer = wf
71
+	var outStream io.Writer
72
+	outStream = wf
72 73
 	errStream := outStream
73 74
 	if !container.Config.Tty {
74 75
 		errStream = stdcopy.NewStdWriter(outStream, stdcopy.Stderr)