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) |