Import docker/docker/client

Signed-off-by: Daniel Nephin <dnephin@gmail.com>
This commit is contained in:
Daniel Nephin 2017-04-17 17:41:20 -04:00
commit e8a454d8a6
194 changed files with 11748 additions and 0 deletions

35
client/README.md Normal file
View File

@ -0,0 +1,35 @@
# Go client for the Docker Engine API
The `docker` command uses this package to communicate with the daemon. It can also be used by your own Go applications to do anything the command-line interface does  running containers, pulling images, managing swarms, etc.
For example, to list running containers (the equivalent of `docker ps`):
```go
package main
import (
"context"
"fmt"
"github.com/docker/docker/api/types"
"github.com/docker/docker/client"
)
func main() {
cli, err := client.NewEnvClient()
if err != nil {
panic(err)
}
containers, err := cli.ContainerList(context.Background(), types.ContainerListOptions{})
if err != nil {
panic(err)
}
for _, container := range containers {
fmt.Printf("%s %s\n", container.ID[:10], container.Image)
}
}
```
[Full documentation is available on GoDoc.](https://godoc.org/github.com/docker/docker/client)

View File

@ -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
}

View File

@ -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{
client: newMockClient(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{
client: newMockClient(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)
}
}

View File

@ -0,0 +1,20 @@
package client
import (
"net/url"
"github.com/docker/docker/api/types"
"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, options types.CheckpointDeleteOptions) error {
query := url.Values{}
if options.CheckpointDir != "" {
query.Set("dir", options.CheckpointDir)
}
resp, err := cli.delete(ctx, "/containers/"+containerID+"/checkpoints/"+options.CheckpointID, query, nil)
ensureReaderClosed(resp)
return err
}

View File

@ -0,0 +1,54 @@
package client
import (
"bytes"
"fmt"
"io/ioutil"
"net/http"
"strings"
"testing"
"github.com/docker/docker/api/types"
"golang.org/x/net/context"
)
func TestCheckpointDeleteError(t *testing.T) {
client := &Client{
client: newMockClient(errorMock(http.StatusInternalServerError, "Server error")),
}
err := client.CheckpointDelete(context.Background(), "container_id", types.CheckpointDeleteOptions{
CheckpointID: "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{
client: newMockClient(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", types.CheckpointDeleteOptions{
CheckpointID: "checkpoint_id",
})
if err != nil {
t.Fatal(err)
}
}

32
client/checkpoint_list.go Normal file
View File

@ -0,0 +1,32 @@
package client
import (
"encoding/json"
"net/http"
"net/url"
"github.com/docker/docker/api/types"
"golang.org/x/net/context"
)
// CheckpointList returns the checkpoints of the given container in the docker host
func (cli *Client) CheckpointList(ctx context.Context, container string, options types.CheckpointListOptions) ([]types.Checkpoint, error) {
var checkpoints []types.Checkpoint
query := url.Values{}
if options.CheckpointDir != "" {
query.Set("dir", options.CheckpointDir)
}
resp, err := cli.get(ctx, "/containers/"+container+"/checkpoints", query, nil)
if err != nil {
if resp.statusCode == http.StatusNotFound {
return checkpoints, containerNotFoundError{container}
}
return checkpoints, err
}
err = json.NewDecoder(resp.body).Decode(&checkpoints)
ensureReaderClosed(resp)
return checkpoints, err
}

View File

@ -0,0 +1,68 @@
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{
client: newMockClient(errorMock(http.StatusInternalServerError, "Server error")),
}
_, err := client.CheckpointList(context.Background(), "container_id", types.CheckpointListOptions{})
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{
client: newMockClient(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", types.CheckpointListOptions{})
if err != nil {
t.Fatal(err)
}
if len(checkpoints) != 1 {
t.Fatalf("expected 1 checkpoint, got %v", checkpoints)
}
}
func TestCheckpointListContainerNotFound(t *testing.T) {
client := &Client{
client: newMockClient(errorMock(http.StatusNotFound, "Server error")),
}
_, err := client.CheckpointList(context.Background(), "unknown", types.CheckpointListOptions{})
if err == nil || !IsErrContainerNotFound(err) {
t.Fatalf("expected a containerNotFound error, got %v", err)
}
}

261
client/client.go Normal file
View File

@ -0,0 +1,261 @@
/*
Package client is a Go client for the Docker Engine API.
The "docker" command uses this package to communicate with the daemon. It can also
be used by your own Go applications to do anything the command-line interface does
- running containers, pulling images, managing swarms, etc.
For more information about the Engine API, see the documentation:
https://docs.docker.com/engine/reference/api/
Usage
You use the library by creating a client object and calling methods on it. The
client can be created either from environment variables with NewEnvClient, or
configured manually with NewClient.
For example, to list running containers (the equivalent of "docker ps"):
package main
import (
"context"
"fmt"
"github.com/docker/docker/api/types"
"github.com/docker/docker/client"
)
func main() {
cli, err := client.NewEnvClient()
if err != nil {
panic(err)
}
containers, err := cli.ContainerList(context.Background(), types.ContainerListOptions{})
if err != nil {
panic(err)
}
for _, container := range containers {
fmt.Printf("%s %s\n", container.ID[:10], container.Image)
}
}
*/
package client
import (
"fmt"
"net/http"
"net/url"
"os"
"path/filepath"
"strings"
"github.com/docker/docker/api"
"github.com/docker/go-connections/sockets"
"github.com/docker/go-connections/tlsconfig"
)
// Client is the API client that performs all operations
// against a docker server.
type Client struct {
// scheme sets the scheme for the client
scheme string
// 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
// client used to send and receive http requests.
client *http.Client
// version of the server to talk to.
version string
// custom http headers configured by users.
customHTTPHeaders map[string]string
// manualOverride is set to true when the version was set by users.
manualOverride bool
}
// 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 = api.DefaultVersion
}
cli, err := NewClient(host, version, client, nil)
if err != nil {
return cli, err
}
if os.Getenv("DOCKER_API_VERSION") != "" {
cli.manualOverride = true
}
return cli, 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
}
if client != nil {
if _, ok := client.Transport.(*http.Transport); !ok {
return nil, fmt.Errorf("unable to verify TLS configuration, invalid transport %v", client.Transport)
}
} else {
transport := new(http.Transport)
sockets.ConfigureTransport(transport, proto, addr)
client = &http.Client{
Transport: transport,
}
}
scheme := "http"
tlsConfig := resolveTLSConfig(client.Transport)
if tlsConfig != nil {
// TODO(stevvooe): This isn't really the right way to write clients in Go.
// `NewClient` should probably only take an `*http.Client` and work from there.
// Unfortunately, the model of having a host-ish/url-thingy as the connection
// string has us confusing protocol and transport layers. We continue doing
// this to avoid breaking existing clients but this should be addressed.
scheme = "https"
}
return &Client{
scheme: scheme,
host: host,
proto: proto,
addr: addr,
basePath: basePath,
client: client,
version: version,
customHTTPHeaders: httpHeaders,
}, nil
}
// Close ensures that transport.Client is closed
// especially needed while using NewClient with *http.Client = nil
// for example
// client.NewClient("unix:///var/run/docker.sock", nil, "v1.18", map[string]string{"User-Agent": "engine-api-cli-1.0"})
func (cli *Client) Close() error {
if t, ok := cli.client.Transport.(*http.Transport); ok {
t.CloseIdleConnections()
}
return 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.
// This operation doesn't acquire a mutex.
func (cli *Client) ClientVersion() string {
return cli.version
}
// UpdateClientVersion updates the version string associated with this
// instance of the Client. This operation doesn't acquire a mutex.
func (cli *Client) UpdateClientVersion(v string) {
if !cli.manualOverride {
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
}
// CustomHTTPHeaders returns the custom http headers associated with this
// instance of the Client. This operation doesn't acquire a mutex.
func (cli *Client) CustomHTTPHeaders() map[string]string {
m := make(map[string]string)
for k, v := range cli.customHTTPHeaders {
m[k] = v
}
return m
}
// SetCustomHTTPHeaders updates the custom http headers associated with this
// instance of the Client. This operation doesn't acquire a mutex.
func (cli *Client) SetCustomHTTPHeaders(headers map[string]string) {
cli.customHTTPHeaders = headers
}

View File

@ -0,0 +1,45 @@
package client
import (
"bytes"
"encoding/json"
"io/ioutil"
"net/http"
"github.com/docker/docker/api/types"
)
func newMockClient(doer func(*http.Request) (*http.Response, error)) *http.Client {
return &http.Client{
Transport: transportFunc(doer),
}
}
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
}
}

284
client/client_test.go Normal file
View File

@ -0,0 +1,284 @@
package client
import (
"bytes"
"encoding/json"
"io/ioutil"
"net/http"
"net/url"
"os"
"runtime"
"strings"
"testing"
"github.com/docker/docker/api"
"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: api.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",
},
{
envs: map[string]string{
"DOCKER_CERT_PATH": "testdata/",
},
expectedVersion: api.DefaultVersion,
},
{
envs: map[string]string{
"DOCKER_CERT_PATH": "testdata/",
"DOCKER_TLS_VERIFY": "1",
},
expectedVersion: api.DefaultVersion,
},
{
envs: map[string]string{
"DOCKER_CERT_PATH": "testdata/",
"DOCKER_HOST": "https://notaunixsocket",
},
expectedVersion: api.DefaultVersion,
},
{
envs: map[string]string{
"DOCKER_HOST": "host",
},
expectedError: "unable to parse docker host `host`",
},
{
envs: map[string]string{
"DOCKER_HOST": "invalid://url",
},
expectedVersion: api.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 {
t.Errorf("expected an error for %v", c)
} else if 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)
}
}
if c.envs["DOCKER_TLS_VERIFY"] != "" {
// pedantic checking that this is handled correctly
tr := apiclient.client.Transport.(*http.Transport)
if tr.TLSClientConfig == nil {
t.Error("no TLS config found when DOCKER_TLS_VERIFY enabled")
}
if tr.TLSClientConfig.InsecureSkipVerify {
t.Error("TLS verification should be enabled")
}
}
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)
}
err = c.Close()
if nil != err {
t.Fatalf("close client failed, error message: %s", err)
}
}
}
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{
client: newMockClient(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 != api.DefaultVersion {
t.Fatalf("Expected %s, got %s", api.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
client/client_unix.go Normal file
View File

@ -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
client/client_windows.go Normal file
View File

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

View File

@ -0,0 +1,37 @@
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)
}
if options.Logs {
query.Set("logs", "1")
}
headers := map[string][]string{"Content-Type": {"text/plain"}}
return cli.postHijacked(ctx, "/containers/"+container+"/attach", query, nil, headers)
}

View File

@ -0,0 +1,55 @@
package client
import (
"encoding/json"
"errors"
"net/url"
"github.com/docker/distribution/reference"
"github.com/docker/docker/api/types"
"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.IDResponse, error) {
var repository, tag string
if options.Reference != "" {
ref, err := reference.ParseNormalizedNamed(options.Reference)
if err != nil {
return types.IDResponse{}, err
}
if _, isCanonical := ref.(reference.Canonical); isCanonical {
return types.IDResponse{}, errors.New("refusing to create a tag with a digest reference")
}
ref = reference.TagNameOnly(ref)
if tagged, ok := ref.(reference.Tagged); ok {
tag = tagged.Tag()
}
repository = reference.FamiliarName(ref)
}
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.IDResponse
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
}

View File

@ -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{
client: newMockClient(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{
client: newMockClient(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.IDResponse{
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 `new_container_id`, got %s", r.ID)
}
}

101
client/container_copy.go Normal file
View File

@ -0,0 +1,101 @@
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")
}
if options.CopyUIDGID {
query.Set("copyUIDGID", "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
}

View File

@ -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{
client: newMockClient(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{
client: newMockClient(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{
client: newMockClient(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', got '%s'", stat.Name)
}
if stat.Mode != 0700 {
t.Fatalf("expected container path stat mode to be 0700, got '%v'", stat.Mode)
}
}
func TestCopyToContainerError(t *testing.T) {
client := &Client{
client: newMockClient(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{
client: newMockClient(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{
client: newMockClient(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{
client: newMockClient(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{
client: newMockClient(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{
client: newMockClient(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{
client: newMockClient(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 GET 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', got '%s'", stat.Name)
}
if stat.Mode != 0700 {
t.Fatalf("expected container path stat mode to be 0700, got '%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))
}
}

View File

@ -0,0 +1,56 @@
package client
import (
"encoding/json"
"net/url"
"strings"
"github.com/docker/docker/api/types/container"
"github.com/docker/docker/api/types/network"
"github.com/docker/docker/api/types/versions"
"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) (container.ContainerCreateCreatedBody, error) {
var response container.ContainerCreateCreatedBody
if err := cli.NewVersionError("1.25", "stop timeout"); config != nil && config.StopTimeout != nil && err != nil {
return response, err
}
// When using API 1.24 and under, the client is responsible for removing the container
if hostConfig != nil && versions.LessThan(cli.ClientVersion(), "1.25") {
hostConfig.AutoRemove = false
}
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
}

View File

@ -0,0 +1,118 @@
package client
import (
"bytes"
"encoding/json"
"fmt"
"io/ioutil"
"net/http"
"strings"
"testing"
"github.com/docker/docker/api/types/container"
"golang.org/x/net/context"
)
func TestContainerCreateError(t *testing.T) {
client := &Client{
client: newMockClient(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 while testing StatusInternalServerError, got %v", err)
}
// 404 doesn't automatically means an unknown image
client = &Client{
client: newMockClient(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 while testing StatusNotFound, got %v", err)
}
}
func TestContainerCreateImageNotFound(t *testing.T) {
client := &Client{
client: newMockClient(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{
client: newMockClient(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(container.ContainerCreateCreatedBody{
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)
}
}
// TestContainerCreateAutoRemove validates that a client using API 1.24 always disables AutoRemove. When using API 1.25
// or up, AutoRemove should not be disabled.
func TestContainerCreateAutoRemove(t *testing.T) {
autoRemoveValidator := func(expectedValue bool) func(req *http.Request) (*http.Response, error) {
return func(req *http.Request) (*http.Response, error) {
var config configWrapper
if err := json.NewDecoder(req.Body).Decode(&config); err != nil {
return nil, err
}
if config.HostConfig.AutoRemove != expectedValue {
return nil, fmt.Errorf("expected AutoRemove to be %v, got %v", expectedValue, config.HostConfig.AutoRemove)
}
b, err := json.Marshal(container.ContainerCreateCreatedBody{
ID: "container_id",
})
if err != nil {
return nil, err
}
return &http.Response{
StatusCode: http.StatusOK,
Body: ioutil.NopCloser(bytes.NewReader(b)),
}, nil
}
}
client := &Client{
client: newMockClient(autoRemoveValidator(false)),
version: "1.24",
}
if _, err := client.ContainerCreate(context.Background(), nil, &container.HostConfig{AutoRemove: true}, nil, ""); err != nil {
t.Fatal(err)
}
client = &Client{
client: newMockClient(autoRemoveValidator(true)),
version: "1.25",
}
if _, err := client.ContainerCreate(context.Background(), nil, &container.HostConfig{AutoRemove: true}, nil, ""); err != nil {
t.Fatal(err)
}
}

23
client/container_diff.go Normal file
View File

@ -0,0 +1,23 @@
package client
import (
"encoding/json"
"net/url"
"github.com/docker/docker/api/types/container"
"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) ([]container.ContainerChangeResponseItem, error) {
var changes []container.ContainerChangeResponseItem
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
}

View File

@ -0,0 +1,61 @@
package client
import (
"bytes"
"encoding/json"
"fmt"
"io/ioutil"
"net/http"
"strings"
"testing"
"github.com/docker/docker/api/types/container"
"golang.org/x/net/context"
)
func TestContainerDiffError(t *testing.T) {
client := &Client{
client: newMockClient(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{
client: newMockClient(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([]container.ContainerChangeResponseItem{
{
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)
}
}

54
client/container_exec.go Normal file
View File

@ -0,0 +1,54 @@
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.IDResponse, error) {
var response types.IDResponse
if err := cli.NewVersionError("1.25", "env"); len(config.Env) != 0 && err != nil {
return response, err
}
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
}

View File

@ -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{
client: newMockClient(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{
client: newMockClient(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.IDResponse{
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{
client: newMockClient(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{
client: newMockClient(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{
client: newMockClient(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{
client: newMockClient(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)
}
}

View File

@ -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
}

View File

@ -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{
client: newMockClient(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{
client: newMockClient(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))
}
}

View File

@ -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
}

View File

@ -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{
client: newMockClient(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{
client: newMockClient(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{
client: newMockClient(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.Image)
}
if r.Name != "name" {
t.Fatalf("expected `name`, got %s", r.Name)
}
}
func TestContainerInspectNode(t *testing.T) {
client := &Client{
client: newMockClient(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.Image)
}
if r.Name != "name" {
t.Fatalf("expected `name`, got %s", r.Name)
}
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
client/container_kill.go Normal file
View File

@ -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
}

View File

@ -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{
client: newMockClient(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{
client: newMockClient(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
client/container_list.go Normal file
View File

@ -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.Filters.Len() > 0 {
filterJSON, err := filters.ToParamWithVersion(cli.version, options.Filters)
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
}

View File

@ -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{
client: newMockClient(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{
client: newMockClient(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",
Filters: filters,
})
if err != nil {
t.Fatal(err)
}
if len(containers) != 2 {
t.Fatalf("expected 2 containers, got %v", containers)
}
}

52
client/container_logs.go Normal file
View File

@ -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
}

View File

@ -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{
client: newMockClient(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{
client: newMockClient(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
client/container_pause.go Normal file
View File

@ -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
}

View File

@ -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{
client: newMockClient(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{
client: newMockClient(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)
}
}

36
client/container_prune.go Normal file
View File

@ -0,0 +1,36 @@
package client
import (
"encoding/json"
"fmt"
"github.com/docker/docker/api/types"
"github.com/docker/docker/api/types/filters"
"golang.org/x/net/context"
)
// ContainersPrune requests the daemon to delete unused data
func (cli *Client) ContainersPrune(ctx context.Context, pruneFilters filters.Args) (types.ContainersPruneReport, error) {
var report types.ContainersPruneReport
if err := cli.NewVersionError("1.25", "container prune"); err != nil {
return report, err
}
query, err := getFiltersQuery(pruneFilters)
if err != nil {
return report, err
}
serverResp, err := cli.post(ctx, "/containers/prune", query, nil, nil)
if err != nil {
return report, err
}
defer ensureReaderClosed(serverResp)
if err := json.NewDecoder(serverResp.body).Decode(&report); err != nil {
return report, fmt.Errorf("Error retrieving disk usage: %v", err)
}
return report, nil
}

View File

@ -0,0 +1,124 @@
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"
"github.com/stretchr/testify/assert"
"golang.org/x/net/context"
)
func TestContainersPruneError(t *testing.T) {
client := &Client{
client: newMockClient(errorMock(http.StatusInternalServerError, "Server error")),
version: "1.25",
}
filters := filters.NewArgs()
_, err := client.ContainersPrune(context.Background(), filters)
assert.EqualError(t, err, "Error response from daemon: Server error")
}
func TestContainersPrune(t *testing.T) {
expectedURL := "/v1.25/containers/prune"
danglingFilters := filters.NewArgs()
danglingFilters.Add("dangling", "true")
noDanglingFilters := filters.NewArgs()
noDanglingFilters.Add("dangling", "false")
danglingUntilFilters := filters.NewArgs()
danglingUntilFilters.Add("dangling", "true")
danglingUntilFilters.Add("until", "2016-12-15T14:00")
labelFilters := filters.NewArgs()
labelFilters.Add("dangling", "true")
labelFilters.Add("label", "label1=foo")
labelFilters.Add("label", "label2!=bar")
listCases := []struct {
filters filters.Args
expectedQueryParams map[string]string
}{
{
filters: filters.Args{},
expectedQueryParams: map[string]string{
"until": "",
"filter": "",
"filters": "",
},
},
{
filters: danglingFilters,
expectedQueryParams: map[string]string{
"until": "",
"filter": "",
"filters": `{"dangling":{"true":true}}`,
},
},
{
filters: danglingUntilFilters,
expectedQueryParams: map[string]string{
"until": "",
"filter": "",
"filters": `{"dangling":{"true":true},"until":{"2016-12-15T14:00":true}}`,
},
},
{
filters: noDanglingFilters,
expectedQueryParams: map[string]string{
"until": "",
"filter": "",
"filters": `{"dangling":{"false":true}}`,
},
},
{
filters: labelFilters,
expectedQueryParams: map[string]string{
"until": "",
"filter": "",
"filters": `{"dangling":{"true":true},"label":{"label1=foo":true,"label2!=bar":true}}`,
},
},
}
for _, listCase := range listCases {
client := &Client{
client: newMockClient(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 listCase.expectedQueryParams {
actual := query.Get(key)
assert.Equal(t, expected, actual)
}
content, err := json.Marshal(types.ContainersPruneReport{
ContainersDeleted: []string{"container_id1", "container_id2"},
SpaceReclaimed: 9999,
})
if err != nil {
return nil, err
}
return &http.Response{
StatusCode: http.StatusOK,
Body: ioutil.NopCloser(bytes.NewReader(content)),
}, nil
}),
version: "1.25",
}
report, err := client.ContainersPrune(context.Background(), listCase.filters)
assert.NoError(t, err)
assert.Len(t, report.ContainersDeleted, 2)
assert.Equal(t, uint64(9999), report.SpaceReclaimed)
}
}

View File

@ -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
}

View File

@ -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{
client: newMockClient(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{
client: newMockClient(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)
}
}

View File

@ -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
}

View File

@ -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{
client: newMockClient(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{
client: newMockClient(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)
}
}

View File

@ -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 uint) error {
query := url.Values{}
query.Set("h", strconv.Itoa(int(height)))
query.Set("w", strconv.Itoa(int(width)))
resp, err := cli.post(ctx, basePath+"/resize", query, nil, nil)
ensureReaderClosed(resp)
return err
}

View File

@ -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{
client: newMockClient(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{
client: newMockClient(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{
client: newMockClient(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{
client: newMockClient(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
}
}

View File

@ -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
}

View File

@ -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{
client: newMockClient(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{
client: newMockClient(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)
}
}

24
client/container_start.go Normal file
View File

@ -0,0 +1,24 @@
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)
}
if len(options.CheckpointDir) != 0 {
query.Set("checkpoint-dir", options.CheckpointDir)
}
resp, err := cli.post(ctx, "/containers/"+containerID+"/start", query, nil, nil)
ensureReaderClosed(resp)
return err
}

View File

@ -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{
client: newMockClient(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{
client: newMockClient(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)
}
}

26
client/container_stats.go Normal file
View File

@ -0,0 +1,26 @@
package client
import (
"net/url"
"github.com/docker/docker/api/types"
"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) (types.ContainerStats, 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 types.ContainerStats{}, err
}
osType := getDockerOS(resp.header.Get("Server"))
return types.ContainerStats{Body: resp.body, OSType: osType}, err
}

View File

@ -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{
client: newMockClient(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{
client: newMockClient(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
}),
}
resp, err := client.ContainerStats(context.Background(), "container_id", c.stream)
if err != nil {
t.Fatal(err)
}
defer resp.Body.Close()
content, err := ioutil.ReadAll(resp.Body)
if err != nil {
t.Fatal(err)
}
if string(content) != "response" {
t.Fatalf("expected response to contain 'response', got %s", string(content))
}
}
}

21
client/container_stop.go Normal file
View File

@ -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
}

View File

@ -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{
client: newMockClient(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{
client: newMockClient(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
client/container_top.go Normal file
View File

@ -0,0 +1,28 @@
package client
import (
"encoding/json"
"net/url"
"strings"
"github.com/docker/docker/api/types/container"
"golang.org/x/net/context"
)
// ContainerTop shows process information from within a container.
func (cli *Client) ContainerTop(ctx context.Context, containerID string, arguments []string) (container.ContainerTopOKBody, error) {
var response container.ContainerTopOKBody
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
}

View File

@ -0,0 +1,74 @@
package client
import (
"bytes"
"encoding/json"
"fmt"
"io/ioutil"
"net/http"
"reflect"
"strings"
"testing"
"github.com/docker/docker/api/types/container"
"golang.org/x/net/context"
)
func TestContainerTopError(t *testing.T) {
client := &Client{
client: newMockClient(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{
client: newMockClient(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(container.ContainerTopOKBody{
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)
}
}

View File

@ -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
}

View File

@ -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{
client: newMockClient(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{
client: newMockClient(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)
}
}

View File

@ -0,0 +1,22 @@
package client
import (
"encoding/json"
"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) (container.ContainerUpdateOKBody, error) {
var response container.ContainerUpdateOKBody
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
}

View File

@ -0,0 +1,58 @@
package client
import (
"bytes"
"encoding/json"
"fmt"
"io/ioutil"
"net/http"
"strings"
"testing"
"github.com/docker/docker/api/types/container"
"golang.org/x/net/context"
)
func TestContainerUpdateError(t *testing.T) {
client := &Client{
client: newMockClient(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{
client: newMockClient(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(container.ContainerUpdateOKBody{})
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
client/container_wait.go Normal file
View File

@ -0,0 +1,26 @@
package client
import (
"encoding/json"
"golang.org/x/net/context"
"github.com/docker/docker/api/types/container"
)
// 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) (int64, error) {
resp, err := cli.post(ctx, "/containers/"+containerID+"/wait", nil, nil, nil)
if err != nil {
return -1, err
}
defer ensureReaderClosed(resp)
var res container.ContainerWaitOKBody
if err := json.NewDecoder(resp.body).Decode(&res); err != nil {
return -1, err
}
return res.StatusCode, nil
}

View File

@ -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/container"
"golang.org/x/net/context"
)
func TestContainerWaitError(t *testing.T) {
client := &Client{
client: newMockClient(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{
client: newMockClient(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(container.ContainerWaitOKBody{
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)
}
}

26
client/disk_usage.go Normal file
View File

@ -0,0 +1,26 @@
package client
import (
"encoding/json"
"fmt"
"github.com/docker/docker/api/types"
"golang.org/x/net/context"
)
// DiskUsage requests the current data usage from the daemon
func (cli *Client) DiskUsage(ctx context.Context) (types.DiskUsage, error) {
var du types.DiskUsage
serverResp, err := cli.get(ctx, "/system/df", nil, nil)
if err != nil {
return du, err
}
defer ensureReaderClosed(serverResp)
if err := json.NewDecoder(serverResp.body).Decode(&du); err != nil {
return du, fmt.Errorf("Error retrieving disk usage: %v", err)
}
return du, nil
}

278
client/errors.go Normal file
View File

@ -0,0 +1,278 @@
package client
import (
"fmt"
"github.com/docker/docker/api/types/versions"
"github.com/pkg/errors"
)
// errConnectionFailed implements an error returned when connection failed.
type errConnectionFailed struct {
host string
}
// Error returns a string representation of an errConnectionFailed
func (err errConnectionFailed) Error() string {
if err.host == "" {
return "Cannot connect to the Docker daemon. Is the docker daemon running on this host?"
}
return fmt.Sprintf("Cannot connect to the Docker daemon at %s. Is the docker daemon running?", err.host)
}
// IsErrConnectionFailed returns true if the error is caused by connection failed.
func IsErrConnectionFailed(err error) bool {
_, ok := errors.Cause(err).(errConnectionFailed)
return ok
}
// ErrorConnectionFailed returns an error with host in the error message when connection to docker daemon failed.
func ErrorConnectionFailed(host string) error {
return errConnectionFailed{host: 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
}
// NotFound 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
}
// NotFound 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
}
// NotFound 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
}
// NotFound indicates that this error type is of NotFound
func (e volumeNotFoundError) NotFound() bool {
return true
}
// Error returns a string representation of a volumeNotFoundError
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)
}
// NotFound 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)
}
// NotFound 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)
}
// NotFound 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
}
// NewVersionError returns an error if the APIVersion required
// if less than the current supported version
func (cli *Client) NewVersionError(APIrequired, feature string) error {
if versions.LessThan(cli.version, APIrequired) {
return fmt.Errorf("%q requires API version %s, but the Docker daemon API version is %s", feature, APIrequired, cli.version)
}
return nil
}
// secretNotFoundError implements an error returned when a secret is not found.
type secretNotFoundError struct {
name string
}
// Error returns a string representation of a secretNotFoundError
func (e secretNotFoundError) Error() string {
return fmt.Sprintf("Error: no such secret: %s", e.name)
}
// NotFound indicates that this error type is of NotFound
func (e secretNotFoundError) NotFound() bool {
return true
}
// IsErrSecretNotFound returns true if the error is caused
// when a secret is not found.
func IsErrSecretNotFound(err error) bool {
_, ok := err.(secretNotFoundError)
return ok
}
// pluginNotFoundError implements an error returned when a plugin is not in the docker host.
type pluginNotFoundError struct {
name string
}
// NotFound indicates that this error type is of NotFound
func (e pluginNotFoundError) NotFound() bool {
return true
}
// Error returns a string representation of a pluginNotFoundError
func (e pluginNotFoundError) Error() string {
return fmt.Sprintf("Error: No such plugin: %s", e.name)
}
// IsErrPluginNotFound returns true if the error is caused
// when a plugin is not found in the docker host.
func IsErrPluginNotFound(err error) bool {
return IsErrNotFound(err)
}

102
client/events.go Normal file
View File

@ -0,0 +1,102 @@
package client
import (
"encoding/json"
"net/url"
"time"
"golang.org/x/net/context"
"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"
)
// Events returns a stream of events in the daemon. It's up to the caller to close the stream
// by cancelling the context. Once the stream has been completely read an io.EOF error will
// be sent over the error channel. If an error is sent all processing will be stopped. It's up
// to the caller to reopen the stream in the event of an error by reinvoking this method.
func (cli *Client) Events(ctx context.Context, options types.EventsOptions) (<-chan events.Message, <-chan error) {
messages := make(chan events.Message)
errs := make(chan error, 1)
started := make(chan struct{})
go func() {
defer close(errs)
query, err := buildEventsQueryParams(cli.version, options)
if err != nil {
close(started)
errs <- err
return
}
resp, err := cli.get(ctx, "/events", query, nil)
if err != nil {
close(started)
errs <- err
return
}
defer resp.body.Close()
decoder := json.NewDecoder(resp.body)
close(started)
for {
select {
case <-ctx.Done():
errs <- ctx.Err()
return
default:
var event events.Message
if err := decoder.Decode(&event); err != nil {
errs <- err
return
}
select {
case messages <- event:
case <-ctx.Done():
errs <- ctx.Err()
return
}
}
}
}()
<-started
return messages, errs
}
func buildEventsQueryParams(cliVersion string, options types.EventsOptions) (url.Values, 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(cliVersion, options.Filters)
if err != nil {
return nil, err
}
query.Set("filters", filterJSON)
}
return query, nil
}

165
client/events_test.go Normal file
View File

@ -0,0 +1,165 @@
package client
import (
"bytes"
"encoding/json"
"fmt"
"io"
"io/ioutil"
"net/http"
"strings"
"testing"
"golang.org/x/net/context"
"github.com/docker/docker/api/types"
"github.com/docker/docker/api/types/events"
"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{
client: newMockClient(errorMock(http.StatusInternalServerError, "Server error")),
}
_, errs := client.Events(context.Background(), e.options)
err := <-errs
if err == nil || !strings.Contains(err.Error(), e.expectedError) {
t.Fatalf("expected an error %q, got %v", e.expectedError, err)
}
}
}
func TestEventsErrorFromServer(t *testing.T) {
client := &Client{
client: newMockClient(errorMock(http.StatusInternalServerError, "Server error")),
}
_, errs := client.Events(context.Background(), types.EventsOptions{})
err := <-errs
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("type", events.ContainerEventType)
expectedFiltersJSON := fmt.Sprintf(`{"type":{"%s":true}}`, events.ContainerEventType)
eventsCases := []struct {
options types.EventsOptions
events []events.Message
expectedEvents map[string]bool
expectedQueryParams map[string]string
}{
{
options: types.EventsOptions{
Filters: filters,
},
expectedQueryParams: map[string]string{
"filters": expectedFiltersJSON,
},
events: []events.Message{},
expectedEvents: make(map[string]bool),
},
{
options: types.EventsOptions{
Filters: filters,
},
expectedQueryParams: map[string]string{
"filters": expectedFiltersJSON,
},
events: []events.Message{
{
Type: "container",
ID: "1",
Action: "create",
},
{
Type: "container",
ID: "2",
Action: "die",
},
{
Type: "container",
ID: "3",
Action: "create",
},
},
expectedEvents: map[string]bool{
"1": true,
"2": true,
"3": true,
},
},
}
for _, eventsCase := range eventsCases {
client := &Client{
client: newMockClient(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)
}
}
buffer := new(bytes.Buffer)
for _, e := range eventsCase.events {
b, _ := json.Marshal(e)
buffer.Write(b)
}
return &http.Response{
StatusCode: http.StatusOK,
Body: ioutil.NopCloser(buffer),
}, nil
}),
}
messages, errs := client.Events(context.Background(), eventsCase.options)
loop:
for {
select {
case err := <-errs:
if err != nil && err != io.EOF {
t.Fatal(err)
}
break loop
case e := <-messages:
_, ok := eventsCase.expectedEvents[e.ID]
if !ok {
t.Fatalf("event received not expected with action %s & id %s", e.Action, e.ID)
}
}
}
}
}

177
client/hijack.go Normal file
View File

@ -0,0 +1,177 @@
package client
import (
"crypto/tls"
"errors"
"fmt"
"net"
"net/http"
"net/http/httputil"
"net/url"
"strings"
"time"
"github.com/docker/docker/api/types"
"github.com/docker/docker/pkg/tlsconfig"
"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
}
apiPath := cli.getAPIPath(path, query)
req, err := http.NewRequest("POST", apiPath, bodyEncoded)
if err != nil {
return types.HijackedResponse{}, err
}
req = cli.addHeaders(req, headers)
req.Host = cli.addr
req.Header.Set("Connection", "Upgrade")
req.Header.Set("Upgrade", "tcp")
conn, err := dial(cli.proto, cli.addr, resolveTLSConfig(cli.client.Transport))
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 = tlsconfig.Clone(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)
}

125
client/image_build.go Normal file
View File

@ -0,0 +1,125 @@
package client
import (
"encoding/base64"
"encoding/json"
"io"
"net/http"
"net/url"
"strconv"
"golang.org/x/net/context"
"github.com/docker/docker/api/types"
"github.com/docker/docker/api/types/container"
)
// 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 := cli.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/x-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 (cli *Client) imageBuildOptionsToQuery(options types.ImageBuildOptions) (url.Values, error) {
query := url.Values{
"t": options.Tags,
"securityopt": options.SecurityOpt,
"extrahosts": options.ExtraHosts,
}
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 {
if err := cli.NewVersionError("1.25", "squash"); err != nil {
return query, err
}
query.Set("squash", "1")
}
if !container.Isolation.IsDefault(options.Isolation) {
query.Set("isolation", string(options.Isolation))
}
query.Set("cpusetcpus", options.CPUSetCPUs)
query.Set("networkmode", options.NetworkMode)
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)
query.Set("target", options.Target)
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))
cacheFromJSON, err := json.Marshal(options.CacheFrom)
if err != nil {
return query, err
}
query.Set("cachefrom", string(cacheFromJSON))
return query, nil
}

233
client/image_build_test.go Normal file
View File

@ -0,0 +1,233 @@
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{
client: newMockClient(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) {
v1 := "value1"
v2 := "value2"
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": &v1,
"ARG2": &v2,
"ARG3": nil,
},
},
expectedQueryParams: map[string]string{
"buildargs": `{"ARG1":"value1","ARG2":"value2","ARG3":null}`,
"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{
client: newMockClient(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/x-tar" {
return nil, fmt.Errorf("Content-type header not properly set in the request. Expected 'application/x-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
client/image_create.go Normal file
View File

@ -0,0 +1,34 @@
package client
import (
"io"
"net/url"
"golang.org/x/net/context"
"github.com/docker/distribution/reference"
"github.com/docker/docker/api/types"
)
// 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) {
ref, err := reference.ParseNormalizedNamed(parentReference)
if err != nil {
return nil, err
}
query := url.Values{}
query.Set("fromImage", reference.FamiliarName(ref))
query.Set("tag", getAPITagFromNamedRef(ref))
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)
}

View File

@ -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{
client: newMockClient(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{
client: newMockClient(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
client/image_history.go Normal file
View File

@ -0,0 +1,22 @@
package client
import (
"encoding/json"
"net/url"
"github.com/docker/docker/api/types/image"
"golang.org/x/net/context"
)
// ImageHistory returns the changes in an image in history format.
func (cli *Client) ImageHistory(ctx context.Context, imageID string) ([]image.HistoryResponseItem, error) {
var history []image.HistoryResponseItem
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
}

View File

@ -0,0 +1,60 @@
package client
import (
"bytes"
"encoding/json"
"fmt"
"io/ioutil"
"net/http"
"strings"
"testing"
"github.com/docker/docker/api/types/image"
"golang.org/x/net/context"
)
func TestImageHistoryError(t *testing.T) {
client := &Client{
client: newMockClient(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{
client: newMockClient(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([]image.HistoryResponseItem{
{
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
client/image_import.go Normal file
View File

@ -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.ParseNormalizedNamed(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
}

View File

@ -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{
client: newMockClient(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{
client: newMockClient(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:imported', 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))
}
}

33
client/image_inspect.go Normal file
View File

@ -0,0 +1,33 @@
package client
import (
"bytes"
"encoding/json"
"io/ioutil"
"net/http"
"github.com/docker/docker/api/types"
"golang.org/x/net/context"
)
// ImageInspectWithRaw returns the image information and its raw representation.
func (cli *Client) ImageInspectWithRaw(ctx context.Context, imageID string) (types.ImageInspect, []byte, error) {
serverResp, err := cli.get(ctx, "/images/"+imageID+"/json", nil, nil)
if err != nil {
if serverResp.statusCode == http.StatusNotFound {
return types.ImageInspect{}, nil, imageNotFoundError{imageID}
}
return types.ImageInspect{}, nil, err
}
defer ensureReaderClosed(serverResp)
body, err := ioutil.ReadAll(serverResp.body)
if err != nil {
return types.ImageInspect{}, nil, err
}
var response types.ImageInspect
rdr := bytes.NewReader(body)
err = json.NewDecoder(rdr).Decode(&response)
return response, body, err
}

View File

@ -0,0 +1,71 @@
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 TestImageInspectError(t *testing.T) {
client := &Client{
client: newMockClient(errorMock(http.StatusInternalServerError, "Server error")),
}
_, _, err := client.ImageInspectWithRaw(context.Background(), "nothing")
if err == nil || err.Error() != "Error response from daemon: Server error" {
t.Fatalf("expected a Server Error, got %v", err)
}
}
func TestImageInspectImageNotFound(t *testing.T) {
client := &Client{
client: newMockClient(errorMock(http.StatusNotFound, "Server error")),
}
_, _, err := client.ImageInspectWithRaw(context.Background(), "unknown")
if err == nil || !IsErrImageNotFound(err) {
t.Fatalf("expected an imageNotFound error, got %v", err)
}
}
func TestImageInspect(t *testing.T) {
expectedURL := "/images/image_id/json"
expectedTags := []string{"tag1", "tag2"}
client := &Client{
client: newMockClient(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.ImageInspect{
ID: "image_id",
RepoTags: expectedTags,
})
if err != nil {
return nil, err
}
return &http.Response{
StatusCode: http.StatusOK,
Body: ioutil.NopCloser(bytes.NewReader(content)),
}, nil
}),
}
imageInspect, _, err := client.ImageInspectWithRaw(context.Background(), "image_id")
if err != nil {
t.Fatal(err)
}
if imageInspect.ID != "image_id" {
t.Fatalf("expected `image_id`, got %s", imageInspect.ID)
}
if !reflect.DeepEqual(imageInspect.RepoTags, expectedTags) {
t.Fatalf("expected `%v`, got %v", expectedTags, imageInspect.RepoTags)
}
}

45
client/image_list.go Normal file
View File

@ -0,0 +1,45 @@
package client
import (
"encoding/json"
"net/url"
"github.com/docker/docker/api/types"
"github.com/docker/docker/api/types/filters"
"github.com/docker/docker/api/types/versions"
"golang.org/x/net/context"
)
// ImageList returns a list of images in the docker host.
func (cli *Client) ImageList(ctx context.Context, options types.ImageListOptions) ([]types.ImageSummary, error) {
var images []types.ImageSummary
query := url.Values{}
optionFilters := options.Filters
referenceFilters := optionFilters.Get("reference")
if versions.LessThan(cli.version, "1.25") && len(referenceFilters) > 0 {
query.Set("filter", referenceFilters[0])
for _, filterValue := range referenceFilters {
optionFilters.Del("reference", filterValue)
}
}
if optionFilters.Len() > 0 {
filterJSON, err := filters.ToParamWithVersion(cli.version, optionFilters)
if err != nil {
return images, err
}
query.Set("filters", filterJSON)
}
if options.All {
query.Set("all", "1")
}
serverResp, err := cli.get(ctx, "/images/json", query, nil)
if err != nil {
return images, err
}
err = json.NewDecoder(serverResp.body).Decode(&images)
ensureReaderClosed(serverResp)
return images, err
}

159
client/image_list_test.go Normal file
View File

@ -0,0 +1,159 @@
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 TestImageListError(t *testing.T) {
client := &Client{
client: newMockClient(errorMock(http.StatusInternalServerError, "Server error")),
}
_, err := client.ImageList(context.Background(), types.ImageListOptions{})
if err == nil || err.Error() != "Error response from daemon: Server error" {
t.Fatalf("expected a Server Error, got %v", err)
}
}
func TestImageList(t *testing.T) {
expectedURL := "/images/json"
noDanglingfilters := filters.NewArgs()
noDanglingfilters.Add("dangling", "false")
filters := filters.NewArgs()
filters.Add("label", "label1")
filters.Add("label", "label2")
filters.Add("dangling", "true")
listCases := []struct {
options types.ImageListOptions
expectedQueryParams map[string]string
}{
{
options: types.ImageListOptions{},
expectedQueryParams: map[string]string{
"all": "",
"filter": "",
"filters": "",
},
},
{
options: types.ImageListOptions{
Filters: filters,
},
expectedQueryParams: map[string]string{
"all": "",
"filter": "",
"filters": `{"dangling":{"true":true},"label":{"label1":true,"label2":true}}`,
},
},
{
options: types.ImageListOptions{
Filters: noDanglingfilters,
},
expectedQueryParams: map[string]string{
"all": "",
"filter": "",
"filters": `{"dangling":{"false":true}}`,
},
},
}
for _, listCase := range listCases {
client := &Client{
client: newMockClient(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 listCase.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)
}
}
content, err := json.Marshal([]types.ImageSummary{
{
ID: "image_id2",
},
{
ID: "image_id2",
},
})
if err != nil {
return nil, err
}
return &http.Response{
StatusCode: http.StatusOK,
Body: ioutil.NopCloser(bytes.NewReader(content)),
}, nil
}),
}
images, err := client.ImageList(context.Background(), listCase.options)
if err != nil {
t.Fatal(err)
}
if len(images) != 2 {
t.Fatalf("expected 2 images, got %v", images)
}
}
}
func TestImageListApiBefore125(t *testing.T) {
expectedFilter := "image:tag"
client := &Client{
client: newMockClient(func(req *http.Request) (*http.Response, error) {
query := req.URL.Query()
actualFilter := query.Get("filter")
if actualFilter != expectedFilter {
return nil, fmt.Errorf("filter not set in URL query properly. Expected '%s', got %s", expectedFilter, actualFilter)
}
actualFilters := query.Get("filters")
if actualFilters != "" {
return nil, fmt.Errorf("filters should have not been present, were with value: %s", actualFilters)
}
content, err := json.Marshal([]types.ImageSummary{
{
ID: "image_id2",
},
{
ID: "image_id2",
},
})
if err != nil {
return nil, err
}
return &http.Response{
StatusCode: http.StatusOK,
Body: ioutil.NopCloser(bytes.NewReader(content)),
}, nil
}),
version: "1.24",
}
filters := filters.NewArgs()
filters.Add("reference", "image:tag")
options := types.ImageListOptions{
Filters: filters,
}
images, err := client.ImageList(context.Background(), options)
if err != nil {
t.Fatal(err)
}
if len(images) != 2 {
t.Fatalf("expected 2 images, got %v", images)
}
}

30
client/image_load.go Normal file
View File

@ -0,0 +1,30 @@
package client
import (
"io"
"net/url"
"golang.org/x/net/context"
"github.com/docker/docker/api/types"
)
// ImageLoad loads an image in the docker host from the client host.
// It's up to the caller to close the io.ReadCloser in the
// ImageLoadResponse returned by this function.
func (cli *Client) ImageLoad(ctx context.Context, input io.Reader, quiet bool) (types.ImageLoadResponse, error) {
v := url.Values{}
v.Set("quiet", "0")
if quiet {
v.Set("quiet", "1")
}
headers := map[string][]string{"Content-Type": {"application/x-tar"}}
resp, err := cli.postRaw(ctx, "/images/load", v, input, headers)
if err != nil {
return types.ImageLoadResponse{}, err
}
return types.ImageLoadResponse{
Body: resp.body,
JSON: resp.header.Get("Content-Type") == "application/json",
}, nil
}

95
client/image_load_test.go Normal file
View File

@ -0,0 +1,95 @@
package client
import (
"bytes"
"fmt"
"io/ioutil"
"net/http"
"strings"
"testing"
"golang.org/x/net/context"
)
func TestImageLoadError(t *testing.T) {
client := &Client{
client: newMockClient(errorMock(http.StatusInternalServerError, "Server error")),
}
_, err := client.ImageLoad(context.Background(), nil, true)
if err == nil || err.Error() != "Error response from daemon: Server error" {
t.Fatalf("expected a Server Error, got %v", err)
}
}
func TestImageLoad(t *testing.T) {
expectedURL := "/images/load"
expectedInput := "inputBody"
expectedOutput := "outputBody"
loadCases := []struct {
quiet bool
responseContentType string
expectedResponseJSON bool
expectedQueryParams map[string]string
}{
{
quiet: false,
responseContentType: "text/plain",
expectedResponseJSON: false,
expectedQueryParams: map[string]string{
"quiet": "0",
},
},
{
quiet: true,
responseContentType: "application/json",
expectedResponseJSON: true,
expectedQueryParams: map[string]string{
"quiet": "1",
},
},
}
for _, loadCase := range loadCases {
client := &Client{
client: newMockClient(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)
}
contentType := req.Header.Get("Content-Type")
if contentType != "application/x-tar" {
return nil, fmt.Errorf("content-type not set in URL headers properly. Expected 'application/x-tar', got %s", contentType)
}
query := req.URL.Query()
for key, expected := range loadCase.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)
}
}
headers := http.Header{}
headers.Add("Content-Type", loadCase.responseContentType)
return &http.Response{
StatusCode: http.StatusOK,
Body: ioutil.NopCloser(bytes.NewReader([]byte(expectedOutput))),
Header: headers,
}, nil
}),
}
input := bytes.NewReader([]byte(expectedInput))
imageLoadResponse, err := client.ImageLoad(context.Background(), input, loadCase.quiet)
if err != nil {
t.Fatal(err)
}
if imageLoadResponse.JSON != loadCase.expectedResponseJSON {
t.Fatalf("expected a JSON response, was not.")
}
body, err := ioutil.ReadAll(imageLoadResponse.Body)
if err != nil {
t.Fatal(err)
}
if string(body) != expectedOutput {
t.Fatalf("expected %s, got %s", expectedOutput, string(body))
}
}
}

36
client/image_prune.go Normal file
View File

@ -0,0 +1,36 @@
package client
import (
"encoding/json"
"fmt"
"github.com/docker/docker/api/types"
"github.com/docker/docker/api/types/filters"
"golang.org/x/net/context"
)
// ImagesPrune requests the daemon to delete unused data
func (cli *Client) ImagesPrune(ctx context.Context, pruneFilters filters.Args) (types.ImagesPruneReport, error) {
var report types.ImagesPruneReport
if err := cli.NewVersionError("1.25", "image prune"); err != nil {
return report, err
}
query, err := getFiltersQuery(pruneFilters)
if err != nil {
return report, err
}
serverResp, err := cli.post(ctx, "/images/prune", query, nil, nil)
if err != nil {
return report, err
}
defer ensureReaderClosed(serverResp)
if err := json.NewDecoder(serverResp.body).Decode(&report); err != nil {
return report, fmt.Errorf("Error retrieving disk usage: %v", err)
}
return report, nil
}

119
client/image_prune_test.go Normal file
View File

@ -0,0 +1,119 @@
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"
"github.com/stretchr/testify/assert"
"golang.org/x/net/context"
)
func TestImagesPruneError(t *testing.T) {
client := &Client{
client: newMockClient(errorMock(http.StatusInternalServerError, "Server error")),
version: "1.25",
}
filters := filters.NewArgs()
_, err := client.ImagesPrune(context.Background(), filters)
assert.EqualError(t, err, "Error response from daemon: Server error")
}
func TestImagesPrune(t *testing.T) {
expectedURL := "/v1.25/images/prune"
danglingFilters := filters.NewArgs()
danglingFilters.Add("dangling", "true")
noDanglingFilters := filters.NewArgs()
noDanglingFilters.Add("dangling", "false")
labelFilters := filters.NewArgs()
labelFilters.Add("dangling", "true")
labelFilters.Add("label", "label1=foo")
labelFilters.Add("label", "label2!=bar")
listCases := []struct {
filters filters.Args
expectedQueryParams map[string]string
}{
{
filters: filters.Args{},
expectedQueryParams: map[string]string{
"until": "",
"filter": "",
"filters": "",
},
},
{
filters: danglingFilters,
expectedQueryParams: map[string]string{
"until": "",
"filter": "",
"filters": `{"dangling":{"true":true}}`,
},
},
{
filters: noDanglingFilters,
expectedQueryParams: map[string]string{
"until": "",
"filter": "",
"filters": `{"dangling":{"false":true}}`,
},
},
{
filters: labelFilters,
expectedQueryParams: map[string]string{
"until": "",
"filter": "",
"filters": `{"dangling":{"true":true},"label":{"label1=foo":true,"label2!=bar":true}}`,
},
},
}
for _, listCase := range listCases {
client := &Client{
client: newMockClient(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 listCase.expectedQueryParams {
actual := query.Get(key)
assert.Equal(t, expected, actual)
}
content, err := json.Marshal(types.ImagesPruneReport{
ImagesDeleted: []types.ImageDeleteResponseItem{
{
Deleted: "image_id1",
},
{
Deleted: "image_id2",
},
},
SpaceReclaimed: 9999,
})
if err != nil {
return nil, err
}
return &http.Response{
StatusCode: http.StatusOK,
Body: ioutil.NopCloser(bytes.NewReader(content)),
}, nil
}),
version: "1.25",
}
report, err := client.ImagesPrune(context.Background(), listCase.filters)
assert.NoError(t, err)
assert.Len(t, report.ImagesDeleted, 2)
assert.Equal(t, uint64(9999), report.SpaceReclaimed)
}
}

61
client/image_pull.go Normal file
View File

@ -0,0 +1,61 @@
package client
import (
"io"
"net/http"
"net/url"
"golang.org/x/net/context"
"github.com/docker/distribution/reference"
"github.com/docker/docker/api/types"
)
// ImagePull requests the docker host to pull an image from a remote registry.
// It executes the privileged function if the operation is unauthorized
// and it tries one more time.
// It's up to the caller to handle the io.ReadCloser and close it properly.
//
// FIXME(vdemeester): there is currently used in a few way in docker/docker
// - if not in trusted content, ref is used to pass the whole reference, and tag is empty
// - if in trusted content, ref is used to pass the reference name, and tag for the digest
func (cli *Client) ImagePull(ctx context.Context, refStr string, options types.ImagePullOptions) (io.ReadCloser, error) {
ref, err := reference.ParseNormalizedNamed(refStr)
if err != nil {
return nil, err
}
query := url.Values{}
query.Set("fromImage", reference.FamiliarName(ref))
if !options.All {
query.Set("tag", getAPITagFromNamedRef(ref))
}
resp, err := cli.tryImageCreate(ctx, query, options.RegistryAuth)
if resp.statusCode == http.StatusUnauthorized && options.PrivilegeFunc != nil {
newAuthHeader, privilegeErr := options.PrivilegeFunc()
if privilegeErr != nil {
return nil, privilegeErr
}
resp, err = cli.tryImageCreate(ctx, query, newAuthHeader)
}
if err != nil {
return nil, err
}
return resp.body, nil
}
// getAPITagFromNamedRef returns a tag from the specified reference.
// This function is necessary as long as the docker "server" api expects
// digests to be sent as tags and makes a distinction between the name
// and tag/digest part of a reference.
func getAPITagFromNamedRef(ref reference.Named) string {
if digested, ok := ref.(reference.Digested); ok {
return digested.Digest().String()
}
ref = reference.TagNameOnly(ref)
if tagged, ok := ref.(reference.Tagged); ok {
return tagged.Tag()
}
return ""
}

199
client/image_pull_test.go Normal file
View File

@ -0,0 +1,199 @@
package client
import (
"bytes"
"fmt"
"io/ioutil"
"net/http"
"strings"
"testing"
"golang.org/x/net/context"
"github.com/docker/docker/api/types"
)
func TestImagePullReferenceParseError(t *testing.T) {
client := &Client{
client: newMockClient(func(req *http.Request) (*http.Response, error) {
return nil, nil
}),
}
// An empty reference is an invalid reference
_, err := client.ImagePull(context.Background(), "", types.ImagePullOptions{})
if err == nil || !strings.Contains(err.Error(), "invalid reference format") {
t.Fatalf("expected an error, got %v", err)
}
}
func TestImagePullAnyError(t *testing.T) {
client := &Client{
client: newMockClient(errorMock(http.StatusInternalServerError, "Server error")),
}
_, err := client.ImagePull(context.Background(), "myimage", types.ImagePullOptions{})
if err == nil || err.Error() != "Error response from daemon: Server error" {
t.Fatalf("expected a Server Error, got %v", err)
}
}
func TestImagePullStatusUnauthorizedError(t *testing.T) {
client := &Client{
client: newMockClient(errorMock(http.StatusUnauthorized, "Unauthorized error")),
}
_, err := client.ImagePull(context.Background(), "myimage", types.ImagePullOptions{})
if err == nil || err.Error() != "Error response from daemon: Unauthorized error" {
t.Fatalf("expected an Unauthorized Error, got %v", err)
}
}
func TestImagePullWithUnauthorizedErrorAndPrivilegeFuncError(t *testing.T) {
client := &Client{
client: newMockClient(errorMock(http.StatusUnauthorized, "Unauthorized error")),
}
privilegeFunc := func() (string, error) {
return "", fmt.Errorf("Error requesting privilege")
}
_, err := client.ImagePull(context.Background(), "myimage", types.ImagePullOptions{
PrivilegeFunc: privilegeFunc,
})
if err == nil || err.Error() != "Error requesting privilege" {
t.Fatalf("expected an error requesting privilege, got %v", err)
}
}
func TestImagePullWithUnauthorizedErrorAndAnotherUnauthorizedError(t *testing.T) {
client := &Client{
client: newMockClient(errorMock(http.StatusUnauthorized, "Unauthorized error")),
}
privilegeFunc := func() (string, error) {
return "a-auth-header", nil
}
_, err := client.ImagePull(context.Background(), "myimage", types.ImagePullOptions{
PrivilegeFunc: privilegeFunc,
})
if err == nil || err.Error() != "Error response from daemon: Unauthorized error" {
t.Fatalf("expected an Unauthorized Error, got %v", err)
}
}
func TestImagePullWithPrivilegedFuncNoError(t *testing.T) {
expectedURL := "/images/create"
client := &Client{
client: newMockClient(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)
}
auth := req.Header.Get("X-Registry-Auth")
if auth == "NotValid" {
return &http.Response{
StatusCode: http.StatusUnauthorized,
Body: ioutil.NopCloser(bytes.NewReader([]byte("Invalid credentials"))),
}, nil
}
if auth != "IAmValid" {
return nil, fmt.Errorf("Invalid auth header : expected %s, got %s", "IAmValid", auth)
}
query := req.URL.Query()
fromImage := query.Get("fromImage")
if fromImage != "myimage" {
return nil, fmt.Errorf("fromimage not set in URL query properly. Expected '%s', got %s", "myimage", fromImage)
}
tag := query.Get("tag")
if tag != "latest" {
return nil, fmt.Errorf("tag not set in URL query properly. Expected '%s', got %s", "latest", tag)
}
return &http.Response{
StatusCode: http.StatusOK,
Body: ioutil.NopCloser(bytes.NewReader([]byte("hello world"))),
}, nil
}),
}
privilegeFunc := func() (string, error) {
return "IAmValid", nil
}
resp, err := client.ImagePull(context.Background(), "myimage", types.ImagePullOptions{
RegistryAuth: "NotValid",
PrivilegeFunc: privilegeFunc,
})
if err != nil {
t.Fatal(err)
}
body, err := ioutil.ReadAll(resp)
if err != nil {
t.Fatal(err)
}
if string(body) != "hello world" {
t.Fatalf("expected 'hello world', got %s", string(body))
}
}
func TestImagePullWithoutErrors(t *testing.T) {
expectedURL := "/images/create"
expectedOutput := "hello world"
pullCases := []struct {
all bool
reference string
expectedImage string
expectedTag string
}{
{
all: false,
reference: "myimage",
expectedImage: "myimage",
expectedTag: "latest",
},
{
all: false,
reference: "myimage:tag",
expectedImage: "myimage",
expectedTag: "tag",
},
{
all: true,
reference: "myimage",
expectedImage: "myimage",
expectedTag: "",
},
{
all: true,
reference: "myimage:anything",
expectedImage: "myimage",
expectedTag: "",
},
}
for _, pullCase := range pullCases {
client := &Client{
client: newMockClient(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()
fromImage := query.Get("fromImage")
if fromImage != pullCase.expectedImage {
return nil, fmt.Errorf("fromimage not set in URL query properly. Expected '%s', got %s", pullCase.expectedImage, fromImage)
}
tag := query.Get("tag")
if tag != pullCase.expectedTag {
return nil, fmt.Errorf("tag not set in URL query properly. Expected '%s', got %s", pullCase.expectedTag, tag)
}
return &http.Response{
StatusCode: http.StatusOK,
Body: ioutil.NopCloser(bytes.NewReader([]byte(expectedOutput))),
}, nil
}),
}
resp, err := client.ImagePull(context.Background(), pullCase.reference, types.ImagePullOptions{
All: pullCase.all,
})
if err != nil {
t.Fatal(err)
}
body, err := ioutil.ReadAll(resp)
if err != nil {
t.Fatal(err)
}
if string(body) != expectedOutput {
t.Fatalf("expected '%s', got %s", expectedOutput, string(body))
}
}
}

56
client/image_push.go Normal file
View File

@ -0,0 +1,56 @@
package client
import (
"errors"
"io"
"net/http"
"net/url"
"golang.org/x/net/context"
"github.com/docker/distribution/reference"
"github.com/docker/docker/api/types"
)
// ImagePush requests the docker host to push an image to a remote registry.
// It executes the privileged function if the operation is unauthorized
// and it tries one more time.
// It's up to the caller to handle the io.ReadCloser and close it properly.
func (cli *Client) ImagePush(ctx context.Context, image string, options types.ImagePushOptions) (io.ReadCloser, error) {
ref, err := reference.ParseNormalizedNamed(image)
if err != nil {
return nil, err
}
if _, isCanonical := ref.(reference.Canonical); isCanonical {
return nil, errors.New("cannot push a digest reference")
}
tag := ""
name := reference.FamiliarName(ref)
if nameTaggedRef, isNamedTagged := ref.(reference.NamedTagged); isNamedTagged {
tag = nameTaggedRef.Tag()
}
query := url.Values{}
query.Set("tag", tag)
resp, err := cli.tryImagePush(ctx, name, query, options.RegistryAuth)
if resp.statusCode == http.StatusUnauthorized && options.PrivilegeFunc != nil {
newAuthHeader, privilegeErr := options.PrivilegeFunc()
if privilegeErr != nil {
return nil, privilegeErr
}
resp, err = cli.tryImagePush(ctx, name, query, newAuthHeader)
}
if err != nil {
return nil, err
}
return resp.body, nil
}
func (cli *Client) tryImagePush(ctx context.Context, imageID string, query url.Values, registryAuth string) (serverResponse, error) {
headers := map[string][]string{"X-Registry-Auth": {registryAuth}}
return cli.post(ctx, "/images/"+imageID+"/push", query, nil, headers)
}

180
client/image_push_test.go Normal file
View File

@ -0,0 +1,180 @@
package client
import (
"bytes"
"fmt"
"io/ioutil"
"net/http"
"strings"
"testing"
"golang.org/x/net/context"
"github.com/docker/docker/api/types"
)
func TestImagePushReferenceError(t *testing.T) {
client := &Client{
client: newMockClient(func(req *http.Request) (*http.Response, error) {
return nil, nil
}),
}
// An empty reference is an invalid reference
_, err := client.ImagePush(context.Background(), "", types.ImagePushOptions{})
if err == nil || !strings.Contains(err.Error(), "invalid reference format") {
t.Fatalf("expected an error, got %v", err)
}
// An canonical reference cannot be pushed
_, err = client.ImagePush(context.Background(), "repo@sha256:ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff", types.ImagePushOptions{})
if err == nil || err.Error() != "cannot push a digest reference" {
t.Fatalf("expected an error, got %v", err)
}
}
func TestImagePushAnyError(t *testing.T) {
client := &Client{
client: newMockClient(errorMock(http.StatusInternalServerError, "Server error")),
}
_, err := client.ImagePush(context.Background(), "myimage", types.ImagePushOptions{})
if err == nil || err.Error() != "Error response from daemon: Server error" {
t.Fatalf("expected a Server Error, got %v", err)
}
}
func TestImagePushStatusUnauthorizedError(t *testing.T) {
client := &Client{
client: newMockClient(errorMock(http.StatusUnauthorized, "Unauthorized error")),
}
_, err := client.ImagePush(context.Background(), "myimage", types.ImagePushOptions{})
if err == nil || err.Error() != "Error response from daemon: Unauthorized error" {
t.Fatalf("expected an Unauthorized Error, got %v", err)
}
}
func TestImagePushWithUnauthorizedErrorAndPrivilegeFuncError(t *testing.T) {
client := &Client{
client: newMockClient(errorMock(http.StatusUnauthorized, "Unauthorized error")),
}
privilegeFunc := func() (string, error) {
return "", fmt.Errorf("Error requesting privilege")
}
_, err := client.ImagePush(context.Background(), "myimage", types.ImagePushOptions{
PrivilegeFunc: privilegeFunc,
})
if err == nil || err.Error() != "Error requesting privilege" {
t.Fatalf("expected an error requesting privilege, got %v", err)
}
}
func TestImagePushWithUnauthorizedErrorAndAnotherUnauthorizedError(t *testing.T) {
client := &Client{
client: newMockClient(errorMock(http.StatusUnauthorized, "Unauthorized error")),
}
privilegeFunc := func() (string, error) {
return "a-auth-header", nil
}
_, err := client.ImagePush(context.Background(), "myimage", types.ImagePushOptions{
PrivilegeFunc: privilegeFunc,
})
if err == nil || err.Error() != "Error response from daemon: Unauthorized error" {
t.Fatalf("expected an Unauthorized Error, got %v", err)
}
}
func TestImagePushWithPrivilegedFuncNoError(t *testing.T) {
expectedURL := "/images/myimage/push"
client := &Client{
client: newMockClient(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)
}
auth := req.Header.Get("X-Registry-Auth")
if auth == "NotValid" {
return &http.Response{
StatusCode: http.StatusUnauthorized,
Body: ioutil.NopCloser(bytes.NewReader([]byte("Invalid credentials"))),
}, nil
}
if auth != "IAmValid" {
return nil, fmt.Errorf("Invalid auth header : expected %s, got %s", "IAmValid", auth)
}
query := req.URL.Query()
tag := query.Get("tag")
if tag != "tag" {
return nil, fmt.Errorf("tag not set in URL query properly. Expected '%s', got %s", "tag", tag)
}
return &http.Response{
StatusCode: http.StatusOK,
Body: ioutil.NopCloser(bytes.NewReader([]byte("hello world"))),
}, nil
}),
}
privilegeFunc := func() (string, error) {
return "IAmValid", nil
}
resp, err := client.ImagePush(context.Background(), "myimage:tag", types.ImagePushOptions{
RegistryAuth: "NotValid",
PrivilegeFunc: privilegeFunc,
})
if err != nil {
t.Fatal(err)
}
body, err := ioutil.ReadAll(resp)
if err != nil {
t.Fatal(err)
}
if string(body) != "hello world" {
t.Fatalf("expected 'hello world', got %s", string(body))
}
}
func TestImagePushWithoutErrors(t *testing.T) {
expectedOutput := "hello world"
expectedURLFormat := "/images/%s/push"
pullCases := []struct {
reference string
expectedImage string
expectedTag string
}{
{
reference: "myimage",
expectedImage: "myimage",
expectedTag: "",
},
{
reference: "myimage:tag",
expectedImage: "myimage",
expectedTag: "tag",
},
}
for _, pullCase := range pullCases {
client := &Client{
client: newMockClient(func(req *http.Request) (*http.Response, error) {
expectedURL := fmt.Sprintf(expectedURLFormat, pullCase.expectedImage)
if !strings.HasPrefix(req.URL.Path, expectedURL) {
return nil, fmt.Errorf("Expected URL '%s', got '%s'", expectedURL, req.URL)
}
query := req.URL.Query()
tag := query.Get("tag")
if tag != pullCase.expectedTag {
return nil, fmt.Errorf("tag not set in URL query properly. Expected '%s', got %s", pullCase.expectedTag, tag)
}
return &http.Response{
StatusCode: http.StatusOK,
Body: ioutil.NopCloser(bytes.NewReader([]byte(expectedOutput))),
}, nil
}),
}
resp, err := client.ImagePush(context.Background(), pullCase.reference, types.ImagePushOptions{})
if err != nil {
t.Fatal(err)
}
body, err := ioutil.ReadAll(resp)
if err != nil {
t.Fatal(err)
}
if string(body) != expectedOutput {
t.Fatalf("expected '%s', got %s", expectedOutput, string(body))
}
}
}

31
client/image_remove.go Normal file
View File

@ -0,0 +1,31 @@
package client
import (
"encoding/json"
"net/url"
"github.com/docker/docker/api/types"
"golang.org/x/net/context"
)
// ImageRemove removes an image from the docker host.
func (cli *Client) ImageRemove(ctx context.Context, imageID string, options types.ImageRemoveOptions) ([]types.ImageDeleteResponseItem, error) {
query := url.Values{}
if options.Force {
query.Set("force", "1")
}
if !options.PruneChildren {
query.Set("noprune", "1")
}
resp, err := cli.delete(ctx, "/images/"+imageID, query, nil)
if err != nil {
return nil, err
}
var dels []types.ImageDeleteResponseItem
err = json.NewDecoder(resp.body).Decode(&dels)
ensureReaderClosed(resp)
return dels, err
}

View File

@ -0,0 +1,95 @@
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 TestImageRemoveError(t *testing.T) {
client := &Client{
client: newMockClient(errorMock(http.StatusInternalServerError, "Server error")),
}
_, err := client.ImageRemove(context.Background(), "image_id", types.ImageRemoveOptions{})
if err == nil || err.Error() != "Error response from daemon: Server error" {
t.Fatalf("expected a Server Error, got %v", err)
}
}
func TestImageRemove(t *testing.T) {
expectedURL := "/images/image_id"
removeCases := []struct {
force bool
pruneChildren bool
expectedQueryParams map[string]string
}{
{
force: false,
pruneChildren: false,
expectedQueryParams: map[string]string{
"force": "",
"noprune": "1",
},
}, {
force: true,
pruneChildren: true,
expectedQueryParams: map[string]string{
"force": "1",
"noprune": "",
},
},
}
for _, removeCase := range removeCases {
client := &Client{
client: newMockClient(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)
}
query := req.URL.Query()
for key, expected := range removeCase.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)
}
}
b, err := json.Marshal([]types.ImageDeleteResponseItem{
{
Untagged: "image_id1",
},
{
Deleted: "image_id",
},
})
if err != nil {
return nil, err
}
return &http.Response{
StatusCode: http.StatusOK,
Body: ioutil.NopCloser(bytes.NewReader(b)),
}, nil
}),
}
imageDeletes, err := client.ImageRemove(context.Background(), "image_id", types.ImageRemoveOptions{
Force: removeCase.force,
PruneChildren: removeCase.pruneChildren,
})
if err != nil {
t.Fatal(err)
}
if len(imageDeletes) != 2 {
t.Fatalf("expected 2 deleted images, got %v", imageDeletes)
}
}
}

22
client/image_save.go Normal file
View File

@ -0,0 +1,22 @@
package client
import (
"io"
"net/url"
"golang.org/x/net/context"
)
// ImageSave retrieves one or more images from the docker host as an io.ReadCloser.
// It's up to the caller to store the images and close the stream.
func (cli *Client) ImageSave(ctx context.Context, imageIDs []string) (io.ReadCloser, error) {
query := url.Values{
"names": imageIDs,
}
resp, err := cli.get(ctx, "/images/get", query, nil)
if err != nil {
return nil, err
}
return resp.body, nil
}

58
client/image_save_test.go Normal file
View File

@ -0,0 +1,58 @@
package client
import (
"bytes"
"fmt"
"io/ioutil"
"net/http"
"reflect"
"testing"
"golang.org/x/net/context"
"strings"
)
func TestImageSaveError(t *testing.T) {
client := &Client{
client: newMockClient(errorMock(http.StatusInternalServerError, "Server error")),
}
_, err := client.ImageSave(context.Background(), []string{"nothing"})
if err == nil || err.Error() != "Error response from daemon: Server error" {
t.Fatalf("expected a Server error, got %v", err)
}
}
func TestImageSave(t *testing.T) {
expectedURL := "/images/get"
client := &Client{
client: newMockClient(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()
names := query["names"]
expectedNames := []string{"image_id1", "image_id2"}
if !reflect.DeepEqual(names, expectedNames) {
return nil, fmt.Errorf("names not set in URL query properly. Expected %v, got %v", names, expectedNames)
}
return &http.Response{
StatusCode: http.StatusOK,
Body: ioutil.NopCloser(bytes.NewReader([]byte("response"))),
}, nil
}),
}
saveResponse, err := client.ImageSave(context.Background(), []string{"image_id1", "image_id2"})
if err != nil {
t.Fatal(err)
}
response, err := ioutil.ReadAll(saveResponse)
if err != nil {
t.Fatal(err)
}
saveResponse.Close()
if string(response) != "response" {
t.Fatalf("expected response to contain 'response', got %s", string(response))
}
}

51
client/image_search.go Normal file
View File

@ -0,0 +1,51 @@
package client
import (
"encoding/json"
"fmt"
"net/http"
"net/url"
"github.com/docker/docker/api/types"
"github.com/docker/docker/api/types/filters"
"github.com/docker/docker/api/types/registry"
"golang.org/x/net/context"
)
// ImageSearch makes the docker host to search by a term in a remote registry.
// The list of results is not sorted in any fashion.
func (cli *Client) ImageSearch(ctx context.Context, term string, options types.ImageSearchOptions) ([]registry.SearchResult, error) {
var results []registry.SearchResult
query := url.Values{}
query.Set("term", term)
query.Set("limit", fmt.Sprintf("%d", options.Limit))
if options.Filters.Len() > 0 {
filterJSON, err := filters.ToParam(options.Filters)
if err != nil {
return results, err
}
query.Set("filters", filterJSON)
}
resp, err := cli.tryImageSearch(ctx, query, options.RegistryAuth)
if resp.statusCode == http.StatusUnauthorized && options.PrivilegeFunc != nil {
newAuthHeader, privilegeErr := options.PrivilegeFunc()
if privilegeErr != nil {
return results, privilegeErr
}
resp, err = cli.tryImageSearch(ctx, query, newAuthHeader)
}
if err != nil {
return results, err
}
err = json.NewDecoder(resp.body).Decode(&results)
ensureReaderClosed(resp)
return results, err
}
func (cli *Client) tryImageSearch(ctx context.Context, query url.Values, registryAuth string) (serverResponse, error) {
headers := map[string][]string{"X-Registry-Auth": {registryAuth}}
return cli.get(ctx, "/images/search", query, headers)
}

165
client/image_search_test.go Normal file
View File

@ -0,0 +1,165 @@
package client
import (
"bytes"
"fmt"
"io/ioutil"
"net/http"
"strings"
"testing"
"golang.org/x/net/context"
"encoding/json"
"github.com/docker/docker/api/types"
"github.com/docker/docker/api/types/filters"
"github.com/docker/docker/api/types/registry"
)
func TestImageSearchAnyError(t *testing.T) {
client := &Client{
client: newMockClient(errorMock(http.StatusInternalServerError, "Server error")),
}
_, err := client.ImageSearch(context.Background(), "some-image", types.ImageSearchOptions{})
if err == nil || err.Error() != "Error response from daemon: Server error" {
t.Fatalf("expected a Server Error, got %v", err)
}
}
func TestImageSearchStatusUnauthorizedError(t *testing.T) {
client := &Client{
client: newMockClient(errorMock(http.StatusUnauthorized, "Unauthorized error")),
}
_, err := client.ImageSearch(context.Background(), "some-image", types.ImageSearchOptions{})
if err == nil || err.Error() != "Error response from daemon: Unauthorized error" {
t.Fatalf("expected an Unauthorized Error, got %v", err)
}
}
func TestImageSearchWithUnauthorizedErrorAndPrivilegeFuncError(t *testing.T) {
client := &Client{
client: newMockClient(errorMock(http.StatusUnauthorized, "Unauthorized error")),
}
privilegeFunc := func() (string, error) {
return "", fmt.Errorf("Error requesting privilege")
}
_, err := client.ImageSearch(context.Background(), "some-image", types.ImageSearchOptions{
PrivilegeFunc: privilegeFunc,
})
if err == nil || err.Error() != "Error requesting privilege" {
t.Fatalf("expected an error requesting privilege, got %v", err)
}
}
func TestImageSearchWithUnauthorizedErrorAndAnotherUnauthorizedError(t *testing.T) {
client := &Client{
client: newMockClient(errorMock(http.StatusUnauthorized, "Unauthorized error")),
}
privilegeFunc := func() (string, error) {
return "a-auth-header", nil
}
_, err := client.ImageSearch(context.Background(), "some-image", types.ImageSearchOptions{
PrivilegeFunc: privilegeFunc,
})
if err == nil || err.Error() != "Error response from daemon: Unauthorized error" {
t.Fatalf("expected an Unauthorized Error, got %v", err)
}
}
func TestImageSearchWithPrivilegedFuncNoError(t *testing.T) {
expectedURL := "/images/search"
client := &Client{
client: newMockClient(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)
}
auth := req.Header.Get("X-Registry-Auth")
if auth == "NotValid" {
return &http.Response{
StatusCode: http.StatusUnauthorized,
Body: ioutil.NopCloser(bytes.NewReader([]byte("Invalid credentials"))),
}, nil
}
if auth != "IAmValid" {
return nil, fmt.Errorf("Invalid auth header : expected 'IAmValid', got %s", auth)
}
query := req.URL.Query()
term := query.Get("term")
if term != "some-image" {
return nil, fmt.Errorf("term not set in URL query properly. Expected 'some-image', got %s", term)
}
content, err := json.Marshal([]registry.SearchResult{
{
Name: "anything",
},
})
if err != nil {
return nil, err
}
return &http.Response{
StatusCode: http.StatusOK,
Body: ioutil.NopCloser(bytes.NewReader(content)),
}, nil
}),
}
privilegeFunc := func() (string, error) {
return "IAmValid", nil
}
results, err := client.ImageSearch(context.Background(), "some-image", types.ImageSearchOptions{
RegistryAuth: "NotValid",
PrivilegeFunc: privilegeFunc,
})
if err != nil {
t.Fatal(err)
}
if len(results) != 1 {
t.Fatalf("expected 1 result, got %v", results)
}
}
func TestImageSearchWithoutErrors(t *testing.T) {
expectedURL := "/images/search"
filterArgs := filters.NewArgs()
filterArgs.Add("is-automated", "true")
filterArgs.Add("stars", "3")
expectedFilters := `{"is-automated":{"true":true},"stars":{"3":true}}`
client := &Client{
client: newMockClient(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()
term := query.Get("term")
if term != "some-image" {
return nil, fmt.Errorf("term not set in URL query properly. Expected 'some-image', got %s", term)
}
filters := query.Get("filters")
if filters != expectedFilters {
return nil, fmt.Errorf("filters not set in URL query properly. Expected '%s', got %s", expectedFilters, filters)
}
content, err := json.Marshal([]registry.SearchResult{
{
Name: "anything",
},
})
if err != nil {
return nil, err
}
return &http.Response{
StatusCode: http.StatusOK,
Body: ioutil.NopCloser(bytes.NewReader(content)),
}, nil
}),
}
results, err := client.ImageSearch(context.Background(), "some-image", types.ImageSearchOptions{
Filters: filterArgs,
})
if err != nil {
t.Fatal(err)
}
if len(results) != 1 {
t.Fatalf("expected a result, got %v", results)
}
}

37
client/image_tag.go Normal file
View File

@ -0,0 +1,37 @@
package client
import (
"net/url"
"github.com/docker/distribution/reference"
"github.com/pkg/errors"
"golang.org/x/net/context"
)
// ImageTag tags an image in the docker host
func (cli *Client) ImageTag(ctx context.Context, source, target string) error {
if _, err := reference.ParseAnyReference(source); err != nil {
return errors.Wrapf(err, "Error parsing reference: %q is not a valid repository/tag", source)
}
ref, err := reference.ParseNormalizedNamed(target)
if err != nil {
return errors.Wrapf(err, "Error parsing reference: %q is not a valid repository/tag", target)
}
if _, isCanonical := ref.(reference.Canonical); isCanonical {
return errors.New("refusing to create a tag with a digest reference")
}
ref = reference.TagNameOnly(ref)
query := url.Values{}
query.Set("repo", reference.FamiliarName(ref))
if tagged, ok := ref.(reference.Tagged); ok {
query.Set("tag", tagged.Tag())
}
resp, err := cli.post(ctx, "/images/"+source+"/tag", query, nil, nil)
ensureReaderClosed(resp)
return err
}

143
client/image_tag_test.go Normal file
View File

@ -0,0 +1,143 @@
package client
import (
"bytes"
"fmt"
"io/ioutil"
"net/http"
"strings"
"testing"
"golang.org/x/net/context"
)
func TestImageTagError(t *testing.T) {
client := &Client{
client: newMockClient(errorMock(http.StatusInternalServerError, "Server error")),
}
err := client.ImageTag(context.Background(), "image_id", "repo:tag")
if err == nil || err.Error() != "Error response from daemon: Server error" {
t.Fatalf("expected a Server Error, got %v", err)
}
}
// Note: this is not testing all the InvalidReference as it's the responsibility
// of distribution/reference package.
func TestImageTagInvalidReference(t *testing.T) {
client := &Client{
client: newMockClient(errorMock(http.StatusInternalServerError, "Server error")),
}
err := client.ImageTag(context.Background(), "image_id", "aa/asdf$$^/aa")
if err == nil || err.Error() != `Error parsing reference: "aa/asdf$$^/aa" is not a valid repository/tag: invalid reference format` {
t.Fatalf("expected ErrReferenceInvalidFormat, got %v", err)
}
}
func TestImageTagInvalidSourceImageName(t *testing.T) {
client := &Client{
client: newMockClient(errorMock(http.StatusInternalServerError, "Server error")),
}
err := client.ImageTag(context.Background(), "invalid_source_image_name_", "repo:tag")
if err == nil || err.Error() != "Error parsing reference: \"invalid_source_image_name_\" is not a valid repository/tag: invalid reference format" {
t.Fatalf("expected Parsing Reference Error, got %v", err)
}
}
func TestImageTagHexSource(t *testing.T) {
client := &Client{
client: newMockClient(errorMock(http.StatusOK, "OK")),
}
err := client.ImageTag(context.Background(), "0d409d33b27e47423b049f7f863faa08655a8c901749c2b25b93ca67d01a470d", "repo:tag")
if err != nil {
t.Fatalf("got error: %v", err)
}
}
func TestImageTag(t *testing.T) {
expectedURL := "/images/image_id/tag"
tagCases := []struct {
reference string
expectedQueryParams map[string]string
}{
{
reference: "repository:tag1",
expectedQueryParams: map[string]string{
"repo": "repository",
"tag": "tag1",
},
}, {
reference: "another_repository:latest",
expectedQueryParams: map[string]string{
"repo": "another_repository",
"tag": "latest",
},
}, {
reference: "another_repository",
expectedQueryParams: map[string]string{
"repo": "another_repository",
"tag": "latest",
},
}, {
reference: "test/another_repository",
expectedQueryParams: map[string]string{
"repo": "test/another_repository",
"tag": "latest",
},
}, {
reference: "test/another_repository:tag1",
expectedQueryParams: map[string]string{
"repo": "test/another_repository",
"tag": "tag1",
},
}, {
reference: "test/test/another_repository:tag1",
expectedQueryParams: map[string]string{
"repo": "test/test/another_repository",
"tag": "tag1",
},
}, {
reference: "test:5000/test/another_repository:tag1",
expectedQueryParams: map[string]string{
"repo": "test:5000/test/another_repository",
"tag": "tag1",
},
}, {
reference: "test:5000/test/another_repository",
expectedQueryParams: map[string]string{
"repo": "test:5000/test/another_repository",
"tag": "latest",
},
},
}
for _, tagCase := range tagCases {
client := &Client{
client: newMockClient(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)
}
query := req.URL.Query()
for key, expected := range tagCase.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(""))),
}, nil
}),
}
err := client.ImageTag(context.Background(), "image_id", tagCase.reference)
if err != nil {
t.Fatal(err)
}
}
}

26
client/info.go Normal file
View File

@ -0,0 +1,26 @@
package client
import (
"encoding/json"
"fmt"
"net/url"
"github.com/docker/docker/api/types"
"golang.org/x/net/context"
)
// Info returns information about the docker server.
func (cli *Client) Info(ctx context.Context) (types.Info, error) {
var info types.Info
serverResp, err := cli.get(ctx, "/info", url.Values{}, nil)
if err != nil {
return info, err
}
defer ensureReaderClosed(serverResp)
if err := json.NewDecoder(serverResp.body).Decode(&info); err != nil {
return info, fmt.Errorf("Error reading remote info: %v", err)
}
return info, nil
}

76
client/info_test.go Normal file
View File

@ -0,0 +1,76 @@
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 TestInfoServerError(t *testing.T) {
client := &Client{
client: newMockClient(errorMock(http.StatusInternalServerError, "Server error")),
}
_, err := client.Info(context.Background())
if err == nil || err.Error() != "Error response from daemon: Server error" {
t.Fatalf("expected a Server Error, got %v", err)
}
}
func TestInfoInvalidResponseJSONError(t *testing.T) {
client := &Client{
client: newMockClient(func(req *http.Request) (*http.Response, error) {
return &http.Response{
StatusCode: http.StatusOK,
Body: ioutil.NopCloser(bytes.NewReader([]byte("invalid json"))),
}, nil
}),
}
_, err := client.Info(context.Background())
if err == nil || !strings.Contains(err.Error(), "invalid character") {
t.Fatalf("expected a 'invalid character' error, got %v", err)
}
}
func TestInfo(t *testing.T) {
expectedURL := "/info"
client := &Client{
client: newMockClient(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)
}
info := &types.Info{
ID: "daemonID",
Containers: 3,
}
b, err := json.Marshal(info)
if err != nil {
return nil, err
}
return &http.Response{
StatusCode: http.StatusOK,
Body: ioutil.NopCloser(bytes.NewReader(b)),
}, nil
}),
}
info, err := client.Info(context.Background())
if err != nil {
t.Fatal(err)
}
if info.ID != "daemonID" {
t.Fatalf("expected daemonID, got %s", info.ID)
}
if info.Containers != 3 {
t.Fatalf("expected 3 containers, got %d", info.Containers)
}
}

173
client/interface.go Normal file
View File

@ -0,0 +1,173 @@
package client
import (
"io"
"time"
"github.com/docker/docker/api/types"
"github.com/docker/docker/api/types/container"
"github.com/docker/docker/api/types/events"
"github.com/docker/docker/api/types/filters"
"github.com/docker/docker/api/types/image"
"github.com/docker/docker/api/types/network"
"github.com/docker/docker/api/types/registry"
"github.com/docker/docker/api/types/swarm"
volumetypes "github.com/docker/docker/api/types/volume"
"golang.org/x/net/context"
)
// CommonAPIClient is the common methods between stable and experimental versions of APIClient.
type CommonAPIClient interface {
ContainerAPIClient
ImageAPIClient
NodeAPIClient
NetworkAPIClient
PluginAPIClient
ServiceAPIClient
SwarmAPIClient
SecretAPIClient
SystemAPIClient
VolumeAPIClient
ClientVersion() string
ServerVersion(ctx context.Context) (types.Version, error)
UpdateClientVersion(v string)
}
// ContainerAPIClient defines API client methods for the containers
type ContainerAPIClient interface {
ContainerAttach(ctx context.Context, container string, options types.ContainerAttachOptions) (types.HijackedResponse, error)
ContainerCommit(ctx context.Context, container string, options types.ContainerCommitOptions) (types.IDResponse, error)
ContainerCreate(ctx context.Context, config *container.Config, hostConfig *container.HostConfig, networkingConfig *network.NetworkingConfig, containerName string) (container.ContainerCreateCreatedBody, error)
ContainerDiff(ctx context.Context, container string) ([]container.ContainerChangeResponseItem, error)
ContainerExecAttach(ctx context.Context, execID string, config types.ExecConfig) (types.HijackedResponse, error)
ContainerExecCreate(ctx context.Context, container string, config types.ExecConfig) (types.IDResponse, error)
ContainerExecInspect(ctx context.Context, execID string) (types.ContainerExecInspect, error)
ContainerExecResize(ctx context.Context, execID string, options types.ResizeOptions) error
ContainerExecStart(ctx context.Context, execID string, config types.ExecStartCheck) error
ContainerExport(ctx context.Context, container string) (io.ReadCloser, error)
ContainerInspect(ctx context.Context, container string) (types.ContainerJSON, error)
ContainerInspectWithRaw(ctx context.Context, container string, getSize bool) (types.ContainerJSON, []byte, error)
ContainerKill(ctx context.Context, container, signal string) error
ContainerList(ctx context.Context, options types.ContainerListOptions) ([]types.Container, error)
ContainerLogs(ctx context.Context, container string, options types.ContainerLogsOptions) (io.ReadCloser, error)
ContainerPause(ctx context.Context, container string) error
ContainerRemove(ctx context.Context, container string, options types.ContainerRemoveOptions) error
ContainerRename(ctx context.Context, container, newContainerName string) error
ContainerResize(ctx context.Context, container string, options types.ResizeOptions) error
ContainerRestart(ctx context.Context, container string, timeout *time.Duration) error
ContainerStatPath(ctx context.Context, container, path string) (types.ContainerPathStat, error)
ContainerStats(ctx context.Context, container string, stream bool) (types.ContainerStats, error)
ContainerStart(ctx context.Context, container string, options types.ContainerStartOptions) error
ContainerStop(ctx context.Context, container string, timeout *time.Duration) error
ContainerTop(ctx context.Context, container string, arguments []string) (container.ContainerTopOKBody, error)
ContainerUnpause(ctx context.Context, container string) error
ContainerUpdate(ctx context.Context, container string, updateConfig container.UpdateConfig) (container.ContainerUpdateOKBody, error)
ContainerWait(ctx context.Context, container string) (int64, error)
CopyFromContainer(ctx context.Context, container, srcPath string) (io.ReadCloser, types.ContainerPathStat, error)
CopyToContainer(ctx context.Context, container, path string, content io.Reader, options types.CopyToContainerOptions) error
ContainersPrune(ctx context.Context, pruneFilters filters.Args) (types.ContainersPruneReport, error)
}
// ImageAPIClient defines API client methods for the images
type ImageAPIClient interface {
ImageBuild(ctx context.Context, context io.Reader, options types.ImageBuildOptions) (types.ImageBuildResponse, error)
ImageCreate(ctx context.Context, parentReference string, options types.ImageCreateOptions) (io.ReadCloser, error)
ImageHistory(ctx context.Context, image string) ([]image.HistoryResponseItem, error)
ImageImport(ctx context.Context, source types.ImageImportSource, ref string, options types.ImageImportOptions) (io.ReadCloser, error)
ImageInspectWithRaw(ctx context.Context, image string) (types.ImageInspect, []byte, error)
ImageList(ctx context.Context, options types.ImageListOptions) ([]types.ImageSummary, error)
ImageLoad(ctx context.Context, input io.Reader, quiet bool) (types.ImageLoadResponse, error)
ImagePull(ctx context.Context, ref string, options types.ImagePullOptions) (io.ReadCloser, error)
ImagePush(ctx context.Context, ref string, options types.ImagePushOptions) (io.ReadCloser, error)
ImageRemove(ctx context.Context, image string, options types.ImageRemoveOptions) ([]types.ImageDeleteResponseItem, error)
ImageSearch(ctx context.Context, term string, options types.ImageSearchOptions) ([]registry.SearchResult, error)
ImageSave(ctx context.Context, images []string) (io.ReadCloser, error)
ImageTag(ctx context.Context, image, ref string) error
ImagesPrune(ctx context.Context, pruneFilter filters.Args) (types.ImagesPruneReport, error)
}
// NetworkAPIClient defines API client methods for the networks
type NetworkAPIClient interface {
NetworkConnect(ctx context.Context, networkID, container string, config *network.EndpointSettings) error
NetworkCreate(ctx context.Context, name string, options types.NetworkCreate) (types.NetworkCreateResponse, error)
NetworkDisconnect(ctx context.Context, networkID, container string, force bool) error
NetworkInspect(ctx context.Context, networkID string, verbose bool) (types.NetworkResource, error)
NetworkInspectWithRaw(ctx context.Context, networkID string, verbose bool) (types.NetworkResource, []byte, error)
NetworkList(ctx context.Context, options types.NetworkListOptions) ([]types.NetworkResource, error)
NetworkRemove(ctx context.Context, networkID string) error
NetworksPrune(ctx context.Context, pruneFilter filters.Args) (types.NetworksPruneReport, error)
}
// NodeAPIClient defines API client methods for the nodes
type NodeAPIClient interface {
NodeInspectWithRaw(ctx context.Context, nodeID string) (swarm.Node, []byte, error)
NodeList(ctx context.Context, options types.NodeListOptions) ([]swarm.Node, error)
NodeRemove(ctx context.Context, nodeID string, options types.NodeRemoveOptions) error
NodeUpdate(ctx context.Context, nodeID string, version swarm.Version, node swarm.NodeSpec) error
}
// PluginAPIClient defines API client methods for the plugins
type PluginAPIClient interface {
PluginList(ctx context.Context, filter filters.Args) (types.PluginsListResponse, error)
PluginRemove(ctx context.Context, name string, options types.PluginRemoveOptions) error
PluginEnable(ctx context.Context, name string, options types.PluginEnableOptions) error
PluginDisable(ctx context.Context, name string, options types.PluginDisableOptions) error
PluginInstall(ctx context.Context, name string, options types.PluginInstallOptions) (io.ReadCloser, error)
PluginUpgrade(ctx context.Context, name string, options types.PluginInstallOptions) (io.ReadCloser, error)
PluginPush(ctx context.Context, name string, registryAuth string) (io.ReadCloser, error)
PluginSet(ctx context.Context, name string, args []string) error
PluginInspectWithRaw(ctx context.Context, name string) (*types.Plugin, []byte, error)
PluginCreate(ctx context.Context, createContext io.Reader, options types.PluginCreateOptions) error
}
// ServiceAPIClient defines API client methods for the services
type ServiceAPIClient interface {
ServiceCreate(ctx context.Context, service swarm.ServiceSpec, options types.ServiceCreateOptions) (types.ServiceCreateResponse, error)
ServiceInspectWithRaw(ctx context.Context, serviceID string, options types.ServiceInspectOptions) (swarm.Service, []byte, error)
ServiceList(ctx context.Context, options types.ServiceListOptions) ([]swarm.Service, error)
ServiceRemove(ctx context.Context, serviceID string) error
ServiceUpdate(ctx context.Context, serviceID string, version swarm.Version, service swarm.ServiceSpec, options types.ServiceUpdateOptions) (types.ServiceUpdateResponse, error)
ServiceLogs(ctx context.Context, serviceID string, options types.ContainerLogsOptions) (io.ReadCloser, error)
TaskLogs(ctx context.Context, taskID string, options types.ContainerLogsOptions) (io.ReadCloser, error)
TaskInspectWithRaw(ctx context.Context, taskID string) (swarm.Task, []byte, error)
TaskList(ctx context.Context, options types.TaskListOptions) ([]swarm.Task, error)
}
// SwarmAPIClient defines API client methods for the swarm
type SwarmAPIClient interface {
SwarmInit(ctx context.Context, req swarm.InitRequest) (string, error)
SwarmJoin(ctx context.Context, req swarm.JoinRequest) error
SwarmGetUnlockKey(ctx context.Context) (types.SwarmUnlockKeyResponse, error)
SwarmUnlock(ctx context.Context, req swarm.UnlockRequest) error
SwarmLeave(ctx context.Context, force bool) error
SwarmInspect(ctx context.Context) (swarm.Swarm, error)
SwarmUpdate(ctx context.Context, version swarm.Version, swarm swarm.Spec, flags swarm.UpdateFlags) error
}
// SystemAPIClient defines API client methods for the system
type SystemAPIClient interface {
Events(ctx context.Context, options types.EventsOptions) (<-chan events.Message, <-chan error)
Info(ctx context.Context) (types.Info, error)
RegistryLogin(ctx context.Context, auth types.AuthConfig) (registry.AuthenticateOKBody, error)
DiskUsage(ctx context.Context) (types.DiskUsage, error)
Ping(ctx context.Context) (types.Ping, error)
}
// VolumeAPIClient defines API client methods for the volumes
type VolumeAPIClient interface {
VolumeCreate(ctx context.Context, options volumetypes.VolumesCreateBody) (types.Volume, error)
VolumeInspect(ctx context.Context, volumeID string) (types.Volume, error)
VolumeInspectWithRaw(ctx context.Context, volumeID string) (types.Volume, []byte, error)
VolumeList(ctx context.Context, filter filters.Args) (volumetypes.VolumesListOKBody, error)
VolumeRemove(ctx context.Context, volumeID string, force bool) error
VolumesPrune(ctx context.Context, pruneFilter filters.Args) (types.VolumesPruneReport, error)
}
// SecretAPIClient defines API client methods for secrets
type SecretAPIClient interface {
SecretList(ctx context.Context, options types.SecretListOptions) ([]swarm.Secret, error)
SecretCreate(ctx context.Context, secret swarm.SecretSpec) (types.SecretCreateResponse, error)
SecretRemove(ctx context.Context, id string) error
SecretInspectWithRaw(ctx context.Context, name string) (swarm.Secret, []byte, error)
SecretUpdate(ctx context.Context, id string, version swarm.Version, secret swarm.SecretSpec) error
}

View File

@ -0,0 +1,17 @@
package client
import (
"github.com/docker/docker/api/types"
"golang.org/x/net/context"
)
type apiClientExperimental interface {
CheckpointAPIClient
}
// CheckpointAPIClient defines API client methods for the checkpoints
type CheckpointAPIClient interface {
CheckpointCreate(ctx context.Context, container string, options types.CheckpointCreateOptions) error
CheckpointDelete(ctx context.Context, container string, options types.CheckpointDeleteOptions) error
CheckpointList(ctx context.Context, container string, options types.CheckpointListOptions) ([]types.Checkpoint, error)
}

View File

@ -0,0 +1,10 @@
package client
// APIClient is an interface that clients that talk with a docker server must implement.
type APIClient interface {
CommonAPIClient
apiClientExperimental
}
// Ensure that Client always implements APIClient.
var _ APIClient = &Client{}

29
client/login.go Normal file
View File

@ -0,0 +1,29 @@
package client
import (
"encoding/json"
"net/http"
"net/url"
"github.com/docker/docker/api/types"
"github.com/docker/docker/api/types/registry"
"golang.org/x/net/context"
)
// RegistryLogin authenticates the docker server with a given docker registry.
// It returns unauthorizedError when the authentication fails.
func (cli *Client) RegistryLogin(ctx context.Context, auth types.AuthConfig) (registry.AuthenticateOKBody, error) {
resp, err := cli.post(ctx, "/auth", url.Values{}, auth, nil)
if resp.statusCode == http.StatusUnauthorized {
return registry.AuthenticateOKBody{}, unauthorizedError{err}
}
if err != nil {
return registry.AuthenticateOKBody{}, err
}
var response registry.AuthenticateOKBody
err = json.NewDecoder(resp.body).Decode(&response)
ensureReaderClosed(resp)
return response, err
}

18
client/network_connect.go Normal file
View File

@ -0,0 +1,18 @@
package client
import (
"github.com/docker/docker/api/types"
"github.com/docker/docker/api/types/network"
"golang.org/x/net/context"
)
// NetworkConnect connects a container to an existent network in the docker host.
func (cli *Client) NetworkConnect(ctx context.Context, networkID, containerID string, config *network.EndpointSettings) error {
nc := types.NetworkConnect{
Container: containerID,
EndpointConfig: config,
}
resp, err := cli.post(ctx, "/networks/"+networkID+"/connect", nil, nc, nil)
ensureReaderClosed(resp)
return err
}

View File

@ -0,0 +1,111 @@
package client
import (
"bytes"
"encoding/json"
"fmt"
"io/ioutil"
"net/http"
"strings"
"testing"
"golang.org/x/net/context"
"github.com/docker/docker/api/types"
"github.com/docker/docker/api/types/network"
)
func TestNetworkConnectError(t *testing.T) {
client := &Client{
client: newMockClient(errorMock(http.StatusInternalServerError, "Server error")),
}
err := client.NetworkConnect(context.Background(), "network_id", "container_id", nil)
if err == nil || err.Error() != "Error response from daemon: Server error" {
t.Fatalf("expected a Server Error, got %v", err)
}
}
func TestNetworkConnectEmptyNilEndpointSettings(t *testing.T) {
expectedURL := "/networks/network_id/connect"
client := &Client{
client: newMockClient(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)
}
var connect types.NetworkConnect
if err := json.NewDecoder(req.Body).Decode(&connect); err != nil {
return nil, err
}
if connect.Container != "container_id" {
return nil, fmt.Errorf("expected 'container_id', got %s", connect.Container)
}
if connect.EndpointConfig != nil {
return nil, fmt.Errorf("expected connect.EndpointConfig to be nil, got %v", connect.EndpointConfig)
}
return &http.Response{
StatusCode: http.StatusOK,
Body: ioutil.NopCloser(bytes.NewReader([]byte(""))),
}, nil
}),
}
err := client.NetworkConnect(context.Background(), "network_id", "container_id", nil)
if err != nil {
t.Fatal(err)
}
}
func TestNetworkConnect(t *testing.T) {
expectedURL := "/networks/network_id/connect"
client := &Client{
client: newMockClient(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)
}
var connect types.NetworkConnect
if err := json.NewDecoder(req.Body).Decode(&connect); err != nil {
return nil, err
}
if connect.Container != "container_id" {
return nil, fmt.Errorf("expected 'container_id', got %s", connect.Container)
}
if connect.EndpointConfig == nil {
return nil, fmt.Errorf("expected connect.EndpointConfig to be not nil, got %v", connect.EndpointConfig)
}
if connect.EndpointConfig.NetworkID != "NetworkID" {
return nil, fmt.Errorf("expected 'NetworkID', got %s", connect.EndpointConfig.NetworkID)
}
return &http.Response{
StatusCode: http.StatusOK,
Body: ioutil.NopCloser(bytes.NewReader([]byte(""))),
}, nil
}),
}
err := client.NetworkConnect(context.Background(), "network_id", "container_id", &network.EndpointSettings{
NetworkID: "NetworkID",
})
if err != nil {
t.Fatal(err)
}
}

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