This moves the engine-api client package to `/docker/docker/client`.
Signed-off-by: Michael Crosby <crosbymichael@gmail.com>
... | ... |
@@ -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 |
) |
... | ... |
@@ -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) { |
... | ... |
@@ -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 |
) |
... | ... |
@@ -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) { |
... | ... |
@@ -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 |
|
... | ... |
@@ -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 |
|
... | ... |
@@ -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,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 |
|
... | ... |
@@ -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 | 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 = ¬ifyingReader{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) |