Browse Source

Move engine-api client package

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

Signed-off-by: Michael Crosby <crosbymichael@gmail.com>
Michael Crosby 8 years ago
parent
commit
7c36a1af03
100 changed files with 4532 additions and 61 deletions
  1. 1 1
      api/client/cli.go
  2. 1 1
      api/client/container/create.go
  3. 1 1
      api/client/container/stats_helpers.go
  4. 1 1
      api/client/container/utils.go
  5. 1 1
      api/client/idresolver/idresolver.go
  6. 1 1
      api/client/node/cmd.go
  7. 1 1
      api/client/service/inspect.go
  8. 1 1
      api/client/stack/common.go
  9. 1 1
      api/client/system/inspect.go
  10. 1 1
      api/client/utils.go
  11. 1 1
      api/server/router/build/backend.go
  12. 3 3
      api/server/router/build/build_routes.go
  13. 2 2
      api/server/router/container/backend.go
  14. 4 4
      api/server/router/container/container_routes.go
  15. 2 2
      api/server/router/container/copy.go
  16. 2 2
      api/server/router/container/exec.go
  17. 2 2
      api/server/router/image/backend.go
  18. 3 3
      api/server/router/image/image_routes.go
  19. 2 2
      api/server/router/network/backend.go
  20. 2 2
      api/server/router/network/filter.go
  21. 3 3
      api/server/router/network/network_routes.go
  22. 1 1
      api/server/router/plugin/backend.go
  23. 1 1
      api/server/router/plugin/plugin_routes.go
  24. 2 2
      api/server/router/swarm/backend.go
  25. 3 3
      api/server/router/swarm/cluster_routes.go
  26. 3 3
      api/server/router/system/backend.go
  27. 5 5
      api/server/router/system/system_routes.go
  28. 1 1
      api/server/router/volume/backend.go
  29. 1 1
      api/server/router/volume/volume_routes.go
  30. 3 3
      api/types/versions/v1p19/types.go
  31. 2 2
      api/types/versions/v1p20/types.go
  32. 1 3
      cliconfig/credentials/file_store.go
  33. 13 0
      client/checkpoint_create.go
  34. 73 0
      client/checkpoint_create_test.go
  35. 12 0
      client/checkpoint_delete.go
  36. 47 0
      client/checkpoint_delete_test.go
  37. 22 0
      client/checkpoint_list.go
  38. 57 0
      client/checkpoint_list_test.go
  39. 156 0
      client/client.go
  40. 76 0
      client/client_mock_test.go
  41. 249 0
      client/client_test.go
  42. 6 0
      client/client_unix.go
  43. 4 0
      client/client_windows.go
  44. 34 0
      client/container_attach.go
  45. 53 0
      client/container_commit.go
  46. 96 0
      client/container_commit_test.go
  47. 97 0
      client/container_copy.go
  48. 244 0
      client/container_copy_test.go
  49. 46 0
      client/container_create.go
  50. 77 0
      client/container_create_test.go
  51. 23 0
      client/container_diff.go
  52. 61 0
      client/container_diff_test.go
  53. 49 0
      client/container_exec.go
  54. 157 0
      client/container_exec_test.go
  55. 20 0
      client/container_export.go
  56. 50 0
      client/container_export_test.go
  57. 54 0
      client/container_inspect.go
  58. 125 0
      client/container_inspect_test.go
  59. 17 0
      client/container_kill.go
  60. 46 0
      client/container_kill_test.go
  61. 56 0
      client/container_list.go
  62. 96 0
      client/container_list_test.go
  63. 52 0
      client/container_logs.go
  64. 133 0
      client/container_logs_test.go
  65. 10 0
      client/container_pause.go
  66. 41 0
      client/container_pause_test.go
  67. 27 0
      client/container_remove.go
  68. 59 0
      client/container_remove_test.go
  69. 16 0
      client/container_rename.go
  70. 46 0
      client/container_rename_test.go
  71. 29 0
      client/container_resize.go
  72. 82 0
      client/container_resize_test.go
  73. 22 0
      client/container_restart.go
  74. 48 0
      client/container_restart_test.go
  75. 21 0
      client/container_start.go
  76. 58 0
      client/container_start_test.go
  77. 24 0
      client/container_stats.go
  78. 70 0
      client/container_stats_test.go
  79. 21 0
      client/container_stop.go
  80. 48 0
      client/container_stop_test.go
  81. 28 0
      client/container_top.go
  82. 74 0
      client/container_top_test.go
  83. 10 0
      client/container_unpause.go
  84. 41 0
      client/container_unpause_test.go
  85. 23 0
      client/container_update.go
  86. 59 0
      client/container_update_test.go
  87. 26 0
      client/container_wait.go
  88. 70 0
      client/container_wait_test.go
  89. 208 0
      client/errors.go
  90. 48 0
      client/events.go
  91. 126 0
      client/events_test.go
  92. 174 0
      client/hijack.go
  93. 123 0
      client/image_build.go
  94. 230 0
      client/image_build_test.go
  95. 34 0
      client/image_create.go
  96. 76 0
      client/image_create_test.go
  97. 22 0
      client/image_history.go
  98. 60 0
      client/image_history_test.go
  99. 37 0
      client/image_import.go
  100. 81 0
      client/image_import_test.go

+ 1 - 1
api/client/cli.go

@@ -14,10 +14,10 @@ import (
 	"github.com/docker/docker/cliconfig"
 	"github.com/docker/docker/cliconfig/configfile"
 	"github.com/docker/docker/cliconfig/credentials"
+	"github.com/docker/docker/client"
 	"github.com/docker/docker/dockerversion"
 	dopts "github.com/docker/docker/opts"
 	"github.com/docker/docker/pkg/term"
-	"github.com/docker/engine-api/client"
 	"github.com/docker/go-connections/sockets"
 	"github.com/docker/go-connections/tlsconfig"
 )

+ 1 - 1
api/client/container/create.go

@@ -14,10 +14,10 @@ import (
 	"github.com/docker/docker/api/types"
 	"github.com/docker/docker/api/types/container"
 	networktypes "github.com/docker/docker/api/types/network"
+	apiclient "github.com/docker/docker/client"
 	"github.com/docker/docker/reference"
 	"github.com/docker/docker/registry"
 	runconfigopts "github.com/docker/docker/runconfig/opts"
-	apiclient "github.com/docker/engine-api/client"
 	"github.com/spf13/cobra"
 	"github.com/spf13/pflag"
 )

+ 1 - 1
api/client/container/stats_helpers.go

@@ -11,7 +11,7 @@ import (
 
 	"github.com/Sirupsen/logrus"
 	"github.com/docker/docker/api/types"
-	"github.com/docker/engine-api/client"
+	"github.com/docker/docker/client"
 	"github.com/docker/go-units"
 	"golang.org/x/net/context"
 )

+ 1 - 1
api/client/container/utils.go

@@ -12,7 +12,7 @@ import (
 	"github.com/docker/docker/api/types"
 	"github.com/docker/docker/api/types/events"
 	"github.com/docker/docker/api/types/filters"
-	clientapi "github.com/docker/engine-api/client"
+	clientapi "github.com/docker/docker/client"
 )
 
 func waitExitOrRemoved(dockerCli *client.DockerCli, ctx context.Context, containerID string, waitRemove bool) (chan int, error) {

+ 1 - 1
api/client/idresolver/idresolver.go

@@ -6,7 +6,7 @@ import (
 	"golang.org/x/net/context"
 
 	"github.com/docker/docker/api/types/swarm"
-	"github.com/docker/engine-api/client"
+	"github.com/docker/docker/client"
 )
 
 // IDResolver provides ID to Name resolution.

+ 1 - 1
api/client/node/cmd.go

@@ -5,7 +5,7 @@ import (
 
 	"github.com/docker/docker/api/client"
 	"github.com/docker/docker/cli"
-	apiclient "github.com/docker/engine-api/client"
+	apiclient "github.com/docker/docker/client"
 	"github.com/spf13/cobra"
 	"golang.org/x/net/context"
 )

+ 1 - 1
api/client/service/inspect.go

@@ -12,8 +12,8 @@ import (
 	"github.com/docker/docker/api/client/inspect"
 	"github.com/docker/docker/api/types/swarm"
 	"github.com/docker/docker/cli"
+	apiclient "github.com/docker/docker/client"
 	"github.com/docker/docker/pkg/ioutils"
-	apiclient "github.com/docker/engine-api/client"
 	"github.com/docker/go-units"
 	"github.com/spf13/cobra"
 )

+ 1 - 1
api/client/stack/common.go

@@ -8,7 +8,7 @@ import (
 	"github.com/docker/docker/api/types"
 	"github.com/docker/docker/api/types/filters"
 	"github.com/docker/docker/api/types/swarm"
-	"github.com/docker/engine-api/client"
+	"github.com/docker/docker/client"
 )
 
 const (

+ 1 - 1
api/client/system/inspect.go

@@ -9,7 +9,7 @@ import (
 	"github.com/docker/docker/api/client"
 	"github.com/docker/docker/api/client/inspect"
 	"github.com/docker/docker/cli"
-	apiclient "github.com/docker/engine-api/client"
+	apiclient "github.com/docker/docker/client"
 	"github.com/spf13/cobra"
 )
 

+ 1 - 1
api/client/utils.go

@@ -15,9 +15,9 @@ import (
 
 	"github.com/Sirupsen/logrus"
 	"github.com/docker/docker/api/types"
+	"github.com/docker/docker/client"
 	"github.com/docker/docker/pkg/signal"
 	"github.com/docker/docker/pkg/term"
-	"github.com/docker/engine-api/client"
 )
 
 func (cli *DockerCli) resizeTty(ctx context.Context, id string, isExec bool) {

+ 1 - 1
api/server/router/build/backend.go

@@ -3,8 +3,8 @@ package build
 import (
 	"io"
 
+	"github.com/docker/docker/api/types"
 	"github.com/docker/docker/api/types/backend"
-	"github.com/docker/engine-api/types"
 	"golang.org/x/net/context"
 )
 

+ 3 - 3
api/server/router/build/build_routes.go

@@ -13,13 +13,13 @@ import (
 
 	"github.com/Sirupsen/logrus"
 	"github.com/docker/docker/api/server/httputils"
+	"github.com/docker/docker/api/types"
 	"github.com/docker/docker/api/types/backend"
+	"github.com/docker/docker/api/types/container"
+	"github.com/docker/docker/api/types/versions"
 	"github.com/docker/docker/pkg/ioutils"
 	"github.com/docker/docker/pkg/progress"
 	"github.com/docker/docker/pkg/streamformatter"
-	"github.com/docker/engine-api/types"
-	"github.com/docker/engine-api/types/container"
-	"github.com/docker/engine-api/types/versions"
 	"github.com/docker/go-units"
 	"golang.org/x/net/context"
 )

+ 2 - 2
api/server/router/container/backend.go

@@ -6,10 +6,10 @@ import (
 
 	"golang.org/x/net/context"
 
+	"github.com/docker/docker/api/types"
 	"github.com/docker/docker/api/types/backend"
+	"github.com/docker/docker/api/types/container"
 	"github.com/docker/docker/pkg/archive"
-	"github.com/docker/engine-api/types"
-	"github.com/docker/engine-api/types/container"
 )
 
 // execBackend includes functions to implement to provide exec functionality.

+ 4 - 4
api/server/router/container/container_routes.go

@@ -12,13 +12,13 @@ import (
 
 	"github.com/Sirupsen/logrus"
 	"github.com/docker/docker/api/server/httputils"
+	"github.com/docker/docker/api/types"
 	"github.com/docker/docker/api/types/backend"
+	"github.com/docker/docker/api/types/container"
+	"github.com/docker/docker/api/types/filters"
+	"github.com/docker/docker/api/types/versions"
 	"github.com/docker/docker/pkg/ioutils"
 	"github.com/docker/docker/pkg/signal"
-	"github.com/docker/engine-api/types"
-	"github.com/docker/engine-api/types/container"
-	"github.com/docker/engine-api/types/filters"
-	"github.com/docker/engine-api/types/versions"
 	"golang.org/x/net/context"
 	"golang.org/x/net/websocket"
 )

+ 2 - 2
api/server/router/container/copy.go

@@ -10,8 +10,8 @@ import (
 	"strings"
 
 	"github.com/docker/docker/api/server/httputils"
-	"github.com/docker/engine-api/types"
-	"github.com/docker/engine-api/types/versions"
+	"github.com/docker/docker/api/types"
+	"github.com/docker/docker/api/types/versions"
 	"golang.org/x/net/context"
 )
 

+ 2 - 2
api/server/router/container/exec.go

@@ -9,9 +9,9 @@ import (
 
 	"github.com/Sirupsen/logrus"
 	"github.com/docker/docker/api/server/httputils"
+	"github.com/docker/docker/api/types"
+	"github.com/docker/docker/api/types/versions"
 	"github.com/docker/docker/pkg/stdcopy"
-	"github.com/docker/engine-api/types"
-	"github.com/docker/engine-api/types/versions"
 	"golang.org/x/net/context"
 )
 

+ 2 - 2
api/server/router/image/backend.go

@@ -3,9 +3,9 @@ package image
 import (
 	"io"
 
+	"github.com/docker/docker/api/types"
 	"github.com/docker/docker/api/types/backend"
-	"github.com/docker/engine-api/types"
-	"github.com/docker/engine-api/types/registry"
+	"github.com/docker/docker/api/types/registry"
 	"golang.org/x/net/context"
 )
 

+ 3 - 3
api/server/router/image/image_routes.go

@@ -10,13 +10,13 @@ import (
 	"strings"
 
 	"github.com/docker/docker/api/server/httputils"
+	"github.com/docker/docker/api/types"
 	"github.com/docker/docker/api/types/backend"
+	"github.com/docker/docker/api/types/container"
+	"github.com/docker/docker/api/types/versions"
 	"github.com/docker/docker/pkg/ioutils"
 	"github.com/docker/docker/pkg/streamformatter"
 	"github.com/docker/docker/registry"
-	"github.com/docker/engine-api/types"
-	"github.com/docker/engine-api/types/container"
-	"github.com/docker/engine-api/types/versions"
 	"golang.org/x/net/context"
 )
 

+ 2 - 2
api/server/router/network/backend.go

@@ -1,8 +1,8 @@
 package network
 
 import (
-	"github.com/docker/engine-api/types"
-	"github.com/docker/engine-api/types/network"
+	"github.com/docker/docker/api/types"
+	"github.com/docker/docker/api/types/network"
 	"github.com/docker/libnetwork"
 )
 

+ 2 - 2
api/server/router/network/filter.go

@@ -3,9 +3,9 @@ package network
 import (
 	"fmt"
 
+	"github.com/docker/docker/api/types"
+	"github.com/docker/docker/api/types/filters"
 	"github.com/docker/docker/runconfig"
-	"github.com/docker/engine-api/types"
-	"github.com/docker/engine-api/types/filters"
 )
 
 var (

+ 3 - 3
api/server/router/network/network_routes.go

@@ -8,10 +8,10 @@ import (
 	"golang.org/x/net/context"
 
 	"github.com/docker/docker/api/server/httputils"
+	"github.com/docker/docker/api/types"
+	"github.com/docker/docker/api/types/filters"
+	"github.com/docker/docker/api/types/network"
 	"github.com/docker/docker/errors"
-	"github.com/docker/engine-api/types"
-	"github.com/docker/engine-api/types/filters"
-	"github.com/docker/engine-api/types/network"
 	"github.com/docker/libnetwork"
 )
 

+ 1 - 1
api/server/router/plugin/backend.go

@@ -5,7 +5,7 @@ package plugin
 import (
 	"net/http"
 
-	enginetypes "github.com/docker/engine-api/types"
+	enginetypes "github.com/docker/docker/api/types"
 )
 
 // Backend for Plugin

+ 1 - 1
api/server/router/plugin/plugin_routes.go

@@ -9,7 +9,7 @@ import (
 	"strings"
 
 	"github.com/docker/docker/api/server/httputils"
-	"github.com/docker/engine-api/types"
+	"github.com/docker/docker/api/types"
 	"golang.org/x/net/context"
 )
 

+ 2 - 2
api/server/router/swarm/backend.go

@@ -1,8 +1,8 @@
 package swarm
 
 import (
-	basictypes "github.com/docker/engine-api/types"
-	types "github.com/docker/engine-api/types/swarm"
+	basictypes "github.com/docker/docker/api/types"
+	types "github.com/docker/docker/api/types/swarm"
 )
 
 // Backend abstracts an swarm commands manager.

+ 3 - 3
api/server/router/swarm/cluster_routes.go

@@ -8,9 +8,9 @@ import (
 
 	"github.com/Sirupsen/logrus"
 	"github.com/docker/docker/api/server/httputils"
-	basictypes "github.com/docker/engine-api/types"
-	"github.com/docker/engine-api/types/filters"
-	types "github.com/docker/engine-api/types/swarm"
+	basictypes "github.com/docker/docker/api/types"
+	"github.com/docker/docker/api/types/filters"
+	types "github.com/docker/docker/api/types/swarm"
 	"golang.org/x/net/context"
 )
 

+ 3 - 3
api/server/router/system/backend.go

@@ -3,9 +3,9 @@ package system
 import (
 	"time"
 
-	"github.com/docker/engine-api/types"
-	"github.com/docker/engine-api/types/events"
-	"github.com/docker/engine-api/types/filters"
+	"github.com/docker/docker/api/types"
+	"github.com/docker/docker/api/types/events"
+	"github.com/docker/docker/api/types/filters"
 	"golang.org/x/net/context"
 )
 

+ 5 - 5
api/server/router/system/system_routes.go

@@ -9,13 +9,13 @@ import (
 	"github.com/Sirupsen/logrus"
 	"github.com/docker/docker/api"
 	"github.com/docker/docker/api/server/httputils"
+	"github.com/docker/docker/api/types"
+	"github.com/docker/docker/api/types/events"
+	"github.com/docker/docker/api/types/filters"
+	timetypes "github.com/docker/docker/api/types/time"
+	"github.com/docker/docker/api/types/versions"
 	"github.com/docker/docker/errors"
 	"github.com/docker/docker/pkg/ioutils"
-	"github.com/docker/engine-api/types"
-	"github.com/docker/engine-api/types/events"
-	"github.com/docker/engine-api/types/filters"
-	timetypes "github.com/docker/engine-api/types/time"
-	"github.com/docker/engine-api/types/versions"
 	"golang.org/x/net/context"
 )
 

+ 1 - 1
api/server/router/volume/backend.go

@@ -2,7 +2,7 @@ package volume
 
 import (
 	// TODO return types need to be refactored into pkg
-	"github.com/docker/engine-api/types"
+	"github.com/docker/docker/api/types"
 )
 
 // Backend is the methods that need to be implemented to provide

+ 1 - 1
api/server/router/volume/volume_routes.go

@@ -5,7 +5,7 @@ import (
 	"net/http"
 
 	"github.com/docker/docker/api/server/httputils"
-	"github.com/docker/engine-api/types"
+	"github.com/docker/docker/api/types"
 	"golang.org/x/net/context"
 )
 

+ 3 - 3
api/types/versions/v1p19/types.go

@@ -2,9 +2,9 @@
 package v1p19
 
 import (
-	"github.com/docker/engine-api/types"
-	"github.com/docker/engine-api/types/container"
-	"github.com/docker/engine-api/types/versions/v1p20"
+	"github.com/docker/docker/api/types"
+	"github.com/docker/docker/api/types/container"
+	"github.com/docker/docker/api/types/versions/v1p20"
 	"github.com/docker/go-connections/nat"
 )
 

+ 2 - 2
api/types/versions/v1p20/types.go

@@ -2,8 +2,8 @@
 package v1p20
 
 import (
-	"github.com/docker/engine-api/types"
-	"github.com/docker/engine-api/types/container"
+	"github.com/docker/docker/api/types"
+	"github.com/docker/docker/api/types/container"
 	"github.com/docker/go-connections/nat"
 )
 

+ 1 - 3
cliconfig/credentials/file_store.go

@@ -1,11 +1,9 @@
 package credentials
 
 import (
-	"github.com/docker/docker/cliconfig/configfile"
-	"github.com/docker/docker/registry"
-
 	"github.com/docker/docker/api/types"
 	"github.com/docker/docker/cliconfig/configfile"
+	"github.com/docker/docker/registry"
 )
 
 // fileStore implements a credentials store using

+ 13 - 0
client/checkpoint_create.go

@@ -0,0 +1,13 @@
+package client
+
+import (
+	"github.com/docker/docker/api/types"
+	"golang.org/x/net/context"
+)
+
+// CheckpointCreate creates a checkpoint from the given container with the given name
+func (cli *Client) CheckpointCreate(ctx context.Context, container string, options types.CheckpointCreateOptions) error {
+	resp, err := cli.post(ctx, "/containers/"+container+"/checkpoints", nil, options, nil)
+	ensureReaderClosed(resp)
+	return err
+}

+ 73 - 0
client/checkpoint_create_test.go

@@ -0,0 +1,73 @@
+package client
+
+import (
+	"bytes"
+	"encoding/json"
+	"fmt"
+	"io/ioutil"
+	"net/http"
+	"strings"
+	"testing"
+
+	"github.com/docker/docker/api/types"
+	"golang.org/x/net/context"
+)
+
+func TestCheckpointCreateError(t *testing.T) {
+	client := &Client{
+		transport: newMockClient(nil, errorMock(http.StatusInternalServerError, "Server error")),
+	}
+	err := client.CheckpointCreate(context.Background(), "nothing", types.CheckpointCreateOptions{
+		CheckpointID: "noting",
+		Exit:         true,
+	})
+
+	if err == nil || err.Error() != "Error response from daemon: Server error" {
+		t.Fatalf("expected a Server Error, got %v", err)
+	}
+}
+
+func TestCheckpointCreate(t *testing.T) {
+	expectedContainerID := "container_id"
+	expectedCheckpointID := "checkpoint_id"
+	expectedURL := "/containers/container_id/checkpoints"
+
+	client := &Client{
+		transport: newMockClient(nil, func(req *http.Request) (*http.Response, error) {
+			if !strings.HasPrefix(req.URL.Path, expectedURL) {
+				return nil, fmt.Errorf("Expected URL '%s', got '%s'", expectedURL, req.URL)
+			}
+
+			if req.Method != "POST" {
+				return nil, fmt.Errorf("expected POST method, got %s", req.Method)
+			}
+
+			createOptions := &types.CheckpointCreateOptions{}
+			if err := json.NewDecoder(req.Body).Decode(createOptions); err != nil {
+				return nil, err
+			}
+
+			if createOptions.CheckpointID != expectedCheckpointID {
+				return nil, fmt.Errorf("expected CheckpointID to be 'checkpoint_id', got %v", createOptions.CheckpointID)
+			}
+
+			if !createOptions.Exit {
+				return nil, fmt.Errorf("expected Exit to be true")
+			}
+
+			return &http.Response{
+				StatusCode: http.StatusOK,
+				Body:       ioutil.NopCloser(bytes.NewReader([]byte(""))),
+			}, nil
+		}),
+	}
+
+	err := client.CheckpointCreate(context.Background(), expectedContainerID, types.CheckpointCreateOptions{
+		CheckpointID: expectedCheckpointID,
+		Exit:         true,
+	})
+
+	if err != nil {
+		t.Fatal(err)
+	}
+}

+ 12 - 0
client/checkpoint_delete.go

@@ -0,0 +1,12 @@
+package client
+
+import (
+	"golang.org/x/net/context"
+)
+
+// CheckpointDelete deletes the checkpoint with the given name from the given container
+func (cli *Client) CheckpointDelete(ctx context.Context, containerID string, checkpointID string) error {
+	resp, err := cli.delete(ctx, "/containers/"+containerID+"/checkpoints/"+checkpointID, nil, nil)
+	ensureReaderClosed(resp)
+	return err
+}

+ 47 - 0
client/checkpoint_delete_test.go

@@ -0,0 +1,47 @@
+package client
+
+import (
+	"bytes"
+	"fmt"
+	"io/ioutil"
+	"net/http"
+	"strings"
+	"testing"
+
+	"golang.org/x/net/context"
+)
+
+func TestCheckpointDeleteError(t *testing.T) {
+	client := &Client{
+		transport: newMockClient(nil, errorMock(http.StatusInternalServerError, "Server error")),
+	}
+
+	err := client.CheckpointDelete(context.Background(), "container_id", "checkpoint_id")
+	if err == nil || err.Error() != "Error response from daemon: Server error" {
+		t.Fatalf("expected a Server Error, got %v", err)
+	}
+}
+
+func TestCheckpointDelete(t *testing.T) {
+	expectedURL := "/containers/container_id/checkpoints/checkpoint_id"
+
+	client := &Client{
+		transport: newMockClient(nil, func(req *http.Request) (*http.Response, error) {
+			if !strings.HasPrefix(req.URL.Path, expectedURL) {
+				return nil, fmt.Errorf("Expected URL '%s', got '%s'", expectedURL, req.URL)
+			}
+			if req.Method != "DELETE" {
+				return nil, fmt.Errorf("expected DELETE method, got %s", req.Method)
+			}
+			return &http.Response{
+				StatusCode: http.StatusOK,
+				Body:       ioutil.NopCloser(bytes.NewReader([]byte(""))),
+			}, nil
+		}),
+	}
+
+	err := client.CheckpointDelete(context.Background(), "container_id", "checkpoint_id")
+	if err != nil {
+		t.Fatal(err)
+	}
+}

+ 22 - 0
client/checkpoint_list.go

@@ -0,0 +1,22 @@
+package client
+
+import (
+	"encoding/json"
+
+	"github.com/docker/docker/api/types"
+	"golang.org/x/net/context"
+)
+
+// CheckpointList returns the volumes configured in the docker host.
+func (cli *Client) CheckpointList(ctx context.Context, container string) ([]types.Checkpoint, error) {
+	var checkpoints []types.Checkpoint
+
+	resp, err := cli.get(ctx, "/containers/"+container+"/checkpoints", nil, nil)
+	if err != nil {
+		return checkpoints, err
+	}
+
+	err = json.NewDecoder(resp.body).Decode(&checkpoints)
+	ensureReaderClosed(resp)
+	return checkpoints, err
+}

+ 57 - 0
client/checkpoint_list_test.go

@@ -0,0 +1,57 @@
+package client
+
+import (
+	"bytes"
+	"encoding/json"
+	"fmt"
+	"io/ioutil"
+	"net/http"
+	"strings"
+	"testing"
+
+	"github.com/docker/docker/api/types"
+	"golang.org/x/net/context"
+)
+
+func TestCheckpointListError(t *testing.T) {
+	client := &Client{
+		transport: newMockClient(nil, errorMock(http.StatusInternalServerError, "Server error")),
+	}
+
+	_, err := client.CheckpointList(context.Background(), "container_id")
+	if err == nil || err.Error() != "Error response from daemon: Server error" {
+		t.Fatalf("expected a Server Error, got %v", err)
+	}
+}
+
+func TestCheckpointList(t *testing.T) {
+	expectedURL := "/containers/container_id/checkpoints"
+
+	client := &Client{
+		transport: newMockClient(nil, func(req *http.Request) (*http.Response, error) {
+			if !strings.HasPrefix(req.URL.Path, expectedURL) {
+				return nil, fmt.Errorf("Expected URL '%s', got '%s'", expectedURL, req.URL)
+			}
+			content, err := json.Marshal([]types.Checkpoint{
+				{
+					Name: "checkpoint",
+				},
+			})
+			if err != nil {
+				return nil, err
+			}
+			return &http.Response{
+				StatusCode: http.StatusOK,
+				Body:       ioutil.NopCloser(bytes.NewReader(content)),
+			}, nil
+		}),
+	}
+
+	checkpoints, err := client.CheckpointList(context.Background(), "container_id")
+	if err != nil {
+		t.Fatal(err)
+	}
+	if len(checkpoints) != 1 {
+		t.Fatalf("expected 1 checkpoint, got %v", checkpoints)
+	}
+}

+ 156 - 0
client/client.go

@@ -0,0 +1,156 @@
+package client
+
+import (
+	"fmt"
+	"net/http"
+	"net/url"
+	"os"
+	"path/filepath"
+	"strings"
+
+	"github.com/docker/docker/client/transport"
+	"github.com/docker/go-connections/tlsconfig"
+)
+
+// DefaultVersion is the version of the current stable API
+const DefaultVersion string = "1.23"
+
+// Client is the API client that performs all operations
+// against a docker server.
+type Client struct {
+	// host holds the server address to connect to
+	host string
+	// proto holds the client protocol i.e. unix.
+	proto string
+	// addr holds the client address.
+	addr string
+	// basePath holds the path to prepend to the requests.
+	basePath string
+	// transport is the interface to send request with, it implements transport.Client.
+	transport transport.Client
+	// version of the server to talk to.
+	version string
+	// custom http headers configured by users.
+	customHTTPHeaders map[string]string
+}
+
+// NewEnvClient initializes a new API client based on environment variables.
+// Use DOCKER_HOST to set the url to the docker server.
+// Use DOCKER_API_VERSION to set the version of the API to reach, leave empty for latest.
+// Use DOCKER_CERT_PATH to load the tls certificates from.
+// Use DOCKER_TLS_VERIFY to enable or disable TLS verification, off by default.
+func NewEnvClient() (*Client, error) {
+	var client *http.Client
+	if dockerCertPath := os.Getenv("DOCKER_CERT_PATH"); dockerCertPath != "" {
+		options := tlsconfig.Options{
+			CAFile:             filepath.Join(dockerCertPath, "ca.pem"),
+			CertFile:           filepath.Join(dockerCertPath, "cert.pem"),
+			KeyFile:            filepath.Join(dockerCertPath, "key.pem"),
+			InsecureSkipVerify: os.Getenv("DOCKER_TLS_VERIFY") == "",
+		}
+		tlsc, err := tlsconfig.Client(options)
+		if err != nil {
+			return nil, err
+		}
+
+		client = &http.Client{
+			Transport: &http.Transport{
+				TLSClientConfig: tlsc,
+			},
+		}
+	}
+
+	host := os.Getenv("DOCKER_HOST")
+	if host == "" {
+		host = DefaultDockerHost
+	}
+
+	version := os.Getenv("DOCKER_API_VERSION")
+	if version == "" {
+		version = DefaultVersion
+	}
+
+	return NewClient(host, version, client, nil)
+}
+
+// NewClient initializes a new API client for the given host and API version.
+// It uses the given http client as transport.
+// It also initializes the custom http headers to add to each request.
+//
+// It won't send any version information if the version number is empty. It is
+// highly recommended that you set a version or your client may break if the
+// server is upgraded.
+func NewClient(host string, version string, client *http.Client, httpHeaders map[string]string) (*Client, error) {
+	proto, addr, basePath, err := ParseHost(host)
+	if err != nil {
+		return nil, err
+	}
+
+	transport, err := transport.NewTransportWithHTTP(proto, addr, client)
+	if err != nil {
+		return nil, err
+	}
+
+	return &Client{
+		host:              host,
+		proto:             proto,
+		addr:              addr,
+		basePath:          basePath,
+		transport:         transport,
+		version:           version,
+		customHTTPHeaders: httpHeaders,
+	}, nil
+}
+
+// getAPIPath returns the versioned request path to call the api.
+// It appends the query parameters to the path if they are not empty.
+func (cli *Client) getAPIPath(p string, query url.Values) string {
+	var apiPath string
+	if cli.version != "" {
+		v := strings.TrimPrefix(cli.version, "v")
+		apiPath = fmt.Sprintf("%s/v%s%s", cli.basePath, v, p)
+	} else {
+		apiPath = fmt.Sprintf("%s%s", cli.basePath, p)
+	}
+
+	u := &url.URL{
+		Path: apiPath,
+	}
+	if len(query) > 0 {
+		u.RawQuery = query.Encode()
+	}
+	return u.String()
+}
+
+// ClientVersion returns the version string associated with this
+// instance of the Client. Note that this value can be changed
+// via the DOCKER_API_VERSION env var.
+func (cli *Client) ClientVersion() string {
+	return cli.version
+}
+
+// UpdateClientVersion updates the version string associated with this
+// instance of the Client.
+func (cli *Client) UpdateClientVersion(v string) {
+	cli.version = v
+}
+
+// ParseHost verifies that the given host strings is valid.
+func ParseHost(host string) (string, string, string, error) {
+	protoAddrParts := strings.SplitN(host, "://", 2)
+	if len(protoAddrParts) == 1 {
+		return "", "", "", fmt.Errorf("unable to parse docker host `%s`", host)
+	}
+
+	var basePath string
+	proto, addr := protoAddrParts[0], protoAddrParts[1]
+	if proto == "tcp" {
+		parsed, err := url.Parse("tcp://" + addr)
+		if err != nil {
+			return "", "", "", err
+		}
+		addr = parsed.Host
+		basePath = parsed.Path
+	}
+	return proto, addr, basePath, nil
+}

+ 76 - 0
client/client_mock_test.go

@@ -0,0 +1,76 @@
+package client
+
+import (
+	"bytes"
+	"crypto/tls"
+	"encoding/json"
+	"io/ioutil"
+	"net/http"
+
+	"github.com/docker/docker/api/types"
+	"github.com/docker/docker/client/transport"
+)
+
+type mockClient struct {
+	do func(*http.Request) (*http.Response, error)
+}
+
+// TLSConfig returns the TLS configuration.
+func (m *mockClient) TLSConfig() *tls.Config {
+	return &tls.Config{}
+}
+
+// Scheme returns protocol scheme to use.
+func (m *mockClient) Scheme() string {
+	return "http"
+}
+
+// Secure returns true if there is a TLS configuration.
+func (m *mockClient) Secure() bool {
+	return false
+}
+
+// NewMockClient returns a mocked client that runs the function supplied as `client.Do` call
+func newMockClient(tlsConfig *tls.Config, doer func(*http.Request) (*http.Response, error)) transport.Client {
+	if tlsConfig != nil {
+		panic("this actually gets set!")
+	}
+
+	return &mockClient{
+		do: doer,
+	}
+}
+
+// Do executes the supplied function for the mock.
+func (m mockClient) Do(req *http.Request) (*http.Response, error) {
+	return m.do(req)
+}
+
+func errorMock(statusCode int, message string) func(req *http.Request) (*http.Response, error) {
+	return func(req *http.Request) (*http.Response, error) {
+		header := http.Header{}
+		header.Set("Content-Type", "application/json")
+
+		body, err := json.Marshal(&types.ErrorResponse{
+			Message: message,
+		})
+		if err != nil {
+			return nil, err
+		}
+
+		return &http.Response{
+			StatusCode: statusCode,
+			Body:       ioutil.NopCloser(bytes.NewReader(body)),
+			Header:     header,
+		}, nil
+	}
+}
+
+func plainTextErrorMock(statusCode int, message string) func(req *http.Request) (*http.Response, error) {
+	return func(req *http.Request) (*http.Response, error) {
+		return &http.Response{
+			StatusCode: statusCode,
+			Body:       ioutil.NopCloser(bytes.NewReader([]byte(message))),
+		}, nil
+	}
+}

+ 249 - 0
client/client_test.go

@@ -0,0 +1,249 @@
+package client
+
+import (
+	"bytes"
+	"encoding/json"
+	"io/ioutil"
+	"net/http"
+	"net/url"
+	"os"
+	"runtime"
+	"strings"
+	"testing"
+
+	"github.com/docker/docker/api/types"
+	"golang.org/x/net/context"
+)
+
+func TestNewEnvClient(t *testing.T) {
+	if runtime.GOOS == "windows" {
+		t.Skip("skipping unix only test for windows")
+	}
+	cases := []struct {
+		envs            map[string]string
+		expectedError   string
+		expectedVersion string
+	}{
+		{
+			envs:            map[string]string{},
+			expectedVersion: DefaultVersion,
+		},
+		{
+			envs: map[string]string{
+				"DOCKER_CERT_PATH": "invalid/path",
+			},
+			expectedError: "Could not load X509 key pair: open invalid/path/cert.pem: no such file or directory. Make sure the key is not encrypted",
+		},
+		{
+			envs: map[string]string{
+				"DOCKER_CERT_PATH": "testdata/",
+			},
+			expectedVersion: DefaultVersion,
+		},
+		{
+			envs: map[string]string{
+				"DOCKER_HOST": "host",
+			},
+			expectedError: "unable to parse docker host `host`",
+		},
+		{
+			envs: map[string]string{
+				"DOCKER_HOST": "invalid://url",
+			},
+			expectedVersion: DefaultVersion,
+		},
+		{
+			envs: map[string]string{
+				"DOCKER_API_VERSION": "anything",
+			},
+			expectedVersion: "anything",
+		},
+		{
+			envs: map[string]string{
+				"DOCKER_API_VERSION": "1.22",
+			},
+			expectedVersion: "1.22",
+		},
+	}
+	for _, c := range cases {
+		recoverEnvs := setupEnvs(t, c.envs)
+		apiclient, err := NewEnvClient()
+		if c.expectedError != "" {
+			if err == nil || err.Error() != c.expectedError {
+				t.Errorf("expected an error %s, got %s, for %v", c.expectedError, err.Error(), c)
+			}
+		} else {
+			if err != nil {
+				t.Error(err)
+			}
+			version := apiclient.ClientVersion()
+			if version != c.expectedVersion {
+				t.Errorf("expected %s, got %s, for %v", c.expectedVersion, version, c)
+			}
+		}
+		recoverEnvs(t)
+	}
+}
+
+func setupEnvs(t *testing.T, envs map[string]string) func(*testing.T) {
+	oldEnvs := map[string]string{}
+	for key, value := range envs {
+		oldEnv := os.Getenv(key)
+		oldEnvs[key] = oldEnv
+		err := os.Setenv(key, value)
+		if err != nil {
+			t.Error(err)
+		}
+	}
+	return func(t *testing.T) {
+		for key, value := range oldEnvs {
+			err := os.Setenv(key, value)
+			if err != nil {
+				t.Error(err)
+			}
+		}
+	}
+}
+
+func TestGetAPIPath(t *testing.T) {
+	cases := []struct {
+		v string
+		p string
+		q url.Values
+		e string
+	}{
+		{"", "/containers/json", nil, "/containers/json"},
+		{"", "/containers/json", url.Values{}, "/containers/json"},
+		{"", "/containers/json", url.Values{"s": []string{"c"}}, "/containers/json?s=c"},
+		{"1.22", "/containers/json", nil, "/v1.22/containers/json"},
+		{"1.22", "/containers/json", url.Values{}, "/v1.22/containers/json"},
+		{"1.22", "/containers/json", url.Values{"s": []string{"c"}}, "/v1.22/containers/json?s=c"},
+		{"v1.22", "/containers/json", nil, "/v1.22/containers/json"},
+		{"v1.22", "/containers/json", url.Values{}, "/v1.22/containers/json"},
+		{"v1.22", "/containers/json", url.Values{"s": []string{"c"}}, "/v1.22/containers/json?s=c"},
+		{"v1.22", "/networks/kiwl$%^", nil, "/v1.22/networks/kiwl$%25%5E"},
+	}
+
+	for _, cs := range cases {
+		c, err := NewClient("unix:///var/run/docker.sock", cs.v, nil, nil)
+		if err != nil {
+			t.Fatal(err)
+		}
+		g := c.getAPIPath(cs.p, cs.q)
+		if g != cs.e {
+			t.Fatalf("Expected %s, got %s", cs.e, g)
+		}
+	}
+}
+
+func TestParseHost(t *testing.T) {
+	cases := []struct {
+		host  string
+		proto string
+		addr  string
+		base  string
+		err   bool
+	}{
+		{"", "", "", "", true},
+		{"foobar", "", "", "", true},
+		{"foo://bar", "foo", "bar", "", false},
+		{"tcp://localhost:2476", "tcp", "localhost:2476", "", false},
+		{"tcp://localhost:2476/path", "tcp", "localhost:2476", "/path", false},
+	}
+
+	for _, cs := range cases {
+		p, a, b, e := ParseHost(cs.host)
+		if cs.err && e == nil {
+			t.Fatalf("expected error, got nil")
+		}
+		if !cs.err && e != nil {
+			t.Fatal(e)
+		}
+		if cs.proto != p {
+			t.Fatalf("expected proto %s, got %s", cs.proto, p)
+		}
+		if cs.addr != a {
+			t.Fatalf("expected addr %s, got %s", cs.addr, a)
+		}
+		if cs.base != b {
+			t.Fatalf("expected base %s, got %s", cs.base, b)
+		}
+	}
+}
+
+func TestUpdateClientVersion(t *testing.T) {
+	client := &Client{
+		transport: newMockClient(nil, func(req *http.Request) (*http.Response, error) {
+			splitQuery := strings.Split(req.URL.Path, "/")
+			queryVersion := splitQuery[1]
+			b, err := json.Marshal(types.Version{
+				APIVersion: queryVersion,
+			})
+			if err != nil {
+				return nil, err
+			}
+			return &http.Response{
+				StatusCode: http.StatusOK,
+				Body:       ioutil.NopCloser(bytes.NewReader(b)),
+			}, nil
+		}),
+	}
+
+	cases := []struct {
+		v string
+	}{
+		{"1.20"},
+		{"v1.21"},
+		{"1.22"},
+		{"v1.22"},
+	}
+
+	for _, cs := range cases {
+		client.UpdateClientVersion(cs.v)
+		r, err := client.ServerVersion(context.Background())
+		if err != nil {
+			t.Fatal(err)
+		}
+		if strings.TrimPrefix(r.APIVersion, "v") != strings.TrimPrefix(cs.v, "v") {
+			t.Fatalf("Expected %s, got %s", cs.v, r.APIVersion)
+		}
+	}
+}
+
+func TestNewEnvClientSetsDefaultVersion(t *testing.T) {
+	// Unset environment variables
+	envVarKeys := []string{
+		"DOCKER_HOST",
+		"DOCKER_API_VERSION",
+		"DOCKER_TLS_VERIFY",
+		"DOCKER_CERT_PATH",
+	}
+	envVarValues := make(map[string]string)
+	for _, key := range envVarKeys {
+		envVarValues[key] = os.Getenv(key)
+		os.Setenv(key, "")
+	}
+
+	client, err := NewEnvClient()
+	if err != nil {
+		t.Fatal(err)
+	}
+	if client.version != DefaultVersion {
+		t.Fatalf("Expected %s, got %s", DefaultVersion, client.version)
+	}
+
+	expected := "1.22"
+	os.Setenv("DOCKER_API_VERSION", expected)
+	client, err = NewEnvClient()
+	if err != nil {
+		t.Fatal(err)
+	}
+	if client.version != expected {
+		t.Fatalf("Expected %s, got %s", expected, client.version)
+	}
+
+	// Restore environment variables
+	for _, key := range envVarKeys {
+		os.Setenv(key, envVarValues[key])
+	}
+}

+ 6 - 0
client/client_unix.go

@@ -0,0 +1,6 @@
+// +build linux freebsd solaris openbsd darwin
+
+package client
+
+// DefaultDockerHost defines os specific default if DOCKER_HOST is unset
+const DefaultDockerHost = "unix:///var/run/docker.sock"

+ 4 - 0
client/client_windows.go

@@ -0,0 +1,4 @@
+package client
+
+// DefaultDockerHost defines os specific default if DOCKER_HOST is unset
+const DefaultDockerHost = "npipe:////./pipe/docker_engine"

+ 34 - 0
client/container_attach.go

@@ -0,0 +1,34 @@
+package client
+
+import (
+	"net/url"
+
+	"github.com/docker/docker/api/types"
+	"golang.org/x/net/context"
+)
+
+// ContainerAttach attaches a connection to a container in the server.
+// It returns a types.HijackedConnection with the hijacked connection
+// and the a reader to get output. It's up to the called to close
+// the hijacked connection by calling types.HijackedResponse.Close.
+func (cli *Client) ContainerAttach(ctx context.Context, container string, options types.ContainerAttachOptions) (types.HijackedResponse, error) {
+	query := url.Values{}
+	if options.Stream {
+		query.Set("stream", "1")
+	}
+	if options.Stdin {
+		query.Set("stdin", "1")
+	}
+	if options.Stdout {
+		query.Set("stdout", "1")
+	}
+	if options.Stderr {
+		query.Set("stderr", "1")
+	}
+	if options.DetachKeys != "" {
+		query.Set("detachKeys", options.DetachKeys)
+	}
+
+	headers := map[string][]string{"Content-Type": {"text/plain"}}
+	return cli.postHijacked(ctx, "/containers/"+container+"/attach", query, nil, headers)
+}

+ 53 - 0
client/container_commit.go

@@ -0,0 +1,53 @@
+package client
+
+import (
+	"encoding/json"
+	"errors"
+	"net/url"
+
+	distreference "github.com/docker/distribution/reference"
+	"github.com/docker/docker/api/types"
+	"github.com/docker/docker/api/types/reference"
+	"golang.org/x/net/context"
+)
+
+// ContainerCommit applies changes into a container and creates a new tagged image.
+func (cli *Client) ContainerCommit(ctx context.Context, container string, options types.ContainerCommitOptions) (types.ContainerCommitResponse, error) {
+	var repository, tag string
+	if options.Reference != "" {
+		distributionRef, err := distreference.ParseNamed(options.Reference)
+		if err != nil {
+			return types.ContainerCommitResponse{}, err
+		}
+
+		if _, isCanonical := distributionRef.(distreference.Canonical); isCanonical {
+			return types.ContainerCommitResponse{}, errors.New("refusing to create a tag with a digest reference")
+		}
+
+		tag = reference.GetTagFromNamedRef(distributionRef)
+		repository = distributionRef.Name()
+	}
+
+	query := url.Values{}
+	query.Set("container", container)
+	query.Set("repo", repository)
+	query.Set("tag", tag)
+	query.Set("comment", options.Comment)
+	query.Set("author", options.Author)
+	for _, change := range options.Changes {
+		query.Add("changes", change)
+	}
+	if options.Pause != true {
+		query.Set("pause", "0")
+	}
+
+	var response types.ContainerCommitResponse
+	resp, err := cli.post(ctx, "/commit", query, options.Config, nil)
+	if err != nil {
+		return response, err
+	}
+
+	err = json.NewDecoder(resp.body).Decode(&response)
+	ensureReaderClosed(resp)
+	return response, err
+}

+ 96 - 0
client/container_commit_test.go

@@ -0,0 +1,96 @@
+package client
+
+import (
+	"bytes"
+	"encoding/json"
+	"fmt"
+	"io/ioutil"
+	"net/http"
+	"strings"
+	"testing"
+
+	"github.com/docker/docker/api/types"
+	"golang.org/x/net/context"
+)
+
+func TestContainerCommitError(t *testing.T) {
+	client := &Client{
+		transport: newMockClient(nil, errorMock(http.StatusInternalServerError, "Server error")),
+	}
+	_, err := client.ContainerCommit(context.Background(), "nothing", types.ContainerCommitOptions{})
+	if err == nil || err.Error() != "Error response from daemon: Server error" {
+		t.Fatalf("expected a Server Error, got %v", err)
+	}
+}
+
+func TestContainerCommit(t *testing.T) {
+	expectedURL := "/commit"
+	expectedContainerID := "container_id"
+	specifiedReference := "repository_name:tag"
+	expectedRepositoryName := "repository_name"
+	expectedTag := "tag"
+	expectedComment := "comment"
+	expectedAuthor := "author"
+	expectedChanges := []string{"change1", "change2"}
+
+	client := &Client{
+		transport: newMockClient(nil, func(req *http.Request) (*http.Response, error) {
+			if !strings.HasPrefix(req.URL.Path, expectedURL) {
+				return nil, fmt.Errorf("Expected URL '%s', got '%s'", expectedURL, req.URL)
+			}
+			query := req.URL.Query()
+			containerID := query.Get("container")
+			if containerID != expectedContainerID {
+				return nil, fmt.Errorf("container id not set in URL query properly. Expected '%s', got %s", expectedContainerID, containerID)
+			}
+			repo := query.Get("repo")
+			if repo != expectedRepositoryName {
+				return nil, fmt.Errorf("container repo not set in URL query properly. Expected '%s', got %s", expectedRepositoryName, repo)
+			}
+			tag := query.Get("tag")
+			if tag != expectedTag {
+				return nil, fmt.Errorf("container tag not set in URL query properly. Expected '%s', got %s'", expectedTag, tag)
+			}
+			comment := query.Get("comment")
+			if comment != expectedComment {
+				return nil, fmt.Errorf("container comment not set in URL query properly. Expected '%s', got %s'", expectedComment, comment)
+			}
+			author := query.Get("author")
+			if author != expectedAuthor {
+				return nil, fmt.Errorf("container author not set in URL query properly. Expected '%s', got %s'", expectedAuthor, author)
+			}
+			pause := query.Get("pause")
+			if pause != "0" {
+				return nil, fmt.Errorf("container pause not set in URL query properly. Expected 'true', got %v'", pause)
+			}
+			changes := query["changes"]
+			if len(changes) != len(expectedChanges) {
+				return nil, fmt.Errorf("expected container changes size to be '%d', got %d", len(expectedChanges), len(changes))
+			}
+			b, err := json.Marshal(types.ContainerCommitResponse{
+				ID: "new_container_id",
+			})
+			if err != nil {
+				return nil, err
+			}
+			return &http.Response{
+				StatusCode: http.StatusOK,
+				Body:       ioutil.NopCloser(bytes.NewReader(b)),
+			}, nil
+		}),
+	}
+
+	r, err := client.ContainerCommit(context.Background(), expectedContainerID, types.ContainerCommitOptions{
+		Reference: specifiedReference,
+		Comment:   expectedComment,
+		Author:    expectedAuthor,
+		Changes:   expectedChanges,
+		Pause:     false,
+	})
+	if err != nil {
+		t.Fatal(err)
+	}
+	if r.ID != "new_container_id" {
+		t.Fatalf("expected `container_id`, got %s", r.ID)
+	}
+}

+ 97 - 0
client/container_copy.go

@@ -0,0 +1,97 @@
+package client
+
+import (
+	"encoding/base64"
+	"encoding/json"
+	"fmt"
+	"io"
+	"net/http"
+	"net/url"
+	"path/filepath"
+	"strings"
+
+	"golang.org/x/net/context"
+
+	"github.com/docker/docker/api/types"
+)
+
+// ContainerStatPath returns Stat information about a path inside the container filesystem.
+func (cli *Client) ContainerStatPath(ctx context.Context, containerID, path string) (types.ContainerPathStat, error) {
+	query := url.Values{}
+	query.Set("path", filepath.ToSlash(path)) // Normalize the paths used in the API.
+
+	urlStr := fmt.Sprintf("/containers/%s/archive", containerID)
+	response, err := cli.head(ctx, urlStr, query, nil)
+	if err != nil {
+		return types.ContainerPathStat{}, err
+	}
+	defer ensureReaderClosed(response)
+	return getContainerPathStatFromHeader(response.header)
+}
+
+// CopyToContainer copies content into the container filesystem.
+func (cli *Client) CopyToContainer(ctx context.Context, container, path string, content io.Reader, options types.CopyToContainerOptions) error {
+	query := url.Values{}
+	query.Set("path", filepath.ToSlash(path)) // Normalize the paths used in the API.
+	// Do not allow for an existing directory to be overwritten by a non-directory and vice versa.
+	if !options.AllowOverwriteDirWithFile {
+		query.Set("noOverwriteDirNonDir", "true")
+	}
+
+	apiPath := fmt.Sprintf("/containers/%s/archive", container)
+
+	response, err := cli.putRaw(ctx, apiPath, query, content, nil)
+	if err != nil {
+		return err
+	}
+	defer ensureReaderClosed(response)
+
+	if response.statusCode != http.StatusOK {
+		return fmt.Errorf("unexpected status code from daemon: %d", response.statusCode)
+	}
+
+	return nil
+}
+
+// CopyFromContainer gets the content from the container and returns it as a Reader
+// to manipulate it in the host. It's up to the caller to close the reader.
+func (cli *Client) CopyFromContainer(ctx context.Context, container, srcPath string) (io.ReadCloser, types.ContainerPathStat, error) {
+	query := make(url.Values, 1)
+	query.Set("path", filepath.ToSlash(srcPath)) // Normalize the paths used in the API.
+
+	apiPath := fmt.Sprintf("/containers/%s/archive", container)
+	response, err := cli.get(ctx, apiPath, query, nil)
+	if err != nil {
+		return nil, types.ContainerPathStat{}, err
+	}
+
+	if response.statusCode != http.StatusOK {
+		return nil, types.ContainerPathStat{}, fmt.Errorf("unexpected status code from daemon: %d", response.statusCode)
+	}
+
+	// In order to get the copy behavior right, we need to know information
+	// about both the source and the destination. The response headers include
+	// stat info about the source that we can use in deciding exactly how to
+	// copy it locally. Along with the stat info about the local destination,
+	// we have everything we need to handle the multiple possibilities there
+	// can be when copying a file/dir from one location to another file/dir.
+	stat, err := getContainerPathStatFromHeader(response.header)
+	if err != nil {
+		return nil, stat, fmt.Errorf("unable to get resource stat from response: %s", err)
+	}
+	return response.body, stat, err
+}
+
+func getContainerPathStatFromHeader(header http.Header) (types.ContainerPathStat, error) {
+	var stat types.ContainerPathStat
+
+	encodedStat := header.Get("X-Docker-Container-Path-Stat")
+	statDecoder := base64.NewDecoder(base64.StdEncoding, strings.NewReader(encodedStat))
+
+	err := json.NewDecoder(statDecoder).Decode(&stat)
+	if err != nil {
+		err = fmt.Errorf("unable to decode container path stat header: %s", err)
+	}
+
+	return stat, err
+}

+ 244 - 0
client/container_copy_test.go

@@ -0,0 +1,244 @@
+package client
+
+import (
+	"bytes"
+	"encoding/base64"
+	"encoding/json"
+	"fmt"
+	"io/ioutil"
+	"net/http"
+	"strings"
+	"testing"
+
+	"golang.org/x/net/context"
+
+	"github.com/docker/docker/api/types"
+)
+
+func TestContainerStatPathError(t *testing.T) {
+	client := &Client{
+		transport: newMockClient(nil, errorMock(http.StatusInternalServerError, "Server error")),
+	}
+	_, err := client.ContainerStatPath(context.Background(), "container_id", "path")
+	if err == nil || err.Error() != "Error response from daemon: Server error" {
+		t.Fatalf("expected a Server error, got %v", err)
+	}
+}
+
+func TestContainerStatPathNoHeaderError(t *testing.T) {
+	client := &Client{
+		transport: newMockClient(nil, func(req *http.Request) (*http.Response, error) {
+			return &http.Response{
+				StatusCode: http.StatusOK,
+				Body:       ioutil.NopCloser(bytes.NewReader([]byte(""))),
+			}, nil
+		}),
+	}
+	_, err := client.ContainerStatPath(context.Background(), "container_id", "path/to/file")
+	if err == nil {
+		t.Fatalf("expected an error, got nothing")
+	}
+}
+
+func TestContainerStatPath(t *testing.T) {
+	expectedURL := "/containers/container_id/archive"
+	expectedPath := "path/to/file"
+	client := &Client{
+		transport: newMockClient(nil, func(req *http.Request) (*http.Response, error) {
+			if !strings.HasPrefix(req.URL.Path, expectedURL) {
+				return nil, fmt.Errorf("Expected URL '%s', got '%s'", expectedURL, req.URL)
+			}
+			if req.Method != "HEAD" {
+				return nil, fmt.Errorf("expected HEAD method, got %s", req.Method)
+			}
+			query := req.URL.Query()
+			path := query.Get("path")
+			if path != expectedPath {
+				return nil, fmt.Errorf("path not set in URL query properly")
+			}
+			content, err := json.Marshal(types.ContainerPathStat{
+				Name: "name",
+				Mode: 0700,
+			})
+			if err != nil {
+				return nil, err
+			}
+			base64PathStat := base64.StdEncoding.EncodeToString(content)
+			return &http.Response{
+				StatusCode: http.StatusOK,
+				Body:       ioutil.NopCloser(bytes.NewReader([]byte(""))),
+				Header: http.Header{
+					"X-Docker-Container-Path-Stat": []string{base64PathStat},
+				},
+			}, nil
+		}),
+	}
+	stat, err := client.ContainerStatPath(context.Background(), "container_id", expectedPath)
+	if err != nil {
+		t.Fatal(err)
+	}
+	if stat.Name != "name" {
+		t.Fatalf("expected container path stat name to be 'name', was '%s'", stat.Name)
+	}
+	if stat.Mode != 0700 {
+		t.Fatalf("expected container path stat mode to be 0700, was '%v'", stat.Mode)
+	}
+}
+
+func TestCopyToContainerError(t *testing.T) {
+	client := &Client{
+		transport: newMockClient(nil, errorMock(http.StatusInternalServerError, "Server error")),
+	}
+	err := client.CopyToContainer(context.Background(), "container_id", "path/to/file", bytes.NewReader([]byte("")), types.CopyToContainerOptions{})
+	if err == nil || err.Error() != "Error response from daemon: Server error" {
+		t.Fatalf("expected a Server error, got %v", err)
+	}
+}
+
+func TestCopyToContainerNotStatusOKError(t *testing.T) {
+	client := &Client{
+		transport: newMockClient(nil, errorMock(http.StatusNoContent, "No content")),
+	}
+	err := client.CopyToContainer(context.Background(), "container_id", "path/to/file", bytes.NewReader([]byte("")), types.CopyToContainerOptions{})
+	if err == nil || err.Error() != "unexpected status code from daemon: 204" {
+		t.Fatalf("expected an unexpected status code error, got %v", err)
+	}
+}
+
+func TestCopyToContainer(t *testing.T) {
+	expectedURL := "/containers/container_id/archive"
+	expectedPath := "path/to/file"
+	client := &Client{
+		transport: newMockClient(nil, func(req *http.Request) (*http.Response, error) {
+			if !strings.HasPrefix(req.URL.Path, expectedURL) {
+				return nil, fmt.Errorf("Expected URL '%s', got '%s'", expectedURL, req.URL)
+			}
+			if req.Method != "PUT" {
+				return nil, fmt.Errorf("expected PUT method, got %s", req.Method)
+			}
+			query := req.URL.Query()
+			path := query.Get("path")
+			if path != expectedPath {
+				return nil, fmt.Errorf("path not set in URL query properly, expected '%s', got %s", expectedPath, path)
+			}
+			noOverwriteDirNonDir := query.Get("noOverwriteDirNonDir")
+			if noOverwriteDirNonDir != "true" {
+				return nil, fmt.Errorf("noOverwriteDirNonDir not set in URL query properly, expected true, got %s", noOverwriteDirNonDir)
+			}
+
+			content, err := ioutil.ReadAll(req.Body)
+			if err != nil {
+				return nil, err
+			}
+			if err := req.Body.Close(); err != nil {
+				return nil, err
+			}
+			if string(content) != "content" {
+				return nil, fmt.Errorf("expected content to be 'content', got %s", string(content))
+			}
+
+			return &http.Response{
+				StatusCode: http.StatusOK,
+				Body:       ioutil.NopCloser(bytes.NewReader([]byte(""))),
+			}, nil
+		}),
+	}
+	err := client.CopyToContainer(context.Background(), "container_id", expectedPath, bytes.NewReader([]byte("content")), types.CopyToContainerOptions{
+		AllowOverwriteDirWithFile: false,
+	})
+	if err != nil {
+		t.Fatal(err)
+	}
+}
+
+func TestCopyFromContainerError(t *testing.T) {
+	client := &Client{
+		transport: newMockClient(nil, errorMock(http.StatusInternalServerError, "Server error")),
+	}
+	_, _, err := client.CopyFromContainer(context.Background(), "container_id", "path/to/file")
+	if err == nil || err.Error() != "Error response from daemon: Server error" {
+		t.Fatalf("expected a Server error, got %v", err)
+	}
+}
+
+func TestCopyFromContainerNotStatusOKError(t *testing.T) {
+	client := &Client{
+		transport: newMockClient(nil, errorMock(http.StatusNoContent, "No content")),
+	}
+	_, _, err := client.CopyFromContainer(context.Background(), "container_id", "path/to/file")
+	if err == nil || err.Error() != "unexpected status code from daemon: 204" {
+		t.Fatalf("expected an unexpected status code error, got %v", err)
+	}
+}
+
+func TestCopyFromContainerNoHeaderError(t *testing.T) {
+	client := &Client{
+		transport: newMockClient(nil, func(req *http.Request) (*http.Response, error) {
+			return &http.Response{
+				StatusCode: http.StatusOK,
+				Body:       ioutil.NopCloser(bytes.NewReader([]byte(""))),
+			}, nil
+		}),
+	}
+	_, _, err := client.CopyFromContainer(context.Background(), "container_id", "path/to/file")
+	if err == nil {
+		t.Fatalf("expected an error, got nothing")
+	}
+}
+
+func TestCopyFromContainer(t *testing.T) {
+	expectedURL := "/containers/container_id/archive"
+	expectedPath := "path/to/file"
+	client := &Client{
+		transport: newMockClient(nil, func(req *http.Request) (*http.Response, error) {
+			if !strings.HasPrefix(req.URL.Path, expectedURL) {
+				return nil, fmt.Errorf("Expected URL '%s', got '%s'", expectedURL, req.URL)
+			}
+			if req.Method != "GET" {
+				return nil, fmt.Errorf("expected PUT method, got %s", req.Method)
+			}
+			query := req.URL.Query()
+			path := query.Get("path")
+			if path != expectedPath {
+				return nil, fmt.Errorf("path not set in URL query properly, expected '%s', got %s", expectedPath, path)
+			}
+
+			headercontent, err := json.Marshal(types.ContainerPathStat{
+				Name: "name",
+				Mode: 0700,
+			})
+			if err != nil {
+				return nil, err
+			}
+			base64PathStat := base64.StdEncoding.EncodeToString(headercontent)
+
+			return &http.Response{
+				StatusCode: http.StatusOK,
+				Body:       ioutil.NopCloser(bytes.NewReader([]byte("content"))),
+				Header: http.Header{
+					"X-Docker-Container-Path-Stat": []string{base64PathStat},
+				},
+			}, nil
+		}),
+	}
+	r, stat, err := client.CopyFromContainer(context.Background(), "container_id", expectedPath)
+	if err != nil {
+		t.Fatal(err)
+	}
+	if stat.Name != "name" {
+		t.Fatalf("expected container path stat name to be 'name', was '%s'", stat.Name)
+	}
+	if stat.Mode != 0700 {
+		t.Fatalf("expected container path stat mode to be 0700, was '%v'", stat.Mode)
+	}
+	content, err := ioutil.ReadAll(r)
+	if err != nil {
+		t.Fatal(err)
+	}
+	if err := r.Close(); err != nil {
+		t.Fatal(err)
+	}
+	if string(content) != "content" {
+		t.Fatalf("expected content to be 'content', got %s", string(content))
+	}
+}

+ 46 - 0
client/container_create.go

@@ -0,0 +1,46 @@
+package client
+
+import (
+	"encoding/json"
+	"net/url"
+	"strings"
+
+	"github.com/docker/docker/api/types"
+	"github.com/docker/docker/api/types/container"
+	"github.com/docker/docker/api/types/network"
+	"golang.org/x/net/context"
+)
+
+type configWrapper struct {
+	*container.Config
+	HostConfig       *container.HostConfig
+	NetworkingConfig *network.NetworkingConfig
+}
+
+// ContainerCreate creates a new container based in the given configuration.
+// It can be associated with a name, but it's not mandatory.
+func (cli *Client) ContainerCreate(ctx context.Context, config *container.Config, hostConfig *container.HostConfig, networkingConfig *network.NetworkingConfig, containerName string) (types.ContainerCreateResponse, error) {
+	var response types.ContainerCreateResponse
+	query := url.Values{}
+	if containerName != "" {
+		query.Set("name", containerName)
+	}
+
+	body := configWrapper{
+		Config:           config,
+		HostConfig:       hostConfig,
+		NetworkingConfig: networkingConfig,
+	}
+
+	serverResp, err := cli.post(ctx, "/containers/create", query, body, nil)
+	if err != nil {
+		if serverResp.statusCode == 404 && strings.Contains(err.Error(), "No such image") {
+			return response, imageNotFoundError{config.Image}
+		}
+		return response, err
+	}
+
+	err = json.NewDecoder(serverResp.body).Decode(&response)
+	ensureReaderClosed(serverResp)
+	return response, err
+}

+ 77 - 0
client/container_create_test.go

@@ -0,0 +1,77 @@
+package client
+
+import (
+	"bytes"
+	"encoding/json"
+	"fmt"
+	"io/ioutil"
+	"net/http"
+	"strings"
+	"testing"
+
+	"github.com/docker/docker/api/types"
+	"github.com/docker/docker/api/types/container"
+	"golang.org/x/net/context"
+)
+
+func TestContainerCreateError(t *testing.T) {
+	client := &Client{
+		transport: newMockClient(nil, errorMock(http.StatusInternalServerError, "Server error")),
+	}
+	_, err := client.ContainerCreate(context.Background(), nil, nil, nil, "nothing")
+	if err == nil || err.Error() != "Error response from daemon: Server error" {
+		t.Fatalf("expected a Server Error, got %v", err)
+	}
+
+	// 404 doesn't automagitally means an unknown image
+	client = &Client{
+		transport: newMockClient(nil, errorMock(http.StatusNotFound, "Server error")),
+	}
+	_, err = client.ContainerCreate(context.Background(), nil, nil, nil, "nothing")
+	if err == nil || err.Error() != "Error response from daemon: Server error" {
+		t.Fatalf("expected a Server Error, got %v", err)
+	}
+}
+
+func TestContainerCreateImageNotFound(t *testing.T) {
+	client := &Client{
+		transport: newMockClient(nil, errorMock(http.StatusNotFound, "No such image")),
+	}
+	_, err := client.ContainerCreate(context.Background(), &container.Config{Image: "unknown_image"}, nil, nil, "unknown")
+	if err == nil || !IsErrImageNotFound(err) {
+		t.Fatalf("expected an imageNotFound error, got %v", err)
+	}
+}
+
+func TestContainerCreateWithName(t *testing.T) {
+	expectedURL := "/containers/create"
+	client := &Client{
+		transport: newMockClient(nil, func(req *http.Request) (*http.Response, error) {
+			if !strings.HasPrefix(req.URL.Path, expectedURL) {
+				return nil, fmt.Errorf("Expected URL '%s', got '%s'", expectedURL, req.URL)
+			}
+			name := req.URL.Query().Get("name")
+			if name != "container_name" {
+				return nil, fmt.Errorf("container name not set in URL query properly. Expected `container_name`, got %s", name)
+			}
+			b, err := json.Marshal(types.ContainerCreateResponse{
+				ID: "container_id",
+			})
+			if err != nil {
+				return nil, err
+			}
+			return &http.Response{
+				StatusCode: http.StatusOK,
+				Body:       ioutil.NopCloser(bytes.NewReader(b)),
+			}, nil
+		}),
+	}
+
+	r, err := client.ContainerCreate(context.Background(), nil, nil, nil, "container_name")
+	if err != nil {
+		t.Fatal(err)
+	}
+	if r.ID != "container_id" {
+		t.Fatalf("expected `container_id`, got %s", r.ID)
+	}
+}

+ 23 - 0
client/container_diff.go

@@ -0,0 +1,23 @@
+package client
+
+import (
+	"encoding/json"
+	"net/url"
+
+	"github.com/docker/docker/api/types"
+	"golang.org/x/net/context"
+)
+
+// ContainerDiff shows differences in a container filesystem since it was started.
+func (cli *Client) ContainerDiff(ctx context.Context, containerID string) ([]types.ContainerChange, error) {
+	var changes []types.ContainerChange
+
+	serverResp, err := cli.get(ctx, "/containers/"+containerID+"/changes", url.Values{}, nil)
+	if err != nil {
+		return changes, err
+	}
+
+	err = json.NewDecoder(serverResp.body).Decode(&changes)
+	ensureReaderClosed(serverResp)
+	return changes, err
+}

+ 61 - 0
client/container_diff_test.go

@@ -0,0 +1,61 @@
+package client
+
+import (
+	"bytes"
+	"encoding/json"
+	"fmt"
+	"io/ioutil"
+	"net/http"
+	"strings"
+	"testing"
+
+	"github.com/docker/docker/api/types"
+	"golang.org/x/net/context"
+)
+
+func TestContainerDiffError(t *testing.T) {
+	client := &Client{
+		transport: newMockClient(nil, errorMock(http.StatusInternalServerError, "Server error")),
+	}
+	_, err := client.ContainerDiff(context.Background(), "nothing")
+	if err == nil || err.Error() != "Error response from daemon: Server error" {
+		t.Fatalf("expected a Server Error, got %v", err)
+	}
+
+}
+
+func TestContainerDiff(t *testing.T) {
+	expectedURL := "/containers/container_id/changes"
+	client := &Client{
+		transport: newMockClient(nil, func(req *http.Request) (*http.Response, error) {
+			if !strings.HasPrefix(req.URL.Path, expectedURL) {
+				return nil, fmt.Errorf("Expected URL '%s', got '%s'", expectedURL, req.URL)
+			}
+			b, err := json.Marshal([]types.ContainerChange{
+				{
+					Kind: 0,
+					Path: "/path/1",
+				},
+				{
+					Kind: 1,
+					Path: "/path/2",
+				},
+			})
+			if err != nil {
+				return nil, err
+			}
+			return &http.Response{
+				StatusCode: http.StatusOK,
+				Body:       ioutil.NopCloser(bytes.NewReader(b)),
+			}, nil
+		}),
+	}
+
+	changes, err := client.ContainerDiff(context.Background(), "container_id")
+	if err != nil {
+		t.Fatal(err)
+	}
+	if len(changes) != 2 {
+		t.Fatalf("expected an array of 2 changes, got %v", changes)
+	}
+}

+ 49 - 0
client/container_exec.go

@@ -0,0 +1,49 @@
+package client
+
+import (
+	"encoding/json"
+
+	"github.com/docker/docker/api/types"
+	"golang.org/x/net/context"
+)
+
+// ContainerExecCreate creates a new exec configuration to run an exec process.
+func (cli *Client) ContainerExecCreate(ctx context.Context, container string, config types.ExecConfig) (types.ContainerExecCreateResponse, error) {
+	var response types.ContainerExecCreateResponse
+	resp, err := cli.post(ctx, "/containers/"+container+"/exec", nil, config, nil)
+	if err != nil {
+		return response, err
+	}
+	err = json.NewDecoder(resp.body).Decode(&response)
+	ensureReaderClosed(resp)
+	return response, err
+}
+
+// ContainerExecStart starts an exec process already created in the docker host.
+func (cli *Client) ContainerExecStart(ctx context.Context, execID string, config types.ExecStartCheck) error {
+	resp, err := cli.post(ctx, "/exec/"+execID+"/start", nil, config, nil)
+	ensureReaderClosed(resp)
+	return err
+}
+
+// ContainerExecAttach attaches a connection to an exec process in the server.
+// It returns a types.HijackedConnection with the hijacked connection
+// and the a reader to get output. It's up to the called to close
+// the hijacked connection by calling types.HijackedResponse.Close.
+func (cli *Client) ContainerExecAttach(ctx context.Context, execID string, config types.ExecConfig) (types.HijackedResponse, error) {
+	headers := map[string][]string{"Content-Type": {"application/json"}}
+	return cli.postHijacked(ctx, "/exec/"+execID+"/start", nil, config, headers)
+}
+
+// ContainerExecInspect returns information about a specific exec process on the docker host.
+func (cli *Client) ContainerExecInspect(ctx context.Context, execID string) (types.ContainerExecInspect, error) {
+	var response types.ContainerExecInspect
+	resp, err := cli.get(ctx, "/exec/"+execID+"/json", nil, nil)
+	if err != nil {
+		return response, err
+	}
+
+	err = json.NewDecoder(resp.body).Decode(&response)
+	ensureReaderClosed(resp)
+	return response, err
+}

+ 157 - 0
client/container_exec_test.go

@@ -0,0 +1,157 @@
+package client
+
+import (
+	"bytes"
+	"encoding/json"
+	"fmt"
+	"io/ioutil"
+	"net/http"
+	"strings"
+	"testing"
+
+	"golang.org/x/net/context"
+
+	"github.com/docker/docker/api/types"
+)
+
+func TestContainerExecCreateError(t *testing.T) {
+	client := &Client{
+		transport: newMockClient(nil, errorMock(http.StatusInternalServerError, "Server error")),
+	}
+	_, err := client.ContainerExecCreate(context.Background(), "container_id", types.ExecConfig{})
+	if err == nil || err.Error() != "Error response from daemon: Server error" {
+		t.Fatalf("expected a Server Error, got %v", err)
+	}
+}
+
+func TestContainerExecCreate(t *testing.T) {
+	expectedURL := "/containers/container_id/exec"
+	client := &Client{
+		transport: newMockClient(nil, func(req *http.Request) (*http.Response, error) {
+			if !strings.HasPrefix(req.URL.Path, expectedURL) {
+				return nil, fmt.Errorf("expected URL '%s', got '%s'", expectedURL, req.URL)
+			}
+			if req.Method != "POST" {
+				return nil, fmt.Errorf("expected POST method, got %s", req.Method)
+			}
+			// FIXME validate the content is the given ExecConfig ?
+			if err := req.ParseForm(); err != nil {
+				return nil, err
+			}
+			execConfig := &types.ExecConfig{}
+			if err := json.NewDecoder(req.Body).Decode(execConfig); err != nil {
+				return nil, err
+			}
+			if execConfig.User != "user" {
+				return nil, fmt.Errorf("expected an execConfig with User == 'user', got %v", execConfig)
+			}
+			b, err := json.Marshal(types.ContainerExecCreateResponse{
+				ID: "exec_id",
+			})
+			if err != nil {
+				return nil, err
+			}
+			return &http.Response{
+				StatusCode: http.StatusOK,
+				Body:       ioutil.NopCloser(bytes.NewReader(b)),
+			}, nil
+		}),
+	}
+
+	r, err := client.ContainerExecCreate(context.Background(), "container_id", types.ExecConfig{
+		User: "user",
+	})
+	if err != nil {
+		t.Fatal(err)
+	}
+	if r.ID != "exec_id" {
+		t.Fatalf("expected `exec_id`, got %s", r.ID)
+	}
+}
+
+func TestContainerExecStartError(t *testing.T) {
+	client := &Client{
+		transport: newMockClient(nil, errorMock(http.StatusInternalServerError, "Server error")),
+	}
+	err := client.ContainerExecStart(context.Background(), "nothing", types.ExecStartCheck{})
+	if err == nil || err.Error() != "Error response from daemon: Server error" {
+		t.Fatalf("expected a Server Error, got %v", err)
+	}
+}
+
+func TestContainerExecStart(t *testing.T) {
+	expectedURL := "/exec/exec_id/start"
+	client := &Client{
+		transport: newMockClient(nil, func(req *http.Request) (*http.Response, error) {
+			if !strings.HasPrefix(req.URL.Path, expectedURL) {
+				return nil, fmt.Errorf("Expected URL '%s', got '%s'", expectedURL, req.URL)
+			}
+			if err := req.ParseForm(); err != nil {
+				return nil, err
+			}
+			execStartCheck := &types.ExecStartCheck{}
+			if err := json.NewDecoder(req.Body).Decode(execStartCheck); err != nil {
+				return nil, err
+			}
+			if execStartCheck.Tty || !execStartCheck.Detach {
+				return nil, fmt.Errorf("expected execStartCheck{Detach:true,Tty:false}, got %v", execStartCheck)
+			}
+
+			return &http.Response{
+				StatusCode: http.StatusOK,
+				Body:       ioutil.NopCloser(bytes.NewReader([]byte(""))),
+			}, nil
+		}),
+	}
+
+	err := client.ContainerExecStart(context.Background(), "exec_id", types.ExecStartCheck{
+		Detach: true,
+		Tty:    false,
+	})
+	if err != nil {
+		t.Fatal(err)
+	}
+}
+
+func TestContainerExecInspectError(t *testing.T) {
+	client := &Client{
+		transport: newMockClient(nil, errorMock(http.StatusInternalServerError, "Server error")),
+	}
+	_, err := client.ContainerExecInspect(context.Background(), "nothing")
+	if err == nil || err.Error() != "Error response from daemon: Server error" {
+		t.Fatalf("expected a Server Error, got %v", err)
+	}
+}
+
+func TestContainerExecInspect(t *testing.T) {
+	expectedURL := "/exec/exec_id/json"
+	client := &Client{
+		transport: newMockClient(nil, func(req *http.Request) (*http.Response, error) {
+			if !strings.HasPrefix(req.URL.Path, expectedURL) {
+				return nil, fmt.Errorf("Expected URL '%s', got '%s'", expectedURL, req.URL)
+			}
+			b, err := json.Marshal(types.ContainerExecInspect{
+				ExecID:      "exec_id",
+				ContainerID: "container_id",
+			})
+			if err != nil {
+				return nil, err
+			}
+			return &http.Response{
+				StatusCode: http.StatusOK,
+				Body:       ioutil.NopCloser(bytes.NewReader(b)),
+			}, nil
+		}),
+	}
+
+	inspect, err := client.ContainerExecInspect(context.Background(), "exec_id")
+	if err != nil {
+		t.Fatal(err)
+	}
+	if inspect.ExecID != "exec_id" {
+		t.Fatalf("expected ExecID to be `exec_id`, got %s", inspect.ExecID)
+	}
+	if inspect.ContainerID != "container_id" {
+		t.Fatalf("expected ContainerID `container_id`, got %s", inspect.ContainerID)
+	}
+}

+ 20 - 0
client/container_export.go

@@ -0,0 +1,20 @@
+package client
+
+import (
+	"io"
+	"net/url"
+
+	"golang.org/x/net/context"
+)
+
+// ContainerExport retrieves the raw contents of a container
+// and returns them as an io.ReadCloser. It's up to the caller
+// to close the stream.
+func (cli *Client) ContainerExport(ctx context.Context, containerID string) (io.ReadCloser, error) {
+	serverResp, err := cli.get(ctx, "/containers/"+containerID+"/export", url.Values{}, nil)
+	if err != nil {
+		return nil, err
+	}
+
+	return serverResp.body, nil
+}

+ 50 - 0
client/container_export_test.go

@@ -0,0 +1,50 @@
+package client
+
+import (
+	"bytes"
+	"fmt"
+	"io/ioutil"
+	"net/http"
+	"strings"
+	"testing"
+
+	"golang.org/x/net/context"
+)
+
+func TestContainerExportError(t *testing.T) {
+	client := &Client{
+		transport: newMockClient(nil, errorMock(http.StatusInternalServerError, "Server error")),
+	}
+	_, err := client.ContainerExport(context.Background(), "nothing")
+	if err == nil || err.Error() != "Error response from daemon: Server error" {
+		t.Fatalf("expected a Server Error, got %v", err)
+	}
+}
+
+func TestContainerExport(t *testing.T) {
+	expectedURL := "/containers/container_id/export"
+	client := &Client{
+		transport: newMockClient(nil, func(r *http.Request) (*http.Response, error) {
+			if !strings.HasPrefix(r.URL.Path, expectedURL) {
+				return nil, fmt.Errorf("Expected URL '%s', got '%s'", expectedURL, r.URL)
+			}
+
+			return &http.Response{
+				StatusCode: http.StatusOK,
+				Body:       ioutil.NopCloser(bytes.NewReader([]byte("response"))),
+			}, nil
+		}),
+	}
+	body, err := client.ContainerExport(context.Background(), "container_id")
+	if err != nil {
+		t.Fatal(err)
+	}
+	defer body.Close()
+	content, err := ioutil.ReadAll(body)
+	if err != nil {
+		t.Fatal(err)
+	}
+	if string(content) != "response" {
+		t.Fatalf("expected response to contain 'response', got %s", string(content))
+	}
+}

+ 54 - 0
client/container_inspect.go

@@ -0,0 +1,54 @@
+package client
+
+import (
+	"bytes"
+	"encoding/json"
+	"io/ioutil"
+	"net/http"
+	"net/url"
+
+	"github.com/docker/docker/api/types"
+	"golang.org/x/net/context"
+)
+
+// ContainerInspect returns the container information.
+func (cli *Client) ContainerInspect(ctx context.Context, containerID string) (types.ContainerJSON, error) {
+	serverResp, err := cli.get(ctx, "/containers/"+containerID+"/json", nil, nil)
+	if err != nil {
+		if serverResp.statusCode == http.StatusNotFound {
+			return types.ContainerJSON{}, containerNotFoundError{containerID}
+		}
+		return types.ContainerJSON{}, err
+	}
+
+	var response types.ContainerJSON
+	err = json.NewDecoder(serverResp.body).Decode(&response)
+	ensureReaderClosed(serverResp)
+	return response, err
+}
+
+// ContainerInspectWithRaw returns the container information and its raw representation.
+func (cli *Client) ContainerInspectWithRaw(ctx context.Context, containerID string, getSize bool) (types.ContainerJSON, []byte, error) {
+	query := url.Values{}
+	if getSize {
+		query.Set("size", "1")
+	}
+	serverResp, err := cli.get(ctx, "/containers/"+containerID+"/json", query, nil)
+	if err != nil {
+		if serverResp.statusCode == http.StatusNotFound {
+			return types.ContainerJSON{}, nil, containerNotFoundError{containerID}
+		}
+		return types.ContainerJSON{}, nil, err
+	}
+	defer ensureReaderClosed(serverResp)
+
+	body, err := ioutil.ReadAll(serverResp.body)
+	if err != nil {
+		return types.ContainerJSON{}, nil, err
+	}
+
+	var response types.ContainerJSON
+	rdr := bytes.NewReader(body)
+	err = json.NewDecoder(rdr).Decode(&response)
+	return response, body, err
+}

+ 125 - 0
client/container_inspect_test.go

@@ -0,0 +1,125 @@
+package client
+
+import (
+	"bytes"
+	"encoding/json"
+	"fmt"
+	"io/ioutil"
+	"net/http"
+	"strings"
+	"testing"
+
+	"github.com/docker/docker/api/types"
+	"golang.org/x/net/context"
+)
+
+func TestContainerInspectError(t *testing.T) {
+	client := &Client{
+		transport: newMockClient(nil, errorMock(http.StatusInternalServerError, "Server error")),
+	}
+
+	_, err := client.ContainerInspect(context.Background(), "nothing")
+	if err == nil || err.Error() != "Error response from daemon: Server error" {
+		t.Fatalf("expected a Server Error, got %v", err)
+	}
+}
+
+func TestContainerInspectContainerNotFound(t *testing.T) {
+	client := &Client{
+		transport: newMockClient(nil, errorMock(http.StatusNotFound, "Server error")),
+	}
+
+	_, err := client.ContainerInspect(context.Background(), "unknown")
+	if err == nil || !IsErrContainerNotFound(err) {
+		t.Fatalf("expected a containerNotFound error, got %v", err)
+	}
+}
+
+func TestContainerInspect(t *testing.T) {
+	expectedURL := "/containers/container_id/json"
+	client := &Client{
+		transport: newMockClient(nil, func(req *http.Request) (*http.Response, error) {
+			if !strings.HasPrefix(req.URL.Path, expectedURL) {
+				return nil, fmt.Errorf("Expected URL '%s', got '%s'", expectedURL, req.URL)
+			}
+			content, err := json.Marshal(types.ContainerJSON{
+				ContainerJSONBase: &types.ContainerJSONBase{
+					ID:    "container_id",
+					Image: "image",
+					Name:  "name",
+				},
+			})
+			if err != nil {
+				return nil, err
+			}
+			return &http.Response{
+				StatusCode: http.StatusOK,
+				Body:       ioutil.NopCloser(bytes.NewReader(content)),
+			}, nil
+		}),
+	}
+
+	r, err := client.ContainerInspect(context.Background(), "container_id")
+	if err != nil {
+		t.Fatal(err)
+	}
+	if r.ID != "container_id" {
+		t.Fatalf("expected `container_id`, got %s", r.ID)
+	}
+	if r.Image != "image" {
+		t.Fatalf("expected `image`, got %s", r.ID)
+	}
+	if r.Name != "name" {
+		t.Fatalf("expected `name`, got %s", r.ID)
+	}
+}
+
+func TestContainerInspectNode(t *testing.T) {
+	client := &Client{
+		transport: newMockClient(nil, func(req *http.Request) (*http.Response, error) {
+			content, err := json.Marshal(types.ContainerJSON{
+				ContainerJSONBase: &types.ContainerJSONBase{
+					ID:    "container_id",
+					Image: "image",
+					Name:  "name",
+					Node: &types.ContainerNode{
+						ID:     "container_node_id",
+						Addr:   "container_node",
+						Labels: map[string]string{"foo": "bar"},
+					},
+				},
+			})
+			if err != nil {
+				return nil, err
+			}
+			return &http.Response{
+				StatusCode: http.StatusOK,
+				Body:       ioutil.NopCloser(bytes.NewReader(content)),
+			}, nil
+		}),
+	}
+
+	r, err := client.ContainerInspect(context.Background(), "container_id")
+	if err != nil {
+		t.Fatal(err)
+	}
+	if r.ID != "container_id" {
+		t.Fatalf("expected `container_id`, got %s", r.ID)
+	}
+	if r.Image != "image" {
+		t.Fatalf("expected `image`, got %s", r.ID)
+	}
+	if r.Name != "name" {
+		t.Fatalf("expected `name`, got %s", r.ID)
+	}
+	if r.Node.ID != "container_node_id" {
+		t.Fatalf("expected `container_node_id`, got %s", r.Node.ID)
+	}
+	if r.Node.Addr != "container_node" {
+		t.Fatalf("expected `container_node`, got %s", r.Node.Addr)
+	}
+	foo, ok := r.Node.Labels["foo"]
+	if foo != "bar" || !ok {
+		t.Fatalf("expected `bar` for label `foo`")
+	}
+}

+ 17 - 0
client/container_kill.go

@@ -0,0 +1,17 @@
+package client
+
+import (
+	"net/url"
+
+	"golang.org/x/net/context"
+)
+
+// ContainerKill terminates the container process but does not remove the container from the docker host.
+func (cli *Client) ContainerKill(ctx context.Context, containerID, signal string) error {
+	query := url.Values{}
+	query.Set("signal", signal)
+
+	resp, err := cli.post(ctx, "/containers/"+containerID+"/kill", query, nil, nil)
+	ensureReaderClosed(resp)
+	return err
+}

+ 46 - 0
client/container_kill_test.go

@@ -0,0 +1,46 @@
+package client
+
+import (
+	"bytes"
+	"fmt"
+	"io/ioutil"
+	"net/http"
+	"strings"
+	"testing"
+
+	"golang.org/x/net/context"
+)
+
+func TestContainerKillError(t *testing.T) {
+	client := &Client{
+		transport: newMockClient(nil, errorMock(http.StatusInternalServerError, "Server error")),
+	}
+	err := client.ContainerKill(context.Background(), "nothing", "SIGKILL")
+	if err == nil || err.Error() != "Error response from daemon: Server error" {
+		t.Fatalf("expected a Server Error, got %v", err)
+	}
+}
+
+func TestContainerKill(t *testing.T) {
+	expectedURL := "/containers/container_id/kill"
+	client := &Client{
+		transport: newMockClient(nil, func(req *http.Request) (*http.Response, error) {
+			if !strings.HasPrefix(req.URL.Path, expectedURL) {
+				return nil, fmt.Errorf("Expected URL '%s', got '%s'", expectedURL, req.URL)
+			}
+			signal := req.URL.Query().Get("signal")
+			if signal != "SIGKILL" {
+				return nil, fmt.Errorf("signal not set in URL query properly. Expected 'SIGKILL', got %s", signal)
+			}
+			return &http.Response{
+				StatusCode: http.StatusOK,
+				Body:       ioutil.NopCloser(bytes.NewReader([]byte(""))),
+			}, nil
+		}),
+	}
+
+	err := client.ContainerKill(context.Background(), "container_id", "SIGKILL")
+	if err != nil {
+		t.Fatal(err)
+	}
+}

+ 56 - 0
client/container_list.go

@@ -0,0 +1,56 @@
+package client
+
+import (
+	"encoding/json"
+	"net/url"
+	"strconv"
+
+	"github.com/docker/docker/api/types"
+	"github.com/docker/docker/api/types/filters"
+	"golang.org/x/net/context"
+)
+
+// ContainerList returns the list of containers in the docker host.
+func (cli *Client) ContainerList(ctx context.Context, options types.ContainerListOptions) ([]types.Container, error) {
+	query := url.Values{}
+
+	if options.All {
+		query.Set("all", "1")
+	}
+
+	if options.Limit != -1 {
+		query.Set("limit", strconv.Itoa(options.Limit))
+	}
+
+	if options.Since != "" {
+		query.Set("since", options.Since)
+	}
+
+	if options.Before != "" {
+		query.Set("before", options.Before)
+	}
+
+	if options.Size {
+		query.Set("size", "1")
+	}
+
+	if options.Filter.Len() > 0 {
+		filterJSON, err := filters.ToParamWithVersion(cli.version, options.Filter)
+
+		if err != nil {
+			return nil, err
+		}
+
+		query.Set("filters", filterJSON)
+	}
+
+	resp, err := cli.get(ctx, "/containers/json", query, nil)
+	if err != nil {
+		return nil, err
+	}
+
+	var containers []types.Container
+	err = json.NewDecoder(resp.body).Decode(&containers)
+	ensureReaderClosed(resp)
+	return containers, err
+}

+ 96 - 0
client/container_list_test.go

@@ -0,0 +1,96 @@
+package client
+
+import (
+	"bytes"
+	"encoding/json"
+	"fmt"
+	"io/ioutil"
+	"net/http"
+	"strings"
+	"testing"
+
+	"github.com/docker/docker/api/types"
+	"github.com/docker/docker/api/types/filters"
+	"golang.org/x/net/context"
+)
+
+func TestContainerListError(t *testing.T) {
+	client := &Client{
+		transport: newMockClient(nil, errorMock(http.StatusInternalServerError, "Server error")),
+	}
+	_, err := client.ContainerList(context.Background(), types.ContainerListOptions{})
+	if err == nil || err.Error() != "Error response from daemon: Server error" {
+		t.Fatalf("expected a Server Error, got %v", err)
+	}
+}
+
+func TestContainerList(t *testing.T) {
+	expectedURL := "/containers/json"
+	expectedFilters := `{"before":{"container":true},"label":{"label1":true,"label2":true}}`
+	client := &Client{
+		transport: newMockClient(nil, func(req *http.Request) (*http.Response, error) {
+			if !strings.HasPrefix(req.URL.Path, expectedURL) {
+				return nil, fmt.Errorf("Expected URL '%s', got '%s'", expectedURL, req.URL)
+			}
+			query := req.URL.Query()
+			all := query.Get("all")
+			if all != "1" {
+				return nil, fmt.Errorf("all not set in URL query properly. Expected '1', got %s", all)
+			}
+			limit := query.Get("limit")
+			if limit != "0" {
+				return nil, fmt.Errorf("limit should have not be present in query. Expected '0', got %s", limit)
+			}
+			since := query.Get("since")
+			if since != "container" {
+				return nil, fmt.Errorf("since not set in URL query properly. Expected 'container', got %s", since)
+			}
+			before := query.Get("before")
+			if before != "" {
+				return nil, fmt.Errorf("before should have not be present in query, go %s", before)
+			}
+			size := query.Get("size")
+			if size != "1" {
+				return nil, fmt.Errorf("size not set in URL query properly. Expected '1', got %s", size)
+			}
+			filters := query.Get("filters")
+			if filters != expectedFilters {
+				return nil, fmt.Errorf("expected filters incoherent '%v' with actual filters %v", expectedFilters, filters)
+			}
+
+			b, err := json.Marshal([]types.Container{
+				{
+					ID: "container_id1",
+				},
+				{
+					ID: "container_id2",
+				},
+			})
+			if err != nil {
+				return nil, err
+			}
+
+			return &http.Response{
+				StatusCode: http.StatusOK,
+				Body:       ioutil.NopCloser(bytes.NewReader(b)),
+			}, nil
+		}),
+	}
+
+	filters := filters.NewArgs()
+	filters.Add("label", "label1")
+	filters.Add("label", "label2")
+	filters.Add("before", "container")
+	containers, err := client.ContainerList(context.Background(), types.ContainerListOptions{
+		Size:   true,
+		All:    true,
+		Since:  "container",
+		Filter: filters,
+	})
+	if err != nil {
+		t.Fatal(err)
+	}
+	if len(containers) != 2 {
+		t.Fatalf("expected 2 containers, got %v", containers)
+	}
+}

+ 52 - 0
client/container_logs.go

@@ -0,0 +1,52 @@
+package client
+
+import (
+	"io"
+	"net/url"
+	"time"
+
+	"golang.org/x/net/context"
+
+	"github.com/docker/docker/api/types"
+	timetypes "github.com/docker/docker/api/types/time"
+)
+
+// ContainerLogs returns the logs generated by a container in an io.ReadCloser.
+// It's up to the caller to close the stream.
+func (cli *Client) ContainerLogs(ctx context.Context, container string, options types.ContainerLogsOptions) (io.ReadCloser, error) {
+	query := url.Values{}
+	if options.ShowStdout {
+		query.Set("stdout", "1")
+	}
+
+	if options.ShowStderr {
+		query.Set("stderr", "1")
+	}
+
+	if options.Since != "" {
+		ts, err := timetypes.GetTimestamp(options.Since, time.Now())
+		if err != nil {
+			return nil, err
+		}
+		query.Set("since", ts)
+	}
+
+	if options.Timestamps {
+		query.Set("timestamps", "1")
+	}
+
+	if options.Details {
+		query.Set("details", "1")
+	}
+
+	if options.Follow {
+		query.Set("follow", "1")
+	}
+	query.Set("tail", options.Tail)
+
+	resp, err := cli.get(ctx, "/containers/"+container+"/logs", query, nil)
+	if err != nil {
+		return nil, err
+	}
+	return resp.body, nil
+}

+ 133 - 0
client/container_logs_test.go

@@ -0,0 +1,133 @@
+package client
+
+import (
+	"bytes"
+	"fmt"
+	"io"
+	"io/ioutil"
+	"log"
+	"net/http"
+	"os"
+	"strings"
+	"testing"
+	"time"
+
+	"github.com/docker/docker/api/types"
+
+	"golang.org/x/net/context"
+)
+
+func TestContainerLogsError(t *testing.T) {
+	client := &Client{
+		transport: newMockClient(nil, errorMock(http.StatusInternalServerError, "Server error")),
+	}
+	_, err := client.ContainerLogs(context.Background(), "container_id", types.ContainerLogsOptions{})
+	if err == nil || err.Error() != "Error response from daemon: Server error" {
+		t.Fatalf("expected a Server Error, got %v", err)
+	}
+	_, err = client.ContainerLogs(context.Background(), "container_id", types.ContainerLogsOptions{
+		Since: "2006-01-02TZ",
+	})
+	if err == nil || !strings.Contains(err.Error(), `parsing time "2006-01-02TZ"`) {
+		t.Fatalf("expected a 'parsing time' error, got %v", err)
+	}
+}
+
+func TestContainerLogs(t *testing.T) {
+	expectedURL := "/containers/container_id/logs"
+	cases := []struct {
+		options             types.ContainerLogsOptions
+		expectedQueryParams map[string]string
+	}{
+		{
+			expectedQueryParams: map[string]string{
+				"tail": "",
+			},
+		},
+		{
+			options: types.ContainerLogsOptions{
+				Tail: "any",
+			},
+			expectedQueryParams: map[string]string{
+				"tail": "any",
+			},
+		},
+		{
+			options: types.ContainerLogsOptions{
+				ShowStdout: true,
+				ShowStderr: true,
+				Timestamps: true,
+				Details:    true,
+				Follow:     true,
+			},
+			expectedQueryParams: map[string]string{
+				"tail":       "",
+				"stdout":     "1",
+				"stderr":     "1",
+				"timestamps": "1",
+				"details":    "1",
+				"follow":     "1",
+			},
+		},
+		{
+			options: types.ContainerLogsOptions{
+				// An complete invalid date, timestamp or go duration will be
+				// passed as is
+				Since: "invalid but valid",
+			},
+			expectedQueryParams: map[string]string{
+				"tail":  "",
+				"since": "invalid but valid",
+			},
+		},
+	}
+	for _, logCase := range cases {
+		client := &Client{
+			transport: newMockClient(nil, func(r *http.Request) (*http.Response, error) {
+				if !strings.HasPrefix(r.URL.Path, expectedURL) {
+					return nil, fmt.Errorf("Expected URL '%s', got '%s'", expectedURL, r.URL)
+				}
+				// Check query parameters
+				query := r.URL.Query()
+				for key, expected := range logCase.expectedQueryParams {
+					actual := query.Get(key)
+					if actual != expected {
+						return nil, fmt.Errorf("%s not set in URL query properly. Expected '%s', got %s", key, expected, actual)
+					}
+				}
+				return &http.Response{
+					StatusCode: http.StatusOK,
+					Body:       ioutil.NopCloser(bytes.NewReader([]byte("response"))),
+				}, nil
+			}),
+		}
+		body, err := client.ContainerLogs(context.Background(), "container_id", logCase.options)
+		if err != nil {
+			t.Fatal(err)
+		}
+		defer body.Close()
+		content, err := ioutil.ReadAll(body)
+		if err != nil {
+			t.Fatal(err)
+		}
+		if string(content) != "response" {
+			t.Fatalf("expected response to contain 'response', got %s", string(content))
+		}
+	}
+}
+
+func ExampleClient_ContainerLogs_withTimeout() {
+	ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
+	defer cancel()
+
+	client, _ := NewEnvClient()
+	reader, err := client.ContainerLogs(ctx, "container_id", types.ContainerLogsOptions{})
+	if err != nil {
+		log.Fatal(err)
+	}
+
+	_, err = io.Copy(os.Stdout, reader)
+	if err != nil && err != io.EOF {
+		log.Fatal(err)
+	}
+}

+ 10 - 0
client/container_pause.go

@@ -0,0 +1,10 @@
+package client
+
+import "golang.org/x/net/context"
+
+// ContainerPause pauses the main process of a given container without terminating it.
+func (cli *Client) ContainerPause(ctx context.Context, containerID string) error {
+	resp, err := cli.post(ctx, "/containers/"+containerID+"/pause", nil, nil, nil)
+	ensureReaderClosed(resp)
+	return err
+}

+ 41 - 0
client/container_pause_test.go

@@ -0,0 +1,41 @@
+package client
+
+import (
+	"bytes"
+	"fmt"
+	"io/ioutil"
+	"net/http"
+	"strings"
+	"testing"
+
+	"golang.org/x/net/context"
+)
+
+func TestContainerPauseError(t *testing.T) {
+	client := &Client{
+		transport: newMockClient(nil, errorMock(http.StatusInternalServerError, "Server error")),
+	}
+	err := client.ContainerPause(context.Background(), "nothing")
+	if err == nil || err.Error() != "Error response from daemon: Server error" {
+		t.Fatalf("expected a Server Error, got %v", err)
+	}
+}
+
+func TestContainerPause(t *testing.T) {
+	expectedURL := "/containers/container_id/pause"
+	client := &Client{
+		transport: newMockClient(nil, func(req *http.Request) (*http.Response, error) {
+			if !strings.HasPrefix(req.URL.Path, expectedURL) {
+				return nil, fmt.Errorf("Expected URL '%s', got '%s'", expectedURL, req.URL)
+			}
+			return &http.Response{
+				StatusCode: http.StatusOK,
+				Body:       ioutil.NopCloser(bytes.NewReader([]byte(""))),
+			}, nil
+		}),
+	}
+	err := client.ContainerPause(context.Background(), "container_id")
+	if err != nil {
+		t.Fatal(err)
+	}
+}

+ 27 - 0
client/container_remove.go

@@ -0,0 +1,27 @@
+package client
+
+import (
+	"net/url"
+
+	"github.com/docker/docker/api/types"
+	"golang.org/x/net/context"
+)
+
+// ContainerRemove kills and removes a container from the docker host.
+func (cli *Client) ContainerRemove(ctx context.Context, containerID string, options types.ContainerRemoveOptions) error {
+	query := url.Values{}
+	if options.RemoveVolumes {
+		query.Set("v", "1")
+	}
+	if options.RemoveLinks {
+		query.Set("link", "1")
+	}
+
+	if options.Force {
+		query.Set("force", "1")
+	}
+
+	resp, err := cli.delete(ctx, "/containers/"+containerID, query, nil)
+	ensureReaderClosed(resp)
+	return err
+}

+ 59 - 0
client/container_remove_test.go

@@ -0,0 +1,59 @@
+package client
+
+import (
+	"bytes"
+	"fmt"
+	"io/ioutil"
+	"net/http"
+	"strings"
+	"testing"
+
+	"github.com/docker/docker/api/types"
+	"golang.org/x/net/context"
+)
+
+func TestContainerRemoveError(t *testing.T) {
+	client := &Client{
+		transport: newMockClient(nil, errorMock(http.StatusInternalServerError, "Server error")),
+	}
+	err := client.ContainerRemove(context.Background(), "container_id", types.ContainerRemoveOptions{})
+	if err == nil || err.Error() != "Error response from daemon: Server error" {
+		t.Fatalf("expected a Server Error, got %v", err)
+	}
+}
+
+func TestContainerRemove(t *testing.T) {
+	expectedURL := "/containers/container_id"
+	client := &Client{
+		transport: newMockClient(nil, func(req *http.Request) (*http.Response, error) {
+			if !strings.HasPrefix(req.URL.Path, expectedURL) {
+				return nil, fmt.Errorf("Expected URL '%s', got '%s'", expectedURL, req.URL)
+			}
+			query := req.URL.Query()
+			volume := query.Get("v")
+			if volume != "1" {
+				return nil, fmt.Errorf("v (volume) not set in URL query properly. Expected '1', got %s", volume)
+			}
+			force := query.Get("force")
+			if force != "1" {
+				return nil, fmt.Errorf("force not set in URL query properly. Expected '1', got %s", force)
+			}
+			link := query.Get("link")
+			if link != "" {
+				return nil, fmt.Errorf("link should have not be present in query, go %s", link)
+			}
+			return &http.Response{
+				StatusCode: http.StatusOK,
+				Body:       ioutil.NopCloser(bytes.NewReader([]byte(""))),
+			}, nil
+		}),
+	}
+
+	err := client.ContainerRemove(context.Background(), "container_id", types.ContainerRemoveOptions{
+		RemoveVolumes: true,
+		Force:         true,
+	})
+	if err != nil {
+		t.Fatal(err)
+	}
+}

+ 16 - 0
client/container_rename.go

@@ -0,0 +1,16 @@
+package client
+
+import (
+	"net/url"
+
+	"golang.org/x/net/context"
+)
+
+// ContainerRename changes the name of a given container.
+func (cli *Client) ContainerRename(ctx context.Context, containerID, newContainerName string) error {
+	query := url.Values{}
+	query.Set("name", newContainerName)
+	resp, err := cli.post(ctx, "/containers/"+containerID+"/rename", query, nil, nil)
+	ensureReaderClosed(resp)
+	return err
+}

+ 46 - 0
client/container_rename_test.go

@@ -0,0 +1,46 @@
+package client
+
+import (
+	"bytes"
+	"fmt"
+	"io/ioutil"
+	"net/http"
+	"strings"
+	"testing"
+
+	"golang.org/x/net/context"
+)
+
+func TestContainerRenameError(t *testing.T) {
+	client := &Client{
+		transport: newMockClient(nil, errorMock(http.StatusInternalServerError, "Server error")),
+	}
+	err := client.ContainerRename(context.Background(), "nothing", "newNothing")
+	if err == nil || err.Error() != "Error response from daemon: Server error" {
+		t.Fatalf("expected a Server Error, got %v", err)
+	}
+}
+
+func TestContainerRename(t *testing.T) {
+	expectedURL := "/containers/container_id/rename"
+	client := &Client{
+		transport: newMockClient(nil, func(req *http.Request) (*http.Response, error) {
+			if !strings.HasPrefix(req.URL.Path, expectedURL) {
+				return nil, fmt.Errorf("Expected URL '%s', got '%s'", expectedURL, req.URL)
+			}
+			name := req.URL.Query().Get("name")
+			if name != "newName" {
+				return nil, fmt.Errorf("name not set in URL query properly. Expected 'newName', got %s", name)
+			}
+			return &http.Response{
+				StatusCode: http.StatusOK,
+				Body:       ioutil.NopCloser(bytes.NewReader([]byte(""))),
+			}, nil
+		}),
+	}
+
+	err := client.ContainerRename(context.Background(), "container_id", "newName")
+	if err != nil {
+		t.Fatal(err)
+	}
+}

+ 29 - 0
client/container_resize.go

@@ -0,0 +1,29 @@
+package client
+
+import (
+	"net/url"
+	"strconv"
+
+	"github.com/docker/docker/api/types"
+	"golang.org/x/net/context"
+)
+
+// ContainerResize changes the size of the tty for a container.
+func (cli *Client) ContainerResize(ctx context.Context, containerID string, options types.ResizeOptions) error {
+	return cli.resize(ctx, "/containers/"+containerID, options.Height, options.Width)
+}
+
+// ContainerExecResize changes the size of the tty for an exec process running inside a container.
+func (cli *Client) ContainerExecResize(ctx context.Context, execID string, options types.ResizeOptions) error {
+	return cli.resize(ctx, "/exec/"+execID, options.Height, options.Width)
+}
+
+func (cli *Client) resize(ctx context.Context, basePath string, height, width int) error {
+	query := url.Values{}
+	query.Set("h", strconv.Itoa(height))
+	query.Set("w", strconv.Itoa(width))
+
+	resp, err := cli.post(ctx, basePath+"/resize", query, nil, nil)
+	ensureReaderClosed(resp)
+	return err
+}

+ 82 - 0
client/container_resize_test.go

@@ -0,0 +1,82 @@
+package client
+
+import (
+	"bytes"
+	"fmt"
+	"io/ioutil"
+	"net/http"
+	"strings"
+	"testing"
+
+	"github.com/docker/docker/api/types"
+	"golang.org/x/net/context"
+)
+
+func TestContainerResizeError(t *testing.T) {
+	client := &Client{
+		transport: newMockClient(nil, errorMock(http.StatusInternalServerError, "Server error")),
+	}
+	err := client.ContainerResize(context.Background(), "container_id", types.ResizeOptions{})
+	if err == nil || err.Error() != "Error response from daemon: Server error" {
+		t.Fatalf("expected a Server Error, got %v", err)
+	}
+}
+
+func TestContainerExecResizeError(t *testing.T) {
+	client := &Client{
+		transport: newMockClient(nil, errorMock(http.StatusInternalServerError, "Server error")),
+	}
+	err := client.ContainerExecResize(context.Background(), "exec_id", types.ResizeOptions{})
+	if err == nil || err.Error() != "Error response from daemon: Server error" {
+		t.Fatalf("expected a Server Error, got %v", err)
+	}
+}
+
+func TestContainerResize(t *testing.T) {
+	client := &Client{
+		transport: newMockClient(nil, resizeTransport("/containers/container_id/resize")),
+	}
+
+	err := client.ContainerResize(context.Background(), "container_id", types.ResizeOptions{
+		Height: 500,
+		Width:  600,
+	})
+	if err != nil {
+		t.Fatal(err)
+	}
+}
+
+func TestContainerExecResize(t *testing.T) {
+	client := &Client{
+		transport: newMockClient(nil, resizeTransport("/exec/exec_id/resize")),
+	}
+
+	err := client.ContainerExecResize(context.Background(), "exec_id", types.ResizeOptions{
+		Height: 500,
+		Width:  600,
+	})
+	if err != nil {
+		t.Fatal(err)
+	}
+}
+
+func resizeTransport(expectedURL string) func(req *http.Request) (*http.Response, error) {
+	return func(req *http.Request) (*http.Response, error) {
+		if !strings.HasPrefix(req.URL.Path, expectedURL) {
+			return nil, fmt.Errorf("Expected URL '%s', got '%s'", expectedURL, req.URL)
+		}
+		query := req.URL.Query()
+		h := query.Get("h")
+		if h != "500" {
+			return nil, fmt.Errorf("h not set in URL query properly. Expected '500', got %s", h)
+		}
+		w := query.Get("w")
+		if w != "600" {
+			return nil, fmt.Errorf("w not set in URL query properly. Expected '600', got %s", w)
+		}
+		return &http.Response{
+			StatusCode: http.StatusOK,
+			Body:       ioutil.NopCloser(bytes.NewReader([]byte(""))),
+		}, nil
+	}
+}

+ 22 - 0
client/container_restart.go

@@ -0,0 +1,22 @@
+package client
+
+import (
+	"net/url"
+	"time"
+
+	timetypes "github.com/docker/docker/api/types/time"
+	"golang.org/x/net/context"
+)
+
+// ContainerRestart stops and starts a container again.
+// It makes the daemon to wait for the container to be up again for
+// a specific amount of time, given the timeout.
+func (cli *Client) ContainerRestart(ctx context.Context, containerID string, timeout *time.Duration) error {
+	query := url.Values{}
+	if timeout != nil {
+		query.Set("t", timetypes.DurationToSecondsString(*timeout))
+	}
+	resp, err := cli.post(ctx, "/containers/"+containerID+"/restart", query, nil, nil)
+	ensureReaderClosed(resp)
+	return err
+}

+ 48 - 0
client/container_restart_test.go

@@ -0,0 +1,48 @@
+package client
+
+import (
+	"bytes"
+	"fmt"
+	"io/ioutil"
+	"net/http"
+	"strings"
+	"testing"
+	"time"
+
+	"golang.org/x/net/context"
+)
+
+func TestContainerRestartError(t *testing.T) {
+	client := &Client{
+		transport: newMockClient(nil, errorMock(http.StatusInternalServerError, "Server error")),
+	}
+	timeout := 0 * time.Second
+	err := client.ContainerRestart(context.Background(), "nothing", &timeout)
+	if err == nil || err.Error() != "Error response from daemon: Server error" {
+		t.Fatalf("expected a Server Error, got %v", err)
+	}
+}
+
+func TestContainerRestart(t *testing.T) {
+	expectedURL := "/containers/container_id/restart"
+	client := &Client{
+		transport: newMockClient(nil, func(req *http.Request) (*http.Response, error) {
+			if !strings.HasPrefix(req.URL.Path, expectedURL) {
+				return nil, fmt.Errorf("Expected URL '%s', got '%s'", expectedURL, req.URL)
+			}
+			t := req.URL.Query().Get("t")
+			if t != "100" {
+				return nil, fmt.Errorf("t (timeout) not set in URL query properly. Expected '100', got %s", t)
+			}
+			return &http.Response{
+				StatusCode: http.StatusOK,
+				Body:       ioutil.NopCloser(bytes.NewReader([]byte(""))),
+			}, nil
+		}),
+	}
+	timeout := 100 * time.Second
+	err := client.ContainerRestart(context.Background(), "container_id", &timeout)
+	if err != nil {
+		t.Fatal(err)
+	}
+}

+ 21 - 0
client/container_start.go

@@ -0,0 +1,21 @@
+package client
+
+import (
+	"net/url"
+
+	"golang.org/x/net/context"
+
+	"github.com/docker/docker/api/types"
+)
+
+// ContainerStart sends a request to the docker daemon to start a container.
+func (cli *Client) ContainerStart(ctx context.Context, containerID string, options types.ContainerStartOptions) error {
+	query := url.Values{}
+	if len(options.CheckpointID) != 0 {
+		query.Set("checkpoint", options.CheckpointID)
+	}
+
+	resp, err := cli.post(ctx, "/containers/"+containerID+"/start", query, nil, nil)
+	ensureReaderClosed(resp)
+	return err
+}

+ 58 - 0
client/container_start_test.go

@@ -0,0 +1,58 @@
+package client
+
+import (
+	"bytes"
+	"encoding/json"
+	"fmt"
+	"io/ioutil"
+	"net/http"
+	"strings"
+	"testing"
+
+	"golang.org/x/net/context"
+
+	"github.com/docker/docker/api/types"
+)
+
+func TestContainerStartError(t *testing.T) {
+	client := &Client{
+		transport: newMockClient(nil, errorMock(http.StatusInternalServerError, "Server error")),
+	}
+	err := client.ContainerStart(context.Background(), "nothing", types.ContainerStartOptions{})
+	if err == nil || err.Error() != "Error response from daemon: Server error" {
+		t.Fatalf("expected a Server Error, got %v", err)
+	}
+}
+
+func TestContainerStart(t *testing.T) {
+	expectedURL := "/containers/container_id/start"
+	client := &Client{
+		transport: newMockClient(nil, func(req *http.Request) (*http.Response, error) {
+			if !strings.HasPrefix(req.URL.Path, expectedURL) {
+				return nil, fmt.Errorf("Expected URL '%s', got '%s'", expectedURL, req.URL)
+			}
+			// we're not expecting any payload, but if one is supplied, check it is valid.
+			if req.Header.Get("Content-Type") == "application/json" {
+				var startConfig interface{}
+				if err := json.NewDecoder(req.Body).Decode(&startConfig); err != nil {
+					return nil, fmt.Errorf("Unable to parse json: %s", err)
+				}
+			}
+
+			checkpoint := req.URL.Query().Get("checkpoint")
+			if checkpoint != "checkpoint_id" {
+				return nil, fmt.Errorf("checkpoint not set in URL query properly. Expected 'checkpoint_id', got %s", checkpoint)
+			}
+
+			return &http.Response{
+				StatusCode: http.StatusOK,
+				Body:       ioutil.NopCloser(bytes.NewReader([]byte(""))),
+			}, nil
+		}),
+	}
+
+	err := client.ContainerStart(context.Background(), "container_id", types.ContainerStartOptions{CheckpointID: "checkpoint_id"})
+	if err != nil {
+		t.Fatal(err)
+	}
+}

+ 24 - 0
client/container_stats.go

@@ -0,0 +1,24 @@
+package client
+
+import (
+	"io"
+	"net/url"
+
+	"golang.org/x/net/context"
+)
+
+// ContainerStats returns near realtime stats for a given container.
+// It's up to the caller to close the io.ReadCloser returned.
+func (cli *Client) ContainerStats(ctx context.Context, containerID string, stream bool) (io.ReadCloser, error) {
+	query := url.Values{}
+	query.Set("stream", "0")
+	if stream {
+		query.Set("stream", "1")
+	}
+
+	resp, err := cli.get(ctx, "/containers/"+containerID+"/stats", query, nil)
+	if err != nil {
+		return nil, err
+	}
+	return resp.body, err
+}

+ 70 - 0
client/container_stats_test.go

@@ -0,0 +1,70 @@
+package client
+
+import (
+	"bytes"
+	"fmt"
+	"io/ioutil"
+	"net/http"
+	"strings"
+	"testing"
+
+	"golang.org/x/net/context"
+)
+
+func TestContainerStatsError(t *testing.T) {
+	client := &Client{
+		transport: newMockClient(nil, errorMock(http.StatusInternalServerError, "Server error")),
+	}
+	_, err := client.ContainerStats(context.Background(), "nothing", false)
+	if err == nil || err.Error() != "Error response from daemon: Server error" {
+		t.Fatalf("expected a Server Error, got %v", err)
+	}
+}
+
+func TestContainerStats(t *testing.T) {
+	expectedURL := "/containers/container_id/stats"
+	cases := []struct {
+		stream         bool
+		expectedStream string
+	}{
+		{
+			expectedStream: "0",
+		},
+		{
+			stream:         true,
+			expectedStream: "1",
+		},
+	}
+	for _, c := range cases {
+		client := &Client{
+			transport: newMockClient(nil, func(r *http.Request) (*http.Response, error) {
+				if !strings.HasPrefix(r.URL.Path, expectedURL) {
+					return nil, fmt.Errorf("Expected URL '%s', got '%s'", expectedURL, r.URL)
+				}
+
+				query := r.URL.Query()
+				stream := query.Get("stream")
+				if stream != c.expectedStream {
+					return nil, fmt.Errorf("stream not set in URL query properly. Expected '%s', got %s", c.expectedStream, stream)
+				}
+
+				return &http.Response{
+					StatusCode: http.StatusOK,
+					Body:       ioutil.NopCloser(bytes.NewReader([]byte("response"))),
+				}, nil
+			}),
+		}
+		body, err := client.ContainerStats(context.Background(), "container_id", c.stream)
+		if err != nil {
+			t.Fatal(err)
+		}
+		defer body.Close()
+		content, err := ioutil.ReadAll(body)
+		if err != nil {
+			t.Fatal(err)
+		}
+		if string(content) != "response" {
+			t.Fatalf("expected response to contain 'response', got %s", string(content))
+		}
+	}
+}

+ 21 - 0
client/container_stop.go

@@ -0,0 +1,21 @@
+package client
+
+import (
+	"net/url"
+	"time"
+
+	timetypes "github.com/docker/docker/api/types/time"
+	"golang.org/x/net/context"
+)
+
+// ContainerStop stops a container without terminating the process.
+// The process is blocked until the container stops or the timeout expires.
+func (cli *Client) ContainerStop(ctx context.Context, containerID string, timeout *time.Duration) error {
+	query := url.Values{}
+	if timeout != nil {
+		query.Set("t", timetypes.DurationToSecondsString(*timeout))
+	}
+	resp, err := cli.post(ctx, "/containers/"+containerID+"/stop", query, nil, nil)
+	ensureReaderClosed(resp)
+	return err
+}

+ 48 - 0
client/container_stop_test.go

@@ -0,0 +1,48 @@
+package client
+
+import (
+	"bytes"
+	"fmt"
+	"io/ioutil"
+	"net/http"
+	"strings"
+	"testing"
+	"time"
+
+	"golang.org/x/net/context"
+)
+
+func TestContainerStopError(t *testing.T) {
+	client := &Client{
+		transport: newMockClient(nil, errorMock(http.StatusInternalServerError, "Server error")),
+	}
+	timeout := 0 * time.Second
+	err := client.ContainerStop(context.Background(), "nothing", &timeout)
+	if err == nil || err.Error() != "Error response from daemon: Server error" {
+		t.Fatalf("expected a Server Error, got %v", err)
+	}
+}
+
+func TestContainerStop(t *testing.T) {
+	expectedURL := "/containers/container_id/stop"
+	client := &Client{
+		transport: newMockClient(nil, func(req *http.Request) (*http.Response, error) {
+			if !strings.HasPrefix(req.URL.Path, expectedURL) {
+				return nil, fmt.Errorf("Expected URL '%s', got '%s'", expectedURL, req.URL)
+			}
+			t := req.URL.Query().Get("t")
+			if t != "100" {
+				return nil, fmt.Errorf("t (timeout) not set in URL query properly. Expected '100', got %s", t)
+			}
+			return &http.Response{
+				StatusCode: http.StatusOK,
+				Body:       ioutil.NopCloser(bytes.NewReader([]byte(""))),
+			}, nil
+		}),
+	}
+	timeout := 100 * time.Second
+	err := client.ContainerStop(context.Background(), "container_id", &timeout)
+	if err != nil {
+		t.Fatal(err)
+	}
+}

+ 28 - 0
client/container_top.go

@@ -0,0 +1,28 @@
+package client
+
+import (
+	"encoding/json"
+	"net/url"
+	"strings"
+
+	"github.com/docker/docker/api/types"
+	"golang.org/x/net/context"
+)
+
+// ContainerTop shows process information from within a container.
+func (cli *Client) ContainerTop(ctx context.Context, containerID string, arguments []string) (types.ContainerProcessList, error) {
+	var response types.ContainerProcessList
+	query := url.Values{}
+	if len(arguments) > 0 {
+		query.Set("ps_args", strings.Join(arguments, " "))
+	}
+
+	resp, err := cli.get(ctx, "/containers/"+containerID+"/top", query, nil)
+	if err != nil {
+		return response, err
+	}
+
+	err = json.NewDecoder(resp.body).Decode(&response)
+	ensureReaderClosed(resp)
+	return response, err
+}

+ 74 - 0
client/container_top_test.go

@@ -0,0 +1,74 @@
+package client
+
+import (
+	"bytes"
+	"encoding/json"
+	"fmt"
+	"io/ioutil"
+	"net/http"
+	"reflect"
+	"strings"
+	"testing"
+
+	"github.com/docker/docker/api/types"
+	"golang.org/x/net/context"
+)
+
+func TestContainerTopError(t *testing.T) {
+	client := &Client{
+		transport: newMockClient(nil, errorMock(http.StatusInternalServerError, "Server error")),
+	}
+	_, err := client.ContainerTop(context.Background(), "nothing", []string{})
+	if err == nil || err.Error() != "Error response from daemon: Server error" {
+		t.Fatalf("expected a Server Error, got %v", err)
+	}
+}
+
+func TestContainerTop(t *testing.T) {
+	expectedURL := "/containers/container_id/top"
+	expectedProcesses := [][]string{
+		{"p1", "p2"},
+		{"p3"},
+	}
+	expectedTitles := []string{"title1", "title2"}
+
+	client := &Client{
+		transport: newMockClient(nil, func(req *http.Request) (*http.Response, error) {
+			if !strings.HasPrefix(req.URL.Path, expectedURL) {
+				return nil, fmt.Errorf("Expected URL '%s', got '%s'", expectedURL, req.URL)
+			}
+			query := req.URL.Query()
+			args := query.Get("ps_args")
+			if args != "arg1 arg2" {
+				return nil, fmt.Errorf("args not set in URL query properly. Expected 'arg1 arg2', got %v", args)
+			}
+
+			b, err := json.Marshal(types.ContainerProcessList{
+				Processes: [][]string{
+					{"p1", "p2"},
+					{"p3"},
+				},
+				Titles: []string{"title1", "title2"},
+			})
+			if err != nil {
+				return nil, err
+			}
+
+			return &http.Response{
+				StatusCode: http.StatusOK,
+				Body:       ioutil.NopCloser(bytes.NewReader(b)),
+			}, nil
+		}),
+	}
+
+	processList, err := client.ContainerTop(context.Background(), "container_id", []string{"arg1", "arg2"})
+	if err != nil {
+		t.Fatal(err)
+	}
+	if !reflect.DeepEqual(expectedProcesses, processList.Processes) {
+		t.Fatalf("Processes: expected %v, got %v", expectedProcesses, processList.Processes)
+	}
+	if !reflect.DeepEqual(expectedTitles, processList.Titles) {
+		t.Fatalf("Titles: expected %v, got %v", expectedTitles, processList.Titles)
+	}
+}

+ 10 - 0
client/container_unpause.go

@@ -0,0 +1,10 @@
+package client
+
+import "golang.org/x/net/context"
+
+// ContainerUnpause resumes the process execution within a container
+func (cli *Client) ContainerUnpause(ctx context.Context, containerID string) error {
+	resp, err := cli.post(ctx, "/containers/"+containerID+"/unpause", nil, nil, nil)
+	ensureReaderClosed(resp)
+	return err
+}

+ 41 - 0
client/container_unpause_test.go

@@ -0,0 +1,41 @@
+package client
+
+import (
+	"bytes"
+	"fmt"
+	"io/ioutil"
+	"net/http"
+	"strings"
+	"testing"
+
+	"golang.org/x/net/context"
+)
+
+func TestContainerUnpauseError(t *testing.T) {
+	client := &Client{
+		transport: newMockClient(nil, errorMock(http.StatusInternalServerError, "Server error")),
+	}
+	err := client.ContainerUnpause(context.Background(), "nothing")
+	if err == nil || err.Error() != "Error response from daemon: Server error" {
+		t.Fatalf("expected a Server Error, got %v", err)
+	}
+}
+
+func TestContainerUnpause(t *testing.T) {
+	expectedURL := "/containers/container_id/unpause"
+	client := &Client{
+		transport: newMockClient(nil, func(req *http.Request) (*http.Response, error) {
+			if !strings.HasPrefix(req.URL.Path, expectedURL) {
+				return nil, fmt.Errorf("Expected URL '%s', got '%s'", expectedURL, req.URL)
+			}
+			return &http.Response{
+				StatusCode: http.StatusOK,
+				Body:       ioutil.NopCloser(bytes.NewReader([]byte(""))),
+			}, nil
+		}),
+	}
+	err := client.ContainerUnpause(context.Background(), "container_id")
+	if err != nil {
+		t.Fatal(err)
+	}
+}

+ 23 - 0
client/container_update.go

@@ -0,0 +1,23 @@
+package client
+
+import (
+	"encoding/json"
+
+	"github.com/docker/docker/api/types"
+	"github.com/docker/docker/api/types/container"
+	"golang.org/x/net/context"
+)
+
+// ContainerUpdate updates resources of a container
+func (cli *Client) ContainerUpdate(ctx context.Context, containerID string, updateConfig container.UpdateConfig) (types.ContainerUpdateResponse, error) {
+	var response types.ContainerUpdateResponse
+	serverResp, err := cli.post(ctx, "/containers/"+containerID+"/update", nil, updateConfig, nil)
+	if err != nil {
+		return response, err
+	}
+
+	err = json.NewDecoder(serverResp.body).Decode(&response)
+
+	ensureReaderClosed(serverResp)
+	return response, err
+}

+ 59 - 0
client/container_update_test.go

@@ -0,0 +1,59 @@
+package client
+
+import (
+	"bytes"
+	"encoding/json"
+	"fmt"
+	"io/ioutil"
+	"net/http"
+	"strings"
+	"testing"
+
+	"github.com/docker/docker/api/types"
+	"github.com/docker/docker/api/types/container"
+	"golang.org/x/net/context"
+)
+
+func TestContainerUpdateError(t *testing.T) {
+	client := &Client{
+		transport: newMockClient(nil, errorMock(http.StatusInternalServerError, "Server error")),
+	}
+	_, err := client.ContainerUpdate(context.Background(), "nothing", container.UpdateConfig{})
+	if err == nil || err.Error() != "Error response from daemon: Server error" {
+		t.Fatalf("expected a Server Error, got %v", err)
+	}
+}
+
+func TestContainerUpdate(t *testing.T) {
+	expectedURL := "/containers/container_id/update"
+
+	client := &Client{
+		transport: newMockClient(nil, func(req *http.Request) (*http.Response, error) {
+			if !strings.HasPrefix(req.URL.Path, expectedURL) {
+				return nil, fmt.Errorf("Expected URL '%s', got '%s'", expectedURL, req.URL)
+			}
+
+			b, err := json.Marshal(types.ContainerUpdateResponse{})
+			if err != nil {
+				return nil, err
+			}
+
+			return &http.Response{
+				StatusCode: http.StatusOK,
+				Body:       ioutil.NopCloser(bytes.NewReader(b)),
+			}, nil
+		}),
+	}
+
+	_, err := client.ContainerUpdate(context.Background(), "container_id", container.UpdateConfig{
+		Resources: container.Resources{
+			CPUPeriod: 1,
+		},
+		RestartPolicy: container.RestartPolicy{
+			Name: "always",
+		},
+	})
+	if err != nil {
+		t.Fatal(err)
+	}
+}

+ 26 - 0
client/container_wait.go

@@ -0,0 +1,26 @@
+package client
+
+import (
+	"encoding/json"
+
+	"golang.org/x/net/context"
+
+	"github.com/docker/docker/api/types"
+)
+
+// ContainerWait pauses execution until a container exits.
+// It returns the API status code as response of its readiness.
+func (cli *Client) ContainerWait(ctx context.Context, containerID string) (int, error) {
+	resp, err := cli.post(ctx, "/containers/"+containerID+"/wait", nil, nil, nil)
+	if err != nil {
+		return -1, err
+	}
+	defer ensureReaderClosed(resp)
+
+	var res types.ContainerWaitResponse
+	if err := json.NewDecoder(resp.body).Decode(&res); err != nil {
+		return -1, err
+	}
+
+	return res.StatusCode, nil
+}

+ 70 - 0
client/container_wait_test.go

@@ -0,0 +1,70 @@
+package client
+
+import (
+	"bytes"
+	"encoding/json"
+	"fmt"
+	"io/ioutil"
+	"log"
+	"net/http"
+	"strings"
+	"testing"
+	"time"
+
+	"github.com/docker/docker/api/types"
+
+	"golang.org/x/net/context"
+)
+
+func TestContainerWaitError(t *testing.T) {
+	client := &Client{
+		transport: newMockClient(nil, errorMock(http.StatusInternalServerError, "Server error")),
+	}
+	code, err := client.ContainerWait(context.Background(), "nothing")
+	if err == nil || err.Error() != "Error response from daemon: Server error" {
+		t.Fatalf("expected a Server Error, got %v", err)
+	}
+	if code != -1 {
+		t.Fatalf("expected a status code equal to '-1', got %d", code)
+	}
+}
+
+func TestContainerWait(t *testing.T) {
+	expectedURL := "/containers/container_id/wait"
+	client := &Client{
+		transport: newMockClient(nil, func(req *http.Request) (*http.Response, error) {
+			if !strings.HasPrefix(req.URL.Path, expectedURL) {
+				return nil, fmt.Errorf("Expected URL '%s', got '%s'", expectedURL, req.URL)
+			}
+			b, err := json.Marshal(types.ContainerWaitResponse{
+				StatusCode: 15,
+			})
+			if err != nil {
+				return nil, err
+			}
+			return &http.Response{
+				StatusCode: http.StatusOK,
+				Body:       ioutil.NopCloser(bytes.NewReader(b)),
+			}, nil
+		}),
+	}
+
+	code, err := client.ContainerWait(context.Background(), "container_id")
+	if err != nil {
+		t.Fatal(err)
+	}
+	if code != 15 {
+		t.Fatalf("expected a status code equal to '15', got %d", code)
+	}
+}
+
+func ExampleClient_ContainerWait_withTimeout() {
+	ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
+	defer cancel()
+
+	client, _ := NewEnvClient()
+	_, err := client.ContainerWait(ctx, "container_id")
+	if err != nil {
+		log.Fatal(err)
+	}
+}

+ 208 - 0
client/errors.go

@@ -0,0 +1,208 @@
+package client
+
+import (
+	"errors"
+	"fmt"
+)
+
+// ErrConnectionFailed is an error raised when the connection between the client and the server failed.
+var ErrConnectionFailed = errors.New("Cannot connect to the Docker daemon. Is the docker daemon running on this host?")
+
+// ErrorConnectionFailed returns an error with host in the error message when connection to docker daemon failed.
+func ErrorConnectionFailed(host string) error {
+	return fmt.Errorf("Cannot connect to the Docker daemon at %s. Is the docker daemon running?", host)
+}
+
+type notFound interface {
+	error
+	NotFound() bool // Is the error a NotFound error
+}
+
+// IsErrNotFound returns true if the error is caused with an
+// object (image, container, network, volume, …) is not found in the docker host.
+func IsErrNotFound(err error) bool {
+	te, ok := err.(notFound)
+	return ok && te.NotFound()
+}
+
+// imageNotFoundError implements an error returned when an image is not in the docker host.
+type imageNotFoundError struct {
+	imageID string
+}
+
+// NoFound indicates that this error type is of NotFound
+func (e imageNotFoundError) NotFound() bool {
+	return true
+}
+
+// Error returns a string representation of an imageNotFoundError
+func (e imageNotFoundError) Error() string {
+	return fmt.Sprintf("Error: No such image: %s", e.imageID)
+}
+
+// IsErrImageNotFound returns true if the error is caused
+// when an image is not found in the docker host.
+func IsErrImageNotFound(err error) bool {
+	return IsErrNotFound(err)
+}
+
+// containerNotFoundError implements an error returned when a container is not in the docker host.
+type containerNotFoundError struct {
+	containerID string
+}
+
+// NoFound indicates that this error type is of NotFound
+func (e containerNotFoundError) NotFound() bool {
+	return true
+}
+
+// Error returns a string representation of a containerNotFoundError
+func (e containerNotFoundError) Error() string {
+	return fmt.Sprintf("Error: No such container: %s", e.containerID)
+}
+
+// IsErrContainerNotFound returns true if the error is caused
+// when a container is not found in the docker host.
+func IsErrContainerNotFound(err error) bool {
+	return IsErrNotFound(err)
+}
+
+// networkNotFoundError implements an error returned when a network is not in the docker host.
+type networkNotFoundError struct {
+	networkID string
+}
+
+// NoFound indicates that this error type is of NotFound
+func (e networkNotFoundError) NotFound() bool {
+	return true
+}
+
+// Error returns a string representation of a networkNotFoundError
+func (e networkNotFoundError) Error() string {
+	return fmt.Sprintf("Error: No such network: %s", e.networkID)
+}
+
+// IsErrNetworkNotFound returns true if the error is caused
+// when a network is not found in the docker host.
+func IsErrNetworkNotFound(err error) bool {
+	return IsErrNotFound(err)
+}
+
+// volumeNotFoundError implements an error returned when a volume is not in the docker host.
+type volumeNotFoundError struct {
+	volumeID string
+}
+
+// NoFound indicates that this error type is of NotFound
+func (e volumeNotFoundError) NotFound() bool {
+	return true
+}
+
+// Error returns a string representation of a networkNotFoundError
+func (e volumeNotFoundError) Error() string {
+	return fmt.Sprintf("Error: No such volume: %s", e.volumeID)
+}
+
+// IsErrVolumeNotFound returns true if the error is caused
+// when a volume is not found in the docker host.
+func IsErrVolumeNotFound(err error) bool {
+	return IsErrNotFound(err)
+}
+
+// unauthorizedError represents an authorization error in a remote registry.
+type unauthorizedError struct {
+	cause error
+}
+
+// Error returns a string representation of an unauthorizedError
+func (u unauthorizedError) Error() string {
+	return u.cause.Error()
+}
+
+// IsErrUnauthorized returns true if the error is caused
+// when a remote registry authentication fails
+func IsErrUnauthorized(err error) bool {
+	_, ok := err.(unauthorizedError)
+	return ok
+}
+
+// nodeNotFoundError implements an error returned when a node is not found.
+type nodeNotFoundError struct {
+	nodeID string
+}
+
+// Error returns a string representation of a nodeNotFoundError
+func (e nodeNotFoundError) Error() string {
+	return fmt.Sprintf("Error: No such node: %s", e.nodeID)
+}
+
+// NoFound indicates that this error type is of NotFound
+func (e nodeNotFoundError) NotFound() bool {
+	return true
+}
+
+// IsErrNodeNotFound returns true if the error is caused
+// when a node is not found.
+func IsErrNodeNotFound(err error) bool {
+	_, ok := err.(nodeNotFoundError)
+	return ok
+}
+
+// serviceNotFoundError implements an error returned when a service is not found.
+type serviceNotFoundError struct {
+	serviceID string
+}
+
+// Error returns a string representation of a serviceNotFoundError
+func (e serviceNotFoundError) Error() string {
+	return fmt.Sprintf("Error: No such service: %s", e.serviceID)
+}
+
+// NoFound indicates that this error type is of NotFound
+func (e serviceNotFoundError) NotFound() bool {
+	return true
+}
+
+// IsErrServiceNotFound returns true if the error is caused
+// when a service is not found.
+func IsErrServiceNotFound(err error) bool {
+	_, ok := err.(serviceNotFoundError)
+	return ok
+}
+
+// taskNotFoundError implements an error returned when a task is not found.
+type taskNotFoundError struct {
+	taskID string
+}
+
+// Error returns a string representation of a taskNotFoundError
+func (e taskNotFoundError) Error() string {
+	return fmt.Sprintf("Error: No such task: %s", e.taskID)
+}
+
+// NoFound indicates that this error type is of NotFound
+func (e taskNotFoundError) NotFound() bool {
+	return true
+}
+
+// IsErrTaskNotFound returns true if the error is caused
+// when a task is not found.
+func IsErrTaskNotFound(err error) bool {
+	_, ok := err.(taskNotFoundError)
+	return ok
+}
+
+type pluginPermissionDenied struct {
+	name string
+}
+
+func (e pluginPermissionDenied) Error() string {
+	return "Permission denied while installing plugin " + e.name
+}
+
+// IsErrPluginPermissionDenied returns true if the error is caused
+// when a user denies a plugin's permissions
+func IsErrPluginPermissionDenied(err error) bool {
+	_, ok := err.(pluginPermissionDenied)
+	return ok
+}

+ 48 - 0
client/events.go

@@ -0,0 +1,48 @@
+package client
+
+import (
+	"io"
+	"net/url"
+	"time"
+
+	"golang.org/x/net/context"
+
+	"github.com/docker/docker/api/types"
+	"github.com/docker/docker/api/types/filters"
+	timetypes "github.com/docker/docker/api/types/time"
+)
+
+// Events returns a stream of events in the daemon in a ReadCloser.
+// It's up to the caller to close the stream.
+func (cli *Client) Events(ctx context.Context, options types.EventsOptions) (io.ReadCloser, error) {
+	query := url.Values{}
+	ref := time.Now()
+
+	if options.Since != "" {
+		ts, err := timetypes.GetTimestamp(options.Since, ref)
+		if err != nil {
+			return nil, err
+		}
+		query.Set("since", ts)
+	}
+	if options.Until != "" {
+		ts, err := timetypes.GetTimestamp(options.Until, ref)
+		if err != nil {
+			return nil, err
+		}
+		query.Set("until", ts)
+	}
+	if options.Filters.Len() > 0 {
+		filterJSON, err := filters.ToParamWithVersion(cli.version, options.Filters)
+		if err != nil {
+			return nil, err
+		}
+		query.Set("filters", filterJSON)
+	}
+
+	serverResponse, err := cli.get(ctx, "/events", query, nil)
+	if err != nil {
+		return nil, err
+	}
+	return serverResponse.body, nil
+}

+ 126 - 0
client/events_test.go

@@ -0,0 +1,126 @@
+package client
+
+import (
+	"bytes"
+	"fmt"
+	"io/ioutil"
+	"net/http"
+	"strings"
+	"testing"
+
+	"golang.org/x/net/context"
+
+	"github.com/docker/docker/api/types"
+	"github.com/docker/docker/api/types/filters"
+)
+
+func TestEventsErrorInOptions(t *testing.T) {
+	errorCases := []struct {
+		options       types.EventsOptions
+		expectedError string
+	}{
+		{
+			options: types.EventsOptions{
+				Since: "2006-01-02TZ",
+			},
+			expectedError: `parsing time "2006-01-02TZ"`,
+		},
+		{
+			options: types.EventsOptions{
+				Until: "2006-01-02TZ",
+			},
+			expectedError: `parsing time "2006-01-02TZ"`,
+		},
+	}
+	for _, e := range errorCases {
+		client := &Client{
+			transport: newMockClient(nil, errorMock(http.StatusInternalServerError, "Server error")),
+		}
+		_, err := client.Events(context.Background(), e.options)
+		if err == nil || !strings.Contains(err.Error(), e.expectedError) {
+			t.Fatalf("expected a error %q, got %v", e.expectedError, err)
+		}
+	}
+}
+
+func TestEventsErrorFromServer(t *testing.T) {
+	client := &Client{
+		transport: newMockClient(nil, errorMock(http.StatusInternalServerError, "Server error")),
+	}
+	_, err := client.Events(context.Background(), types.EventsOptions{})
+	if err == nil || err.Error() != "Error response from daemon: Server error" {
+		t.Fatalf("expected a Server Error, got %v", err)
+	}
+}
+
+func TestEvents(t *testing.T) {
+	expectedURL := "/events"
+
+	filters := filters.NewArgs()
+	filters.Add("label", "label1")
+	filters.Add("label", "label2")
+	expectedFiltersJSON := `{"label":{"label1":true,"label2":true}}`
+
+	eventsCases := []struct {
+		options             types.EventsOptions
+		expectedQueryParams map[string]string
+	}{
+		{
+			options: types.EventsOptions{
+				Since: "invalid but valid",
+			},
+			expectedQueryParams: map[string]string{
+				"since": "invalid but valid",
+			},
+		},
+		{
+			options: types.EventsOptions{
+				Until: "invalid but valid",
+			},
+			expectedQueryParams: map[string]string{
+				"until": "invalid but valid",
+			},
+		},
+		{
+			options: types.EventsOptions{
+				Filters: filters,
+			},
+			expectedQueryParams: map[string]string{
+				"filters": expectedFiltersJSON,
+			},
+		},
+	}
+
+	for _, eventsCase := range eventsCases {
+		client := &Client{
+			transport: newMockClient(nil, func(req *http.Request) (*http.Response, error) {
+				if !strings.HasPrefix(req.URL.Path, expectedURL) {
+					return nil, fmt.Errorf("Expected URL '%s', got '%s'", expectedURL, req.URL)
+				}
+				query := req.URL.Query()
+				for key, expected := range eventsCase.expectedQueryParams {
+					actual := query.Get(key)
+					if actual != expected {
+						return nil, fmt.Errorf("%s not set in URL query properly. Expected '%s', got %s", key, expected, actual)
+					}
+				}
+				return &http.Response{
+					StatusCode: http.StatusOK,
+					Body:       ioutil.NopCloser(bytes.NewReader([]byte("response"))),
+				}, nil
+			}),
+		}
+		body, err := client.Events(context.Background(), eventsCase.options)
+		if err != nil {
+			t.Fatal(err)
+		}
+		defer body.Close()
+		content, err := ioutil.ReadAll(body)
+		if err != nil {
+			t.Fatal(err)
+		}
+		if string(content) != "response" {
+			t.Fatalf("expected response to contain 'response', got %s", string(content))
+		}
+	}
+}

+ 174 - 0
client/hijack.go

@@ -0,0 +1,174 @@
+package client
+
+import (
+	"crypto/tls"
+	"errors"
+	"fmt"
+	"net"
+	"net/http/httputil"
+	"net/url"
+	"strings"
+	"time"
+
+	"github.com/docker/docker/api/types"
+	"github.com/docker/docker/client/transport"
+	"github.com/docker/go-connections/sockets"
+	"golang.org/x/net/context"
+)
+
+// tlsClientCon holds tls information and a dialed connection.
+type tlsClientCon struct {
+	*tls.Conn
+	rawConn net.Conn
+}
+
+func (c *tlsClientCon) CloseWrite() error {
+	// Go standard tls.Conn doesn't provide the CloseWrite() method so we do it
+	// on its underlying connection.
+	if conn, ok := c.rawConn.(types.CloseWriter); ok {
+		return conn.CloseWrite()
+	}
+	return nil
+}
+
+// postHijacked sends a POST request and hijacks the connection.
+func (cli *Client) postHijacked(ctx context.Context, path string, query url.Values, body interface{}, headers map[string][]string) (types.HijackedResponse, error) {
+	bodyEncoded, err := encodeData(body)
+	if err != nil {
+		return types.HijackedResponse{}, err
+	}
+
+	req, err := cli.newRequest("POST", path, query, bodyEncoded, headers)
+	if err != nil {
+		return types.HijackedResponse{}, err
+	}
+	req.Host = cli.addr
+
+	req.Header.Set("Connection", "Upgrade")
+	req.Header.Set("Upgrade", "tcp")
+
+	conn, err := dial(cli.proto, cli.addr, cli.transport.TLSConfig())
+	if err != nil {
+		if strings.Contains(err.Error(), "connection refused") {
+			return types.HijackedResponse{}, fmt.Errorf("Cannot connect to the Docker daemon. Is 'docker daemon' running on this host?")
+		}
+		return types.HijackedResponse{}, err
+	}
+
+	// When we set up a TCP connection for hijack, there could be long periods
+	// of inactivity (a long running command with no output) that in certain
+	// network setups may cause ECONNTIMEOUT, leaving the client in an unknown
+	// state. Setting TCP KeepAlive on the socket connection will prohibit
+	// ECONNTIMEOUT unless the socket connection truly is broken
+	if tcpConn, ok := conn.(*net.TCPConn); ok {
+		tcpConn.SetKeepAlive(true)
+		tcpConn.SetKeepAlivePeriod(30 * time.Second)
+	}
+
+	clientconn := httputil.NewClientConn(conn, nil)
+	defer clientconn.Close()
+
+	// Server hijacks the connection, error 'connection closed' expected
+	_, err = clientconn.Do(req)
+
+	rwc, br := clientconn.Hijack()
+
+	return types.HijackedResponse{Conn: rwc, Reader: br}, err
+}
+
+func tlsDial(network, addr string, config *tls.Config) (net.Conn, error) {
+	return tlsDialWithDialer(new(net.Dialer), network, addr, config)
+}
+
+// We need to copy Go's implementation of tls.Dial (pkg/cryptor/tls/tls.go) in
+// order to return our custom tlsClientCon struct which holds both the tls.Conn
+// object _and_ its underlying raw connection. The rationale for this is that
+// we need to be able to close the write end of the connection when attaching,
+// which tls.Conn does not provide.
+func tlsDialWithDialer(dialer *net.Dialer, network, addr string, config *tls.Config) (net.Conn, error) {
+	// We want the Timeout and Deadline values from dialer to cover the
+	// whole process: TCP connection and TLS handshake. This means that we
+	// also need to start our own timers now.
+	timeout := dialer.Timeout
+
+	if !dialer.Deadline.IsZero() {
+		deadlineTimeout := dialer.Deadline.Sub(time.Now())
+		if timeout == 0 || deadlineTimeout < timeout {
+			timeout = deadlineTimeout
+		}
+	}
+
+	var errChannel chan error
+
+	if timeout != 0 {
+		errChannel = make(chan error, 2)
+		time.AfterFunc(timeout, func() {
+			errChannel <- errors.New("")
+		})
+	}
+
+	proxyDialer, err := sockets.DialerFromEnvironment(dialer)
+	if err != nil {
+		return nil, err
+	}
+
+	rawConn, err := proxyDialer.Dial(network, addr)
+	if err != nil {
+		return nil, err
+	}
+	// When we set up a TCP connection for hijack, there could be long periods
+	// of inactivity (a long running command with no output) that in certain
+	// network setups may cause ECONNTIMEOUT, leaving the client in an unknown
+	// state. Setting TCP KeepAlive on the socket connection will prohibit
+	// ECONNTIMEOUT unless the socket connection truly is broken
+	if tcpConn, ok := rawConn.(*net.TCPConn); ok {
+		tcpConn.SetKeepAlive(true)
+		tcpConn.SetKeepAlivePeriod(30 * time.Second)
+	}
+
+	colonPos := strings.LastIndex(addr, ":")
+	if colonPos == -1 {
+		colonPos = len(addr)
+	}
+	hostname := addr[:colonPos]
+
+	// If no ServerName is set, infer the ServerName
+	// from the hostname we're connecting to.
+	if config.ServerName == "" {
+		// Make a copy to avoid polluting argument or default.
+		config = transport.TLSConfigClone(config)
+		config.ServerName = hostname
+	}
+
+	conn := tls.Client(rawConn, config)
+
+	if timeout == 0 {
+		err = conn.Handshake()
+	} else {
+		go func() {
+			errChannel <- conn.Handshake()
+		}()
+
+		err = <-errChannel
+	}
+
+	if err != nil {
+		rawConn.Close()
+		return nil, err
+	}
+
+	// This is Docker difference with standard's crypto/tls package: returned a
+	// wrapper which holds both the TLS and raw connections.
+	return &tlsClientCon{conn, rawConn}, nil
+}
+
+func dial(proto, addr string, tlsConfig *tls.Config) (net.Conn, error) {
+	if tlsConfig != nil && proto != "unix" && proto != "npipe" {
+		// Notice this isn't Go standard's tls.Dial function
+		return tlsDial(proto, addr, tlsConfig)
+	}
+	if proto == "npipe" {
+		return sockets.DialPipe(addr, 32*time.Second)
+	}
+	return net.Dial(proto, addr)
+}

+ 123 - 0
client/image_build.go

@@ -0,0 +1,123 @@
+package client
+
+import (
+	"encoding/base64"
+	"encoding/json"
+	"io"
+	"net/http"
+	"net/url"
+	"regexp"
+	"strconv"
+
+	"golang.org/x/net/context"
+
+	"github.com/docker/docker/api/types"
+	"github.com/docker/docker/api/types/container"
+)
+
+var headerRegexp = regexp.MustCompile(`\ADocker/.+\s\((.+)\)\z`)
+
+// ImageBuild sends request to the daemon to build images.
+// The Body in the response implement an io.ReadCloser and it's up to the caller to
+// close it.
+func (cli *Client) ImageBuild(ctx context.Context, buildContext io.Reader, options types.ImageBuildOptions) (types.ImageBuildResponse, error) {
+	query, err := imageBuildOptionsToQuery(options)
+	if err != nil {
+		return types.ImageBuildResponse{}, err
+	}
+
+	headers := http.Header(make(map[string][]string))
+	buf, err := json.Marshal(options.AuthConfigs)
+	if err != nil {
+		return types.ImageBuildResponse{}, err
+	}
+	headers.Add("X-Registry-Config", base64.URLEncoding.EncodeToString(buf))
+	headers.Set("Content-Type", "application/tar")
+
+	serverResp, err := cli.postRaw(ctx, "/build", query, buildContext, headers)
+	if err != nil {
+		return types.ImageBuildResponse{}, err
+	}
+
+	osType := getDockerOS(serverResp.header.Get("Server"))
+
+	return types.ImageBuildResponse{
+		Body:   serverResp.body,
+		OSType: osType,
+	}, nil
+}
+
+func imageBuildOptionsToQuery(options types.ImageBuildOptions) (url.Values, error) {
+	query := url.Values{
+		"t": options.Tags,
+	}
+	if options.SuppressOutput {
+		query.Set("q", "1")
+	}
+	if options.RemoteContext != "" {
+		query.Set("remote", options.RemoteContext)
+	}
+	if options.NoCache {
+		query.Set("nocache", "1")
+	}
+	if options.Remove {
+		query.Set("rm", "1")
+	} else {
+		query.Set("rm", "0")
+	}
+
+	if options.ForceRemove {
+		query.Set("forcerm", "1")
+	}
+
+	if options.PullParent {
+		query.Set("pull", "1")
+	}
+
+	if options.Squash {
+		query.Set("squash", "1")
+	}
+
+	if !container.Isolation.IsDefault(options.Isolation) {
+		query.Set("isolation", string(options.Isolation))
+	}
+
+	query.Set("cpusetcpus", options.CPUSetCPUs)
+	query.Set("cpusetmems", options.CPUSetMems)
+	query.Set("cpushares", strconv.FormatInt(options.CPUShares, 10))
+	query.Set("cpuquota", strconv.FormatInt(options.CPUQuota, 10))
+	query.Set("cpuperiod", strconv.FormatInt(options.CPUPeriod, 10))
+	query.Set("memory", strconv.FormatInt(options.Memory, 10))
+	query.Set("memswap", strconv.FormatInt(options.MemorySwap, 10))
+	query.Set("cgroupparent", options.CgroupParent)
+	query.Set("shmsize", strconv.FormatInt(options.ShmSize, 10))
+	query.Set("dockerfile", options.Dockerfile)
+
+	ulimitsJSON, err := json.Marshal(options.Ulimits)
+	if err != nil {
+		return query, err
+	}
+	query.Set("ulimits", string(ulimitsJSON))
+
+	buildArgsJSON, err := json.Marshal(options.BuildArgs)
+	if err != nil {
+		return query, err
+	}
+	query.Set("buildargs", string(buildArgsJSON))
+
+	labelsJSON, err := json.Marshal(options.Labels)
+	if err != nil {
+		return query, err
+	}
+	query.Set("labels", string(labelsJSON))
+	return query, nil
+}
+
+func getDockerOS(serverHeader string) string {
+	var osType string
+	matches := headerRegexp.FindStringSubmatch(serverHeader)
+	if len(matches) > 0 {
+		osType = matches[1]
+	}
+	return osType
+}

+ 230 - 0
client/image_build_test.go

@@ -0,0 +1,230 @@
+package client
+
+import (
+	"bytes"
+	"fmt"
+	"io/ioutil"
+	"net/http"
+	"reflect"
+	"strings"
+	"testing"
+
+	"golang.org/x/net/context"
+
+	"github.com/docker/docker/api/types"
+	"github.com/docker/docker/api/types/container"
+	"github.com/docker/go-units"
+)
+
+func TestImageBuildError(t *testing.T) {
+	client := &Client{
+		transport: newMockClient(nil, errorMock(http.StatusInternalServerError, "Server error")),
+	}
+	_, err := client.ImageBuild(context.Background(), nil, types.ImageBuildOptions{})
+	if err == nil || err.Error() != "Error response from daemon: Server error" {
+		t.Fatalf("expected a Server Error, got %v", err)
+	}
+}
+
+func TestImageBuild(t *testing.T) {
+	emptyRegistryConfig := "bnVsbA=="
+	buildCases := []struct {
+		buildOptions           types.ImageBuildOptions
+		expectedQueryParams    map[string]string
+		expectedTags           []string
+		expectedRegistryConfig string
+	}{
+		{
+			buildOptions: types.ImageBuildOptions{
+				SuppressOutput: true,
+				NoCache:        true,
+				Remove:         true,
+				ForceRemove:    true,
+				PullParent:     true,
+			},
+			expectedQueryParams: map[string]string{
+				"q":       "1",
+				"nocache": "1",
+				"rm":      "1",
+				"forcerm": "1",
+				"pull":    "1",
+			},
+			expectedTags:           []string{},
+			expectedRegistryConfig: emptyRegistryConfig,
+		},
+		{
+			buildOptions: types.ImageBuildOptions{
+				SuppressOutput: false,
+				NoCache:        false,
+				Remove:         false,
+				ForceRemove:    false,
+				PullParent:     false,
+			},
+			expectedQueryParams: map[string]string{
+				"q":       "",
+				"nocache": "",
+				"rm":      "0",
+				"forcerm": "",
+				"pull":    "",
+			},
+			expectedTags:           []string{},
+			expectedRegistryConfig: emptyRegistryConfig,
+		},
+		{
+			buildOptions: types.ImageBuildOptions{
+				RemoteContext: "remoteContext",
+				Isolation:     container.Isolation("isolation"),
+				CPUSetCPUs:    "2",
+				CPUSetMems:    "12",
+				CPUShares:     20,
+				CPUQuota:      10,
+				CPUPeriod:     30,
+				Memory:        256,
+				MemorySwap:    512,
+				ShmSize:       10,
+				CgroupParent:  "cgroup_parent",
+				Dockerfile:    "Dockerfile",
+			},
+			expectedQueryParams: map[string]string{
+				"remote":       "remoteContext",
+				"isolation":    "isolation",
+				"cpusetcpus":   "2",
+				"cpusetmems":   "12",
+				"cpushares":    "20",
+				"cpuquota":     "10",
+				"cpuperiod":    "30",
+				"memory":       "256",
+				"memswap":      "512",
+				"shmsize":      "10",
+				"cgroupparent": "cgroup_parent",
+				"dockerfile":   "Dockerfile",
+				"rm":           "0",
+			},
+			expectedTags:           []string{},
+			expectedRegistryConfig: emptyRegistryConfig,
+		},
+		{
+			buildOptions: types.ImageBuildOptions{
+				BuildArgs: map[string]string{
+					"ARG1": "value1",
+					"ARG2": "value2",
+				},
+			},
+			expectedQueryParams: map[string]string{
+				"buildargs": `{"ARG1":"value1","ARG2":"value2"}`,
+				"rm":        "0",
+			},
+			expectedTags:           []string{},
+			expectedRegistryConfig: emptyRegistryConfig,
+		},
+		{
+			buildOptions: types.ImageBuildOptions{
+				Ulimits: []*units.Ulimit{
+					{
+						Name: "nproc",
+						Hard: 65557,
+						Soft: 65557,
+					},
+					{
+						Name: "nofile",
+						Hard: 20000,
+						Soft: 40000,
+					},
+				},
+			},
+			expectedQueryParams: map[string]string{
+				"ulimits": `[{"Name":"nproc","Hard":65557,"Soft":65557},{"Name":"nofile","Hard":20000,"Soft":40000}]`,
+				"rm":      "0",
+			},
+			expectedTags:           []string{},
+			expectedRegistryConfig: emptyRegistryConfig,
+		},
+		{
+			buildOptions: types.ImageBuildOptions{
+				AuthConfigs: map[string]types.AuthConfig{
+					"https://index.docker.io/v1/": {
+						Auth: "dG90bwo=",
+					},
+				},
+			},
+			expectedQueryParams: map[string]string{
+				"rm": "0",
+			},
+			expectedTags:           []string{},
+			expectedRegistryConfig: "eyJodHRwczovL2luZGV4LmRvY2tlci5pby92MS8iOnsiYXV0aCI6ImRHOTBid289In19",
+		},
+	}
+	for _, buildCase := range buildCases {
+		expectedURL := "/build"
+		client := &Client{
+			transport: newMockClient(nil, func(r *http.Request) (*http.Response, error) {
+				if !strings.HasPrefix(r.URL.Path, expectedURL) {
+					return nil, fmt.Errorf("Expected URL '%s', got '%s'", expectedURL, r.URL)
+				}
+				// Check request headers
+				registryConfig := r.Header.Get("X-Registry-Config")
+				if registryConfig != buildCase.expectedRegistryConfig {
+					return nil, fmt.Errorf("X-Registry-Config header not properly set in the request. Expected '%s', got %s", buildCase.expectedRegistryConfig, registryConfig)
+				}
+				contentType := r.Header.Get("Content-Type")
+				if contentType != "application/tar" {
+					return nil, fmt.Errorf("Content-type header not properly set in the request. Expected 'application/tar', got %s", contentType)
+				}
+
+				// Check query parameters
+				query := r.URL.Query()
+				for key, expected := range buildCase.expectedQueryParams {
+					actual := query.Get(key)
+					if actual != expected {
+						return nil, fmt.Errorf("%s not set in URL query properly. Expected '%s', got %s", key, expected, actual)
+					}
+				}
+
+				// Check tags
+				if len(buildCase.expectedTags) > 0 {
+					tags := query["t"]
+					if !reflect.DeepEqual(tags, buildCase.expectedTags) {
+						return nil, fmt.Errorf("t (tags) not set in URL query properly. Expected '%s', got %s", buildCase.expectedTags, tags)
+					}
+				}
+
+				headers := http.Header{}
+				headers.Add("Server", "Docker/v1.23 (MyOS)")
+				return &http.Response{
+					StatusCode: http.StatusOK,
+					Body:       ioutil.NopCloser(bytes.NewReader([]byte("body"))),
+					Header:     headers,
+				}, nil
+			}),
+		}
+		buildResponse, err := client.ImageBuild(context.Background(), nil, buildCase.buildOptions)
+		if err != nil {
+			t.Fatal(err)
+		}
+		if buildResponse.OSType != "MyOS" {
+			t.Fatalf("expected OSType to be 'MyOS', got %s", buildResponse.OSType)
+		}
+		response, err := ioutil.ReadAll(buildResponse.Body)
+		if err != nil {
+			t.Fatal(err)
+		}
+		buildResponse.Body.Close()
+		if string(response) != "body" {
+			t.Fatalf("expected Body to contain 'body' string, got %s", response)
+		}
+	}
+}
+
+func TestGetDockerOS(t *testing.T) {
+	cases := map[string]string{
+		"Docker/v1.22 (linux)":   "linux",
+		"Docker/v1.22 (windows)": "windows",
+		"Foo/v1.22 (bar)":        "",
+	}
+	for header, os := range cases {
+		g := getDockerOS(header)
+		if g != os {
+			t.Fatalf("Expected %s, got %s", os, g)
+		}
+	}
+}

+ 34 - 0
client/image_create.go

@@ -0,0 +1,34 @@
+package client
+
+import (
+	"io"
+	"net/url"
+
+	"golang.org/x/net/context"
+
+	"github.com/docker/docker/api/types"
+	"github.com/docker/docker/api/types/reference"
+)
+
+// ImageCreate creates a new image based in the parent options.
+// It returns the JSON content in the response body.
+func (cli *Client) ImageCreate(ctx context.Context, parentReference string, options types.ImageCreateOptions) (io.ReadCloser, error) {
+	repository, tag, err := reference.Parse(parentReference)
+	if err != nil {
+		return nil, err
+	}
+
+	query := url.Values{}
+	query.Set("fromImage", repository)
+	query.Set("tag", tag)
+	resp, err := cli.tryImageCreate(ctx, query, options.RegistryAuth)
+	if err != nil {
+		return nil, err
+	}
+	return resp.body, nil
+}
+
+func (cli *Client) tryImageCreate(ctx context.Context, query url.Values, registryAuth string) (serverResponse, error) {
+	headers := map[string][]string{"X-Registry-Auth": {registryAuth}}
+	return cli.post(ctx, "/images/create", query, nil, headers)
+}

+ 76 - 0
client/image_create_test.go

@@ -0,0 +1,76 @@
+package client
+
+import (
+	"bytes"
+	"fmt"
+	"io/ioutil"
+	"net/http"
+	"strings"
+	"testing"
+
+	"golang.org/x/net/context"
+
+	"github.com/docker/docker/api/types"
+)
+
+func TestImageCreateError(t *testing.T) {
+	client := &Client{
+		transport: newMockClient(nil, errorMock(http.StatusInternalServerError, "Server error")),
+	}
+	_, err := client.ImageCreate(context.Background(), "reference", types.ImageCreateOptions{})
+	if err == nil || err.Error() != "Error response from daemon: Server error" {
+		t.Fatalf("expected a Server error, got %v", err)
+	}
+}
+
+func TestImageCreate(t *testing.T) {
+	expectedURL := "/images/create"
+	expectedImage := "test:5000/my_image"
+	expectedTag := "sha256:ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff"
+	expectedReference := fmt.Sprintf("%s@%s", expectedImage, expectedTag)
+	expectedRegistryAuth := "eyJodHRwczovL2luZGV4LmRvY2tlci5pby92MS8iOnsiYXV0aCI6ImRHOTBid289IiwiZW1haWwiOiJqb2huQGRvZS5jb20ifX0="
+	client := &Client{
+		transport: newMockClient(nil, func(r *http.Request) (*http.Response, error) {
+			if !strings.HasPrefix(r.URL.Path, expectedURL) {
+				return nil, fmt.Errorf("Expected URL '%s', got '%s'", expectedURL, r.URL)
+			}
+			registryAuth := r.Header.Get("X-Registry-Auth")
+			if registryAuth != expectedRegistryAuth {
+				return nil, fmt.Errorf("X-Registry-Auth header not properly set in the request. Expected '%s', got %s", expectedRegistryAuth, registryAuth)
+			}
+
+			query := r.URL.Query()
+			fromImage := query.Get("fromImage")
+			if fromImage != expectedImage {
+				return nil, fmt.Errorf("fromImage not set in URL query properly. Expected '%s', got %s", expectedImage, fromImage)
+			}
+
+			tag := query.Get("tag")
+			if tag != expectedTag {
+				return nil, fmt.Errorf("tag not set in URL query properly. Expected '%s', got %s", expectedTag, tag)
+			}
+
+			return &http.Response{
+				StatusCode: http.StatusOK,
+				Body:       ioutil.NopCloser(bytes.NewReader([]byte("body"))),
+			}, nil
+		}),
+	}
+
+	createResponse, err := client.ImageCreate(context.Background(), expectedReference, types.ImageCreateOptions{
+		RegistryAuth: expectedRegistryAuth,
+	})
+	if err != nil {
+		t.Fatal(err)
+	}
+	response, err := ioutil.ReadAll(createResponse)
+	if err != nil {
+		t.Fatal(err)
+	}
+	if err = createResponse.Close(); err != nil {
+		t.Fatal(err)
+	}
+	if string(response) != "body" {
+		t.Fatalf("expected Body to contain 'body' string, got %s", response)
+	}
+}

+ 22 - 0
client/image_history.go

@@ -0,0 +1,22 @@
+package client
+
+import (
+	"encoding/json"
+	"net/url"
+
+	"github.com/docker/docker/api/types"
+	"golang.org/x/net/context"
+)
+
+// ImageHistory returns the changes in an image in history format.
+func (cli *Client) ImageHistory(ctx context.Context, imageID string) ([]types.ImageHistory, error) {
+	var history []types.ImageHistory
+	serverResp, err := cli.get(ctx, "/images/"+imageID+"/history", url.Values{}, nil)
+	if err != nil {
+		return history, err
+	}
+
+	err = json.NewDecoder(serverResp.body).Decode(&history)
+	ensureReaderClosed(serverResp)
+	return history, err
+}

+ 60 - 0
client/image_history_test.go

@@ -0,0 +1,60 @@
+package client
+
+import (
+	"bytes"
+	"encoding/json"
+	"fmt"
+	"io/ioutil"
+	"net/http"
+	"strings"
+	"testing"
+
+	"github.com/docker/docker/api/types"
+	"golang.org/x/net/context"
+)
+
+func TestImageHistoryError(t *testing.T) {
+	client := &Client{
+		transport: newMockClient(nil, errorMock(http.StatusInternalServerError, "Server error")),
+	}
+	_, err := client.ImageHistory(context.Background(), "nothing")
+	if err == nil || err.Error() != "Error response from daemon: Server error" {
+		t.Fatalf("expected a Server error, got %v", err)
+	}
+}
+
+func TestImageHistory(t *testing.T) {
+	expectedURL := "/images/image_id/history"
+	client := &Client{
+		transport: newMockClient(nil, func(r *http.Request) (*http.Response, error) {
+			if !strings.HasPrefix(r.URL.Path, expectedURL) {
+				return nil, fmt.Errorf("Expected URL '%s', got '%s'", expectedURL, r.URL)
+			}
+			b, err := json.Marshal([]types.ImageHistory{
+				{
+					ID:   "image_id1",
+					Tags: []string{"tag1", "tag2"},
+				},
+				{
+					ID:   "image_id2",
+					Tags: []string{"tag1", "tag2"},
+				},
+			})
+			if err != nil {
+				return nil, err
+			}
+
+			return &http.Response{
+				StatusCode: http.StatusOK,
+				Body:       ioutil.NopCloser(bytes.NewReader(b)),
+			}, nil
+		}),
+	}
+	imageHistories, err := client.ImageHistory(context.Background(), "image_id")
+	if err != nil {
+		t.Fatal(err)
+	}
+	if len(imageHistories) != 2 {
+		t.Fatalf("expected 2 containers, got %v", imageHistories)
+	}
+}

+ 37 - 0
client/image_import.go

@@ -0,0 +1,37 @@
+package client
+
+import (
+	"io"
+	"net/url"
+
+	"golang.org/x/net/context"
+
+	"github.com/docker/distribution/reference"
+	"github.com/docker/docker/api/types"
+)
+
+// ImageImport creates a new image based in the source options.
+// It returns the JSON content in the response body.
+func (cli *Client) ImageImport(ctx context.Context, source types.ImageImportSource, ref string, options types.ImageImportOptions) (io.ReadCloser, error) {
+	if ref != "" {
+		//Check if the given image name can be resolved
+		if _, err := reference.ParseNamed(ref); err != nil {
+			return nil, err
+		}
+	}
+
+	query := url.Values{}
+	query.Set("fromSrc", source.SourceName)
+	query.Set("repo", ref)
+	query.Set("tag", options.Tag)
+	query.Set("message", options.Message)
+	for _, change := range options.Changes {
+		query.Add("changes", change)
+	}
+
+	resp, err := cli.postRaw(ctx, "/images/create", query, source.Source, nil)
+	if err != nil {
+		return nil, err
+	}
+	return resp.body, nil
+}

+ 81 - 0
client/image_import_test.go

@@ -0,0 +1,81 @@
+package client
+
+import (
+	"bytes"
+	"fmt"
+	"io/ioutil"
+	"net/http"
+	"reflect"
+	"strings"
+	"testing"
+
+	"github.com/docker/docker/api/types"
+	"golang.org/x/net/context"
+)
+
+func TestImageImportError(t *testing.T) {
+	client := &Client{
+		transport: newMockClient(nil, errorMock(http.StatusInternalServerError, "Server error")),
+	}
+	_, err := client.ImageImport(context.Background(), types.ImageImportSource{}, "image:tag", types.ImageImportOptions{})
+	if err == nil || err.Error() != "Error response from daemon: Server error" {
+		t.Fatalf("expected a Server error, got %v", err)
+	}
+}
+
+func TestImageImport(t *testing.T) {
+	expectedURL := "/images/create"
+	client := &Client{
+		transport: newMockClient(nil, func(r *http.Request) (*http.Response, error) {
+			if !strings.HasPrefix(r.URL.Path, expectedURL) {
+				return nil, fmt.Errorf("Expected URL '%s', got '%s'", expectedURL, r.URL)
+			}
+			query := r.URL.Query()
+			fromSrc := query.Get("fromSrc")
+			if fromSrc != "image_source" {
+				return nil, fmt.Errorf("fromSrc not set in URL query properly. Expected 'image_source', got %s", fromSrc)
+			}
+			repo := query.Get("repo")
+			if repo != "repository_name:imported" {
+				return nil, fmt.Errorf("repo not set in URL query properly. Expected 'repository_name', got %s", repo)
+			}
+			tag := query.Get("tag")
+			if tag != "imported" {
+				return nil, fmt.Errorf("tag not set in URL query properly. Expected 'imported', got %s", tag)
+			}
+			message := query.Get("message")
+			if message != "A message" {
+				return nil, fmt.Errorf("message not set in URL query properly. Expected 'A message', got %s", message)
+			}
+			changes := query["changes"]
+			expectedChanges := []string{"change1", "change2"}
+			if !reflect.DeepEqual(expectedChanges, changes) {
+				return nil, fmt.Errorf("changes not set in URL query properly. Expected %v, got %v", expectedChanges, changes)
+			}
+
+			return &http.Response{
+				StatusCode: http.StatusOK,
+				Body:       ioutil.NopCloser(bytes.NewReader([]byte("response"))),
+			}, nil
+		}),
+	}
+	importResponse, err := client.ImageImport(context.Background(), types.ImageImportSource{
+		Source:     strings.NewReader("source"),
+		SourceName: "image_source",
+	}, "repository_name:imported", types.ImageImportOptions{
+		Tag:     "imported",
+		Message: "A message",
+		Changes: []string{"change1", "change2"},
+	})
+	if err != nil {
+		t.Fatal(err)
+	}
+	response, err := ioutil.ReadAll(importResponse)
+	if err != nil {
+		t.Fatal(err)
+	}
+	importResponse.Close()
+	if string(response) != "response" {
+		t.Fatalf("expected response to contain 'response', got %s", string(response))
+	}
+}

Some files were not shown because too many files changed in this diff