Commit 7c0ff0f6 authored by Tim Potter's avatar Tim Potter

Imported Upstream version 0.3.3

parent b2d7647d
......@@ -4,13 +4,6 @@
notifications:
email: false
go:
- 1.5
- tip
install:
- go get -t ./...
- go get github.com/golang/lint/golint
script:
- go vet ./...
- test -z "$(golint ./... | tee /dev/stderr)"
- test -z "$(gofmt -s -l . | tee /dev/stderr)"
- go test -v ./...
- 1.6
install: make deps
script: make validate && make test
......@@ -2,6 +2,52 @@
Items starting with DEPRECATE are important deprecation notices. For more information on the list of deprecated APIs please have a look at https://docs.docker.com/misc/deprecated/ where target removal dates can also be found.
## 0.3.0 (2016-03-22)
### Client
- Add context to every function.
- Fix issue loading a default TLS CA.
- Allow to configure the client with a given http.Client.
- Add support for Windows named pipes.
- Set default host for Solaris.
- Add quiet flag for image load.
- Add ability to hijack connections through a proxy.
- Correctly set content type for image load.
- Add support for getting token for login.
### Types
- Add struct for update restart policy.
- Add human friendly State for container.
- Use OS specific host when DOCKER_HOST is not set.
- Rename Status in info to SystemStatus.
- Add internal flag to network inspect.
- Add disk quota field to container.
- Add EnableIPv6 fields.
- Add Mounts to container.
- Add cgroup driver to info.
- Add userns to host config.
- Remove email from AuthConfig.
- Make AuthConfig fields optional.
- Add IO resource settings for Windows.
- Add storage driver to host config.
- Update NetworkName to return proper user defined network names.
- Support joining cgroups by container id.
- Add KernelMemory to info.
- Add UsernsMode to container config.
- Add CPU resource control for Windows.
- Add AutoRemove to host config.
- Add Status field to Volume.
- Add Label to Image, Network and Volume.
- Add RootFS to container.
## 0.2.3 (2016-02-02)
### Types
- Add missing status field.
## 0.2.2 (2016-01-13)
### Client
......
......@@ -14,6 +14,7 @@
"calavera",
"dongluochen",
"mhbauer",
"vdemeester"
]
[people]
......@@ -37,3 +38,8 @@
Name = "Morgan Bauer"
Email = "mbauer@us.ibm.com"
GitHub = "mhbauer"
[people.vdemeester]
Name = "Vincent Demeester"
Email = "vincent@sbr.pm"
GitHub = "vdemeester"
.PHONY: all deps test validate
all: deps test validate
deps:
go get -t ./...
go get github.com/golang/lint/golint
test:
go test -race -cover ./...
validate:
go vet ./...
test -z "$(golint ./... | tee /dev/stderr)"
test -z "$(gofmt -s -l . | tee /dev/stderr)"
......@@ -20,8 +20,10 @@ package main
import (
"fmt"
"github.com/docker/engine-api/client"
"github.com/docker/engine-api/types"
"golang.org/x/net/context"
)
func main() {
......@@ -32,7 +34,7 @@ func main() {
}
options := types.ContainerListOptions{All: true}
containers, err := cli.ContainerList(options)
containers, err := cli.ContainerList(context.Background(), options)
if err != nil {
panic(err)
}
......@@ -53,6 +55,14 @@ The server package includes API endpoints that applications compatible with the
This package is still pending to be extracted from the Docker engine.
## Developing
engine-api requires some minimal libraries that you can download running `make deps`.
To run tests, use the command `make test`. We use build tags to isolate functions and structures that are only available for testing.
To validate the sources, use the command `make validate`.
## License
engine-api is licensed under the Apache License, Version 2.0. See [LICENSE](LICENSE) for the full license text.
version: "{build}"
# Source Config
clone_folder: c:\gopath\src\github.com\docker\engine-api
# Build host
environment:
GOPATH: c:\gopath
GOVERSION: 1.6
init:
- git config --global core.autocrlf input
# Build
install:
# Install Go 1.6.
- rmdir c:\go /s /q
- appveyor DownloadFile https://storage.googleapis.com/golang/go%GOVERSION%.windows-amd64.msi
- msiexec /i go%GOVERSION%.windows-amd64.msi /q
- set Path=c:\go\bin;c:\gopath\bin;%Path%
- go version
- go env
build: false
deploy: false
before_test:
- go get -t ./...
- go get github.com/golang/lint/golint
test_script:
- go vet ./...
- golint ./...
- gofmt -s -l .
- go test -race -cover -v -tags=test ./...
package client
import (
"crypto/tls"
"fmt"
"net"
"net/http"
"net/url"
"os"
"path/filepath"
"strings"
"time"
"github.com/docker/engine-api/client/transport"
"github.com/docker/go-connections/tlsconfig"
)
// Client is the API client that performs all operations
......@@ -19,17 +19,13 @@ type Client struct {
proto string
// addr holds the client address.
addr string
// basePath holds the path to prepend to the requests
// basePath holds the path to prepend to the requests.
basePath string
// scheme holds the scheme of the client i.e. https.
scheme string
// tlsConfig holds the tls configuration to use in hijacked requests.
tlsConfig *tls.Config
// httpClient holds the client transport instance. Exported to keep the old code running.
httpClient *http.Client
// transport is the interface to sends request with, it implements transport.Client.
transport transport.Client
// version of the server to talk to.
version string
// custom http headers configured by users
// custom http headers configured by users.
customHTTPHeaders map[string]string
}
......@@ -39,58 +35,53 @@ type Client struct {
// 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 transport *http.Transport
var client *http.Client
if dockerCertPath := os.Getenv("DOCKER_CERT_PATH"); dockerCertPath != "" {
tlsc := &tls.Config{}
cert, err := tls.LoadX509KeyPair(filepath.Join(dockerCertPath, "cert.pem"), filepath.Join(dockerCertPath, "key.pem"))
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, fmt.Errorf("Error loading x509 key pair: %s", err)
return nil, err
}
tlsc.Certificates = append(tlsc.Certificates, cert)
tlsc.InsecureSkipVerify = os.Getenv("DOCKER_TLS_VERIFY") == ""
transport = &http.Transport{
TLSClientConfig: tlsc,
client = &http.Client{
Transport: &http.Transport{
TLSClientConfig: tlsc,
},
}
}
return NewClient(os.Getenv("DOCKER_HOST"), os.Getenv("DOCKER_API_VERSION"), transport, nil)
host := os.Getenv("DOCKER_HOST")
if host == "" {
host = DefaultDockerHost
}
return NewClient(host, os.Getenv("DOCKER_API_VERSION"), client, nil)
}
// NewClient initializes a new API client for the given host and API version.
// It won't send any version information if the version number is empty.
// It uses the transport to create a new http client.
// It uses the given http client as transport.
// It also initializes the custom http headers to add to each request.
func NewClient(host string, version string, transport *http.Transport, httpHeaders map[string]string) (*Client, error) {
var (
basePath string
scheme = "http"
protoAddrParts = strings.SplitN(host, "://", 2)
proto, addr = protoAddrParts[0], protoAddrParts[1]
)
if proto == "tcp" {
parsed, err := url.Parse("tcp://" + addr)
if err != nil {
return nil, err
}
addr = parsed.Host
basePath = parsed.Path
func NewClient(host string, version string, client *http.Client, httpHeaders map[string]string) (*Client, error) {
proto, addr, basePath, err := ParseHost(host)
if err != nil {
return nil, err
}
transport = configureTransport(transport, proto, addr)
if transport.TLSClientConfig != nil {
scheme = "https"
transport, err := transport.NewTransportWithHTTP(proto, addr, client)
if err != nil {
return nil, err
}
return &Client{
proto: proto,
addr: addr,
basePath: basePath,
scheme: scheme,
tlsConfig: transport.TLSClientConfig,
httpClient: &http.Client{Transport: transport},
transport: transport,
version: version,
customHTTPHeaders: httpHeaders,
}, nil
......@@ -106,10 +97,14 @@ func (cli *Client) getAPIPath(p string, query url.Values) string {
} else {
apiPath = fmt.Sprintf("%s%s", cli.basePath, p)
}
u := &url.URL{
Path: apiPath,
}
if len(query) > 0 {
apiPath += "?" + query.Encode()
u.RawQuery = query.Encode()
}
return apiPath
return u.String()
}
// ClientVersion returns the version string associated with this
......@@ -119,23 +114,22 @@ func (cli *Client) ClientVersion() string {
return cli.version
}
func configureTransport(tr *http.Transport, proto, addr string) *http.Transport {
if tr == nil {
tr = &http.Transport{}
// 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)
}
// Why 32? See https://github.com/docker/docker/pull/8035.
timeout := 32 * time.Second
if proto == "unix" {
// No need for compression in local communications.
tr.DisableCompression = true
tr.Dial = func(_, _ string) (net.Conn, error) {
return net.DialTimeout(proto, addr, timeout)
var basePath string
proto, addr := protoAddrParts[0], protoAddrParts[1]
if proto == "tcp" {
parsed, err := url.Parse("tcp://" + addr)
if err != nil {
return "", "", "", err
}
} else {
tr.Proxy = http.ProxyFromEnvironment
tr.Dial = (&net.Dialer{Timeout: timeout}).Dial
addr = parsed.Host
basePath = parsed.Path
}
return tr
return proto, addr, basePath, nil
}
package client
// DefaultDockerHost defines os specific default if DOCKER_HOST is unset
const DefaultDockerHost = "tcp://127.0.0.1:2375"
package client
import (
"bytes"
"crypto/tls"
"io/ioutil"
"net/http"
"github.com/docker/engine-api/client/transport"
)
type mockClient struct {
do func(*http.Request) (*http.Response, error)
}
// TLSConfig returns the TLS configuration.
func (t *mockClient) TLSConfig() *tls.Config {
return &tls.Config{}
}
// Scheme returns protocol scheme to use.
func (t *mockClient) Scheme() string {
return "http"
}
// Secure returns true if there is a TLS configuration.
func (t *mockClient) Secure() bool {
return false
}
// NewMockClient returns a mocked client that runs the function supplied as `client.Do` call
func newMockClient(tlsConfig *tls.Config, doer func(*http.Request) (*http.Response, error)) transport.Client {
if tlsConfig != nil {
panic("this actually gets set!")
}
return &mockClient{
do: doer,
}
}
// Do executes the supplied function for the mock.
func (m mockClient) Do(req *http.Request) (*http.Response, error) {
return m.do(req)
}
func errorMock(statusCode int, message string) func(req *http.Request) (*http.Response, error) {
return func(req *http.Request) (*http.Response, error) {
return &http.Response{
StatusCode: statusCode,
Body: ioutil.NopCloser(bytes.NewReader([]byte(message))),
}, nil
}
}
......@@ -21,6 +21,7 @@ func TestGetAPIPath(t *testing.T) {
{"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 {
......@@ -34,3 +35,38 @@ func TestGetAPIPath(t *testing.T) {
}
}
}
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)
}
}
}
// +build linux freebsd solaris openbsd
package client
// DefaultDockerHost defines os specific default if DOCKER_HOST is unset
const DefaultDockerHost = "unix:///var/run/docker.sock"
package client
// DefaultDockerHost defines os specific default if DOCKER_HOST is unset
const DefaultDockerHost = "npipe:////./pipe/docker_engine"
......@@ -4,13 +4,14 @@ import (
"net/url"
"github.com/docker/engine-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(options types.ContainerAttachOptions) (types.HijackedResponse, error) {
func (cli *Client) ContainerAttach(ctx context.Context, options types.ContainerAttachOptions) (types.HijackedResponse, error) {
query := url.Values{}
if options.Stream {
query.Set("stream", "1")
......@@ -29,5 +30,5 @@ func (cli *Client) ContainerAttach(options types.ContainerAttachOptions) (types.
}
headers := map[string][]string{"Content-Type": {"text/plain"}}
return cli.postHijacked("/containers/"+options.ContainerID+"/attach", query, nil, headers)
return cli.postHijacked(ctx, "/containers/"+options.ContainerID+"/attach", query, nil, headers)
}
......@@ -5,10 +5,11 @@ import (
"net/url"
"github.com/docker/engine-api/types"
"golang.org/x/net/context"
)
// ContainerCommit applies changes into a container and creates a new tagged image.
func (cli *Client) ContainerCommit(options types.ContainerCommitOptions) (types.ContainerCommitResponse, error) {
func (cli *Client) ContainerCommit(ctx context.Context, options types.ContainerCommitOptions) (types.ContainerCommitResponse, error) {
query := url.Values{}
query.Set("container", options.ContainerID)
query.Set("repo", options.RepositoryName)
......@@ -23,15 +24,12 @@ func (cli *Client) ContainerCommit(options types.ContainerCommitOptions) (types.
}
var response types.ContainerCommitResponse
resp, err := cli.post("/commit", query, options.Config, nil)
resp, err := cli.post(ctx, "/commit", query, options.Config, nil)
if err != nil {
return response, err
}
defer ensureReaderClosed(resp)
if err := json.NewDecoder(resp.body).Decode(&response); err != nil {
return response, err
}
return response, nil
err = json.NewDecoder(resp.body).Decode(&response)
ensureReaderClosed(resp)
return response, err
}
package client
import (
"bytes"
"encoding/json"
"fmt"
"io/ioutil"
"net/http"
"testing"
"github.com/docker/engine-api/types"
"golang.org/x/net/context"
)
func TestContainerCommitError(t *testing.T) {
client := &Client{
transport: newMockClient(nil, errorMock(http.StatusInternalServerError, "Server error")),
}
_, err := client.ContainerCommit(context.Background(), types.ContainerCommitOptions{
ContainerID: "nothing",
})
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) {
expectedContainerID := "container_id"
expectedRepositoryName := "repository_name"
expectedTag := "tag"
expectedComment := "comment"
expectedAuthor := "author"
expectedChanges := []string{"change1", "change2"}
client := &Client{
transport: newMockClient(nil, func(req *http.Request) (*http.Response, error) {
query := req.URL.Query()
containerID := query.Get("container")
if containerID != expectedContainerID {
return nil, fmt.Errorf("container id not set in URL query properly. Expected '%s', got %s", expectedContainerID, containerID)
}
repo := query.Get("repo")
if repo != expectedRepositoryName {
return nil, fmt.Errorf("container repo not set in URL query properly. Expected '%s', got %s", expectedRepositoryName, repo)
}
tag := query.Get("tag")
if tag != expectedTag {
return nil, fmt.Errorf("container tag not set in URL query properly. Expected '%s', got %s'", expectedTag, tag)
}
comment := query.Get("comment")
if comment != expectedComment {
return nil, fmt.Errorf("container comment not set in URL query properly. Expected '%s', got %s'", expectedComment, comment)
}
author := query.Get("author")
if author != expectedAuthor {
return nil, fmt.Errorf("container author not set in URL query properly. Expected '%s', got %s'", expectedAuthor, author)
}
pause := query.Get("pause")
if pause != "0" {
return nil, fmt.Errorf("container pause not set in URL query properly. Expected 'true', got %v'", pause)
}
changes := query["changes"]
if len(changes) != len(expectedChanges) {
return nil, fmt.Errorf("expected container changes size to be '%d', got %d", len(expectedChanges), len(changes))
}
b, err := json.Marshal(types.ContainerCommitResponse{
ID: "new_container_id",
})
if err != nil {
return nil, err
}
return &http.Response{
StatusCode: http.StatusOK,
Body: ioutil.NopCloser(bytes.NewReader(b)),
}, nil
}),
}
r, err := client.ContainerCommit(context.Background(), types.ContainerCommitOptions{
ContainerID: expectedContainerID,
RepositoryName: expectedRepositoryName,
Tag: expectedTag,
Comment: expectedComment,
Author: expectedAuthor,
Changes: expectedChanges,
Pause: false,
})
if err != nil {
t.Fatal(err)
}
if r.ID != "new_container_id" {
t.Fatalf("expected `container_id`, got %s", r.ID)
}
}
......@@ -10,16 +10,18 @@ import (
"path/filepath"
"strings"
"golang.org/x/net/context"
"github.com/docker/engine-api/types"
)
// ContainerStatPath returns Stat information about a path inside the container filesystem.
func (cli *Client) ContainerStatPath(containerID, path string) (types.ContainerPathStat, error) {
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(urlStr, query, nil)
response, err := cli.head(ctx, urlStr, query, nil)
if err != nil {
return types.ContainerPathStat{}, err
}
......@@ -28,7 +30,7 @@ func (cli *Client) ContainerStatPath(containerID, path string) (types.ContainerP
}
// CopyToContainer copies content into the container filesystem.
func (cli *Client) CopyToContainer(options types.CopyToContainerOptions) error {
func (cli *Client) CopyToContainer(ctx context.Context, options types.CopyToContainerOptions) error {
query := url.Values{}
query.Set("path", filepath.ToSlash(options.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.
......@@ -38,7 +40,7 @@ func (cli *Client) CopyToContainer(options types.CopyToContainerOptions) error {
path := fmt.Sprintf("/containers/%s/archive", options.ContainerID)
response, err := cli.putRaw(path, query, options.Content, nil)
response, err := cli.putRaw(ctx, path, query, options.Content, nil)
if err != nil {
return err
}
......@@ -53,12 +55,12 @@ func (cli *Client) CopyToContainer(options types.CopyToContainerOptions) error {
// CopyFromContainer get the content from the container and return it as a Reader
// to manipulate it in the host. It's up to the caller to close the reader.
func (cli *Client) CopyFromContainer(containerID, srcPath string) (io.ReadCloser, types.ContainerPathStat, error) {