Bump github.com/fsouza/go-dockerclient

To include https://github.com/fsouza/go-dockerclient/pull/562 , which
fixes https://github.com/weaveworks/scope/issues/1767
This commit is contained in:
Alfonso Acosta
2016-08-15 09:34:12 +00:00
parent 6f9912501d
commit c39b16ae9f
12 changed files with 377 additions and 152 deletions

View File

@@ -46,23 +46,47 @@ type dockerConfig struct {
Email string `json:"email"`
}
// NewAuthConfigurationsFromDockerCfg returns AuthConfigurations from the
// ~/.dockercfg file.
func NewAuthConfigurationsFromDockerCfg() (*AuthConfigurations, error) {
var r io.Reader
var err error
p := path.Join(os.Getenv("HOME"), ".docker", "config.json")
r, err = os.Open(p)
// NewAuthConfigurationsFromFile returns AuthConfigurations from a path containing JSON
// in the same format as the .dockercfg file.
func NewAuthConfigurationsFromFile(path string) (*AuthConfigurations, error) {
r, err := os.Open(path)
if err != nil {
p := path.Join(os.Getenv("HOME"), ".dockercfg")
r, err = os.Open(p)
if err != nil {
return nil, err
}
return nil, err
}
return NewAuthConfigurations(r)
}
func cfgPaths(dockerConfigEnv string, homeEnv string) []string {
var paths []string
if dockerConfigEnv != "" {
paths = append(paths, path.Join(dockerConfigEnv, "config.json"))
}
if homeEnv != "" {
paths = append(paths, path.Join(homeEnv, ".docker", "config.json"))
paths = append(paths, path.Join(homeEnv, ".dockercfg"))
}
return paths
}
// NewAuthConfigurationsFromDockerCfg returns AuthConfigurations from
// system config files. The following files are checked in the order listed:
// - $DOCKER_CONFIG/config.json if DOCKER_CONFIG set in the environment,
// - $HOME/.docker/config.json
// - $HOME/.dockercfg
func NewAuthConfigurationsFromDockerCfg() (*AuthConfigurations, error) {
err := fmt.Errorf("No docker configuration found")
var auths *AuthConfigurations
pathsToTry := cfgPaths(os.Getenv("DOCKER_CONFIG"), os.Getenv("HOME"))
for _, path := range pathsToTry {
auths, err = NewAuthConfigurationsFromFile(path)
if err == nil {
return auths, nil
}
}
return auths, err
}
// NewAuthConfigurations returns AuthConfigurations from a JSON encoded string in the
// same format as the .dockercfg file.
func NewAuthConfigurations(r io.Reader) (*AuthConfigurations, error) {

View File

@@ -1,17 +0,0 @@
// Copyright 2016 go-dockerclient authors. All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.
// +build go1.5
package docker
import "net/http"
func cancelable(client *http.Client, req *http.Request) func() {
ch := make(chan struct{})
req.Cancel = ch
return func() {
close(ch)
}
}

View File

@@ -1,19 +0,0 @@
// Copyright 2016 go-dockerclient authors. All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.
// +build !go1.5
package docker
import "net/http"
func cancelable(client *http.Client, req *http.Request) func() {
return func() {
if rc, ok := client.Transport.(interface {
CancelRequest(*http.Request)
}); ok {
rc.CancelRequest(req)
}
}
}

View File

@@ -34,6 +34,8 @@ import (
"github.com/docker/docker/pkg/homedir"
"github.com/docker/docker/pkg/stdcopy"
"github.com/hashicorp/go-cleanhttp"
"golang.org/x/net/context"
"golang.org/x/net/context/ctxhttp"
)
const userAgent = "go-dockerclient"
@@ -49,8 +51,8 @@ var (
ErrInactivityTimeout = errors.New("inactivity time exceeded timeout")
apiVersion112, _ = NewAPIVersion("1.12")
apiVersion119, _ = NewAPIVersion("1.19")
apiVersion124, _ = NewAPIVersion("1.24")
)
// APIVersion is an internal representation of a version of the Remote API.
@@ -144,6 +146,9 @@ type Client struct {
serverAPIVersion APIVersion
expectedAPIVersion APIVersion
unixHTTPClient *http.Client
// A timeout to use when using both the unixHTTPClient and HTTPClient
timeout time.Duration
}
// NewClient returns a Client instance ready for communication with the given
@@ -316,6 +321,12 @@ func NewVersionedTLSClientFromBytes(endpoint string, certPEMBlock, keyPEMBlock,
}, nil
}
// SetTimeout takes a timeout and applies it to subsequent requests to the
// docker engine
func (c *Client) SetTimeout(t time.Duration) {
c.timeout = t
}
func (c *Client) checkAPIVersion() error {
serverAPIVersionString, err := c.getServerAPIVersionString()
if err != nil {
@@ -379,6 +390,7 @@ type doOptions struct {
data interface{}
forceJSON bool
headers map[string]string
context context.Context
}
func (c *Client) do(method, path string, doOptions doOptions) (*http.Response, error) {
@@ -405,6 +417,12 @@ func (c *Client) do(method, path string, doOptions doOptions) (*http.Response, e
} else {
u = c.getURL(path)
}
// If the user has provided a timeout, apply it.
if c.timeout != 0 {
httpClient.Timeout = c.timeout
}
req, err := http.NewRequest(method, u, params)
if err != nil {
return nil, err
@@ -419,12 +437,19 @@ func (c *Client) do(method, path string, doOptions doOptions) (*http.Response, e
for k, v := range doOptions.headers {
req.Header.Set(k, v)
}
resp, err := httpClient.Do(req)
ctx := doOptions.context
if ctx == nil {
ctx = context.Background()
}
resp, err := ctxhttp.Do(ctx, httpClient, req)
if err != nil {
if strings.Contains(err.Error(), "connection refused") {
return nil, ErrConnectionRefused
}
return nil, err
return nil, chooseError(ctx, err)
}
if resp.StatusCode < 200 || resp.StatusCode >= 400 {
return nil, newError(resp)
@@ -445,6 +470,17 @@ type streamOptions struct {
// Timeout with no data is received, it's reset every time new data
// arrives
inactivityTimeout time.Duration
context context.Context
}
// if error in context, return that instead of generic http error
func chooseError(ctx context.Context, err error) error {
select {
case <-ctx.Done():
return ctx.Err()
default:
return err
}
}
func (c *Client) stream(method, path string, streamOptions streamOptions) error {
@@ -477,18 +513,30 @@ func (c *Client) stream(method, path string, streamOptions streamOptions) error
if streamOptions.stderr == nil {
streamOptions.stderr = ioutil.Discard
}
cancelRequest := cancelable(c.HTTPClient, req)
// make a sub-context so that our active cancellation does not affect parent
ctx := streamOptions.context
if ctx == nil {
ctx = context.Background()
}
subCtx, cancelRequest := context.WithCancel(ctx)
defer cancelRequest()
if protocol == "unix" {
dial, err := c.Dialer.Dial(protocol, address)
if err != nil {
return err
}
cancelRequest = func() { dial.Close() }
defer dial.Close()
go func() {
select {
case <-subCtx.Done():
dial.Close()
}
}()
breader := bufio.NewReader(dial)
err = req.Write(dial)
if err != nil {
return err
return chooseError(subCtx, err)
}
// ReadResponse may hang if server does not replay
@@ -504,14 +552,15 @@ func (c *Client) stream(method, path string, streamOptions streamOptions) error
if strings.Contains(err.Error(), "connection refused") {
return ErrConnectionRefused
}
return err
return chooseError(subCtx, err)
}
} else {
if resp, err = c.HTTPClient.Do(req); err != nil {
if resp, err = ctxhttp.Do(subCtx, c.HTTPClient, req); err != nil {
if strings.Contains(err.Error(), "connection refused") {
return ErrConnectionRefused
}
return err
return chooseError(subCtx, err)
}
}
defer resp.Body.Close()
@@ -528,7 +577,7 @@ func (c *Client) stream(method, path string, streamOptions streamOptions) error
if atomic.LoadUint32(&canceled) != 0 {
return ErrInactivityTimeout
}
return err
return chooseError(subCtx, err)
}
return nil
}
@@ -676,7 +725,7 @@ func (c *Client) hijack(method, path string, hijackOptions hijackOptions) (Close
}
}
errs := make(chan error)
errs := make(chan error, 1)
quit := make(chan struct{})
go func() {
clientconn := httputil.NewClientConn(dial, nil)
@@ -690,7 +739,7 @@ func (c *Client) hijack(method, path string, hijackOptions hijackOptions) (Close
defer rwc.Close()
errChanOut := make(chan error, 1)
errChanIn := make(chan error, 1)
errChanIn := make(chan error, 2)
if hijackOptions.stdout == nil && hijackOptions.stderr == nil {
close(errChanOut)
} else {
@@ -740,14 +789,12 @@ func (c *Client) hijack(method, path string, hijackOptions hijackOptions) (Close
select {
case errIn = <-errChanIn:
case <-quit:
return
}
var errOut error
select {
case errOut = <-errChanOut:
case <-quit:
return
}
if errIn != nil {

View File

@@ -16,6 +16,7 @@ import (
"time"
"github.com/docker/go-units"
"golang.org/x/net/context"
)
// ErrContainerAlreadyExists is the error returned by CreateContainer when the
@@ -32,6 +33,7 @@ type ListContainersOptions struct {
Since string
Before string
Filters map[string][]string
Context context.Context
}
// APIPort is a type that represents a port mapping returned by the Docker API
@@ -82,7 +84,7 @@ type NetworkList struct {
// See https://goo.gl/47a6tO for more details.
func (c *Client) ListContainers(opts ListContainersOptions) ([]APIContainers, error) {
path := "/containers/json?" + queryString(opts)
resp, err := c.do("GET", path, doOptions{})
resp, err := c.do("GET", path, doOptions{context: opts.Context})
if err != nil {
return nil, err
}
@@ -282,12 +284,15 @@ type Config struct {
AttachStderr bool `json:"AttachStderr,omitempty" yaml:"AttachStderr,omitempty"`
PortSpecs []string `json:"PortSpecs,omitempty" yaml:"PortSpecs,omitempty"`
ExposedPorts map[Port]struct{} `json:"ExposedPorts,omitempty" yaml:"ExposedPorts,omitempty"`
PublishService string `json:"PublishService,omitempty" yaml:"PublishService,omitempty"`
ArgsEscaped bool `json:"ArgsEscaped,omitempty" yaml:"ArgsEscaped,omitempty"`
StopSignal string `json:"StopSignal,omitempty" yaml:"StopSignal,omitempty"`
Tty bool `json:"Tty,omitempty" yaml:"Tty,omitempty"`
OpenStdin bool `json:"OpenStdin,omitempty" yaml:"OpenStdin,omitempty"`
StdinOnce bool `json:"StdinOnce,omitempty" yaml:"StdinOnce,omitempty"`
Env []string `json:"Env,omitempty" yaml:"Env,omitempty"`
Cmd []string `json:"Cmd" yaml:"Cmd"`
Healthcheck *HealthConfig `json:"Healthcheck,omitempty" yaml:"Healthcheck,omitempty"`
DNS []string `json:"Dns,omitempty" yaml:"Dns,omitempty"` // For Docker API v1.9 and below only
Image string `json:"Image,omitempty" yaml:"Image,omitempty"`
Volumes map[string]struct{} `json:"Volumes,omitempty" yaml:"Volumes,omitempty"`
@@ -347,6 +352,29 @@ type GraphDriver struct {
Data map[string]string `json:"Data,omitempty" yaml:"Data,omitempty"`
}
// HealthConfig holds configuration settings for the HEALTHCHECK feature
//
// It has been added in the version 1.24 of the Docker API, available since
// Docker 1.12.
type HealthConfig struct {
// Test is the test to perform to check that the container is healthy.
// An empty slice means to inherit the default.
// The options are:
// {} : inherit healthcheck
// {"NONE"} : disable healthcheck
// {"CMD", args...} : exec arguments directly
// {"CMD-SHELL", command} : run command with system's default shell
Test []string `json:"Test,omitempty" yaml:"Test,omitempty"`
// Zero means to inherit. Durations are expressed as integer nanoseconds.
Interval time.Duration `json:"Interval,omitempty" yaml:"Interval,omitempty"` // Interval is the time to wait between checks.
Timeout time.Duration `json:"Timeout,omitempty" yaml:"Timeout,omitempty"` // Timeout is the time to wait before considering the check to have hung.
// Retries is the number of consecutive failures needed to consider a container as unhealthy.
// Zero means inherit.
Retries int `json:"Retries,omitempty" yaml:"Retries,omitempty"`
}
// Container is the type encompasing everything about a container - its config,
// hostconfig, etc.
type Container struct {
@@ -400,13 +428,18 @@ type UpdateContainerOptions struct {
MemoryReservation int `json:"MemoryReservation"`
KernelMemory int `json:"KernelMemory"`
RestartPolicy RestartPolicy `json:"RestartPolicy,omitempty"`
Context context.Context
}
// UpdateContainer updates the container at ID with the options
//
// See https://goo.gl/Y6fXUy for more details.
func (c *Client) UpdateContainer(id string, opts UpdateContainerOptions) error {
resp, err := c.do("POST", fmt.Sprintf("/containers/"+id+"/update"), doOptions{data: opts, forceJSON: true})
resp, err := c.do("POST", fmt.Sprintf("/containers/"+id+"/update"), doOptions{
data: opts,
forceJSON: true,
context: opts.Context,
})
if err != nil {
return err
}
@@ -422,14 +455,17 @@ type RenameContainerOptions struct {
ID string `qs:"-"`
// New name
Name string `json:"name,omitempty" yaml:"name,omitempty"`
Name string `json:"name,omitempty" yaml:"name,omitempty"`
Context context.Context
}
// RenameContainer updates and existing containers name
//
// See https://goo.gl/laSOIy for more details.
func (c *Client) RenameContainer(opts RenameContainerOptions) error {
resp, err := c.do("POST", fmt.Sprintf("/containers/"+opts.ID+"/rename?%s", queryString(opts)), doOptions{})
resp, err := c.do("POST", fmt.Sprintf("/containers/"+opts.ID+"/rename?%s", queryString(opts)), doOptions{
context: opts.Context,
})
if err != nil {
return err
}
@@ -485,6 +521,7 @@ type CreateContainerOptions struct {
Config *Config `qs:"-"`
HostConfig *HostConfig `qs:"-"`
NetworkingConfig *NetworkingConfig `qs:"-"`
Context context.Context
}
// CreateContainer creates a new container, returning the container instance,
@@ -506,6 +543,7 @@ func (c *Client) CreateContainer(opts CreateContainerOptions) (*Container, error
opts.HostConfig,
opts.NetworkingConfig,
},
context: opts.Context,
},
)
@@ -632,43 +670,61 @@ type HostConfig struct {
LogConfig LogConfig `json:"LogConfig,omitempty" yaml:"LogConfig,omitempty"`
ReadonlyRootfs bool `json:"ReadonlyRootfs,omitempty" yaml:"ReadonlyRootfs,omitempty"`
SecurityOpt []string `json:"SecurityOpt,omitempty" yaml:"SecurityOpt,omitempty"`
Cgroup string `json:"Cgroup,omitempty" yaml:"Cgroup,omitempty"`
CgroupParent string `json:"CgroupParent,omitempty" yaml:"CgroupParent,omitempty"`
Memory int64 `json:"Memory,omitempty" yaml:"Memory,omitempty"`
MemoryReservation int64 `json:"MemoryReservation,omitempty" yaml:"MemoryReservation,omitempty"`
KernelMemory int64 `json:"KernelMemory,omitempty" yaml:"KernelMemory,omitempty"`
MemorySwap int64 `json:"MemorySwap,omitempty" yaml:"MemorySwap,omitempty"`
MemorySwappiness int64 `json:"MemorySwappiness,omitempty" yaml:"MemorySwappiness,omitempty"`
OOMKillDisable bool `json:"OomKillDisable,omitempty" yaml:"OomKillDisable"`
OOMKillDisable bool `json:"OomKillDisable,omitempty" yaml:"OomKillDisable,omitempty"`
CPUShares int64 `json:"CpuShares,omitempty" yaml:"CpuShares,omitempty"`
CPUSet string `json:"Cpuset,omitempty" yaml:"Cpuset,omitempty"`
CPUSetCPUs string `json:"CpusetCpus,omitempty" yaml:"CpusetCpus,omitempty"`
CPUSetMEMs string `json:"CpusetMems,omitempty" yaml:"CpusetMems,omitempty"`
CPUQuota int64 `json:"CpuQuota,omitempty" yaml:"CpuQuota,omitempty"`
CPUPeriod int64 `json:"CpuPeriod,omitempty" yaml:"CpuPeriod,omitempty"`
BlkioWeight int64 `json:"BlkioWeight,omitempty" yaml:"BlkioWeight"`
BlkioWeightDevice []BlockWeight `json:"BlkioWeightDevice,omitempty" yaml:"BlkioWeightDevice"`
BlkioDeviceReadBps []BlockLimit `json:"BlkioDeviceReadBps,omitempty" yaml:"BlkioDeviceReadBps"`
BlkioDeviceReadIOps []BlockLimit `json:"BlkioDeviceReadIOps,omitempty" yaml:"BlkioDeviceReadIOps"`
BlkioDeviceWriteBps []BlockLimit `json:"BlkioDeviceWriteBps,omitempty" yaml:"BlkioDeviceWriteBps"`
BlkioDeviceWriteIOps []BlockLimit `json:"BlkioDeviceWriteIOps,omitempty" yaml:"BlkioDeviceWriteIOps"`
BlkioWeight int64 `json:"BlkioWeight,omitempty" yaml:"BlkioWeight,omitempty"`
BlkioWeightDevice []BlockWeight `json:"BlkioWeightDevice,omitempty" yaml:"BlkioWeightDevice,omitempty"`
BlkioDeviceReadBps []BlockLimit `json:"BlkioDeviceReadBps,omitempty" yaml:"BlkioDeviceReadBps,omitempty"`
BlkioDeviceReadIOps []BlockLimit `json:"BlkioDeviceReadIOps,omitempty" yaml:"BlkioDeviceReadIOps,omitempty"`
BlkioDeviceWriteBps []BlockLimit `json:"BlkioDeviceWriteBps,omitempty" yaml:"BlkioDeviceWriteBps,omitempty"`
BlkioDeviceWriteIOps []BlockLimit `json:"BlkioDeviceWriteIOps,omitempty" yaml:"BlkioDeviceWriteIOps,omitempty"`
Ulimits []ULimit `json:"Ulimits,omitempty" yaml:"Ulimits,omitempty"`
VolumeDriver string `json:"VolumeDriver,omitempty" yaml:"VolumeDriver,omitempty"`
OomScoreAdj int `json:"OomScoreAdj,omitempty" yaml:"OomScoreAdj,omitempty"`
PidsLimit int64 `json:"PidsLimit,omitempty" yaml:"PidsLimit,omitempty"`
ShmSize int64 `json:"ShmSize,omitempty" yaml:"ShmSize,omitempty"`
Tmpfs map[string]string `json:"Tmpfs,omitempty" yaml:"Tmpfs,omitempty"`
AutoRemove bool `json:"AutoRemove,omitempty" yaml:"AutoRemove,omitempty"`
StorageOpt map[string]string `json:"StorageOpt,omitempty" yaml:"StorageOpt,omitempty"`
Sysctls map[string]string `json:"Sysctls,omitempty" yaml:"Sysctls,omitempty"`
}
// NetworkingConfig represents the container's networking configuration for each of its interfaces
// Carries the networking configs specified in the `docker run` and `docker network connect` commands
type NetworkingConfig struct {
EndpointsConfig map[string]*EndpointConfig // Endpoint configs for each connecting network
EndpointsConfig map[string]*EndpointConfig `json:"EndpointsConfig" yaml:"EndpointsConfig"` // Endpoint configs for each connecting network
}
// StartContainer starts a container, returning an error in case of failure.
//
// Passing the HostConfig to this method has been deprecated in Docker API 1.22
// (Docker Engine 1.10.x) and totally removed in Docker API 1.24 (Docker Engine
// 1.12.x). The client will ignore the parameter when communicating with Docker
// API 1.24 or greater.
//
// See https://goo.gl/MrBAJv for more details.
func (c *Client) StartContainer(id string, hostConfig *HostConfig) error {
var opts doOptions
path := "/containers/" + id + "/start"
resp, err := c.do("POST", path, doOptions{data: hostConfig, forceJSON: true})
if c.serverAPIVersion == nil {
c.checkAPIVersion()
}
if c.serverAPIVersion != nil && c.serverAPIVersion.LessThan(apiVersion124) {
opts = doOptions{data: hostConfig, forceJSON: true}
}
resp, err := c.do("POST", path, opts)
if err != nil {
if e, ok := err.(*Error); ok && e.Status == http.StatusNotFound {
return &NoSuchContainer{ID: id, Err: err}
@@ -897,6 +953,7 @@ type StatsOptions struct {
// Timeout with no data is received, it's reset every time new data
// arrives
InactivityTimeout time.Duration `qs:"-"`
Context context.Context
}
// Stats sends container statistics for the given container to the given channel.
@@ -936,6 +993,7 @@ func (c *Client) Stats(opts StatsOptions) (retErr error) {
stdout: writeCloser,
timeout: opts.Timeout,
inactivityTimeout: opts.InactivityTimeout,
context: opts.Context,
})
if err != nil {
dockerError, ok := err.(*Error)
@@ -986,7 +1044,8 @@ type KillContainerOptions struct {
// The signal to send to the container. When omitted, Docker server
// will assume SIGKILL.
Signal Signal
Signal Signal
Context context.Context
}
// KillContainer sends a signal to a container, returning an error in case of
@@ -995,7 +1054,7 @@ type KillContainerOptions struct {
// See https://goo.gl/hkS9i8 for more details.
func (c *Client) KillContainer(opts KillContainerOptions) error {
path := "/containers/" + opts.ID + "/kill" + "?" + queryString(opts)
resp, err := c.do("POST", path, doOptions{})
resp, err := c.do("POST", path, doOptions{context: opts.Context})
if err != nil {
if e, ok := err.(*Error); ok && e.Status == http.StatusNotFound {
return &NoSuchContainer{ID: opts.ID}
@@ -1019,7 +1078,8 @@ type RemoveContainerOptions struct {
// A flag that indicates whether Docker should remove the container
// even if it is currently running.
Force bool
Force bool
Context context.Context
}
// RemoveContainer removes a container, returning an error in case of failure.
@@ -1027,7 +1087,7 @@ type RemoveContainerOptions struct {
// See https://goo.gl/RQyX62 for more details.
func (c *Client) RemoveContainer(opts RemoveContainerOptions) error {
path := "/containers/" + opts.ID + "?" + queryString(opts)
resp, err := c.do("DELETE", path, doOptions{})
resp, err := c.do("DELETE", path, doOptions{context: opts.Context})
if err != nil {
if e, ok := err.(*Error); ok && e.Status == http.StatusNotFound {
return &NoSuchContainer{ID: opts.ID}
@@ -1046,6 +1106,7 @@ type UploadToContainerOptions struct {
InputStream io.Reader `json:"-" qs:"-"`
Path string `qs:"path"`
NoOverwriteDirNonDir bool `qs:"noOverwriteDirNonDir"`
Context context.Context
}
// UploadToContainer uploads a tar archive to be extracted to a path in the
@@ -1056,7 +1117,8 @@ func (c *Client) UploadToContainer(id string, opts UploadToContainerOptions) err
url := fmt.Sprintf("/containers/%s/archive?", id) + queryString(opts)
return c.stream("PUT", url, streamOptions{
in: opts.InputStream,
in: opts.InputStream,
context: opts.Context,
})
}
@@ -1068,6 +1130,7 @@ type DownloadFromContainerOptions struct {
OutputStream io.Writer `json:"-" qs:"-"`
Path string `qs:"path"`
InactivityTimeout time.Duration `qs:"-"`
Context context.Context
}
// DownloadFromContainer downloads a tar archive of files or folders in a container.
@@ -1080,6 +1143,7 @@ func (c *Client) DownloadFromContainer(id string, opts DownloadFromContainerOpti
setRawTerminal: true,
stdout: opts.OutputStream,
inactivityTimeout: opts.InactivityTimeout,
context: opts.Context,
})
}
@@ -1090,6 +1154,7 @@ type CopyFromContainerOptions struct {
OutputStream io.Writer `json:"-"`
Container string `json:"-"`
Resource string
Context context.Context `json:"-"`
}
// CopyFromContainer has been DEPRECATED, please use DownloadFromContainerOptions along with DownloadFromContainer.
@@ -1100,7 +1165,10 @@ func (c *Client) CopyFromContainer(opts CopyFromContainerOptions) error {
return &NoSuchContainer{ID: opts.Container}
}
url := fmt.Sprintf("/containers/%s/copy", opts.Container)
resp, err := c.do("POST", url, doOptions{data: opts})
resp, err := c.do("POST", url, doOptions{
data: opts,
context: opts.Context,
})
if err != nil {
if e, ok := err.(*Error); ok && e.Status == http.StatusNotFound {
return &NoSuchContainer{ID: opts.Container}
@@ -1142,6 +1210,7 @@ type CommitContainerOptions struct {
Message string `qs:"comment"`
Author string
Run *Config `qs:"-"`
Context context.Context
}
// CommitContainer creates a new image from a container's changes.
@@ -1149,7 +1218,10 @@ type CommitContainerOptions struct {
// See https://goo.gl/mqfoCw for more details.
func (c *Client) CommitContainer(opts CommitContainerOptions) (*Image, error) {
path := "/commit?" + queryString(opts)
resp, err := c.do("POST", path, doOptions{data: opts.Run})
resp, err := c.do("POST", path, doOptions{
data: opts.Run,
context: opts.Context,
})
if err != nil {
if e, ok := err.(*Error); ok && e.Status == http.StatusNotFound {
return nil, &NoSuchContainer{ID: opts.Container}
@@ -1247,6 +1319,7 @@ type LogsOptions struct {
// Use raw terminal? Usually true when the container contains a TTY.
RawTerminal bool `qs:"-"`
Context context.Context
}
// Logs gets stdout and stderr logs from the specified container.
@@ -1265,6 +1338,7 @@ func (c *Client) Logs(opts LogsOptions) error {
stdout: opts.OutputStream,
stderr: opts.ErrorStream,
inactivityTimeout: opts.InactivityTimeout,
context: opts.Context,
})
}
@@ -1291,6 +1365,7 @@ type ExportContainerOptions struct {
ID string
OutputStream io.Writer
InactivityTimeout time.Duration `qs:"-"`
Context context.Context
}
// ExportContainer export the contents of container id as tar archive
@@ -1306,6 +1381,7 @@ func (c *Client) ExportContainer(opts ExportContainerOptions) error {
setRawTerminal: true,
stdout: opts.OutputStream,
inactivityTimeout: opts.InactivityTimeout,
context: opts.Context,
})
}

View File

@@ -109,7 +109,7 @@ func (c *Client) RemoveEventListener(listener chan *APIEvents) error {
if err != nil {
return err
}
if len(c.eventMonitor.listeners) == 0 {
if c.eventMonitor.listernersCount() == 0 {
c.eventMonitor.disableEventMonitoring()
}
return nil
@@ -150,6 +150,12 @@ func (eventState *eventMonitoringState) closeListeners() {
eventState.listeners = nil
}
func (eventState *eventMonitoringState) listernersCount() int {
eventState.RLock()
defer eventState.RUnlock()
return len(eventState.listeners)
}
func listenerExists(a chan<- *APIEvents, list *[]chan<- *APIEvents) bool {
for _, b := range *list {
if b == a {

View File

@@ -11,6 +11,8 @@ import (
"net/http"
"net/url"
"strconv"
"golang.org/x/net/context"
)
// Exec is the type representing a `docker exec` instance and containing the
@@ -23,13 +25,14 @@ type Exec struct {
//
// See https://goo.gl/1KSIb7 for more details
type CreateExecOptions struct {
AttachStdin bool `json:"AttachStdin,omitempty" yaml:"AttachStdin,omitempty"`
AttachStdout bool `json:"AttachStdout,omitempty" yaml:"AttachStdout,omitempty"`
AttachStderr bool `json:"AttachStderr,omitempty" yaml:"AttachStderr,omitempty"`
Tty bool `json:"Tty,omitempty" yaml:"Tty,omitempty"`
Cmd []string `json:"Cmd,omitempty" yaml:"Cmd,omitempty"`
Container string `json:"Container,omitempty" yaml:"Container,omitempty"`
User string `json:"User,omitempty" yaml:"User,omitempty"`
AttachStdin bool `json:"AttachStdin,omitempty" yaml:"AttachStdin,omitempty"`
AttachStdout bool `json:"AttachStdout,omitempty" yaml:"AttachStdout,omitempty"`
AttachStderr bool `json:"AttachStderr,omitempty" yaml:"AttachStderr,omitempty"`
Tty bool `json:"Tty,omitempty" yaml:"Tty,omitempty"`
Cmd []string `json:"Cmd,omitempty" yaml:"Cmd,omitempty"`
Container string `json:"Container,omitempty" yaml:"Container,omitempty"`
User string `json:"User,omitempty" yaml:"User,omitempty"`
Context context.Context `json:"-"`
}
// CreateExec sets up an exec instance in a running container `id`, returning the exec
@@ -38,7 +41,7 @@ type CreateExecOptions struct {
// See https://goo.gl/1KSIb7 for more details
func (c *Client) CreateExec(opts CreateExecOptions) (*Exec, error) {
path := fmt.Sprintf("/containers/%s/exec", opts.Container)
resp, err := c.do("POST", path, doOptions{data: opts})
resp, err := c.do("POST", path, doOptions{data: opts, context: opts.Context})
if err != nil {
if e, ok := err.(*Error); ok && e.Status == http.StatusNotFound {
return nil, &NoSuchContainer{ID: opts.Container}
@@ -75,6 +78,8 @@ type StartExecOptions struct {
// It must be an unbuffered channel. Using a buffered channel can lead
// to unexpected behavior.
Success chan struct{} `json:"-"`
Context context.Context `json:"-"`
}
// StartExec starts a previously set up exec instance id. If opts.Detach is
@@ -106,7 +111,7 @@ func (c *Client) StartExecNonBlocking(id string, opts StartExecOptions) (CloseWa
path := fmt.Sprintf("/exec/%s/start", id)
if opts.Detach {
resp, err := c.do("POST", path, doOptions{data: opts})
resp, err := c.do("POST", path, doOptions{data: opts, context: opts.Context})
if err != nil {
if e, ok := err.(*Error); ok && e.Status == http.StatusNotFound {
return nil, &NoSuchExec{ID: id}

View File

@@ -15,6 +15,8 @@ import (
"net/url"
"os"
"time"
"golang.org/x/net/context"
)
// APIImages represent an image returned in the ListImages call.
@@ -99,6 +101,7 @@ type ListImagesOptions struct {
Filters map[string][]string
Digests bool
Filter string
Context context.Context
}
// ListImages returns the list of available images in the server.
@@ -106,7 +109,7 @@ type ListImagesOptions struct {
// See https://goo.gl/xBe1u3 for more details.
func (c *Client) ListImages(opts ListImagesOptions) ([]APIImages, error) {
path := "/images/json?" + queryString(opts)
resp, err := c.do("GET", path, doOptions{})
resp, err := c.do("GET", path, doOptions{context: opts.Context})
if err != nil {
return nil, err
}
@@ -169,6 +172,7 @@ func (c *Client) RemoveImage(name string) error {
type RemoveImageOptions struct {
Force bool `qs:"force"`
NoPrune bool `qs:"noprune"`
Context context.Context
}
// RemoveImageExtended removes an image by its name or ID.
@@ -177,7 +181,7 @@ type RemoveImageOptions struct {
// See https://goo.gl/V3ZWnK for more details.
func (c *Client) RemoveImageExtended(name string, opts RemoveImageOptions) error {
uri := fmt.Sprintf("/images/%s?%s", name, queryString(&opts))
resp, err := c.do("DELETE", uri, doOptions{})
resp, err := c.do("DELETE", uri, doOptions{context: opts.Context})
if err != nil {
if e, ok := err.(*Error); ok && e.Status == http.StatusNotFound {
return ErrNoSuchImage
@@ -246,6 +250,8 @@ type PushImageOptions struct {
OutputStream io.Writer `qs:"-"`
RawJSONStream bool `qs:"-"`
InactivityTimeout time.Duration `qs:"-"`
Context context.Context
}
// PushImage pushes an image to a remote registry, logging progress to w.
@@ -271,6 +277,7 @@ func (c *Client) PushImage(opts PushImageOptions, auth AuthConfiguration) error
headers: headers,
stdout: opts.OutputStream,
inactivityTimeout: opts.InactivityTimeout,
context: opts.Context,
})
}
@@ -280,12 +287,17 @@ func (c *Client) PushImage(opts PushImageOptions, auth AuthConfiguration) error
// See https://goo.gl/iJkZjD for more details.
type PullImageOptions struct {
Repository string `qs:"fromImage"`
Registry string
Tag string
// Only required for Docker Engine 1.9 or 1.10 w/ Remote API < 1.21
// and Docker Engine < 1.9
// This parameter was removed in Docker Engine 1.11
Registry string
OutputStream io.Writer `qs:"-"`
RawJSONStream bool `qs:"-"`
InactivityTimeout time.Duration `qs:"-"`
Context context.Context
}
// PullImage pulls an image from a remote registry, logging progress to
@@ -301,10 +313,10 @@ func (c *Client) PullImage(opts PullImageOptions, auth AuthConfiguration) error
if err != nil {
return err
}
return c.createImage(queryString(&opts), headers, nil, opts.OutputStream, opts.RawJSONStream, opts.InactivityTimeout)
return c.createImage(queryString(&opts), headers, nil, opts.OutputStream, opts.RawJSONStream, opts.InactivityTimeout, opts.Context)
}
func (c *Client) createImage(qs string, headers map[string]string, in io.Reader, w io.Writer, rawJSONStream bool, timeout time.Duration) error {
func (c *Client) createImage(qs string, headers map[string]string, in io.Reader, w io.Writer, rawJSONStream bool, timeout time.Duration, context context.Context) error {
path := "/images/create?" + qs
return c.stream("POST", path, streamOptions{
setRawTerminal: true,
@@ -313,6 +325,7 @@ func (c *Client) createImage(qs string, headers map[string]string, in io.Reader,
stdout: w,
rawJSONStream: rawJSONStream,
inactivityTimeout: timeout,
context: context,
})
}
@@ -321,6 +334,7 @@ func (c *Client) createImage(qs string, headers map[string]string, in io.Reader,
// See https://goo.gl/JyClMX for more details.
type LoadImageOptions struct {
InputStream io.Reader
Context context.Context
}
// LoadImage imports a tarball docker image
@@ -330,6 +344,7 @@ func (c *Client) LoadImage(opts LoadImageOptions) error {
return c.stream("POST", "/images/load", streamOptions{
setRawTerminal: true,
in: opts.InputStream,
context: opts.Context,
})
}
@@ -339,7 +354,8 @@ func (c *Client) LoadImage(opts LoadImageOptions) error {
type ExportImageOptions struct {
Name string
OutputStream io.Writer
InactivityTimeout time.Duration `qs:"-"`
InactivityTimeout time.Duration
Context context.Context
}
// ExportImage exports an image (as a tar file) into the stream.
@@ -350,6 +366,7 @@ func (c *Client) ExportImage(opts ExportImageOptions) error {
setRawTerminal: true,
stdout: opts.OutputStream,
inactivityTimeout: opts.InactivityTimeout,
context: opts.Context,
})
}
@@ -360,6 +377,7 @@ type ExportImagesOptions struct {
Names []string
OutputStream io.Writer `qs:"-"`
InactivityTimeout time.Duration `qs:"-"`
Context context.Context
}
// ExportImages exports one or more images (as a tar file) into the stream
@@ -389,6 +407,7 @@ type ImportImageOptions struct {
OutputStream io.Writer `qs:"-"`
RawJSONStream bool `qs:"-"`
InactivityTimeout time.Duration `qs:"-"`
Context context.Context
}
// ImportImage imports an image from a url, a file or stdin
@@ -409,7 +428,7 @@ func (c *Client) ImportImage(opts ImportImageOptions) error {
opts.InputStream = f
opts.Source = "-"
}
return c.createImage(queryString(&opts), nil, opts.InputStream, opts.OutputStream, opts.RawJSONStream, opts.InactivityTimeout)
return c.createImage(queryString(&opts), nil, opts.InputStream, opts.OutputStream, opts.RawJSONStream, opts.InactivityTimeout, opts.Context)
}
// BuildImageOptions present the set of informations available for building an
@@ -441,6 +460,7 @@ type BuildImageOptions struct {
Ulimits []ULimit `qs:"-"`
BuildArgs []BuildArg `qs:"-"`
InactivityTimeout time.Duration `qs:"-"`
Context context.Context
}
// BuildArg represents arguments that can be passed to the image when building
@@ -512,6 +532,7 @@ func (c *Client) BuildImage(opts BuildImageOptions) error {
in: opts.InputStream,
stdout: opts.OutputStream,
inactivityTimeout: opts.InactivityTimeout,
context: opts.Context,
})
}
@@ -529,9 +550,10 @@ func (c *Client) versionedAuthConfigs(authConfigs AuthConfigurations) interface{
//
// See https://goo.gl/98ZzkU for more details.
type TagImageOptions struct {
Repo string
Tag string
Force bool
Repo string
Tag string
Force bool
Context context.Context
}
// TagImage adds a tag to the image identified by the given name.
@@ -541,8 +563,9 @@ func (c *Client) TagImage(name string, opts TagImageOptions) error {
if name == "" {
return ErrNoSuchImage
}
resp, err := c.do("POST", fmt.Sprintf("/images/"+name+"/tag?%s",
queryString(&opts)), doOptions{})
resp, err := c.do("POST", "/images/"+name+"/tag?"+queryString(&opts), doOptions{
context: opts.Context,
})
if err != nil {
return err

View File

@@ -10,6 +10,8 @@ import (
"errors"
"fmt"
"net/http"
"golang.org/x/net/context"
)
// ErrNetworkAlreadyExists is the error returned by CreateNetwork when the
@@ -108,21 +110,23 @@ func (c *Client) NetworkInfo(id string) (*Network, error) {
//
// See https://goo.gl/6GugX3 for more details.
type CreateNetworkOptions struct {
Name string `json:"Name"`
CheckDuplicate bool `json:"CheckDuplicate"`
Driver string `json:"Driver"`
IPAM IPAMOptions `json:"IPAM"`
Options map[string]interface{} `json:"Options"`
Internal bool `json:"Internal"`
EnableIPv6 bool `json:"EnableIPv6"`
Name string `json:"Name" yaml:"Name"`
CheckDuplicate bool `json:"CheckDuplicate" yaml:"CheckDuplicate"`
Driver string `json:"Driver" yaml:"Driver"`
IPAM IPAMOptions `json:"IPAM" yaml:"IPAM"`
Options map[string]interface{} `json:"Options" yaml:"Options"`
Label map[string]string `json:"Labels" yaml:"Labels"`
Internal bool `json:"Internal" yaml:"Internal"`
EnableIPv6 bool `json:"EnableIPv6" yaml:"EnableIPv6"`
Context context.Context `json:"-"`
}
// IPAMOptions controls IP Address Management when creating a network
//
// See https://goo.gl/T8kRVH for more details.
type IPAMOptions struct {
Driver string `json:"Driver"`
Config []IPAMConfig `json:"Config"`
Driver string `json:"Driver" yaml:"Driver"`
Config []IPAMConfig `json:"Config" yaml:"Config"`
}
// IPAMConfig represents IPAM configurations
@@ -144,7 +148,8 @@ func (c *Client) CreateNetwork(opts CreateNetworkOptions) (*Network, error) {
"POST",
"/networks/create",
doOptions{
data: opts,
data: opts,
context: opts.Context,
},
)
if err != nil {
@@ -200,24 +205,26 @@ type NetworkConnectionOptions struct {
// Force is only applicable to the DisconnectNetwork call
Force bool
Context context.Context `json:"-"`
}
// EndpointConfig stores network endpoint details
//
// See https://goo.gl/RV7BJU for more details.
type EndpointConfig struct {
IPAMConfig *EndpointIPAMConfig
Links []string
Aliases []string
NetworkID string
EndpointID string
Gateway string
IPAddress string
IPPrefixLen int
IPv6Gateway string
GlobalIPv6Address string
GlobalIPv6PrefixLen int
MacAddress string
IPAMConfig *EndpointIPAMConfig `json:"IPAMConfig,omitempty" yaml:"IPAMConfig,omitempty"`
Links []string `json:"Links,omitempty" yaml:"Links,omitempty"`
Aliases []string `json:"Aliases,omitempty" yaml:"Aliases,omitempty"`
NetworkID string `json:"NetworkID,omitempty" yaml:"NetworkID,omitempty"`
EndpointID string `json:"EndpointID,omitempty" yaml:"EndpointID,omitempty"`
Gateway string `json:"Gateway,omitempty" yaml:"Gateway,omitempty"`
IPAddress string `json:"IPAddress,omitempty" yaml:"IPAddress,omitempty"`
IPPrefixLen int `json:"IPPrefixLen,omitempty" yaml:"IPPrefixLen,omitempty"`
IPv6Gateway string `json:"IPv6Gateway,omitempty" yaml:"IPv6Gateway,omitempty"`
GlobalIPv6Address string `json:"GlobalIPv6Address,omitempty" yaml:"GlobalIPv6Address,omitempty"`
GlobalIPv6PrefixLen int `json:"GlobalIPv6PrefixLen,omitempty" yaml:"GlobalIPv6PrefixLen,omitempty"`
MacAddress string `json:"MacAddress,omitempty" yaml:"MacAddress,omitempty"`
}
// EndpointIPAMConfig represents IPAM configurations for an
@@ -234,7 +241,10 @@ type EndpointIPAMConfig struct {
//
// See https://goo.gl/6GugX3 for more details.
func (c *Client) ConnectNetwork(id string, opts NetworkConnectionOptions) error {
resp, err := c.do("POST", "/networks/"+id+"/connect", doOptions{data: opts})
resp, err := c.do("POST", "/networks/"+id+"/connect", doOptions{
data: opts,
context: opts.Context,
})
if err != nil {
if e, ok := err.(*Error); ok && e.Status == http.StatusNotFound {
return &NoSuchNetworkOrContainer{NetworkID: id, ContainerID: opts.Container}

View File

@@ -9,9 +9,12 @@ package testing
import (
"archive/tar"
"crypto/rand"
"crypto/tls"
"crypto/x509"
"encoding/json"
"errors"
"fmt"
"io"
"io/ioutil"
mathrand "math/rand"
"net"
@@ -66,6 +69,22 @@ type volumeCounter struct {
count int
}
func buildDockerServer(listener net.Listener, containerChan chan<- *docker.Container, hook func(*http.Request)) *DockerServer {
server := DockerServer{
listener: listener,
imgIDs: make(map[string]string),
hook: hook,
failures: make(map[string]string),
execCallbacks: make(map[string]func()),
statsCallbacks: make(map[string]func(string) docker.Stats),
customHandlers: make(map[string]http.Handler),
uploadedFiles: make(map[string]string),
cChan: containerChan,
}
server.buildMuxer()
return &server
}
// NewServer returns a new instance of the fake server, in standalone mode. Use
// the method URL to get the URL of the server.
//
@@ -82,20 +101,41 @@ func NewServer(bind string, containerChan chan<- *docker.Container, hook func(*h
if err != nil {
return nil, err
}
server := DockerServer{
listener: listener,
imgIDs: make(map[string]string),
hook: hook,
failures: make(map[string]string),
execCallbacks: make(map[string]func()),
statsCallbacks: make(map[string]func(string) docker.Stats),
customHandlers: make(map[string]http.Handler),
uploadedFiles: make(map[string]string),
cChan: containerChan,
server := buildDockerServer(listener, containerChan, hook)
go http.Serve(listener, server)
return server, nil
}
type TLSConfig struct {
CertPath string
CertKeyPath string
RootCAPath string
}
func NewTLSServer(bind string, containerChan chan<- *docker.Container, hook func(*http.Request), tlsConfig TLSConfig) (*DockerServer, error) {
listener, err := net.Listen("tcp", bind)
if err != nil {
return nil, err
}
server.buildMuxer()
go http.Serve(listener, &server)
return &server, nil
defaultCertificate, err := tls.LoadX509KeyPair(tlsConfig.CertPath, tlsConfig.CertKeyPath)
if err != nil {
return nil, err
}
tlsServerConfig := new(tls.Config)
tlsServerConfig.Certificates = []tls.Certificate{defaultCertificate}
if tlsConfig.RootCAPath != "" {
rootCertPEM, err := ioutil.ReadFile(tlsConfig.RootCAPath)
if err != nil {
return nil, err
}
certsPool := x509.NewCertPool()
certsPool.AppendCertsFromPEM(rootCertPEM)
tlsServerConfig.RootCAs = certsPool
}
tlsListener := tls.NewListener(listener, tlsServerConfig)
server := buildDockerServer(tlsListener, containerChan, hook)
go http.Serve(tlsListener, server)
return server, nil
}
func (s *DockerServer) notify(container *docker.Container) {
@@ -145,6 +185,7 @@ func (s *DockerServer) buildMuxer() {
s.mux.Path("/volumes/{name:.*}").Methods("GET").HandlerFunc(s.handlerWrapper(s.inspectVolume))
s.mux.Path("/volumes/{name:.*}").Methods("DELETE").HandlerFunc(s.handlerWrapper(s.removeVolume))
s.mux.Path("/info").Methods("GET").HandlerFunc(s.handlerWrapper(s.infoDocker))
s.mux.Path("/version").Methods("GET").HandlerFunc(s.handlerWrapper(s.versionDocker))
}
// SetHook changes the hook function used by the server.
@@ -557,14 +598,22 @@ func (s *DockerServer) startContainer(w http.ResponseWriter, r *http.Request) {
s.cMut.Lock()
defer s.cMut.Unlock()
defer r.Body.Close()
var hostConfig docker.HostConfig
if container.State.Running {
http.Error(w, "", http.StatusNotModified)
return
}
var hostConfig *docker.HostConfig
err = json.NewDecoder(r.Body).Decode(&hostConfig)
if err != nil {
if err != nil && err != io.EOF {
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
container.HostConfig = &hostConfig
if len(hostConfig.PortBindings) > 0 {
if hostConfig == nil {
hostConfig = container.HostConfig
} else {
container.HostConfig = hostConfig
}
if hostConfig != nil && len(hostConfig.PortBindings) > 0 {
ports := map[docker.Port][]docker.PortBinding{}
for key, items := range hostConfig.PortBindings {
bindings := make([]docker.PortBinding, len(items))
@@ -585,10 +634,6 @@ func (s *DockerServer) startContainer(w http.ResponseWriter, r *http.Request) {
}
container.NetworkSettings.Ports = ports
}
if container.State.Running {
http.Error(w, "", http.StatusNotModified)
return
}
container.State.Running = true
s.notify(container)
}
@@ -1332,3 +1377,19 @@ func (s *DockerServer) infoDocker(w http.ResponseWriter, r *http.Request) {
w.WriteHeader(http.StatusOK)
json.NewEncoder(w).Encode(envs)
}
func (s *DockerServer) versionDocker(w http.ResponseWriter, r *http.Request) {
envs := map[string]interface{}{
"Version": "1.10.1",
"Os": "linux",
"KernelVersion": "3.13.0-77-generic",
"GoVersion": "go1.4.2",
"GitCommit": "9e83765",
"Arch": "amd64",
"ApiVersion": "1.22",
"BuildTime": "2015-12-01T07:09:13.444803460+00:00",
"Experimental": false,
}
w.WriteHeader(http.StatusOK)
json.NewEncoder(w).Encode(envs)
}

View File

@@ -8,6 +8,8 @@ import (
"encoding/json"
"errors"
"net/http"
"golang.org/x/net/context"
)
var (
@@ -33,13 +35,16 @@ type Volume struct {
// See https://goo.gl/FZA4BK for more details.
type ListVolumesOptions struct {
Filters map[string][]string
Context context.Context
}
// ListVolumes returns a list of available volumes in the server.
//
// See https://goo.gl/FZA4BK for more details.
func (c *Client) ListVolumes(opts ListVolumesOptions) ([]Volume, error) {
resp, err := c.do("GET", "/volumes?"+queryString(opts), doOptions{})
resp, err := c.do("GET", "/volumes?"+queryString(opts), doOptions{
context: opts.Context,
})
if err != nil {
return nil, err
}
@@ -70,13 +75,17 @@ type CreateVolumeOptions struct {
Name string
Driver string
DriverOpts map[string]string
Context context.Context `json:"-"`
}
// CreateVolume creates a volume on the server.
//
// See https://goo.gl/pBUbZ9 for more details.
func (c *Client) CreateVolume(opts CreateVolumeOptions) (*Volume, error) {
resp, err := c.do("POST", "/volumes/create", doOptions{data: opts})
resp, err := c.do("POST", "/volumes/create", doOptions{
data: opts,
context: opts.Context,
})
if err != nil {
return nil, err
}

2
vendor/manifest vendored
View File

@@ -753,7 +753,7 @@
"importpath": "github.com/fsouza/go-dockerclient",
"repository": "https://github.com/fsouza/go-dockerclient",
"vcs": "git",
"revision": "3c8f092cb1e9d1e18a07c1d05d993e69a6676097",
"revision": "4a2e5288244cd60febbfc7bdd07891eab9efa6c1",
"branch": "master",
"notests": true
},