Merge pull request #1235 from weaveworks/970-reinstating-restarting-ruckus

Update docker client, to get better state strings in the UI
This commit is contained in:
Tom Wilkie
2016-04-06 18:23:34 +02:00
39 changed files with 2019 additions and 206 deletions

View File

@@ -31,6 +31,7 @@ const (
ContainerHostname = "docker_container_hostname"
ContainerIPsWithScopes = "docker_container_ips_with_scopes"
ContainerState = "docker_container_state"
ContainerStateHuman = "docker_container_state_human"
ContainerUptime = "docker_container_uptime"
ContainerRestartCount = "docker_container_restart_count"
ContainerNetworkMode = "docker_container_network_mode"
@@ -55,9 +56,12 @@ const (
CPUUsageInKernelmode = "docker_cpu_usage_in_kernelmode"
CPUSystemCPUUsage = "docker_cpu_system_cpu_usage"
StateRunning = "running"
StateStopped = "stopped"
StatePaused = "paused"
StateCreated = "created"
StateDead = "dead"
StateExited = "exited"
StatePaused = "paused"
StateRestarting = "restarting"
StateRunning = "running"
NetworkModeHost = "host"
@@ -90,6 +94,7 @@ type Container interface {
Hostname() string
GetNode(string, []net.IP) report.Node
State() string
StateString() string
HasTTY() bool
Container() *docker.Container
StartGatheringStats() error
@@ -144,12 +149,11 @@ func (c *container) HasTTY() bool {
}
func (c *container) State() string {
if c.container.State.Paused {
return StatePaused
} else if c.container.State.Running {
return StateRunning
}
return StateStopped
return c.container.State.String()
}
func (c *container) StateString() string {
return c.container.State.StateString()
}
func (c *container) Container() *docker.Container {
@@ -333,16 +337,15 @@ func (c *container) GetNode(hostID string, localAddrs []net.IP) report.Node {
ipsWithScopes = append(ipsWithScopes, report.MakeScopedAddressNodeID(hostID, ip))
}
state := c.State()
result := report.MakeNodeWith(map[string]string{
ContainerID: c.ID(),
ContainerName: strings.TrimPrefix(c.container.Name, "/"),
ContainerCreated: c.container.Created.Format(time.RFC822),
ContainerCommand: c.container.Path + " " + strings.Join(c.container.Args, " "),
ImageID: c.Image(),
ContainerHostname: c.Hostname(),
ContainerState: state,
ContainerID: c.ID(),
ContainerName: strings.TrimPrefix(c.container.Name, "/"),
ContainerCreated: c.container.Created.Format(time.RFC822),
ContainerCommand: c.container.Path + " " + strings.Join(c.container.Args, " "),
ImageID: c.Image(),
ContainerHostname: c.Hostname(),
ContainerState: c.StateString(),
ContainerStateHuman: c.State(),
}).WithSets(report.EmptySets.
Add(ContainerPorts, c.ports(localAddrs)).
Add(ContainerIPs, report.MakeStringSet(ips...)).
@@ -390,3 +393,9 @@ func ExtractContainerIPsWithScopes(nmd report.Node) []string {
v, _ := nmd.Sets.Lookup(ContainerIPsWithScopes)
return []string(v)
}
// ContainerIsStopped checks if the docker container is in one of our "stopped" states
func ContainerIsStopped(c Container) bool {
state := c.StateString()
return (state != StateRunning && state != StateRestarting && state != StatePaused)
}

View File

@@ -74,15 +74,16 @@ func TestContainer(t *testing.T) {
// Now see if we go them
uptime := (now.Sub(startTime) / time.Second) * time.Second
want := report.MakeNode().WithLatests(map[string]string{
"docker_container_command": " ",
"docker_container_created": "01 Jan 01 00:00 UTC",
"docker_container_id": "ping",
"docker_container_name": "pong",
"docker_image_id": "baz",
"docker_label_foo1": "bar1",
"docker_label_foo2": "bar2",
"docker_container_state": "running",
"docker_container_uptime": uptime.String(),
"docker_container_command": " ",
"docker_container_created": "01 Jan 01 00:00 UTC",
"docker_container_id": "ping",
"docker_container_name": "pong",
"docker_image_id": "baz",
"docker_label_foo1": "bar1",
"docker_label_foo2": "bar2",
"docker_container_state": "running",
"docker_container_state_human": "Up 6 years",
"docker_container_uptime": uptime.String(),
}).WithSets(report.EmptySets.
Add("docker_container_ports", report.MakeStringSet("1.2.3.4:80->80/tcp", "81/tcp")).
Add("docker_container_ips", report.MakeStringSet("1.2.3.4")).

View File

@@ -39,6 +39,10 @@ func (c *mockContainer) Hostname() string {
}
func (c *mockContainer) State() string {
return "Up 3 minutes"
}
func (c *mockContainer) StateString() string {
return docker.StateRunning
}

View File

@@ -78,7 +78,7 @@ func (t *Tagger) tag(tree process.Tree, topology *report.Topology) {
}
})
if c == nil || c.State() == StateStopped || c.PID() == 1 {
if c == nil || ContainerIsStopped(c) || c.PID() == 1 {
continue
}

View File

@@ -20,7 +20,7 @@ var labels = map[string]string{
docker.ContainerIPs: "IPs",
docker.ContainerPorts: "Ports",
docker.ContainerRestartCount: "Restart #",
docker.ContainerState: "State",
docker.ContainerStateHuman: "State",
docker.ContainerUptime: "Uptime",
docker.ImageID: "Image ID",
docker.MemoryUsage: "Memory",

View File

@@ -23,7 +23,7 @@ var (
}
containerNodeMetadata = []MetadataRowTemplate{
Latest{ID: docker.ContainerID, Truncate: 12, Prime: true},
Latest{ID: docker.ContainerState, Prime: true},
Latest{ID: docker.ContainerStateHuman, Prime: true},
Latest{ID: docker.ContainerCommand, Prime: true},
Latest{ID: docker.ImageID, Truncate: 12},
Latest{ID: docker.ContainerUptime},

View File

@@ -22,13 +22,13 @@ func TestNodeMetadata(t *testing.T) {
node: report.MakeNodeWith(map[string]string{
docker.ContainerID: fixture.ClientContainerID,
docker.LabelPrefix + "label1": "label1value",
docker.ContainerState: docker.StateRunning,
docker.ContainerStateHuman: docker.StateRunning,
}).WithTopology(report.Container).WithSets(report.EmptySets.
Add(docker.ContainerIPs, report.MakeStringSet("10.10.10.0/24", "10.10.10.1/24")),
),
want: []detailed.MetadataRow{
{ID: docker.ContainerID, Value: fixture.ClientContainerID, Prime: true},
{ID: docker.ContainerState, Value: "running", Prime: true},
{ID: docker.ContainerStateHuman, Value: "running", Prime: true},
{ID: docker.ContainerIPs, Value: "10.10.10.0/24, 10.10.10.1/24"},
},
},

View File

@@ -178,7 +178,7 @@ func TestMakeDetailedContainerNode(t *testing.T) {
Pseudo: false,
Metadata: []detailed.MetadataRow{
{ID: "docker_container_id", Value: fixture.ServerContainerID, Prime: true},
{ID: "docker_container_state", Value: "running", Prime: true},
{ID: "docker_container_state_human", Value: "running", Prime: true},
{ID: "docker_image_id", Value: fixture.ServerContainerImageID},
},
DockerLabels: []detailed.MetadataRow{

View File

@@ -161,18 +161,18 @@ func FilterNoop(in Renderer) Renderer {
// FilterStopped filters out stopped containers.
func FilterStopped(r Renderer) Renderer {
return MakeFilter(IsStopped, r)
return MakeFilter(IsRunning, r)
}
// IsStopped checks if the node is a stopped docker container
func IsStopped(n report.Node) bool {
containerState, ok := n.Latest.Lookup(docker.ContainerState)
return !ok || containerState != docker.StateStopped
// IsRunning checks if the node is a running docker container
func IsRunning(n report.Node) bool {
state, ok := n.Latest.Lookup(docker.ContainerState)
return !ok || (state == docker.StateRunning || state == docker.StateRestarting || state == docker.StatePaused)
}
// FilterRunning filters out running containers.
func FilterRunning(r Renderer) Renderer {
return MakeFilter(Complement(IsStopped), r)
return MakeFilter(Complement(IsRunning), r)
}
// FilterNonProcspied removes endpoints which were not found in procspy.

View File

@@ -258,6 +258,7 @@ var (
kubernetes.PodID: ClientPodID,
kubernetes.Namespace: KubernetesNamespace,
docker.ContainerState: docker.StateRunning,
docker.ContainerStateHuman: docker.StateRunning,
}).WithID(ClientContainerNodeID).WithTopology(report.Container).WithParents(report.EmptySets.
Add("host", report.MakeStringSet(ClientHostNodeID)).
Add("container_image", report.MakeStringSet(ClientContainerImageNodeID)).
@@ -270,6 +271,7 @@ var (
docker.ContainerID: ServerContainerID,
docker.ContainerName: "task-name-5-server-aceb93e2f2b797caba01",
docker.ContainerState: docker.StateRunning,
docker.ContainerStateHuman: docker.StateRunning,
docker.ImageID: ServerContainerImageID,
report.HostNodeID: ServerHostNodeID,
docker.LabelPrefix + detailed.AmazonECSContainerNameLabel: "server",

View File

@@ -14,6 +14,7 @@ Ben Marini <ben@remind101.com>
Ben McCann <benmccann.com>
Ben Parees <bparees@redhat.com>
Benno van den Berg <bennovandenberg@gmail.com>
Bradley Cicenas <bradley.cicenas@gmail.com>
Brendan Fosberry <brendan@codeship.com>
Brian Lalor <blalor@bravo5.org>
Brian P. Hamachek <brian@brianhama.com>
@@ -48,6 +49,8 @@ Fabio Rehm <fgrehm@gmail.com>
Fatih Arslan <ftharsln@gmail.com>
Flavia Missi <flaviamissi@gmail.com>
Francisco Souza <f@souza.cc>
Frank Groeneveld <frank@frankgroeneveld.nl>
George Moura <gwmoura@gmail.com>
Grégoire Delattre <gregoire.delattre@gmail.com>
Guillermo Álvarez Fernández <guillermo@cientifico.net>
Harry Zhang <harryzhang@zju.edu.cn>
@@ -84,7 +87,9 @@ Michael Schmatz <michaelschmatz@gmail.com>
Michal Fojtik <mfojtik@redhat.com>
Mike Dillon <mike.dillon@synctree.com>
Mrunal Patel <mrunalp@gmail.com>
Nate Jones <nate@endot.org>
Nguyen Sy Thanh Son <sonnst@sigma-solutions.eu>
Nicholas Van Wiggeren <nvanwiggeren@digitalocean.com>
Nick Ethier <ncethier@gmail.com>
Omeid Matten <public@omeid.me>
Orivej Desh <orivej@gmx.fr>
@@ -98,9 +103,11 @@ Philippe Lafoucrière <philippe.lafoucriere@tech-angels.com>
Rafe Colton <rafael.colton@gmail.com>
Rob Miller <rob@kalistra.com>
Robert Williamson <williamson.robert@gmail.com>
Roman Khlystik <roman.khlystik@gmail.com>
Salvador Gironès <salvadorgirones@gmail.com>
Sam Rijs <srijs@airpost.net>
Sami Wagiaalla <swagiaal@redhat.com>
Samuel Archambault <sarchambault@lapresse.ca>
Samuel Karp <skarp@amazon.com>
Silas Sewell <silas@sewell.org>
Simon Eskildsen <sirup@sirupsen.com>

View File

@@ -1,4 +1,4 @@
Copyright (c) 2015, go-dockerclient authors
Copyright (c) 2016, go-dockerclient authors
All rights reserved.
Redistribution and use in source and binary forms, with or without

View File

@@ -11,8 +11,7 @@
cov \
clean
SRCS = $(shell git ls-files '*.go' | grep -v '^external/')
PKGS = ./. ./testing
PKGS = . ./testing
all: test
@@ -22,32 +21,31 @@ vendor:
lint:
@ go get -v github.com/golang/lint/golint
$(foreach file,$(SRCS),golint $(file) || exit;)
@for file in $$(git ls-files '*.go' | grep -v 'external/'); do \
export output="$$(golint $${file} | grep -v 'type name will be used as docker.DockerInfo')"; \
[ -n "$${output}" ] && echo "$${output}" && export status=1; \
done; \
exit $${status:-0}
vet:
@-go get -v golang.org/x/tools/cmd/vet
$(foreach pkg,$(PKGS),go vet $(pkg);)
fmt:
gofmt -w $(SRCS)
gofmt -s -w $(PKGS)
fmtcheck:
$(foreach file,$(SRCS),gofmt -d $(file);)
prepare_docker:
sudo stop docker
sudo rm -rf /var/lib/docker
sudo rm -f `which docker`
sudo apt-key adv --keyserver hkp://p80.pool.sks-keyservers.net:80 --recv-keys 58118E89F3A912897C070ADBF76221572C52609D
echo "deb https://apt.dockerproject.org/repo ubuntu-trusty main" | sudo tee /etc/apt/sources.list.d/docker.list
sudo apt-get update
sudo apt-get install docker-engine=$(DOCKER_VERSION)-0~$(shell lsb_release -cs) -y --force-yes -o Dpkg::Options::="--force-confdef" -o Dpkg::Options::="--force-confold"
@ export output=$$(gofmt -s -d $(PKGS)); \
[ -n "$${output}" ] && echo "$${output}" && export status=1; \
exit $${status:-0}
pretest: lint vet fmtcheck
test: pretest
gotest:
$(foreach pkg,$(PKGS),go test $(pkg) || exit;)
test: pretest gotest
integration:
go test -tags docker_integration -run TestIntegration -v

View File

@@ -4,7 +4,7 @@
[![GoDoc](https://img.shields.io/badge/api-Godoc-blue.svg?style=flat-square)](https://godoc.org/github.com/fsouza/go-dockerclient)
This package presents a client for the Docker remote API. It also provides
support for the extensions in the [Swarm API](https://docs.docker.com/swarm/api/swarm-api/).
support for the extensions in the [Swarm API](https://docs.docker.com/swarm/swarm-api/).
This package also provides support for docker's network API, which is a simple
passthrough to the libnetwork remote API. Note that docker's network API is

View File

@@ -82,10 +82,12 @@ func parseDockerConfig(r io.Reader) (map[string]dockerConfig, error) {
buf.ReadFrom(r)
byteData := buf.Bytes()
var confsWrapper map[string]map[string]dockerConfig
confsWrapper := struct {
Auths map[string]dockerConfig `json:"auths"`
}{}
if err := json.Unmarshal(byteData, &confsWrapper); err == nil {
if confs, ok := confsWrapper["auths"]; ok {
return confs, nil
if len(confsWrapper.Auths) > 0 {
return confsWrapper.Auths, nil
}
}

View File

@@ -49,6 +49,34 @@ func TestAuthBadConfig(t *testing.T) {
}
}
func TestAuthAndOtherFields(t *testing.T) {
auth := base64.StdEncoding.EncodeToString([]byte("user:pass"))
read := strings.NewReader(fmt.Sprintf(`{
"auths":{"docker.io":{"auth":"%s","email":"user@example.com"}},
"detachKeys": "ctrl-e,e",
"HttpHeaders": { "MyHeader": "MyValue" }}`, auth))
ac, err := NewAuthConfigurations(read)
if err != nil {
t.Error(err)
}
c, ok := ac.Configs["docker.io"]
if !ok {
t.Error("NewAuthConfigurations: Expected Configs to contain docker.io")
}
if got, want := c.Email, "user@example.com"; got != want {
t.Errorf(`AuthConfigurations.Configs["docker.io"].Email: wrong result. Want %q. Got %q`, want, got)
}
if got, want := c.Username, "user"; got != want {
t.Errorf(`AuthConfigurations.Configs["docker.io"].Username: wrong result. Want %q. Got %q`, want, got)
}
if got, want := c.Password, "pass"; got != want {
t.Errorf(`AuthConfigurations.Configs["docker.io"].Password: wrong result. Want %q. Got %q`, want, got)
}
if got, want := c.ServerAddress, "docker.io"; got != want {
t.Errorf(`AuthConfigurations.Configs["docker.io"].ServerAddress: wrong result. Want %q. Got %q`, want, got)
}
}
func TestAuthConfig(t *testing.T) {
auth := base64.StdEncoding.EncodeToString([]byte("user:pass"))
read := strings.NewReader(fmt.Sprintf(`{"auths":{"docker.io":{"auth":"%s","email":"user@example.com"}}}`, auth))

View File

@@ -555,6 +555,8 @@ type hijackOptions struct {
data interface{}
}
// CloseWaiter is an interface with methods for closing the underlying resource
// and then waiting for it to finish processing.
type CloseWaiter interface {
io.Closer
Wait() error

View File

@@ -14,6 +14,8 @@ import (
"strconv"
"strings"
"time"
"github.com/fsouza/go-dockerclient/external/github.com/docker/go-units"
)
// ErrContainerAlreadyExists is the error returned by CreateContainer when the
@@ -52,7 +54,14 @@ type APIContainers struct {
SizeRw int64 `json:"SizeRw,omitempty" yaml:"SizeRw,omitempty"`
SizeRootFs int64 `json:"SizeRootFs,omitempty" yaml:"SizeRootFs,omitempty"`
Names []string `json:"Names,omitempty" yaml:"Names,omitempty"`
Labels map[string]string `json:"Labels,omitempty" yaml:"Labels, omitempty"`
Labels map[string]string `json:"Labels,omitempty" yaml:"Labels,omitempty"`
Networks NetworkList `json:"NetworkSettings,omitempty" yaml:"NetworkSettings,omitempty"`
}
// NetworkList encapsulates a map of networks, as returned by the Docker API in
// ListContainers.
type NetworkList struct {
Networks map[string]ContainerNetwork `json:"Networks" yaml:"Networks,omitempty"`
}
// ListContainers returns a slice of containers matching the given criteria.
@@ -92,26 +101,73 @@ func (p Port) Proto() string {
// State represents the state of a container.
type State struct {
Running bool `json:"Running,omitempty" yaml:"Running,omitempty"`
Paused bool `json:"Paused,omitempty" yaml:"Paused,omitempty"`
Restarting bool `json:"Restarting,omitempty" yaml:"Restarting,omitempty"`
OOMKilled bool `json:"OOMKilled,omitempty" yaml:"OOMKilled,omitempty"`
Pid int `json:"Pid,omitempty" yaml:"Pid,omitempty"`
ExitCode int `json:"ExitCode,omitempty" yaml:"ExitCode,omitempty"`
Error string `json:"Error,omitempty" yaml:"Error,omitempty"`
StartedAt time.Time `json:"StartedAt,omitempty" yaml:"StartedAt,omitempty"`
FinishedAt time.Time `json:"FinishedAt,omitempty" yaml:"FinishedAt,omitempty"`
Status string `json:"Status,omitempty" yaml:"Status,omitempty"`
Running bool `json:"Running,omitempty" yaml:"Running,omitempty"`
Paused bool `json:"Paused,omitempty" yaml:"Paused,omitempty"`
Restarting bool `json:"Restarting,omitempty" yaml:"Restarting,omitempty"`
OOMKilled bool `json:"OOMKilled,omitempty" yaml:"OOMKilled,omitempty"`
RemovalInProgress bool `json:"RemovalInProgress,omitempty" yaml:"RemovalInProgress,omitempty"`
Dead bool `json:"Dead,omitempty" yaml:"Dead,omitempty"`
Pid int `json:"Pid,omitempty" yaml:"Pid,omitempty"`
ExitCode int `json:"ExitCode,omitempty" yaml:"ExitCode,omitempty"`
Error string `json:"Error,omitempty" yaml:"Error,omitempty"`
StartedAt time.Time `json:"StartedAt,omitempty" yaml:"StartedAt,omitempty"`
FinishedAt time.Time `json:"FinishedAt,omitempty" yaml:"FinishedAt,omitempty"`
}
// String returns the string representation of a state.
// String returns a human-readable description of the state
func (s *State) String() string {
if s.Running {
if s.Paused {
return fmt.Sprintf("Up %s (Paused)", units.HumanDuration(time.Now().UTC().Sub(s.StartedAt)))
}
if s.Restarting {
return fmt.Sprintf("Restarting (%d) %s ago", s.ExitCode, units.HumanDuration(time.Now().UTC().Sub(s.FinishedAt)))
}
return fmt.Sprintf("Up %s", units.HumanDuration(time.Now().UTC().Sub(s.StartedAt)))
}
if s.RemovalInProgress {
return "Removal In Progress"
}
if s.Dead {
return "Dead"
}
if s.StartedAt.IsZero() {
return "Created"
}
if s.FinishedAt.IsZero() {
return ""
}
return fmt.Sprintf("Exited (%d) %s ago", s.ExitCode, units.HumanDuration(time.Now().UTC().Sub(s.FinishedAt)))
}
// StateString returns a single string to describe state
func (s *State) StateString() string {
if s.Running {
if s.Paused {
return "paused"
}
return fmt.Sprintf("Up %s", time.Now().UTC().Sub(s.StartedAt))
if s.Restarting {
return "restarting"
}
return "running"
}
return fmt.Sprintf("Exit %d", s.ExitCode)
if s.Dead {
return "dead"
}
if s.StartedAt.IsZero() {
return "created"
}
return "exited"
}
// PortBinding represents the host/container port mapping as returned in the
@@ -135,6 +191,7 @@ type ContainerNetwork struct {
IPAddress string `json:"IPAddress,omitempty" yaml:"IPAddress,omitempty"`
Gateway string `json:"Gateway,omitempty" yaml:"Gateway,omitempty"`
EndpointID string `json:"EndpointID,omitempty" yaml:"EndpointID,omitempty"`
NetworkID string `json:"NetworkID,omitempty" yaml:"NetworkID,omitempty"`
}
// NetworkSettings contains network-related information about a container
@@ -308,6 +365,34 @@ type Container struct {
AppArmorProfile string `json:"AppArmorProfile,omitempty" yaml:"AppArmorProfile,omitempty"`
}
// UpdateContainerOptions specify parameters to the UpdateContainer function.
//
// See https://goo.gl/Y6fXUy for more details.
type UpdateContainerOptions struct {
BlkioWeight int `json:"BlkioWeight"`
CPUShares int `json:"CpuShares"`
CPUPeriod int `json:"CpuPeriod"`
CPUQuota int `json:"CpuQuota"`
CpusetCpus string `json:"CpusetCpus"`
CpusetMems string `json:"CpusetMems"`
Memory int `json:"Memory"`
MemorySwap int `json:"MemorySwap"`
MemoryReservation int `json:"MemoryReservation"`
KernelMemory int `json:"KernelMemory"`
}
// 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})
if err != nil {
return err
}
defer resp.Body.Close()
return nil
}
// RenameContainerOptions specify parameters to the RenameContainer function.
//
// See https://goo.gl/laSOIy for more details.
@@ -469,48 +554,71 @@ type Device struct {
CgroupPermissions string `json:"CgroupPermissions,omitempty" yaml:"CgroupPermissions,omitempty"`
}
// BlockWeight represents a relative device weight for an individual device inside
// of a container
//
// See https://goo.gl/FSdP0H for more details.
type BlockWeight struct {
Path string `json:"Path,omitempty"`
Weight string `json:"Weight,omitempty"`
}
// BlockLimit represents a read/write limit in IOPS or Bandwidth for a device
// inside of a container
//
// See https://goo.gl/FSdP0H for more details.
type BlockLimit struct {
Path string `json:"Path,omitempty"`
Rate string `json:"Rate,omitempty"`
}
// HostConfig contains the container options related to starting a container on
// a given host
type HostConfig struct {
Binds []string `json:"Binds,omitempty" yaml:"Binds,omitempty"`
CapAdd []string `json:"CapAdd,omitempty" yaml:"CapAdd,omitempty"`
CapDrop []string `json:"CapDrop,omitempty" yaml:"CapDrop,omitempty"`
GroupAdd []string `json:"GroupAdd,omitempty" yaml:"GroupAdd,omitempty"`
ContainerIDFile string `json:"ContainerIDFile,omitempty" yaml:"ContainerIDFile,omitempty"`
LxcConf []KeyValuePair `json:"LxcConf,omitempty" yaml:"LxcConf,omitempty"`
Privileged bool `json:"Privileged,omitempty" yaml:"Privileged,omitempty"`
PortBindings map[Port][]PortBinding `json:"PortBindings,omitempty" yaml:"PortBindings,omitempty"`
Links []string `json:"Links,omitempty" yaml:"Links,omitempty"`
PublishAllPorts bool `json:"PublishAllPorts,omitempty" yaml:"PublishAllPorts,omitempty"`
DNS []string `json:"Dns,omitempty" yaml:"Dns,omitempty"` // For Docker API v1.10 and above only
DNSOptions []string `json:"DnsOptions,omitempty" yaml:"DnsOptions,omitempty"`
DNSSearch []string `json:"DnsSearch,omitempty" yaml:"DnsSearch,omitempty"`
ExtraHosts []string `json:"ExtraHosts,omitempty" yaml:"ExtraHosts,omitempty"`
VolumesFrom []string `json:"VolumesFrom,omitempty" yaml:"VolumesFrom,omitempty"`
NetworkMode string `json:"NetworkMode,omitempty" yaml:"NetworkMode,omitempty"`
IpcMode string `json:"IpcMode,omitempty" yaml:"IpcMode,omitempty"`
PidMode string `json:"PidMode,omitempty" yaml:"PidMode,omitempty"`
UTSMode string `json:"UTSMode,omitempty" yaml:"UTSMode,omitempty"`
RestartPolicy RestartPolicy `json:"RestartPolicy,omitempty" yaml:"RestartPolicy,omitempty"`
Devices []Device `json:"Devices,omitempty" yaml:"Devices,omitempty"`
LogConfig LogConfig `json:"LogConfig,omitempty" yaml:"LogConfig,omitempty"`
ReadonlyRootfs bool `json:"ReadonlyRootfs,omitempty" yaml:"ReadonlyRootfs,omitempty"`
SecurityOpt []string `json:"SecurityOpt,omitempty" yaml:"SecurityOpt,omitempty"`
CgroupParent string `json:"CgroupParent,omitempty" yaml:"CgroupParent,omitempty"`
Memory int64 `json:"Memory,omitempty" yaml:"Memory,omitempty"`
MemorySwap int64 `json:"MemorySwap,omitempty" yaml:"MemorySwap,omitempty"`
MemorySwappiness int64 `json:"MemorySwappiness,omitempty" yaml:"MemorySwappiness,omitempty"`
OOMKillDisable bool `json:"OomKillDisable,omitempty" yaml:"OomKillDisable"`
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"`
Ulimits []ULimit `json:"Ulimits,omitempty" yaml:"Ulimits,omitempty"`
VolumeDriver string `json:"VolumeDriver,omitempty" yaml:"VolumeDriver,omitempty"`
OomScoreAdj int `json:"OomScoreAdj,omitempty" yaml:"OomScoreAdj,omitempty"`
Binds []string `json:"Binds,omitempty" yaml:"Binds,omitempty"`
CapAdd []string `json:"CapAdd,omitempty" yaml:"CapAdd,omitempty"`
CapDrop []string `json:"CapDrop,omitempty" yaml:"CapDrop,omitempty"`
GroupAdd []string `json:"GroupAdd,omitempty" yaml:"GroupAdd,omitempty"`
ContainerIDFile string `json:"ContainerIDFile,omitempty" yaml:"ContainerIDFile,omitempty"`
LxcConf []KeyValuePair `json:"LxcConf,omitempty" yaml:"LxcConf,omitempty"`
Privileged bool `json:"Privileged,omitempty" yaml:"Privileged,omitempty"`
PortBindings map[Port][]PortBinding `json:"PortBindings,omitempty" yaml:"PortBindings,omitempty"`
Links []string `json:"Links,omitempty" yaml:"Links,omitempty"`
PublishAllPorts bool `json:"PublishAllPorts,omitempty" yaml:"PublishAllPorts,omitempty"`
DNS []string `json:"Dns,omitempty" yaml:"Dns,omitempty"` // For Docker API v1.10 and above only
DNSOptions []string `json:"DnsOptions,omitempty" yaml:"DnsOptions,omitempty"`
DNSSearch []string `json:"DnsSearch,omitempty" yaml:"DnsSearch,omitempty"`
ExtraHosts []string `json:"ExtraHosts,omitempty" yaml:"ExtraHosts,omitempty"`
VolumesFrom []string `json:"VolumesFrom,omitempty" yaml:"VolumesFrom,omitempty"`
NetworkMode string `json:"NetworkMode,omitempty" yaml:"NetworkMode,omitempty"`
IpcMode string `json:"IpcMode,omitempty" yaml:"IpcMode,omitempty"`
PidMode string `json:"PidMode,omitempty" yaml:"PidMode,omitempty"`
UTSMode string `json:"UTSMode,omitempty" yaml:"UTSMode,omitempty"`
RestartPolicy RestartPolicy `json:"RestartPolicy,omitempty" yaml:"RestartPolicy,omitempty"`
Devices []Device `json:"Devices,omitempty" yaml:"Devices,omitempty"`
LogConfig LogConfig `json:"LogConfig,omitempty" yaml:"LogConfig,omitempty"`
ReadonlyRootfs bool `json:"ReadonlyRootfs,omitempty" yaml:"ReadonlyRootfs,omitempty"`
SecurityOpt []string `json:"SecurityOpt,omitempty" yaml:"SecurityOpt,omitempty"`
CgroupParent string `json:"CgroupParent,omitempty" yaml:"CgroupParent,omitempty"`
Memory int64 `json:"Memory,omitempty" yaml:"Memory,omitempty"`
MemorySwap int64 `json:"MemorySwap,omitempty" yaml:"MemorySwap,omitempty"`
MemorySwappiness int64 `json:"MemorySwappiness,omitempty" yaml:"MemorySwappiness,omitempty"`
OOMKillDisable bool `json:"OomKillDisable,omitempty" yaml:"OomKillDisable"`
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"`
Ulimits []ULimit `json:"Ulimits,omitempty" yaml:"Ulimits,omitempty"`
VolumeDriver string `json:"VolumeDriver,omitempty" yaml:"VolumeDriver,omitempty"`
OomScoreAdj int `json:"OomScoreAdj,omitempty" yaml:"OomScoreAdj,omitempty"`
}
// StartContainer starts a container, returning an error in case of failure.

View File

@@ -16,7 +16,6 @@ import (
"net/url"
"os"
"reflect"
"regexp"
"runtime"
"strconv"
"strings"
@@ -32,13 +31,37 @@ func TestStateString(t *testing.T) {
input State
expected string
}{
{State{Running: true, Paused: true}, "^paused$"},
{State{Running: true, StartedAt: started}, "^Up 3h.*$"},
{State{Running: false, ExitCode: 7}, "^Exit 7$"},
{State{Running: true, Paused: true, StartedAt: started}, "Up 3 hours (Paused)"},
{State{Running: true, Restarting: true, ExitCode: 7, FinishedAt: started}, "Restarting (7) 3 hours ago"},
{State{Running: true, StartedAt: started}, "Up 3 hours"},
{State{RemovalInProgress: true}, "Removal In Progress"},
{State{Dead: true}, "Dead"},
{State{}, "Created"},
{State{StartedAt: started}, ""},
{State{ExitCode: 7, StartedAt: started, FinishedAt: started}, "Exited (7) 3 hours ago"},
}
for _, tt := range tests {
re := regexp.MustCompile(tt.expected)
if got := tt.input.String(); !re.MatchString(got) {
if got := tt.input.String(); got != tt.expected {
t.Errorf("State.String(): wrong result. Want %q. Got %q.", tt.expected, got)
}
}
}
func TestStateStateString(t *testing.T) {
started := time.Now().Add(-3 * time.Hour)
var tests = []struct {
input State
expected string
}{
{State{Running: true, Paused: true}, "paused"},
{State{Running: true, Restarting: true}, "restarting"},
{State{Running: true}, "running"},
{State{Dead: true}, "dead"},
{State{}, "created"},
{State{StartedAt: started}, "exited"},
}
for _, tt := range tests {
if got := tt.input.StateString(); got != tt.expected {
t.Errorf("State.String(): wrong result. Want %q. Got %q.", tt.expected, got)
}
}
@@ -438,6 +461,7 @@ func TestInspectContainerNetwork(t *testing.T) {
"MacAddress": "",
"Networks": {
"swl-net": {
"NetworkID": "7ea29fc1412292a2d7bba362f9253545fecdfa8ce9a6e37dd10ba8bee7129812",
"EndpointID": "683e3092275782a53c3b0968cc7e3a10f23264022ded9cb20490902f96fc5981",
"Gateway": "",
"IPAddress": "10.0.0.3",
@@ -454,7 +478,8 @@ func TestInspectContainerNetwork(t *testing.T) {
fakeRT := &FakeRoundTripper{message: jsonContainer, status: http.StatusOK}
client := newTestClient(fakeRT)
id := "81e1bbe20b55"
exp := "10.0.0.3"
expIP := "10.0.0.3"
expNetworkID := "7ea29fc1412292a2d7bba362f9253545fecdfa8ce9a6e37dd10ba8bee7129812"
container, err := client.InspectContainer(id)
if err != nil {
@@ -471,8 +496,19 @@ func TestInspectContainerNetwork(t *testing.T) {
t.Logf("%s %v", net, ip)
}
}
if ip != exp {
t.Errorf("InspectContainerNetworks(%q): Expected %#v. Got %#v.", id, exp, ip)
if ip != expIP {
t.Errorf("InspectContainerNetworks(%q): Expected %#v. Got %#v.", id, expIP, ip)
}
var networkID string
for _, net := range networks.MapKeys() {
if net.Interface().(string) == container.HostConfig.NetworkMode {
networkID = networks.MapIndex(net).FieldByName("NetworkID").Interface().(string)
t.Logf("%s %v", net, networkID)
}
}
if networkID != expNetworkID {
t.Errorf("InspectContainerNetworks(%q): Expected %#v. Got %#v.", id, expNetworkID, networkID)
}
} else {
t.Errorf("InspectContainerNetworks(%q): No method Networks for NetworkSettings", id)
@@ -728,6 +764,36 @@ func TestCreateContainerWithHostConfig(t *testing.T) {
}
}
func TestUpdateContainer(t *testing.T) {
fakeRT := &FakeRoundTripper{message: "", status: http.StatusOK}
client := newTestClient(fakeRT)
id := "4fa6e0f0c6786287e131c3852c58a2e01cc697a68231826813597e4994f1d6e2"
update := UpdateContainerOptions{Memory: 12345, CpusetMems: "0,1"}
err := client.UpdateContainer(id, update)
if err != nil {
t.Fatal(err)
}
req := fakeRT.requests[0]
if req.Method != "POST" {
t.Errorf("UpdateContainer: wrong HTTP method. Want %q. Got %q.", "POST", req.Method)
}
expectedURL, _ := url.Parse(client.getURL("/containers/" + id + "/update"))
if gotPath := req.URL.Path; gotPath != expectedURL.Path {
t.Errorf("UpdateContainer: Wrong path in request. Want %q. Got %q.", expectedURL.Path, gotPath)
}
expectedContentType := "application/json"
if contentType := req.Header.Get("Content-Type"); contentType != expectedContentType {
t.Errorf("UpdateContainer: Wrong content-type in request. Want %q. Got %q.", expectedContentType, contentType)
}
var out UpdateContainerOptions
if err := json.NewDecoder(req.Body).Decode(&out); err != nil {
t.Fatal(err)
}
if !reflect.DeepEqual(out, update) {
t.Errorf("UpdateContainer: wrong body, got: %#v, want %#v", out, update)
}
}
func TestStartContainer(t *testing.T) {
fakeRT := &FakeRoundTripper{message: "", status: http.StatusOK}
client := newTestClient(fakeRT)

View File

@@ -18,12 +18,38 @@ import (
"time"
)
// APIEvents represents an event returned by the API.
// APIEvents represents events coming from the Docker API
// The fields in the Docker API changed in API version 1.22, and
// events for more than images and containers are now fired off.
// To maintain forward and backward compatibility, go-dockerclient
// replicates the event in both the new and old format as faithfully as possible.
//
// For events that only exist in 1.22 in later, `Status` is filled in as
// `"Type:Action"` instead of just `Action` to allow for older clients to
// differentiate and not break if they rely on the pre-1.22 Status types.
//
// The transformEvent method can be consulted for more information about how
// events are translated from new/old API formats
type APIEvents struct {
Status string `json:"Status,omitempty" yaml:"Status,omitempty"`
ID string `json:"ID,omitempty" yaml:"ID,omitempty"`
From string `json:"From,omitempty" yaml:"From,omitempty"`
Time int64 `json:"Time,omitempty" yaml:"Time,omitempty"`
// New API Fields in 1.22
Action string `json:"action,omitempty"`
Type string `json:"type,omitempty"`
Actor APIActor `json:"actor,omitempty"`
// Old API fields for < 1.22
Status string `json:"status,omitempty"`
ID string `json:"id,omitempty"`
From string `json:"from,omitempty"`
// Fields in both
Time int64 `json:"time,omitempty"`
TimeNano int64 `json:"timeNano,omitempty"`
}
// APIActor represents an actor that accomplishes something for an event
type APIActor struct {
ID string `json:"id,omitempty"`
Attributes map[string]string `json:"attributes,omitempty"`
}
type eventMonitoringState struct {
@@ -52,6 +78,7 @@ var (
// EOFEvent is sent when the event listener receives an EOF error.
EOFEvent = &APIEvents{
Type: "EOF",
Status: "EOF",
}
)
@@ -297,8 +324,47 @@ func (c *Client) eventHijack(startTime int64, eventChan chan *APIEvents, errChan
if !c.eventMonitor.isEnabled() {
return
}
transformEvent(&event)
eventChan <- &event
}
}(res, conn)
return nil
}
// transformEvent takes an event and determines what version it is from
// then populates both versions of the event
func transformEvent(event *APIEvents) {
// if event version is <= 1.21 there will be no Action and no Type
if event.Action == "" && event.Type == "" {
event.Action = event.Status
event.Actor.ID = event.ID
event.Actor.Attributes = map[string]string{}
switch event.Status {
case "delete", "import", "pull", "push", "tag", "untag":
event.Type = "image"
default:
event.Type = "container"
if event.From != "" {
event.Actor.Attributes["image"] = event.From
}
}
} else {
if event.Status == "" {
if event.Type == "image" || event.Type == "container" {
event.Status = event.Action
} else {
// Because just the Status has been overloaded with different Types
// if an event is not for an image or a container, we prepend the type
// to avoid problems for people relying on actions being only for
// images and containers
event.Status = event.Type + ":" + event.Action
}
}
if event.ID == "" {
event.ID = event.Actor.ID
}
if event.From == "" {
event.From = event.Actor.Attributes["image"]
}
}
}

View File

@@ -8,10 +8,10 @@ import (
"bufio"
"crypto/tls"
"crypto/x509"
"fmt"
"io/ioutil"
"net/http"
"net/http/httptest"
"reflect"
"strings"
"testing"
"time"
@@ -51,12 +51,168 @@ func TestTLSEventListeners(t *testing.T) {
}
func testEventListeners(testName string, t *testing.T, buildServer func(http.Handler) *httptest.Server, buildClient func(string) (*Client, error)) {
response := `{"status":"create","id":"dfdf82bd3881","from":"base:latest","time":1374067924}
response := `{"action":"pull","type":"image","actor":{"id":"busybox:latest","attributes":{}},"time":1442421700,"timeNano":1442421700598988358}
{"action":"create","type":"container","actor":{"id":"5745704abe9caa5","attributes":{"image":"busybox"}},"time":1442421716,"timeNano":1442421716853979870}
{"action":"attach","type":"container","actor":{"id":"5745704abe9caa5","attributes":{"image":"busybox"}},"time":1442421716,"timeNano":1442421716894759198}
{"action":"start","type":"container","actor":{"id":"5745704abe9caa5","attributes":{"image":"busybox"}},"time":1442421716,"timeNano":1442421716983607193}
{"status":"create","id":"dfdf82bd3881","from":"base:latest","time":1374067924}
{"status":"start","id":"dfdf82bd3881","from":"base:latest","time":1374067924}
{"status":"stop","id":"dfdf82bd3881","from":"base:latest","time":1374067966}
{"status":"destroy","id":"dfdf82bd3881","from":"base:latest","time":1374067970}
`
{"Action":"create","Actor":{"Attributes":{"HAProxyMode":"http","HealthCheck":"HttpGet","HealthCheckArgs":"http://127.0.0.1:39051/status/check","ServicePort_8080":"17801","image":"datanerd.us/siteeng/sample-app-go:latest","name":"sample-app-client-go-69818c1223ddb5"},"ID":"a925eaf4084d5c3bcf337b2abb05f566ebb94276dff34f6effb00d8ecd380e16"},"Type":"container","from":"datanerd.us/siteeng/sample-app-go:latest","id":"a925eaf4084d5c3bcf337b2abb05f566ebb94276dff34f6effb00d8ecd380e16","status":"create","time":1459133932,"timeNano":1459133932961735842}`
wantResponse := []*APIEvents{
{
Action: "pull",
Type: "image",
Actor: APIActor{
ID: "busybox:latest",
Attributes: map[string]string{},
},
Status: "pull",
ID: "busybox:latest",
Time: 1442421700,
TimeNano: 1442421700598988358,
},
{
Action: "create",
Type: "container",
Actor: APIActor{
ID: "5745704abe9caa5",
Attributes: map[string]string{
"image": "busybox",
},
},
Status: "create",
ID: "5745704abe9caa5",
From: "busybox",
Time: 1442421716,
TimeNano: 1442421716853979870,
},
{
Action: "attach",
Type: "container",
Actor: APIActor{
ID: "5745704abe9caa5",
Attributes: map[string]string{
"image": "busybox",
},
},
Status: "attach",
ID: "5745704abe9caa5",
From: "busybox",
Time: 1442421716,
TimeNano: 1442421716894759198,
},
{
Action: "start",
Type: "container",
Actor: APIActor{
ID: "5745704abe9caa5",
Attributes: map[string]string{
"image": "busybox",
},
},
Status: "start",
ID: "5745704abe9caa5",
From: "busybox",
Time: 1442421716,
TimeNano: 1442421716983607193,
},
{
Action: "create",
Type: "container",
Actor: APIActor{
ID: "dfdf82bd3881",
Attributes: map[string]string{
"image": "base:latest",
},
},
Status: "create",
ID: "dfdf82bd3881",
From: "base:latest",
Time: 1374067924,
},
{
Action: "start",
Type: "container",
Actor: APIActor{
ID: "dfdf82bd3881",
Attributes: map[string]string{
"image": "base:latest",
},
},
Status: "start",
ID: "dfdf82bd3881",
From: "base:latest",
Time: 1374067924,
},
{
Action: "stop",
Type: "container",
Actor: APIActor{
ID: "dfdf82bd3881",
Attributes: map[string]string{
"image": "base:latest",
},
},
Status: "stop",
ID: "dfdf82bd3881",
From: "base:latest",
Time: 1374067966,
},
{
Action: "destroy",
Type: "container",
Actor: APIActor{
ID: "dfdf82bd3881",
Attributes: map[string]string{
"image": "base:latest",
},
},
Status: "destroy",
ID: "dfdf82bd3881",
From: "base:latest",
Time: 1374067970,
},
{
Action: "create",
Type: "container",
Status: "create",
From: "datanerd.us/siteeng/sample-app-go:latest",
ID: "a925eaf4084d5c3bcf337b2abb05f566ebb94276dff34f6effb00d8ecd380e16",
Time: 1459133932,
TimeNano: 1459133932961735842,
Actor: APIActor{
ID: "a925eaf4084d5c3bcf337b2abb05f566ebb94276dff34f6effb00d8ecd380e16",
Attributes: map[string]string{
"HAProxyMode": "http",
"HealthCheck": "HttpGet",
"HealthCheckArgs": "http://127.0.0.1:39051/status/check",
"ServicePort_8080": "17801",
"image": "datanerd.us/siteeng/sample-app-go:latest",
"name": "sample-app-client-go-69818c1223ddb5",
},
},
},
}
server := buildServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
rsc := bufio.NewScanner(strings.NewReader(response))
for rsc.Scan() {
@@ -87,46 +243,16 @@ func testEventListeners(testName string, t *testing.T, buildServer func(http.Han
}
timeout := time.After(1 * time.Second)
var count int
for {
for i := 0; i < 9; i++ {
select {
case msg := <-listener:
t.Logf("Received: %v", *msg)
count++
err = checkEvent(count, msg)
if err != nil {
t.Fatalf("Check event failed: %s", err)
}
if count == 4 {
return
t.Logf("%d: Received: %v", i, msg)
if !reflect.DeepEqual(msg, wantResponse[i]) {
t.Fatalf("%d: wanted: %#v\n got: %#v", i, wantResponse[i], msg)
}
case <-timeout:
t.Fatalf("%s timed out waiting on events", testName)
}
}
}
func checkEvent(index int, event *APIEvents) error {
if event.ID != "dfdf82bd3881" {
return fmt.Errorf("event ID did not match. Expected dfdf82bd3881 got %s", event.ID)
}
if event.From != "base:latest" {
return fmt.Errorf("event from did not match. Expected base:latest got %s", event.From)
}
var status string
switch index {
case 1:
status = "create"
case 2:
status = "start"
case 3:
status = "stop"
case 4:
status = "destroy"
}
if event.Status != status {
return fmt.Errorf("event status did not match. Expected %s got %s", status, event.Status)
}
return nil
}

View File

@@ -0,0 +1,978 @@
// +build !windows
// TODO Windows: Some of these tests may be salvagable and portable to Windows.
package archive
import (
"bytes"
"crypto/sha256"
"encoding/hex"
"fmt"
"io"
"io/ioutil"
"os"
"path/filepath"
"strings"
"testing"
)
func removeAllPaths(paths ...string) {
for _, path := range paths {
os.RemoveAll(path)
}
}
func getTestTempDirs(t *testing.T) (tmpDirA, tmpDirB string) {
var err error
if tmpDirA, err = ioutil.TempDir("", "archive-copy-test"); err != nil {
t.Fatal(err)
}
if tmpDirB, err = ioutil.TempDir("", "archive-copy-test"); err != nil {
t.Fatal(err)
}
return
}
func isNotDir(err error) bool {
return strings.Contains(err.Error(), "not a directory")
}
func joinTrailingSep(pathElements ...string) string {
joined := filepath.Join(pathElements...)
return fmt.Sprintf("%s%c", joined, filepath.Separator)
}
func fileContentsEqual(t *testing.T, filenameA, filenameB string) (err error) {
t.Logf("checking for equal file contents: %q and %q\n", filenameA, filenameB)
fileA, err := os.Open(filenameA)
if err != nil {
return
}
defer fileA.Close()
fileB, err := os.Open(filenameB)
if err != nil {
return
}
defer fileB.Close()
hasher := sha256.New()
if _, err = io.Copy(hasher, fileA); err != nil {
return
}
hashA := hasher.Sum(nil)
hasher.Reset()
if _, err = io.Copy(hasher, fileB); err != nil {
return
}
hashB := hasher.Sum(nil)
if !bytes.Equal(hashA, hashB) {
err = fmt.Errorf("file content hashes not equal - expected %s, got %s", hex.EncodeToString(hashA), hex.EncodeToString(hashB))
}
return
}
func dirContentsEqual(t *testing.T, newDir, oldDir string) (err error) {
t.Logf("checking for equal directory contents: %q and %q\n", newDir, oldDir)
var changes []Change
if changes, err = ChangesDirs(newDir, oldDir); err != nil {
return
}
if len(changes) != 0 {
err = fmt.Errorf("expected no changes between directories, but got: %v", changes)
}
return
}
func logDirContents(t *testing.T, dirPath string) {
logWalkedPaths := filepath.WalkFunc(func(path string, info os.FileInfo, err error) error {
if err != nil {
t.Errorf("stat error for path %q: %s", path, err)
return nil
}
if info.IsDir() {
path = joinTrailingSep(path)
}
t.Logf("\t%s", path)
return nil
})
t.Logf("logging directory contents: %q", dirPath)
if err := filepath.Walk(dirPath, logWalkedPaths); err != nil {
t.Fatal(err)
}
}
func testCopyHelper(t *testing.T, srcPath, dstPath string) (err error) {
t.Logf("copying from %q to %q (not follow symbol link)", srcPath, dstPath)
return CopyResource(srcPath, dstPath, false)
}
func testCopyHelperFSym(t *testing.T, srcPath, dstPath string) (err error) {
t.Logf("copying from %q to %q (follow symbol link)", srcPath, dstPath)
return CopyResource(srcPath, dstPath, true)
}
// Basic assumptions about SRC and DST:
// 1. SRC must exist.
// 2. If SRC ends with a trailing separator, it must be a directory.
// 3. DST parent directory must exist.
// 4. If DST exists as a file, it must not end with a trailing separator.
// First get these easy error cases out of the way.
// Test for error when SRC does not exist.
func TestCopyErrSrcNotExists(t *testing.T) {
tmpDirA, tmpDirB := getTestTempDirs(t)
defer removeAllPaths(tmpDirA, tmpDirB)
if _, err := CopyInfoSourcePath(filepath.Join(tmpDirA, "file1"), false); !os.IsNotExist(err) {
t.Fatalf("expected IsNotExist error, but got %T: %s", err, err)
}
}
// Test for error when SRC ends in a trailing
// path separator but it exists as a file.
func TestCopyErrSrcNotDir(t *testing.T) {
tmpDirA, tmpDirB := getTestTempDirs(t)
defer removeAllPaths(tmpDirA, tmpDirB)
// Load A with some sample files and directories.
createSampleDir(t, tmpDirA)
if _, err := CopyInfoSourcePath(joinTrailingSep(tmpDirA, "file1"), false); !isNotDir(err) {
t.Fatalf("expected IsNotDir error, but got %T: %s", err, err)
}
}
// Test for error when SRC is a valid file or directory,
// but the DST parent directory does not exist.
func TestCopyErrDstParentNotExists(t *testing.T) {
tmpDirA, tmpDirB := getTestTempDirs(t)
defer removeAllPaths(tmpDirA, tmpDirB)
// Load A with some sample files and directories.
createSampleDir(t, tmpDirA)
srcInfo := CopyInfo{Path: filepath.Join(tmpDirA, "file1"), Exists: true, IsDir: false}
// Try with a file source.
content, err := TarResource(srcInfo)
if err != nil {
t.Fatalf("unexpected error %T: %s", err, err)
}
defer content.Close()
// Copy to a file whose parent does not exist.
if err = CopyTo(content, srcInfo, filepath.Join(tmpDirB, "fakeParentDir", "file1")); err == nil {
t.Fatal("expected IsNotExist error, but got nil instead")
}
if !os.IsNotExist(err) {
t.Fatalf("expected IsNotExist error, but got %T: %s", err, err)
}
// Try with a directory source.
srcInfo = CopyInfo{Path: filepath.Join(tmpDirA, "dir1"), Exists: true, IsDir: true}
content, err = TarResource(srcInfo)
if err != nil {
t.Fatalf("unexpected error %T: %s", err, err)
}
defer content.Close()
// Copy to a directory whose parent does not exist.
if err = CopyTo(content, srcInfo, joinTrailingSep(tmpDirB, "fakeParentDir", "fakeDstDir")); err == nil {
t.Fatal("expected IsNotExist error, but got nil instead")
}
if !os.IsNotExist(err) {
t.Fatalf("expected IsNotExist error, but got %T: %s", err, err)
}
}
// Test for error when DST ends in a trailing
// path separator but exists as a file.
func TestCopyErrDstNotDir(t *testing.T) {
tmpDirA, tmpDirB := getTestTempDirs(t)
defer removeAllPaths(tmpDirA, tmpDirB)
// Load A and B with some sample files and directories.
createSampleDir(t, tmpDirA)
createSampleDir(t, tmpDirB)
// Try with a file source.
srcInfo := CopyInfo{Path: filepath.Join(tmpDirA, "file1"), Exists: true, IsDir: false}
content, err := TarResource(srcInfo)
if err != nil {
t.Fatalf("unexpected error %T: %s", err, err)
}
defer content.Close()
if err = CopyTo(content, srcInfo, joinTrailingSep(tmpDirB, "file1")); err == nil {
t.Fatal("expected IsNotDir error, but got nil instead")
}
if !isNotDir(err) {
t.Fatalf("expected IsNotDir error, but got %T: %s", err, err)
}
// Try with a directory source.
srcInfo = CopyInfo{Path: filepath.Join(tmpDirA, "dir1"), Exists: true, IsDir: true}
content, err = TarResource(srcInfo)
if err != nil {
t.Fatalf("unexpected error %T: %s", err, err)
}
defer content.Close()
if err = CopyTo(content, srcInfo, joinTrailingSep(tmpDirB, "file1")); err == nil {
t.Fatal("expected IsNotDir error, but got nil instead")
}
if !isNotDir(err) {
t.Fatalf("expected IsNotDir error, but got %T: %s", err, err)
}
}
// Possibilities are reduced to the remaining 10 cases:
//
// case | srcIsDir | onlyDirContents | dstExists | dstIsDir | dstTrSep | action
// ===================================================================================================
// A | no | - | no | - | no | create file
// B | no | - | no | - | yes | error
// C | no | - | yes | no | - | overwrite file
// D | no | - | yes | yes | - | create file in dst dir
// E | yes | no | no | - | - | create dir, copy contents
// F | yes | no | yes | no | - | error
// G | yes | no | yes | yes | - | copy dir and contents
// H | yes | yes | no | - | - | create dir, copy contents
// I | yes | yes | yes | no | - | error
// J | yes | yes | yes | yes | - | copy dir contents
//
// A. SRC specifies a file and DST (no trailing path separator) doesn't
// exist. This should create a file with the name DST and copy the
// contents of the source file into it.
func TestCopyCaseA(t *testing.T) {
tmpDirA, tmpDirB := getTestTempDirs(t)
defer removeAllPaths(tmpDirA, tmpDirB)
// Load A with some sample files and directories.
createSampleDir(t, tmpDirA)
srcPath := filepath.Join(tmpDirA, "file1")
dstPath := filepath.Join(tmpDirB, "itWorks.txt")
var err error
if err = testCopyHelper(t, srcPath, dstPath); err != nil {
t.Fatalf("unexpected error %T: %s", err, err)
}
if err = fileContentsEqual(t, srcPath, dstPath); err != nil {
t.Fatal(err)
}
os.Remove(dstPath)
symlinkPath := filepath.Join(tmpDirA, "symlink3")
symlinkPath1 := filepath.Join(tmpDirA, "symlink4")
linkTarget := filepath.Join(tmpDirA, "file1")
if err = testCopyHelperFSym(t, symlinkPath, dstPath); err != nil {
t.Fatalf("unexpected error %T: %s", err, err)
}
if err = fileContentsEqual(t, linkTarget, dstPath); err != nil {
t.Fatal(err)
}
os.Remove(dstPath)
if err = testCopyHelperFSym(t, symlinkPath1, dstPath); err != nil {
t.Fatalf("unexpected error %T: %s", err, err)
}
if err = fileContentsEqual(t, linkTarget, dstPath); err != nil {
t.Fatal(err)
}
}
// B. SRC specifies a file and DST (with trailing path separator) doesn't
// exist. This should cause an error because the copy operation cannot
// create a directory when copying a single file.
func TestCopyCaseB(t *testing.T) {
tmpDirA, tmpDirB := getTestTempDirs(t)
defer removeAllPaths(tmpDirA, tmpDirB)
// Load A with some sample files and directories.
createSampleDir(t, tmpDirA)
srcPath := filepath.Join(tmpDirA, "file1")
dstDir := joinTrailingSep(tmpDirB, "testDir")
var err error
if err = testCopyHelper(t, srcPath, dstDir); err == nil {
t.Fatal("expected ErrDirNotExists error, but got nil instead")
}
if err != ErrDirNotExists {
t.Fatalf("expected ErrDirNotExists error, but got %T: %s", err, err)
}
symlinkPath := filepath.Join(tmpDirA, "symlink3")
if err = testCopyHelperFSym(t, symlinkPath, dstDir); err == nil {
t.Fatal("expected ErrDirNotExists error, but got nil instead")
}
if err != ErrDirNotExists {
t.Fatalf("expected ErrDirNotExists error, but got %T: %s", err, err)
}
}
// C. SRC specifies a file and DST exists as a file. This should overwrite
// the file at DST with the contents of the source file.
func TestCopyCaseC(t *testing.T) {
tmpDirA, tmpDirB := getTestTempDirs(t)
defer removeAllPaths(tmpDirA, tmpDirB)
// Load A and B with some sample files and directories.
createSampleDir(t, tmpDirA)
createSampleDir(t, tmpDirB)
srcPath := filepath.Join(tmpDirA, "file1")
dstPath := filepath.Join(tmpDirB, "file2")
var err error
// Ensure they start out different.
if err = fileContentsEqual(t, srcPath, dstPath); err == nil {
t.Fatal("expected different file contents")
}
if err = testCopyHelper(t, srcPath, dstPath); err != nil {
t.Fatalf("unexpected error %T: %s", err, err)
}
if err = fileContentsEqual(t, srcPath, dstPath); err != nil {
t.Fatal(err)
}
}
// C. Symbol link following version:
// SRC specifies a file and DST exists as a file. This should overwrite
// the file at DST with the contents of the source file.
func TestCopyCaseCFSym(t *testing.T) {
tmpDirA, tmpDirB := getTestTempDirs(t)
defer removeAllPaths(tmpDirA, tmpDirB)
// Load A and B with some sample files and directories.
createSampleDir(t, tmpDirA)
createSampleDir(t, tmpDirB)
symlinkPathBad := filepath.Join(tmpDirA, "symlink1")
symlinkPath := filepath.Join(tmpDirA, "symlink3")
linkTarget := filepath.Join(tmpDirA, "file1")
dstPath := filepath.Join(tmpDirB, "file2")
var err error
// first to test broken link
if err = testCopyHelperFSym(t, symlinkPathBad, dstPath); err == nil {
t.Fatalf("unexpected error %T: %s", err, err)
}
// test symbol link -> symbol link -> target
// Ensure they start out different.
if err = fileContentsEqual(t, linkTarget, dstPath); err == nil {
t.Fatal("expected different file contents")
}
if err = testCopyHelperFSym(t, symlinkPath, dstPath); err != nil {
t.Fatalf("unexpected error %T: %s", err, err)
}
if err = fileContentsEqual(t, linkTarget, dstPath); err != nil {
t.Fatal(err)
}
}
// D. SRC specifies a file and DST exists as a directory. This should place
// a copy of the source file inside it using the basename from SRC. Ensure
// this works whether DST has a trailing path separator or not.
func TestCopyCaseD(t *testing.T) {
tmpDirA, tmpDirB := getTestTempDirs(t)
defer removeAllPaths(tmpDirA, tmpDirB)
// Load A and B with some sample files and directories.
createSampleDir(t, tmpDirA)
createSampleDir(t, tmpDirB)
srcPath := filepath.Join(tmpDirA, "file1")
dstDir := filepath.Join(tmpDirB, "dir1")
dstPath := filepath.Join(dstDir, "file1")
var err error
// Ensure that dstPath doesn't exist.
if _, err = os.Stat(dstPath); !os.IsNotExist(err) {
t.Fatalf("did not expect dstPath %q to exist", dstPath)
}
if err = testCopyHelper(t, srcPath, dstDir); err != nil {
t.Fatalf("unexpected error %T: %s", err, err)
}
if err = fileContentsEqual(t, srcPath, dstPath); err != nil {
t.Fatal(err)
}
// Now try again but using a trailing path separator for dstDir.
if err = os.RemoveAll(dstDir); err != nil {
t.Fatalf("unable to remove dstDir: %s", err)
}
if err = os.MkdirAll(dstDir, os.FileMode(0755)); err != nil {
t.Fatalf("unable to make dstDir: %s", err)
}
dstDir = joinTrailingSep(tmpDirB, "dir1")
if err = testCopyHelper(t, srcPath, dstDir); err != nil {
t.Fatalf("unexpected error %T: %s", err, err)
}
if err = fileContentsEqual(t, srcPath, dstPath); err != nil {
t.Fatal(err)
}
}
// D. Symbol link following version:
// SRC specifies a file and DST exists as a directory. This should place
// a copy of the source file inside it using the basename from SRC. Ensure
// this works whether DST has a trailing path separator or not.
func TestCopyCaseDFSym(t *testing.T) {
tmpDirA, tmpDirB := getTestTempDirs(t)
defer removeAllPaths(tmpDirA, tmpDirB)
// Load A and B with some sample files and directories.
createSampleDir(t, tmpDirA)
createSampleDir(t, tmpDirB)
srcPath := filepath.Join(tmpDirA, "symlink4")
linkTarget := filepath.Join(tmpDirA, "file1")
dstDir := filepath.Join(tmpDirB, "dir1")
dstPath := filepath.Join(dstDir, "symlink4")
var err error
// Ensure that dstPath doesn't exist.
if _, err = os.Stat(dstPath); !os.IsNotExist(err) {
t.Fatalf("did not expect dstPath %q to exist", dstPath)
}
if err = testCopyHelperFSym(t, srcPath, dstDir); err != nil {
t.Fatalf("unexpected error %T: %s", err, err)
}
if err = fileContentsEqual(t, linkTarget, dstPath); err != nil {
t.Fatal(err)
}
// Now try again but using a trailing path separator for dstDir.
if err = os.RemoveAll(dstDir); err != nil {
t.Fatalf("unable to remove dstDir: %s", err)
}
if err = os.MkdirAll(dstDir, os.FileMode(0755)); err != nil {
t.Fatalf("unable to make dstDir: %s", err)
}
dstDir = joinTrailingSep(tmpDirB, "dir1")
if err = testCopyHelperFSym(t, srcPath, dstDir); err != nil {
t.Fatalf("unexpected error %T: %s", err, err)
}
if err = fileContentsEqual(t, linkTarget, dstPath); err != nil {
t.Fatal(err)
}
}
// E. SRC specifies a directory and DST does not exist. This should create a
// directory at DST and copy the contents of the SRC directory into the DST
// directory. Ensure this works whether DST has a trailing path separator or
// not.
func TestCopyCaseE(t *testing.T) {
tmpDirA, tmpDirB := getTestTempDirs(t)
defer removeAllPaths(tmpDirA, tmpDirB)
// Load A with some sample files and directories.
createSampleDir(t, tmpDirA)
srcDir := filepath.Join(tmpDirA, "dir1")
dstDir := filepath.Join(tmpDirB, "testDir")
var err error
if err = testCopyHelper(t, srcDir, dstDir); err != nil {
t.Fatalf("unexpected error %T: %s", err, err)
}
if err = dirContentsEqual(t, dstDir, srcDir); err != nil {
t.Log("dir contents not equal")
logDirContents(t, tmpDirA)
logDirContents(t, tmpDirB)
t.Fatal(err)
}
// Now try again but using a trailing path separator for dstDir.
if err = os.RemoveAll(dstDir); err != nil {
t.Fatalf("unable to remove dstDir: %s", err)
}
dstDir = joinTrailingSep(tmpDirB, "testDir")
if err = testCopyHelper(t, srcDir, dstDir); err != nil {
t.Fatalf("unexpected error %T: %s", err, err)
}
if err = dirContentsEqual(t, dstDir, srcDir); err != nil {
t.Fatal(err)
}
}
// E. Symbol link following version:
// SRC specifies a directory and DST does not exist. This should create a
// directory at DST and copy the contents of the SRC directory into the DST
// directory. Ensure this works whether DST has a trailing path separator or
// not.
func TestCopyCaseEFSym(t *testing.T) {
tmpDirA, tmpDirB := getTestTempDirs(t)
defer removeAllPaths(tmpDirA, tmpDirB)
// Load A with some sample files and directories.
createSampleDir(t, tmpDirA)
srcDir := filepath.Join(tmpDirA, "dirSymlink")
linkTarget := filepath.Join(tmpDirA, "dir1")
dstDir := filepath.Join(tmpDirB, "testDir")
var err error
if err = testCopyHelperFSym(t, srcDir, dstDir); err != nil {
t.Fatalf("unexpected error %T: %s", err, err)
}
if err = dirContentsEqual(t, dstDir, linkTarget); err != nil {
t.Log("dir contents not equal")
logDirContents(t, tmpDirA)
logDirContents(t, tmpDirB)
t.Fatal(err)
}
// Now try again but using a trailing path separator for dstDir.
if err = os.RemoveAll(dstDir); err != nil {
t.Fatalf("unable to remove dstDir: %s", err)
}
dstDir = joinTrailingSep(tmpDirB, "testDir")
if err = testCopyHelperFSym(t, srcDir, dstDir); err != nil {
t.Fatalf("unexpected error %T: %s", err, err)
}
if err = dirContentsEqual(t, dstDir, linkTarget); err != nil {
t.Fatal(err)
}
}
// F. SRC specifies a directory and DST exists as a file. This should cause an
// error as it is not possible to overwrite a file with a directory.
func TestCopyCaseF(t *testing.T) {
tmpDirA, tmpDirB := getTestTempDirs(t)
defer removeAllPaths(tmpDirA, tmpDirB)
// Load A and B with some sample files and directories.
createSampleDir(t, tmpDirA)
createSampleDir(t, tmpDirB)
srcDir := filepath.Join(tmpDirA, "dir1")
symSrcDir := filepath.Join(tmpDirA, "dirSymlink")
dstFile := filepath.Join(tmpDirB, "file1")
var err error
if err = testCopyHelper(t, srcDir, dstFile); err == nil {
t.Fatal("expected ErrCannotCopyDir error, but got nil instead")
}
if err != ErrCannotCopyDir {
t.Fatalf("expected ErrCannotCopyDir error, but got %T: %s", err, err)
}
// now test with symbol link
if err = testCopyHelperFSym(t, symSrcDir, dstFile); err == nil {
t.Fatal("expected ErrCannotCopyDir error, but got nil instead")
}
if err != ErrCannotCopyDir {
t.Fatalf("expected ErrCannotCopyDir error, but got %T: %s", err, err)
}
}
// G. SRC specifies a directory and DST exists as a directory. This should copy
// the SRC directory and all its contents to the DST directory. Ensure this
// works whether DST has a trailing path separator or not.
func TestCopyCaseG(t *testing.T) {
tmpDirA, tmpDirB := getTestTempDirs(t)
defer removeAllPaths(tmpDirA, tmpDirB)
// Load A and B with some sample files and directories.
createSampleDir(t, tmpDirA)
createSampleDir(t, tmpDirB)
srcDir := filepath.Join(tmpDirA, "dir1")
dstDir := filepath.Join(tmpDirB, "dir2")
resultDir := filepath.Join(dstDir, "dir1")
var err error
if err = testCopyHelper(t, srcDir, dstDir); err != nil {
t.Fatalf("unexpected error %T: %s", err, err)
}
if err = dirContentsEqual(t, resultDir, srcDir); err != nil {
t.Fatal(err)
}
// Now try again but using a trailing path separator for dstDir.
if err = os.RemoveAll(dstDir); err != nil {
t.Fatalf("unable to remove dstDir: %s", err)
}
if err = os.MkdirAll(dstDir, os.FileMode(0755)); err != nil {
t.Fatalf("unable to make dstDir: %s", err)
}
dstDir = joinTrailingSep(tmpDirB, "dir2")
if err = testCopyHelper(t, srcDir, dstDir); err != nil {
t.Fatalf("unexpected error %T: %s", err, err)
}
if err = dirContentsEqual(t, resultDir, srcDir); err != nil {
t.Fatal(err)
}
}
// G. Symbol link version:
// SRC specifies a directory and DST exists as a directory. This should copy
// the SRC directory and all its contents to the DST directory. Ensure this
// works whether DST has a trailing path separator or not.
func TestCopyCaseGFSym(t *testing.T) {
tmpDirA, tmpDirB := getTestTempDirs(t)
defer removeAllPaths(tmpDirA, tmpDirB)
// Load A and B with some sample files and directories.
createSampleDir(t, tmpDirA)
createSampleDir(t, tmpDirB)
srcDir := filepath.Join(tmpDirA, "dirSymlink")
linkTarget := filepath.Join(tmpDirA, "dir1")
dstDir := filepath.Join(tmpDirB, "dir2")
resultDir := filepath.Join(dstDir, "dirSymlink")
var err error
if err = testCopyHelperFSym(t, srcDir, dstDir); err != nil {
t.Fatalf("unexpected error %T: %s", err, err)
}
if err = dirContentsEqual(t, resultDir, linkTarget); err != nil {
t.Fatal(err)
}
// Now try again but using a trailing path separator for dstDir.
if err = os.RemoveAll(dstDir); err != nil {
t.Fatalf("unable to remove dstDir: %s", err)
}
if err = os.MkdirAll(dstDir, os.FileMode(0755)); err != nil {
t.Fatalf("unable to make dstDir: %s", err)
}
dstDir = joinTrailingSep(tmpDirB, "dir2")
if err = testCopyHelperFSym(t, srcDir, dstDir); err != nil {
t.Fatalf("unexpected error %T: %s", err, err)
}
if err = dirContentsEqual(t, resultDir, linkTarget); err != nil {
t.Fatal(err)
}
}
// H. SRC specifies a directory's contents only and DST does not exist. This
// should create a directory at DST and copy the contents of the SRC
// directory (but not the directory itself) into the DST directory. Ensure
// this works whether DST has a trailing path separator or not.
func TestCopyCaseH(t *testing.T) {
tmpDirA, tmpDirB := getTestTempDirs(t)
defer removeAllPaths(tmpDirA, tmpDirB)
// Load A with some sample files and directories.
createSampleDir(t, tmpDirA)
srcDir := joinTrailingSep(tmpDirA, "dir1") + "."
dstDir := filepath.Join(tmpDirB, "testDir")
var err error
if err = testCopyHelper(t, srcDir, dstDir); err != nil {
t.Fatalf("unexpected error %T: %s", err, err)
}
if err = dirContentsEqual(t, dstDir, srcDir); err != nil {
t.Log("dir contents not equal")
logDirContents(t, tmpDirA)
logDirContents(t, tmpDirB)
t.Fatal(err)
}
// Now try again but using a trailing path separator for dstDir.
if err = os.RemoveAll(dstDir); err != nil {
t.Fatalf("unable to remove dstDir: %s", err)
}
dstDir = joinTrailingSep(tmpDirB, "testDir")
if err = testCopyHelper(t, srcDir, dstDir); err != nil {
t.Fatalf("unexpected error %T: %s", err, err)
}
if err = dirContentsEqual(t, dstDir, srcDir); err != nil {
t.Log("dir contents not equal")
logDirContents(t, tmpDirA)
logDirContents(t, tmpDirB)
t.Fatal(err)
}
}
// H. Symbol link following version:
// SRC specifies a directory's contents only and DST does not exist. This
// should create a directory at DST and copy the contents of the SRC
// directory (but not the directory itself) into the DST directory. Ensure
// this works whether DST has a trailing path separator or not.
func TestCopyCaseHFSym(t *testing.T) {
tmpDirA, tmpDirB := getTestTempDirs(t)
defer removeAllPaths(tmpDirA, tmpDirB)
// Load A with some sample files and directories.
createSampleDir(t, tmpDirA)
srcDir := joinTrailingSep(tmpDirA, "dirSymlink") + "."
linkTarget := filepath.Join(tmpDirA, "dir1")
dstDir := filepath.Join(tmpDirB, "testDir")
var err error
if err = testCopyHelperFSym(t, srcDir, dstDir); err != nil {
t.Fatalf("unexpected error %T: %s", err, err)
}
if err = dirContentsEqual(t, dstDir, linkTarget); err != nil {
t.Log("dir contents not equal")
logDirContents(t, tmpDirA)
logDirContents(t, tmpDirB)
t.Fatal(err)
}
// Now try again but using a trailing path separator for dstDir.
if err = os.RemoveAll(dstDir); err != nil {
t.Fatalf("unable to remove dstDir: %s", err)
}
dstDir = joinTrailingSep(tmpDirB, "testDir")
if err = testCopyHelperFSym(t, srcDir, dstDir); err != nil {
t.Fatalf("unexpected error %T: %s", err, err)
}
if err = dirContentsEqual(t, dstDir, linkTarget); err != nil {
t.Log("dir contents not equal")
logDirContents(t, tmpDirA)
logDirContents(t, tmpDirB)
t.Fatal(err)
}
}
// I. SRC specifies a directory's contents only and DST exists as a file. This
// should cause an error as it is not possible to overwrite a file with a
// directory.
func TestCopyCaseI(t *testing.T) {
tmpDirA, tmpDirB := getTestTempDirs(t)
defer removeAllPaths(tmpDirA, tmpDirB)
// Load A and B with some sample files and directories.
createSampleDir(t, tmpDirA)
createSampleDir(t, tmpDirB)
srcDir := joinTrailingSep(tmpDirA, "dir1") + "."
symSrcDir := filepath.Join(tmpDirB, "dirSymlink")
dstFile := filepath.Join(tmpDirB, "file1")
var err error
if err = testCopyHelper(t, srcDir, dstFile); err == nil {
t.Fatal("expected ErrCannotCopyDir error, but got nil instead")
}
if err != ErrCannotCopyDir {
t.Fatalf("expected ErrCannotCopyDir error, but got %T: %s", err, err)
}
// now try with symbol link of dir
if err = testCopyHelperFSym(t, symSrcDir, dstFile); err == nil {
t.Fatal("expected ErrCannotCopyDir error, but got nil instead")
}
if err != ErrCannotCopyDir {
t.Fatalf("expected ErrCannotCopyDir error, but got %T: %s", err, err)
}
}
// J. SRC specifies a directory's contents only and DST exists as a directory.
// This should copy the contents of the SRC directory (but not the directory
// itself) into the DST directory. Ensure this works whether DST has a
// trailing path separator or not.
func TestCopyCaseJ(t *testing.T) {
tmpDirA, tmpDirB := getTestTempDirs(t)
defer removeAllPaths(tmpDirA, tmpDirB)
// Load A and B with some sample files and directories.
createSampleDir(t, tmpDirA)
createSampleDir(t, tmpDirB)
srcDir := joinTrailingSep(tmpDirA, "dir1") + "."
dstDir := filepath.Join(tmpDirB, "dir5")
var err error
// first to create an empty dir
if err = os.MkdirAll(dstDir, os.FileMode(0755)); err != nil {
t.Fatalf("unable to make dstDir: %s", err)
}
if err = testCopyHelper(t, srcDir, dstDir); err != nil {
t.Fatalf("unexpected error %T: %s", err, err)
}
if err = dirContentsEqual(t, dstDir, srcDir); err != nil {
t.Fatal(err)
}
// Now try again but using a trailing path separator for dstDir.
if err = os.RemoveAll(dstDir); err != nil {
t.Fatalf("unable to remove dstDir: %s", err)
}
if err = os.MkdirAll(dstDir, os.FileMode(0755)); err != nil {
t.Fatalf("unable to make dstDir: %s", err)
}
dstDir = joinTrailingSep(tmpDirB, "dir5")
if err = testCopyHelper(t, srcDir, dstDir); err != nil {
t.Fatalf("unexpected error %T: %s", err, err)
}
if err = dirContentsEqual(t, dstDir, srcDir); err != nil {
t.Fatal(err)
}
}
// J. Symbol link following version:
// SRC specifies a directory's contents only and DST exists as a directory.
// This should copy the contents of the SRC directory (but not the directory
// itself) into the DST directory. Ensure this works whether DST has a
// trailing path separator or not.
func TestCopyCaseJFSym(t *testing.T) {
tmpDirA, tmpDirB := getTestTempDirs(t)
defer removeAllPaths(tmpDirA, tmpDirB)
// Load A and B with some sample files and directories.
createSampleDir(t, tmpDirA)
createSampleDir(t, tmpDirB)
srcDir := joinTrailingSep(tmpDirA, "dirSymlink") + "."
linkTarget := filepath.Join(tmpDirA, "dir1")
dstDir := filepath.Join(tmpDirB, "dir5")
var err error
// first to create an empty dir
if err = os.MkdirAll(dstDir, os.FileMode(0755)); err != nil {
t.Fatalf("unable to make dstDir: %s", err)
}
if err = testCopyHelperFSym(t, srcDir, dstDir); err != nil {
t.Fatalf("unexpected error %T: %s", err, err)
}
if err = dirContentsEqual(t, dstDir, linkTarget); err != nil {
t.Fatal(err)
}
// Now try again but using a trailing path separator for dstDir.
if err = os.RemoveAll(dstDir); err != nil {
t.Fatalf("unable to remove dstDir: %s", err)
}
if err = os.MkdirAll(dstDir, os.FileMode(0755)); err != nil {
t.Fatalf("unable to make dstDir: %s", err)
}
dstDir = joinTrailingSep(tmpDirB, "dir5")
if err = testCopyHelperFSym(t, srcDir, dstDir); err != nil {
t.Fatalf("unexpected error %T: %s", err, err)
}
if err = dirContentsEqual(t, dstDir, linkTarget); err != nil {
t.Fatal(err)
}
}

View File

@@ -0,0 +1,14 @@
// +build !windows
package system
import (
"time"
)
//setCTime will set the create time on a file. On Unix, the create
//time is updated as a side effect of setting the modified time, so
//no action is required.
func setCTime(path string, ctime time.Time) error {
return nil
}

View File

@@ -0,0 +1,27 @@
// +build windows
package system
import (
"syscall"
"time"
)
//setCTime will set the create time on a file. On Windows, this requires
//calling SetFileTime and explicitly including the create time.
func setCTime(path string, ctime time.Time) error {
ctimespec := syscall.NsecToTimespec(ctime.UnixNano())
pathp, e := syscall.UTF16PtrFromString(path)
if e != nil {
return e
}
h, e := syscall.CreateFile(pathp,
syscall.FILE_WRITE_ATTRIBUTES, syscall.FILE_SHARE_WRITE, nil,
syscall.OPEN_EXISTING, syscall.FILE_FLAG_BACKUP_SEMANTICS, 0)
if e != nil {
return e
}
defer syscall.Close(h)
c := syscall.NsecToFiletime(syscall.TimespecToNsec(ctimespec))
return syscall.SetFileTime(h, &c, nil, nil)
}

View File

@@ -0,0 +1,15 @@
package system
import (
"syscall"
)
// fromStatT creates a system.StatT type from a syscall.Stat_t type
func fromStatT(s *syscall.Stat_t) (*StatT, error) {
return &StatT{size: s.Size,
mode: uint32(s.Mode),
uid: s.Uid,
gid: s.Gid,
rdev: uint64(s.Rdev),
mtim: s.Mtim}, nil
}

View File

@@ -1,4 +1,4 @@
// +build !linux,!windows,!freebsd,!solaris
// +build !linux,!windows,!freebsd,!solaris,!openbsd
package system

View File

@@ -210,13 +210,13 @@ type CancelFunc func()
// call cancel as soon as the operations running in this Context complete.
func WithCancel(parent Context) (ctx Context, cancel CancelFunc) {
c := newCancelCtx(parent)
propagateCancel(parent, &c)
return &c, func() { c.cancel(true, Canceled) }
propagateCancel(parent, c)
return c, func() { c.cancel(true, Canceled) }
}
// newCancelCtx returns an initialized cancelCtx.
func newCancelCtx(parent Context) cancelCtx {
return cancelCtx{
func newCancelCtx(parent Context) *cancelCtx {
return &cancelCtx{
Context: parent,
done: make(chan struct{}),
}
@@ -259,7 +259,7 @@ func parentCancelCtx(parent Context) (*cancelCtx, bool) {
case *cancelCtx:
return c, true
case *timerCtx:
return &c.cancelCtx, true
return c.cancelCtx, true
case *valueCtx:
parent = c.Context
default:
@@ -377,7 +377,7 @@ func WithDeadline(parent Context, deadline time.Time) (Context, CancelFunc) {
// implement Done and Err. It implements cancel by stopping its timer then
// delegating to cancelCtx.cancel.
type timerCtx struct {
cancelCtx
*cancelCtx
timer *time.Timer // Under cancelCtx.mu.
deadline time.Time

View File

@@ -32,6 +32,7 @@ type APIImages struct {
// Image is the type representing a docker image and its various properties
type Image struct {
ID string `json:"Id" yaml:"Id"`
RepoTags []string `json:"RepoTags,omitempty" yaml:"RepoTags,omitempty"`
Parent string `json:"Parent,omitempty" yaml:"Parent,omitempty"`
Comment string `json:"Comment,omitempty" yaml:"Comment,omitempty"`
Created time.Time `json:"Created,omitempty" yaml:"Created,omitempty"`
@@ -421,6 +422,17 @@ type BuildImageOptions struct {
AuthConfigs AuthConfigurations `qs:"-"` // for newer docker X-Registry-Config header
ContextDir string `qs:"-"`
Ulimits []ULimit `qs:"-"`
BuildArgs []BuildArg `qs:"-"`
}
// BuildArg represents arguments that can be passed to the image when building
// it from a Dockerfile.
//
// For more details about the Docker building process, see
// http://goo.gl/tlPXPu.
type BuildArg struct {
Name string `json:"Name,omitempty" yaml:"Name,omitempty"`
Value string `json:"Value,omitempty" yaml:"Value,omitempty"`
}
// BuildImage builds an image from a tarball's url or a Dockerfile in the input
@@ -463,6 +475,18 @@ func (c *Client) BuildImage(opts BuildImageOptions) error {
}
}
if len(opts.BuildArgs) > 0 {
v := make(map[string]string)
for _, arg := range opts.BuildArgs {
v[arg.Name] = arg.Value
}
if b, err := json.Marshal(v); err == nil {
item := url.Values(map[string][]string{})
item.Add("buildargs", string(b))
qs = fmt.Sprintf("%s&%s", qs, item.Encode())
}
}
return c.stream("POST", fmt.Sprintf("/build?%s", qs), streamOptions{
setRawTerminal: true,
rawJSONStream: opts.RawJSONStream,

View File

@@ -685,6 +685,7 @@ func TestBuildImageParameters(t *testing.T) {
CPUPeriod: 100000,
CPUSetCPUs: "0-3",
Ulimits: []ULimit{{Name: "nofile", Soft: 100, Hard: 200}},
BuildArgs: []BuildArg{{Name: "SOME_VAR", Value: "some_value"}},
InputStream: &buf,
OutputStream: &buf,
}
@@ -706,7 +707,8 @@ func TestBuildImageParameters(t *testing.T) {
"cpuquota": {"7500"},
"cpuperiod": {"100000"},
"cpusetcpus": {"0-3"},
"ulimits": {"[{\"Name\":\"nofile\",\"Soft\":100,\"Hard\":200}]"},
"ulimits": {`[{"Name":"nofile","Soft":100,"Hard":200}]`},
"buildargs": {`{"SOME_VAR":"some_value"}`},
}
got := map[string][]string(req.URL.Query())
if !reflect.DeepEqual(got, expected) {

View File

@@ -46,7 +46,7 @@ func TestIntegrationPullCreateStartLogs(t *testing.T) {
t.Error(err)
}
if status != 0 {
t.Error("WaitContainer(%q): wrong status. Want 0. Got %d", container.ID, status)
t.Errorf("WaitContainer(%q): wrong status. Want 0. Got %d", container.ID, status)
}
var stdout, stderr bytes.Buffer
logsOpts := LogsOptions{

View File

@@ -4,7 +4,10 @@
package docker
import "strings"
import (
"encoding/json"
"strings"
)
// Version returns version information about the docker server.
//
@@ -22,17 +25,81 @@ func (c *Client) Version() (*Env, error) {
return &env, nil
}
// DockerInfo contains information about the Docker server
//
// See https://goo.gl/bHUoz9 for more details.
type DockerInfo struct {
ID string
Containers int
ContainersRunning int
ContainersPaused int
ContainersStopped int
Images int
Driver string
DriverStatus [][2]string
SystemStatus [][2]string
Plugins PluginsInfo
MemoryLimit bool
SwapLimit bool
KernelMemory bool
CPUCfsPeriod bool `json:"CpuCfsPeriod"`
CPUCfsQuota bool `json:"CpuCfsQuota"`
CPUShares bool
CPUSet bool
IPv4Forwarding bool
BridgeNfIptables bool
BridgeNfIP6tables bool `json:"BridgeNfIp6tables"`
Debug bool
NFd int
OomKillDisable bool
NGoroutines int
SystemTime string
ExecutionDriver string
LoggingDriver string
CgroupDriver string
NEventsListener int
KernelVersion string
OperatingSystem string
OSType string
Architecture string
IndexServerAddress string
NCPU int
MemTotal int64
DockerRootDir string
HTTPProxy string `json:"HttpProxy"`
HTTPSProxy string `json:"HttpsProxy"`
NoProxy string
Name string
Labels []string
ExperimentalBuild bool
ServerVersion string
ClusterStore string
ClusterAdvertise string
}
// PluginsInfo is a struct with the plugins registered with the docker daemon
//
// for more information, see: https://goo.gl/bHUoz9
type PluginsInfo struct {
// List of Volume plugins registered
Volume []string
// List of Network plugins registered
Network []string
// List of Authorization plugins registered
Authorization []string
}
// Info returns system-wide information about the Docker server.
//
// See https://goo.gl/ElTHi2 for more details.
func (c *Client) Info() (*Env, error) {
func (c *Client) Info() (*DockerInfo, error) {
resp, err := c.do("GET", "/info", doOptions{})
if err != nil {
return nil, err
}
defer resp.Body.Close()
var info Env
if err := info.Decode(resp.Body); err != nil {
var info DockerInfo
if err := json.NewDecoder(resp.Body).Decode(&info); err != nil {
return nil, err
}
return &info, nil

View File

@@ -8,7 +8,6 @@ import (
"net/http"
"net/url"
"reflect"
"sort"
"testing"
)
@@ -71,32 +70,29 @@ func TestInfo(t *testing.T) {
body := `{
"Containers":11,
"Images":16,
"Debug":0,
"Debug":false,
"NFd":11,
"NGoroutines":21,
"MemoryLimit":1,
"SwapLimit":0
"MemoryLimit":true,
"SwapLimit":false
}`
fakeRT := FakeRoundTripper{message: body, status: http.StatusOK}
client := newTestClient(&fakeRT)
expected := Env{}
expected.SetInt("Containers", 11)
expected.SetInt("Images", 16)
expected.SetBool("Debug", false)
expected.SetInt("NFd", 11)
expected.SetInt("NGoroutines", 21)
expected.SetBool("MemoryLimit", true)
expected.SetBool("SwapLimit", false)
expected := &DockerInfo{
Containers: 11,
Images: 16,
Debug: false,
NFd: 11,
NGoroutines: 21,
MemoryLimit: true,
SwapLimit: false,
}
info, err := client.Info()
if err != nil {
t.Fatal(err)
}
infoSlice := []string(*info)
expectedSlice := []string(expected)
sort.Strings(infoSlice)
sort.Strings(expectedSlice)
if !reflect.DeepEqual(expectedSlice, infoSlice) {
t.Errorf("Info(): Wrong result.\nWant %#v.\nGot %#v.", expected, *info)
if !reflect.DeepEqual(expected, info) {
t.Errorf("Info(): Wrong result.\nWant %#v.\nGot %#v.", expected, info)
}
req := fakeRT.requests[0]
if req.Method != "GET" {

View File

@@ -5,6 +5,7 @@
package docker
import (
"bytes"
"encoding/json"
"errors"
"fmt"
@@ -26,6 +27,7 @@ type Network struct {
IPAM IPAMOptions
Containers map[string]Endpoint
Options map[string]string
Internal bool
}
// Endpoint contains network resources allocated and used for a container in a network
@@ -55,6 +57,31 @@ func (c *Client) ListNetworks() ([]Network, error) {
return networks, nil
}
// NetworkFilterOpts is an aggregation of key=value that Docker
// uses to filter networks
type NetworkFilterOpts map[string]map[string]bool
// FilteredListNetworks returns all networks with the filters applied
//
// See goo.gl/zd2mx4 for more details.
func (c *Client) FilteredListNetworks(opts NetworkFilterOpts) ([]Network, error) {
params := bytes.NewBuffer(nil)
if err := json.NewEncoder(params).Encode(&opts); err != nil {
return nil, err
}
path := "/networks?filters=" + params.String()
resp, err := c.do("GET", path, doOptions{})
if err != nil {
return nil, err
}
defer resp.Body.Close()
var networks []Network
if err := json.NewDecoder(resp.Body).Decode(&networks); err != nil {
return nil, err
}
return networks, nil
}
// NetworkInfo returns information about a network by its ID.
//
// See https://goo.gl/6GugX3 for more details.
@@ -158,14 +185,40 @@ func (c *Client) RemoveNetwork(id string) error {
return nil
}
// NetworkConnectionOptions specify parameters to the ConnectNetwork and DisconnectNetwork function.
// NetworkConnectionOptions specify parameters to the ConnectNetwork and
// DisconnectNetwork function.
//
// See https://goo.gl/6GugX3 for more details.
// See https://goo.gl/RV7BJU for more details.
type NetworkConnectionOptions struct {
Container string
// EndpointConfig is only applicable to the ConnectNetwork call
EndpointConfig *EndpointConfig `json:"EndpointConfig,omitempty"`
// Force is only applicable to the DisconnectNetwork call
Force bool
}
// ConnectNetwork adds a container to a network or returns an error in case of failure.
// EndpointConfig stores network endpoint details
//
// See https://goo.gl/RV7BJU for more details.
type EndpointConfig struct {
IPAMConfig *EndpointIPAMConfig
Links []string
Aliases []string
}
// EndpointIPAMConfig represents IPAM configurations for an
// endpoint
//
// See https://goo.gl/RV7BJU for more details.
type EndpointIPAMConfig struct {
IPv4Address string `json:",omitempty"`
IPv6Address string `json:",omitempty"`
}
// ConnectNetwork adds a container to a network or returns an error in case of
// failure.
//
// See https://goo.gl/6GugX3 for more details.
func (c *Client) ConnectNetwork(id string, opts NetworkConnectionOptions) error {
@@ -180,7 +233,8 @@ func (c *Client) ConnectNetwork(id string, opts NetworkConnectionOptions) error
return nil
}
// DisconnectNetwork removes a container from a network or returns an error in case of failure.
// DisconnectNetwork removes a container from a network or returns an error in
// case of failure.
//
// See https://goo.gl/6GugX3 for more details.
func (c *Client) DisconnectNetwork(id string, opts NetworkConnectionOptions) error {
@@ -204,7 +258,8 @@ func (err *NoSuchNetwork) Error() string {
return fmt.Sprintf("No such network: %s", err.ID)
}
// NoSuchNetwork is the error returned when a given network or container does not exist.
// NoSuchNetworkOrContainer is the error returned when a given network or
// container does not exist.
type NoSuchNetworkOrContainer struct {
NetworkID string
ContainerID string

View File

@@ -5,6 +5,7 @@
package docker
import (
"bytes"
"encoding/json"
"net/http"
"net/url"
@@ -42,6 +43,39 @@ func TestListNetworks(t *testing.T) {
}
}
func TestFilteredListNetworks(t *testing.T) {
jsonNetworks := `[
{
"ID": "9fb1e39c",
"Name": "foo",
"Type": "bridge",
"Endpoints":[{"ID": "c080be979dda", "Name": "lllll2222", "Network": "9fb1e39c"}]
}
]`
var expected []Network
err := json.Unmarshal([]byte(jsonNetworks), &expected)
if err != nil {
t.Fatal(err)
}
wantQuery := "filters={\"name\":{\"blah\":true}}\n"
fakeRT := &FakeRoundTripper{message: jsonNetworks, status: http.StatusOK}
client := newTestClient(fakeRT)
opts := NetworkFilterOpts{
"name": map[string]bool{"blah": true},
}
containers, err := client.FilteredListNetworks(opts)
if err != nil {
t.Fatal(err)
}
if !reflect.DeepEqual(containers, expected) {
t.Errorf("ListNetworks: Expected %#v. Got %#v.", expected, containers)
}
query := fakeRT.requests[0].URL.RawQuery
if query != wantQuery {
t.Errorf("FilteredListNetworks: Expected query: %q, got: %q", wantQuery, query)
}
}
func TestNetworkInfo(t *testing.T) {
jsonNetwork := `{
"ID": "8dfafdbc3a40",
@@ -118,7 +152,7 @@ func TestNetworkConnect(t *testing.T) {
id := "8dfafdbc3a40"
fakeRT := &FakeRoundTripper{message: "", status: http.StatusNoContent}
client := newTestClient(fakeRT)
opts := NetworkConnectionOptions{"foobar"}
opts := NetworkConnectionOptions{Container: "foobar"}
err := client.ConnectNetwork(id, opts)
if err != nil {
t.Fatal(err)
@@ -134,9 +168,46 @@ func TestNetworkConnect(t *testing.T) {
}
}
func TestNetworkConnectWithEndpoint(t *testing.T) {
wantJSON := `{"Container":"foobar","EndpointConfig":{"IPAMConfig":{"IPv4Address":"8.8.8.8"},"Links":null,"Aliases":null},"Force":false}`
var wantObj NetworkConnectionOptions
json.NewDecoder(bytes.NewBuffer([]byte(wantJSON))).Decode(&wantObj)
id := "8dfafdbc3a40"
fakeRT := &FakeRoundTripper{message: "", status: http.StatusNoContent}
client := newTestClient(fakeRT)
opts := NetworkConnectionOptions{
Container: "foobar",
EndpointConfig: &EndpointConfig{
IPAMConfig: &EndpointIPAMConfig{
IPv4Address: "8.8.8.8",
},
},
}
err := client.ConnectNetwork(id, opts)
if err != nil {
t.Fatal(err)
}
req := fakeRT.requests[0]
expectedMethod := "POST"
if req.Method != expectedMethod {
t.Errorf("ConnectNetwork(%q): Wrong HTTP method. Want %s. Got %s.", id, expectedMethod, req.Method)
}
u, _ := url.Parse(client.getURL("/networks/" + id + "/connect"))
if req.URL.Path != u.Path {
t.Errorf("ConnectNetwork(%q): Wrong request path. Want %q. Got %q.", id, u.Path, req.URL.Path)
}
var in NetworkConnectionOptions
if err := json.NewDecoder(req.Body).Decode(&in); err != nil {
t.Errorf("ConnectNetwork: error parsing JSON data sent: %q", err)
}
if !reflect.DeepEqual(in, wantObj) {
t.Errorf("ConnectNetwork: wanted %#v send, got: %#v", wantObj, in)
}
}
func TestNetworkConnectNotFound(t *testing.T) {
client := newTestClient(&FakeRoundTripper{message: "no such network container", status: http.StatusNotFound})
opts := NetworkConnectionOptions{"foobar"}
opts := NetworkConnectionOptions{Container: "foobar"}
err := client.ConnectNetwork("8dfafdbc3a40", opts)
if serr, ok := err.(*NoSuchNetworkOrContainer); !ok {
t.Errorf("ConnectNetwork: wrong error type: %s.", serr)
@@ -147,7 +218,7 @@ func TestNetworkDisconnect(t *testing.T) {
id := "8dfafdbc3a40"
fakeRT := &FakeRoundTripper{message: "", status: http.StatusNoContent}
client := newTestClient(fakeRT)
opts := NetworkConnectionOptions{"foobar"}
opts := NetworkConnectionOptions{Container: "foobar"}
err := client.DisconnectNetwork(id, opts)
if err != nil {
t.Fatal(err)
@@ -165,7 +236,7 @@ func TestNetworkDisconnect(t *testing.T) {
func TestNetworkDisconnectNotFound(t *testing.T) {
client := newTestClient(&FakeRoundTripper{message: "no such network container", status: http.StatusNotFound})
opts := NetworkConnectionOptions{"foobar"}
opts := NetworkConnectionOptions{Container: "foobar"}
err := client.DisconnectNetwork("8dfafdbc3a40", opts)
if serr, ok := err.(*NoSuchNetworkOrContainer); !ok {
t.Errorf("DisconnectNetwork: wrong error type: %s.", serr)

View File

@@ -144,6 +144,7 @@ func (s *DockerServer) buildMuxer() {
s.mux.Path("/volumes/create").Methods("POST").HandlerFunc(s.handlerWrapper(s.createVolume))
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))
}
// SetHook changes the hook function used by the server.
@@ -743,10 +744,9 @@ func (s *DockerServer) commitContainer(w http.ResponseWriter, r *http.Request) {
http.Error(w, err.Error(), http.StatusNotFound)
return
}
var config *docker.Config
config := new(docker.Config)
runConfig := r.URL.Query().Get("run")
if runConfig != "" {
config = new(docker.Config)
err = json.Unmarshal([]byte(runConfig), config)
if err != nil {
http.Error(w, err.Error(), http.StatusBadRequest)
@@ -828,7 +828,8 @@ func (s *DockerServer) pullImage(w http.ResponseWriter, r *http.Request) {
fromImageName := r.URL.Query().Get("fromImage")
tag := r.URL.Query().Get("tag")
image := docker.Image{
ID: s.generateID(),
ID: s.generateID(),
Config: &docker.Config{},
}
s.iMut.Lock()
s.images = append(s.images, image)
@@ -1244,3 +1245,86 @@ func (s *DockerServer) removeVolume(w http.ResponseWriter, r *http.Request) {
s.volStore[vol.volume.Name] = nil
w.WriteHeader(http.StatusNoContent)
}
func (s *DockerServer) infoDocker(w http.ResponseWriter, r *http.Request) {
s.cMut.RLock()
defer s.cMut.RUnlock()
s.iMut.RLock()
defer s.iMut.RUnlock()
var running, stopped, paused int
for _, c := range s.containers {
if c.State.Running {
running++
} else {
stopped++
}
if c.State.Paused {
paused++
}
}
envs := map[string]interface{}{
"ID": "AAAA:XXXX:0000:BBBB:AAAA:XXXX:0000:BBBB:AAAA:XXXX:0000:BBBB",
"Containers": len(s.containers),
"ContainersRunning": running,
"ContainersPaused": paused,
"ContainersStopped": stopped,
"Images": len(s.images),
"Driver": "aufs",
"DriverStatus": [][]string{},
"SystemStatus": nil,
"Plugins": map[string]interface{}{
"Volume": []string{
"local",
},
"Network": []string{
"bridge",
"null",
"host",
},
"Authorization": nil,
},
"MemoryLimit": true,
"SwapLimit": false,
"CpuCfsPeriod": true,
"CpuCfsQuota": true,
"CPUShares": true,
"CPUSet": true,
"IPv4Forwarding": true,
"BridgeNfIptables": true,
"BridgeNfIp6tables": true,
"Debug": false,
"NFd": 79,
"OomKillDisable": true,
"NGoroutines": 101,
"SystemTime": "2016-02-25T18:13:10.25870078Z",
"ExecutionDriver": "native-0.2",
"LoggingDriver": "json-file",
"NEventsListener": 0,
"KernelVersion": "3.13.0-77-generic",
"OperatingSystem": "Ubuntu 14.04.3 LTS",
"OSType": "linux",
"Architecture": "x86_64",
"IndexServerAddress": "https://index.docker.io/v1/",
"RegistryConfig": map[string]interface{}{
"InsecureRegistryCIDRs": []string{},
"IndexConfigs": map[string]interface{}{},
"Mirrors": nil,
},
"InitSha1": "e2042dbb0fcf49bb9da199186d9a5063cda92a01",
"InitPath": "/usr/lib/docker/dockerinit",
"NCPU": 1,
"MemTotal": 2099204096,
"DockerRootDir": "/var/lib/docker",
"HttpProxy": "",
"HttpsProxy": "",
"NoProxy": "",
"Name": "vagrant-ubuntu-trusty-64",
"Labels": nil,
"ExperimentalBuild": false,
"ServerVersion": "1.10.1",
"ClusterStore": "",
"ClusterAdvertise": "",
}
w.WriteHeader(http.StatusOK)
json.NewEncoder(w).Encode(envs)
}

View File

@@ -381,6 +381,9 @@ func TestCommitContainer(t *testing.T) {
if got := recorder.Body.String(); got != expected {
t.Errorf("CommitContainer: wrong response body. Want %q. Got %q.", expected, got)
}
if server.images[0].Config == nil {
t.Error("CommitContainer: image Config should not be nil.")
}
}
func TestCommitContainerComplete(t *testing.T) {
@@ -1059,6 +1062,9 @@ func TestPullImage(t *testing.T) {
if _, ok := server.imgIDs["base"]; !ok {
t.Error("PullImage: Repository should not be empty.")
}
if server.images[0].Config == nil {
t.Error("PullImage: Image Config should not be nil.")
}
}
func TestPullImageWithTag(t *testing.T) {
@@ -2101,3 +2107,26 @@ func TestUploadToContainerMissingContainer(t *testing.T) {
t.Errorf("UploadToContainer: wrong status. Want %d. Got %d.", http.StatusNotFound, recorder.Code)
}
}
func TestInfoDocker(t *testing.T) {
server, _ := NewServer("127.0.0.1:0", nil, nil)
addContainers(server, 1)
server.buildMuxer()
recorder := httptest.NewRecorder()
request, _ := http.NewRequest("GET", "/info", nil)
server.ServeHTTP(recorder, request)
if recorder.Code != http.StatusOK {
t.Fatalf("InfoDocker: wrong status. Want %d. Got %d.", http.StatusOK, recorder.Code)
}
var infoData map[string]interface{}
err := json.Unmarshal(recorder.Body.Bytes(), &infoData)
if err != nil {
t.Fatal(err)
}
if infoData["Containers"].(float64) != 1.0 {
t.Fatalf("InfoDocker: wrong containers count. Want %f. Got %f.", 1.0, infoData["Containers"])
}
if infoData["DockerRootDir"].(string) != "/var/lib/docker" {
t.Fatalf("InfoDocker: wrong docker root. Want /var/lib/docker. Got %s.", infoData["DockerRootDir"])
}
}

View File

@@ -0,0 +1,17 @@
#!/bin/bash -x
# 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.
if [[ $TRAVIS_OS_NAME == "linux" ]]; then
sudo stop docker || true
sudo rm -rf /var/lib/docker
sudo rm -f `which docker`
set -e
sudo apt-key adv --keyserver hkp://p80.pool.sks-keyservers.net:80 --recv-keys 58118E89F3A912897C070ADBF76221572C52609D
echo "deb https://apt.dockerproject.org/repo ubuntu-trusty main" | sudo tee /etc/apt/sources.list.d/docker.list
sudo apt-get update
sudo apt-get install docker-engine=${DOCKER_VERSION}-0~$(lsb_release -cs) -y --force-yes -o Dpkg::Options::="--force-confdef" -o Dpkg::Options::="--force-confold"
fi

View File

@@ -0,0 +1,15 @@
#!/bin/bash -ex
# 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.
if ! [[ $TRAVIS_GO_VERSION =~ ^1\.[34] ]]; then
make lint
fi
make vet fmtcheck gotest
if [[ $TRAVIS_OS_NAME == "linux" ]]; then
DOCKER_HOST=tcp://127.0.0.1:2375 make integration
fi

2
vendor/manifest vendored
View File

@@ -546,7 +546,7 @@
{
"importpath": "github.com/fsouza/go-dockerclient",
"repository": "https://github.com/fsouza/go-dockerclient",
"revision": "0099401a7342ad77e71ca9f9a57c5e72fb80f6b2",
"revision": "1b46a3bbaf08d19385915795bae730a318f3dd69",
"branch": "master"
},
{