mirror of https://github.com/docker/cli.git
Import docker/docker/client
Signed-off-by: Daniel Nephin <dnephin@gmail.com>
This commit is contained in:
commit
e8a454d8a6
|
@ -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)
|
|
@ -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
|
||||||
|
}
|
|
@ -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)
|
||||||
|
}
|
||||||
|
}
|
|
@ -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
|
||||||
|
}
|
|
@ -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)
|
||||||
|
}
|
||||||
|
}
|
|
@ -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
|
||||||
|
}
|
|
@ -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)
|
||||||
|
}
|
||||||
|
}
|
|
@ -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
|
||||||
|
}
|
|
@ -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
|
||||||
|
}
|
||||||
|
}
|
|
@ -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])
|
||||||
|
}
|
||||||
|
}
|
|
@ -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"
|
|
@ -0,0 +1,4 @@
|
||||||
|
package client
|
||||||
|
|
||||||
|
// DefaultDockerHost defines os specific default if DOCKER_HOST is unset
|
||||||
|
const DefaultDockerHost = "npipe:////./pipe/docker_engine"
|
|
@ -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)
|
||||||
|
}
|
|
@ -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
|
||||||
|
}
|
|
@ -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)
|
||||||
|
}
|
||||||
|
}
|
|
@ -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
|
||||||
|
}
|
|
@ -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))
|
||||||
|
}
|
||||||
|
}
|
|
@ -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
|
||||||
|
}
|
|
@ -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)
|
||||||
|
}
|
||||||
|
}
|
|
@ -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
|
||||||
|
}
|
|
@ -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)
|
||||||
|
}
|
||||||
|
}
|
|
@ -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
|
||||||
|
}
|
|
@ -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)
|
||||||
|
}
|
||||||
|
}
|
|
@ -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
|
||||||
|
}
|
|
@ -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))
|
||||||
|
}
|
||||||
|
}
|
|
@ -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
|
||||||
|
}
|
|
@ -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`")
|
||||||
|
}
|
||||||
|
}
|
|
@ -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
|
||||||
|
}
|
|
@ -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)
|
||||||
|
}
|
||||||
|
}
|
|
@ -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
|
||||||
|
}
|
|
@ -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)
|
||||||
|
}
|
||||||
|
}
|
|
@ -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
|
||||||
|
}
|
|
@ -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)
|
||||||
|
}
|
||||||
|
}
|
|
@ -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
|
||||||
|
}
|
|
@ -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)
|
||||||
|
}
|
||||||
|
}
|
|
@ -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
|
||||||
|
}
|
|
@ -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)
|
||||||
|
}
|
||||||
|
}
|
|
@ -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
|
||||||
|
}
|
|
@ -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)
|
||||||
|
}
|
||||||
|
}
|
|
@ -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
|
||||||
|
}
|
|
@ -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)
|
||||||
|
}
|
||||||
|
}
|
|
@ -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
|
||||||
|
}
|
|
@ -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
|
||||||
|
}
|
||||||
|
}
|
|
@ -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
|
||||||
|
}
|
|
@ -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)
|
||||||
|
}
|
||||||
|
}
|
|
@ -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
|
||||||
|
}
|
|
@ -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)
|
||||||
|
}
|
||||||
|
}
|
|
@ -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
|
||||||
|
}
|
|
@ -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))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -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
|
||||||
|
}
|
|
@ -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)
|
||||||
|
}
|
||||||
|
}
|
|
@ -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
|
||||||
|
}
|
|
@ -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)
|
||||||
|
}
|
||||||
|
}
|
|
@ -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
|
||||||
|
}
|
|
@ -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)
|
||||||
|
}
|
||||||
|
}
|
|
@ -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
|
||||||
|
}
|
|
@ -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)
|
||||||
|
}
|
||||||
|
}
|
|
@ -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
|
||||||
|
}
|
|
@ -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)
|
||||||
|
}
|
||||||
|
}
|
|
@ -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
|
||||||
|
}
|
|
@ -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)
|
||||||
|
}
|
|
@ -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
|
||||||
|
}
|
|
@ -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)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -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)
|
||||||
|
}
|
|
@ -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
|
||||||
|
}
|
|
@ -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)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -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)
|
||||||
|
}
|
|
@ -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)
|
||||||
|
}
|
||||||
|
}
|
|
@ -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
|
||||||
|
}
|
|
@ -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)
|
||||||
|
}
|
||||||
|
}
|
|
@ -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
|
||||||
|
}
|
|
@ -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))
|
||||||
|
}
|
||||||
|
}
|
|
@ -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
|
||||||
|
}
|
|
@ -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)
|
||||||
|
}
|
||||||
|
}
|
|
@ -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
|
||||||
|
}
|
|
@ -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)
|
||||||
|
}
|
||||||
|
}
|
|
@ -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
|
||||||
|
}
|
|
@ -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))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -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
|
||||||
|
}
|
|
@ -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)
|
||||||
|
}
|
||||||
|
}
|
|
@ -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 ""
|
||||||
|
}
|
|
@ -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))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -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)
|
||||||
|
}
|
|
@ -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))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -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
|
||||||
|
}
|
|
@ -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)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -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
|
||||||
|
}
|
|
@ -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))
|
||||||
|
}
|
||||||
|
}
|
|
@ -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)
|
||||||
|
}
|
|
@ -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)
|
||||||
|
}
|
||||||
|
}
|
|
@ -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
|
||||||
|
}
|
|
@ -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)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -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
|
||||||
|
}
|
|
@ -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)
|
||||||
|
}
|
||||||
|
}
|
|
@ -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
|
||||||
|
}
|
|
@ -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)
|
||||||
|
}
|
|
@ -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{}
|
|
@ -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
|
||||||
|
}
|
|
@ -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
|
||||||
|
}
|
|
@ -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
Loading…
Reference in New Issue