Merge pull request #589 from monnand/update-docker-client

update github.com/fsouza/go-dockerclient
This commit is contained in:
Daniel Smith 2014-07-23 18:00:24 -07:00
commit 65575953c0
13 changed files with 1003 additions and 146 deletions

View File

@ -239,7 +239,7 @@ func (kl *Kubelet) runContainer(pod *Pod, container *api.Container, podVolumes v
ExposedPorts: exposedPorts,
Hostname: container.Name,
Image: container.Image,
Memory: int64(container.Memory),
Memory: uint64(container.Memory),
CpuShares: int64(milliCPUToShares(container.CPU)),
Volumes: volumes,
WorkingDir: container.WorkingDir,

View File

@ -2,6 +2,7 @@ language: go
go:
- 1.1.2
- 1.2
- 1.3
- tip
env:
- GOARCH=amd64

View File

@ -10,6 +10,7 @@ Ed <edrocksit@gmail.com>
Eric Anderson <anderson@copperegg.com>
Flavia Missi <flaviamissi@gmail.com>
Francisco Souza <f@souza.cc>
Jari Kolehmainen <jari.kolehmainen@digia.com>
Jason Wilder <jwilder@litl.com>
Jean-Baptiste Dalido <jeanbaptiste@appgratis.com>
Jeff Mitchell <jeffrey.mitchell@gmail.com>
@ -19,6 +20,7 @@ Omeid Matten <public@omeid.me>
Paul Morie <pmorie@gmail.com>
Peter Jihoon Kim <raingrove@gmail.com>
Philippe Lafoucrière <philippe.lafoucriere@tech-angels.com>
Rafe Colton <r.colton@modcloth.com>
Salvador Gironès <salvadorgirones@gmail.com>
Simon Eskildsen <sirup@sirupsen.com>
Simon Menke <simon.menke@gmail.com>

View File

@ -9,12 +9,6 @@ This package presents a client for the Docker remote API.
For more details, check the [remote API documentation](http://docs.docker.io/en/latest/reference/api/docker_remote_api/).
##Versioning
* Version 0.1 is compatible with Docker v0.7.1
* The master is compatible with Docker's master
## Example
package main

View File

@ -47,20 +47,17 @@ type ApiVersion []int
// <minor> and <patch> are integer numbers.
func NewApiVersion(input string) (ApiVersion, error) {
if !strings.Contains(input, ".") {
return nil, fmt.Errorf("Unable to parse version '%s'", input)
return nil, fmt.Errorf("Unable to parse version %q", input)
}
arr := strings.Split(input, ".")
ret := make(ApiVersion, len(arr))
var err error
for i, val := range arr {
ret[i], err = strconv.Atoi(val)
if err != nil {
return nil, err
return nil, fmt.Errorf("Unable to parse version %q: %q is not an integer", input, val)
}
}
return ret, nil
}
@ -203,6 +200,21 @@ func parseApiVersionString(input string) (version uint16, err error) {
return version, nil
}
// Ping pings the docker server
//
// See http://goo.gl/stJENm for more details.
func (c *Client) Ping() error {
path := "/_ping"
body, status, err := c.do("GET", path, nil)
if err != nil {
return err
}
if status != http.StatusOK {
return newError(status, body)
}
return nil
}
func (c *Client) getServerApiVersionString() (version string, err error) {
body, status, err := c.do("GET", "/version", nil)
if err != nil {
@ -257,6 +269,7 @@ func (c *Client) do(method, path string, data interface{}) ([]byte, int, error)
if err != nil {
return nil, -1, err
}
defer dial.Close()
clientconn := httputil.NewClientConn(dial, nil)
resp, err = clientconn.Do(req)
if err != nil {
@ -283,11 +296,10 @@ func (c *Client) do(method, path string, data interface{}) ([]byte, int, error)
return body, resp.StatusCode, nil
}
func (c *Client) stream(method, path string, headers map[string]string, in io.Reader, out io.Writer) error {
func (c *Client) stream(method, path string, setRawTerminal bool, headers map[string]string, in io.Reader, stdout, stderr io.Writer) error {
if (method == "POST" || method == "PUT") && in == nil {
in = bytes.NewReader(nil)
}
if path != "/version" && !c.SkipServerVersionCheck && c.expectedApiVersion == nil {
err := c.checkApiVersion()
if err != nil {
@ -308,8 +320,11 @@ func (c *Client) stream(method, path string, headers map[string]string, in io.Re
var resp *http.Response
protocol := c.endpointURL.Scheme
address := c.endpointURL.Path
if out == nil {
out = ioutil.Discard
if stdout == nil {
stdout = ioutil.Discard
}
if stderr == nil {
stderr = ioutil.Discard
}
if protocol == "unix" {
dial, err := net.Dial(protocol, address)
@ -346,20 +361,23 @@ func (c *Client) stream(method, path string, headers map[string]string, in io.Re
return err
}
if m.Stream != "" {
fmt.Fprint(out, m.Stream)
fmt.Fprint(stdout, m.Stream)
} else if m.Progress != "" {
fmt.Fprintf(out, "%s %s\r", m.Status, m.Progress)
fmt.Fprintf(stdout, "%s %s\r", m.Status, m.Progress)
} else if m.Error != "" {
return errors.New(m.Error)
}
if m.Status != "" {
fmt.Fprintln(out, m.Status)
fmt.Fprintln(stdout, m.Status)
}
}
} else {
if _, err := io.Copy(out, resp.Body); err != nil {
return err
if setRawTerminal {
_, err = io.Copy(stdout, resp.Body)
} else {
_, err = utils.StdCopy(stdout, stderr, resp.Body)
}
return err
}
return nil
}
@ -371,6 +389,12 @@ func (c *Client) hijack(method, path string, success chan struct{}, setRawTermin
return err
}
}
if stdout == nil {
stdout = ioutil.Discard
}
if stderr == nil {
stderr = ioutil.Discard
}
req, err := http.NewRequest(method, c.getURL(path), nil)
if err != nil {
return err

View File

@ -148,6 +148,25 @@ func TestQueryString(t *testing.T) {
}
}
func TestNewApiVersionFailures(t *testing.T) {
var tests = []struct {
input string
expectedError string
}{
{"1-0", `Unable to parse version "1-0"`},
{"1.0-beta", `Unable to parse version "1.0-beta": "0-beta" is not an integer`},
}
for _, tt := range tests {
v, err := NewApiVersion(tt.input)
if v != nil {
t.Errorf("Expected <nil> version, got %v.", v)
}
if err.Error() != tt.expectedError {
t.Errorf("NewApiVersion(%q): wrong error. Want %q. Got %q", tt.input, tt.expectedError, err.Error())
}
}
}
func TestApiVersions(t *testing.T) {
var tests = []struct {
a string
@ -192,6 +211,41 @@ func TestApiVersions(t *testing.T) {
}
}
func TestPing(t *testing.T) {
fakeRT := &FakeRoundTripper{message: "", status: http.StatusOK}
client := newTestClient(fakeRT)
err := client.Ping()
if err != nil {
t.Fatal(err)
}
}
func TestPingFailing(t *testing.T) {
fakeRT := &FakeRoundTripper{message: "", status: http.StatusInternalServerError}
client := newTestClient(fakeRT)
err := client.Ping()
if err == nil {
t.Fatal("Expected non nil error, got nil")
}
expectedErrMsg := "API error (500): "
if err.Error() != expectedErrMsg {
t.Fatalf("Expected error to be %q, got: %q", expectedErrMsg, err.Error())
}
}
func TestPingFailingWrongStatus(t *testing.T) {
fakeRT := &FakeRoundTripper{message: "", status: http.StatusAccepted}
client := newTestClient(fakeRT)
err := client.Ping()
if err == nil {
t.Fatal("Expected non nil error, got nil")
}
expectedErrMsg := "API error (202): "
if err.Error() != expectedErrMsg {
t.Fatalf("Expected error to be %q, got: %q", expectedErrMsg, err.Error())
}
}
type FakeRoundTripper struct {
message string
status int

View File

@ -13,7 +13,6 @@ import (
"net/url"
"strconv"
"strings"
"sync"
"time"
)
@ -87,22 +86,19 @@ func (p Port) Proto() string {
// State represents the state of a container.
type State struct {
sync.RWMutex
Running bool
Paused bool
Pid int
ExitCode int
StartedAt time.Time
FinishedAt time.Time
Ghost bool
}
// String returns the string representation of a state.
func (s *State) String() string {
s.RLock()
defer s.RUnlock()
if s.Running {
if s.Ghost {
return "Ghost"
if s.Paused {
return "paused"
}
return fmt.Sprintf("Up %s", time.Now().UTC().Sub(s.StartedAt))
}
@ -162,8 +158,8 @@ type Config struct {
Hostname string
Domainname string
User string
Memory int64
MemorySwap int64
Memory uint64
MemorySwap uint64
CpuShares int64
AttachStdin bool
AttachStdout bool
@ -351,6 +347,36 @@ func (c *Client) RestartContainer(id string, timeout uint) error {
return nil
}
// PauseContainer pauses the given container.
//
// See http://goo.gl/AM5t42 for more details.
func (c *Client) PauseContainer(id string) error {
path := fmt.Sprintf("/containers/%s/pause", id)
_, status, err := c.do("POST", path, nil)
if status == http.StatusNotFound {
return &NoSuchContainer{ID: id}
}
if err != nil {
return err
}
return nil
}
// UnpauseContainer pauses the given container.
//
// See http://goo.gl/eBrNSL for more details.
func (c *Client) UnpauseContainer(id string) error {
path := fmt.Sprintf("/containers/%s/unpause", id)
_, status, err := c.do("POST", path, nil)
if status == http.StatusNotFound {
return &NoSuchContainer{ID: id}
}
if err != nil {
return err
}
return nil
}
// KillContainerOptions represents the set of options that can be used in a
// call to KillContainer.
type KillContainerOptions struct {
@ -542,10 +568,15 @@ func (c *Client) AttachToContainer(opts AttachToContainerOptions) error {
type LogsOptions struct {
Container string `qs:"-"`
OutputStream io.Writer `qs:"-"`
ErrorStream io.Writer `qs:"-"`
Follow bool
Stdout bool
Stderr bool
Timestamps bool
Tail string
// Use raw terminal? Usually true when the container contains a TTY.
RawTerminal bool `qs:"-"`
}
// Logs gets stdout and stderr logs from the specified container.
@ -555,8 +586,11 @@ func (c *Client) Logs(opts LogsOptions) error {
if opts.Container == "" {
return &NoSuchContainer{ID: opts.Container}
}
if opts.Tail == "" {
opts.Tail = "all"
}
path := "/containers/" + opts.Container + "/logs?" + queryString(opts)
return c.stream("GET", path, nil, nil, opts.OutputStream)
return c.stream("GET", path, opts.RawTerminal, nil, nil, opts.OutputStream, opts.ErrorStream)
}
// ResizeContainerTTY resizes the terminal to the given height and width.
@ -586,7 +620,7 @@ func (c *Client) ExportContainer(opts ExportContainerOptions) error {
return NoSuchContainer{ID: opts.ID}
}
url := fmt.Sprintf("/containers/%s/export", opts.ID)
return c.stream("GET", url, nil, nil, opts.OutputStream)
return c.stream("GET", url, true, nil, nil, opts.OutputStream, nil)
}
// NoSuchContainer is the error returned when a given container does not exist.

View File

@ -14,12 +14,32 @@ import (
"net/url"
"os"
"reflect"
"regexp"
"runtime"
"strconv"
"strings"
"testing"
"time"
)
func TestStateString(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, StartedAt: started}, "^Up 3h.*$"},
{State{Running: false, ExitCode: 7}, "^Exit 7$"},
}
for _, tt := range tests {
re := regexp.MustCompile(tt.expected)
if got := tt.input.String(); !re.MatchString(got) {
t.Errorf("State.String(): wrong result. Want %q. Got %q.", tt.expected, got)
}
}
}
func TestListContainers(t *testing.T) {
jsonContainers := `[
{
@ -132,8 +152,8 @@ func TestInspectContainer(t *testing.T) {
"Config": {
"Hostname": "4fa6e0f0c678",
"User": "",
"Memory": 0,
"MemorySwap": 0,
"Memory": 17179869184,
"MemorySwap": 34359738368,
"AttachStdin": false,
"AttachStdout": true,
"AttachStderr": true,
@ -445,6 +465,60 @@ func TestRestartContainerNotFound(t *testing.T) {
}
}
func TestPauseContainer(t *testing.T) {
fakeRT := &FakeRoundTripper{message: "", status: http.StatusNoContent}
client := newTestClient(fakeRT)
id := "4fa6e0f0c6786287e131c3852c58a2e01cc697a68231826813597e4994f1d6e2"
err := client.PauseContainer(id)
if err != nil {
t.Fatal(err)
}
req := fakeRT.requests[0]
if req.Method != "POST" {
t.Errorf("PauseContainer(%q): wrong HTTP method. Want %q. Got %q.", id, "POST", req.Method)
}
expectedURL, _ := url.Parse(client.getURL("/containers/" + id + "/pause"))
if gotPath := req.URL.Path; gotPath != expectedURL.Path {
t.Errorf("PauseContainer(%q): Wrong path in request. Want %q. Got %q.", id, expectedURL.Path, gotPath)
}
}
func TestPauseContainerNotFound(t *testing.T) {
client := newTestClient(&FakeRoundTripper{message: "no such container", status: http.StatusNotFound})
err := client.PauseContainer("a2334")
expected := &NoSuchContainer{ID: "a2334"}
if !reflect.DeepEqual(err, expected) {
t.Errorf("PauseContainer: Wrong error returned. Want %#v. Got %#v.", expected, err)
}
}
func TestUnpauseContainer(t *testing.T) {
fakeRT := &FakeRoundTripper{message: "", status: http.StatusNoContent}
client := newTestClient(fakeRT)
id := "4fa6e0f0c6786287e131c3852c58a2e01cc697a68231826813597e4994f1d6e2"
err := client.UnpauseContainer(id)
if err != nil {
t.Fatal(err)
}
req := fakeRT.requests[0]
if req.Method != "POST" {
t.Errorf("PauseContainer(%q): wrong HTTP method. Want %q. Got %q.", id, "POST", req.Method)
}
expectedURL, _ := url.Parse(client.getURL("/containers/" + id + "/unpause"))
if gotPath := req.URL.Path; gotPath != expectedURL.Path {
t.Errorf("PauseContainer(%q): Wrong path in request. Want %q. Got %q.", id, expectedURL.Path, gotPath)
}
}
func TestUnpauseContainerNotFound(t *testing.T) {
client := newTestClient(&FakeRoundTripper{message: "no such container", status: http.StatusNotFound})
err := client.UnpauseContainer("a2334")
expected := &NoSuchContainer{ID: "a2334"}
if !reflect.DeepEqual(err, expected) {
t.Errorf("PauseContainer: Wrong error returned. Want %#v. Got %#v.", expected, err)
}
}
func TestKillContainer(t *testing.T) {
fakeRT := &FakeRoundTripper{message: "", status: http.StatusNoContent}
client := newTestClient(fakeRT)
@ -736,7 +810,7 @@ func TestAttachToContainer(t *testing.T) {
Stream: true,
RawTerminal: true,
}
var err = client.AttachToContainer(opts)
err := client.AttachToContainer(opts)
if err != nil {
t.Fatal(err)
}
@ -781,6 +855,63 @@ func TestAttachToContainerSentinel(t *testing.T) {
success <- <-success
}
func TestAttachToContainerNilStdout(t *testing.T) {
var reader = strings.NewReader("send value")
var req http.Request
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
w.Write([]byte{1, 0, 0, 0, 0, 0, 0, 5})
w.Write([]byte("hello"))
req = *r
}))
defer server.Close()
client, _ := NewClient(server.URL)
client.SkipServerVersionCheck = true
var stderr bytes.Buffer
opts := AttachToContainerOptions{
Container: "a123456",
OutputStream: nil,
ErrorStream: &stderr,
InputStream: reader,
Stdin: true,
Stdout: true,
Stderr: true,
Stream: true,
RawTerminal: true,
}
err := client.AttachToContainer(opts)
if err != nil {
t.Fatal(err)
}
}
func TestAttachToContainerNilStderr(t *testing.T) {
var reader = strings.NewReader("send value")
var req http.Request
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
w.Write([]byte{1, 0, 0, 0, 0, 0, 0, 5})
w.Write([]byte("hello"))
req = *r
}))
defer server.Close()
client, _ := NewClient(server.URL)
client.SkipServerVersionCheck = true
var stdout bytes.Buffer
opts := AttachToContainerOptions{
Container: "a123456",
OutputStream: &stdout,
InputStream: reader,
Stdin: true,
Stdout: true,
Stderr: true,
Stream: true,
RawTerminal: true,
}
err := client.AttachToContainer(opts)
if err != nil {
t.Fatal(err)
}
}
func TestAttachToContainerRawTerminalFalse(t *testing.T) {
input := strings.NewReader("send value")
var req http.Request
@ -838,6 +969,8 @@ func TestAttachToContainerWithoutContainer(t *testing.T) {
func TestLogs(t *testing.T) {
var req http.Request
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
prefix := []byte{1, 0, 0, 0, 0, 0, 0, 19}
w.Write(prefix)
w.Write([]byte("something happened!"))
req = *r
}))
@ -873,6 +1006,7 @@ func TestLogs(t *testing.T) {
"stdout": {"1"},
"stderr": {"1"},
"timestamps": {"1"},
"tail": {"all"},
}
got := map[string][]string(req.URL.Query())
if !reflect.DeepEqual(got, expectedQs) {
@ -880,6 +1014,133 @@ func TestLogs(t *testing.T) {
}
}
func TestLogsNilStdoutDoesntFail(t *testing.T) {
var req http.Request
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
prefix := []byte{1, 0, 0, 0, 0, 0, 0, 19}
w.Write(prefix)
w.Write([]byte("something happened!"))
req = *r
}))
defer server.Close()
client, _ := NewClient(server.URL)
client.SkipServerVersionCheck = true
opts := LogsOptions{
Container: "a123456",
Follow: true,
Stdout: true,
Stderr: true,
Timestamps: true,
}
err := client.Logs(opts)
if err != nil {
t.Fatal(err)
}
}
func TestLogsNilStderrDoesntFail(t *testing.T) {
var req http.Request
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
prefix := []byte{2, 0, 0, 0, 0, 0, 0, 19}
w.Write(prefix)
w.Write([]byte("something happened!"))
req = *r
}))
defer server.Close()
client, _ := NewClient(server.URL)
client.SkipServerVersionCheck = true
opts := LogsOptions{
Container: "a123456",
Follow: true,
Stdout: true,
Stderr: true,
Timestamps: true,
}
err := client.Logs(opts)
if err != nil {
t.Fatal(err)
}
}
func TestLogsSpecifyingTail(t *testing.T) {
var req http.Request
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
prefix := []byte{1, 0, 0, 0, 0, 0, 0, 19}
w.Write(prefix)
w.Write([]byte("something happened!"))
req = *r
}))
defer server.Close()
client, _ := NewClient(server.URL)
client.SkipServerVersionCheck = true
var buf bytes.Buffer
opts := LogsOptions{
Container: "a123456",
OutputStream: &buf,
Follow: true,
Stdout: true,
Stderr: true,
Timestamps: true,
Tail: "100",
}
err := client.Logs(opts)
if err != nil {
t.Fatal(err)
}
expected := "something happened!"
if buf.String() != expected {
t.Errorf("Logs: wrong output. Want %q. Got %q.", expected, buf.String())
}
if req.Method != "GET" {
t.Errorf("Logs: wrong HTTP method. Want GET. Got %s.", req.Method)
}
u, _ := url.Parse(client.getURL("/containers/a123456/logs"))
if req.URL.Path != u.Path {
t.Errorf("AttachToContainer for logs: wrong HTTP path. Want %q. Got %q.", u.Path, req.URL.Path)
}
expectedQs := map[string][]string{
"follow": {"1"},
"stdout": {"1"},
"stderr": {"1"},
"timestamps": {"1"},
"tail": {"100"},
}
got := map[string][]string(req.URL.Query())
if !reflect.DeepEqual(got, expectedQs) {
t.Errorf("Logs: wrong query string. Want %#v. Got %#v.", expectedQs, got)
}
}
func TestLogsRawTerminal(t *testing.T) {
var req http.Request
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
w.Write([]byte("something happened!"))
req = *r
}))
defer server.Close()
client, _ := NewClient(server.URL)
client.SkipServerVersionCheck = true
var buf bytes.Buffer
opts := LogsOptions{
Container: "a123456",
OutputStream: &buf,
Follow: true,
RawTerminal: true,
Stdout: true,
Stderr: true,
Timestamps: true,
Tail: "100",
}
err := client.Logs(opts)
if err != nil {
t.Fatal(err)
}
expected := "something happened!"
if buf.String() != expected {
t.Errorf("Logs: wrong output. Want %q. Got %q.", expected, buf.String())
}
}
func TestLogsNoContainer(t *testing.T) {
var client Client
err := client.Logs(LogsOptions{})

View File

@ -154,6 +154,9 @@ type PushImageOptions struct {
// Name of the image
Name string
// Tag of the image
Tag string
// Registry server to push the image
Registry string
@ -187,7 +190,7 @@ func (c *Client) PushImage(opts PushImageOptions, auth AuthConfiguration) error
headers["X-Registry-Auth"] = base64.URLEncoding.EncodeToString(buf.Bytes())
return c.stream("POST", path, headers, nil, opts.OutputStream)
return c.stream("POST", path, true, headers, nil, opts.OutputStream, nil)
}
// PullImageOptions present the set of options available for pulling an image
@ -219,7 +222,7 @@ func (c *Client) PullImage(opts PullImageOptions, auth AuthConfiguration) error
func (c *Client) createImage(qs string, headers map[string]string, in io.Reader, w io.Writer) error {
path := "/images/create?" + qs
return c.stream("POST", path, headers, in, w)
return c.stream("POST", path, true, headers, in, w, nil)
}
// ImportImageOptions present the set of informations available for importing
@ -286,13 +289,14 @@ func (c *Client) BuildImage(opts BuildImageOptions) error {
return ErrMissingRepo
}
return c.stream("POST", fmt.Sprintf("/build?%s",
queryString(&opts)), headers, opts.InputStream, opts.OutputStream)
queryString(&opts)), true, headers, opts.InputStream, opts.OutputStream, nil)
}
// TagImageOptions present the set of options to tag an image
type TagImageOptions struct {
Repo string `qs:"repo"`
Force bool `qs:"force"`
Repo string
Tag string
Force bool
}
// TagImage adds a tag to the image 'name'

View File

@ -18,7 +18,6 @@ import (
mathrand "math/rand"
"net"
"net/http"
"reflect"
"regexp"
"strconv"
"strings"
@ -34,43 +33,55 @@ import (
//
// For more details on the remote API, check http://goo.gl/yMI1S.
type DockerServer struct {
containers []*docker.Container
cMut sync.RWMutex
images []docker.Image
iMut sync.RWMutex
imgIDs map[string]string
listener net.Listener
mux *mux.Router
hook func(*http.Request)
failures map[string]FailureSpec
}
// FailureSpec is used with PrepareFailure and describes in which situations
// the request should fail. UrlRegex is mandatory, if a container id is sent
// on the request you can also specify the other properties.
type FailureSpec struct {
UrlRegex string
ContainerPath string
ContainerArgs []string
containers []*docker.Container
cMut sync.RWMutex
images []docker.Image
iMut sync.RWMutex
imgIDs map[string]string
listener net.Listener
mux *mux.Router
hook func(*http.Request)
failures map[string]string
customHandlers map[string]http.Handler
handlerMutex sync.RWMutex
cChan chan<- *docker.Container
}
// NewServer returns a new instance of the fake server, in standalone mode. Use
// the method URL to get the URL of the server.
//
// It receives the bind address (use 127.0.0.1:0 for getting an available port
// on the host) and a hook function, that will be called on every request.
func NewServer(bind string, hook func(*http.Request)) (*DockerServer, error) {
// on the host), a channel of containers and a hook function, that will be
// called on every request.
//
// The fake server will send containers in the channel whenever the container
// changes its state, via the HTTP API (i.e.: create, start and stop). This
// channel may be nil, which means that the server won't notify on state
// changes.
func NewServer(bind string, containerChan chan<- *docker.Container, hook func(*http.Request)) (*DockerServer, error) {
listener, err := net.Listen("tcp", bind)
if err != nil {
return nil, err
}
server := DockerServer{listener: listener, imgIDs: make(map[string]string), hook: hook,
failures: make(map[string]FailureSpec)}
server := DockerServer{
listener: listener,
imgIDs: make(map[string]string),
hook: hook,
failures: make(map[string]string),
customHandlers: make(map[string]http.Handler),
cChan: containerChan,
}
server.buildMuxer()
go http.Serve(listener, &server)
return &server, nil
}
func (s *DockerServer) notify(container *docker.Container) {
if s.cChan != nil {
s.cChan <- container
}
}
func (s *DockerServer) buildMuxer() {
s.mux = mux.NewRouter()
s.mux.Path("/commit").Methods("POST").HandlerFunc(s.handlerWrapper(s.commitContainer))
@ -79,6 +90,8 @@ func (s *DockerServer) buildMuxer() {
s.mux.Path("/containers/{id:.*}/json").Methods("GET").HandlerFunc(s.handlerWrapper(s.inspectContainer))
s.mux.Path("/containers/{id:.*}/start").Methods("POST").HandlerFunc(s.handlerWrapper(s.startContainer))
s.mux.Path("/containers/{id:.*}/stop").Methods("POST").HandlerFunc(s.handlerWrapper(s.stopContainer))
s.mux.Path("/containers/{id:.*}/pause").Methods("POST").HandlerFunc(s.handlerWrapper(s.pauseContainer))
s.mux.Path("/containers/{id:.*}/unpause").Methods("POST").HandlerFunc(s.handlerWrapper(s.unpauseContainer))
s.mux.Path("/containers/{id:.*}/wait").Methods("POST").HandlerFunc(s.handlerWrapper(s.waitContainer))
s.mux.Path("/containers/{id:.*}/attach").Methods("POST").HandlerFunc(s.handlerWrapper(s.attachContainer))
s.mux.Path("/containers/{id:.*}").Methods("DELETE").HandlerFunc(s.handlerWrapper(s.removeContainer))
@ -89,19 +102,45 @@ func (s *DockerServer) buildMuxer() {
s.mux.Path("/images/{name:.*}/json").Methods("GET").HandlerFunc(s.handlerWrapper(s.inspectImage))
s.mux.Path("/images/{name:.*}/push").Methods("POST").HandlerFunc(s.handlerWrapper(s.pushImage))
s.mux.Path("/events").Methods("GET").HandlerFunc(s.listEvents)
s.mux.Path("/_ping").Methods("GET").HandlerFunc(s.handlerWrapper(s.pingDocker))
}
// PrepareFailure adds a new expected failure based on a FailureSpec
// it receives an id for the failure and the spec.
func (s *DockerServer) PrepareFailure(id string, spec FailureSpec) {
s.failures[id] = spec
// PrepareFailure adds a new expected failure based on a URL regexp it receives
// an id for the failure.
func (s *DockerServer) PrepareFailure(id string, urlRegexp string) {
s.failures[id] = urlRegexp
}
// ResetFailure removes an expected failure identified by the id
// ResetFailure removes an expected failure identified by the given id.
func (s *DockerServer) ResetFailure(id string) {
delete(s.failures, id)
}
// CustomHandler registers a custom handler for a specific path.
//
// For example:
//
// server.CustomHandler("/containers/json", http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
// http.Error(w, "Something wrong is not right", http.StatusInternalServerError)
// }))
func (s *DockerServer) CustomHandler(path string, handler http.Handler) {
s.handlerMutex.Lock()
s.customHandlers[path] = handler
s.handlerMutex.Unlock()
}
// MutateContainer changes the state of a container, returning an error if the
// given id does not match to any container "running" in the server.
func (s *DockerServer) MutateContainer(id string, state docker.State) error {
for _, container := range s.containers {
if container.ID == id {
container.State = state
return nil
}
}
return errors.New("container not found")
}
// Stop stops the server.
func (s *DockerServer) Stop() {
if s.listener != nil {
@ -119,6 +158,12 @@ func (s *DockerServer) URL() string {
// ServeHTTP handles HTTP requests sent to the server.
func (s *DockerServer) ServeHTTP(w http.ResponseWriter, r *http.Request) {
s.handlerMutex.RLock()
defer s.handlerMutex.RUnlock()
if handler, ok := s.customHandlers[r.URL.Path]; ok {
handler.ServeHTTP(w, r)
return
}
s.mux.ServeHTTP(w, r)
if s.hook != nil {
s.hook(r)
@ -127,8 +172,8 @@ func (s *DockerServer) ServeHTTP(w http.ResponseWriter, r *http.Request) {
func (s *DockerServer) handlerWrapper(f func(http.ResponseWriter, *http.Request)) func(http.ResponseWriter, *http.Request) {
return func(w http.ResponseWriter, r *http.Request) {
for errorId, spec := range s.failures {
matched, err := regexp.MatchString(spec.UrlRegex, r.URL.Path)
for errorID, urlRegexp := range s.failures {
matched, err := regexp.MatchString(urlRegexp, r.URL.Path)
if err != nil {
http.Error(w, err.Error(), http.StatusBadRequest)
return
@ -136,21 +181,7 @@ func (s *DockerServer) handlerWrapper(f func(http.ResponseWriter, *http.Request)
if !matched {
continue
}
id := mux.Vars(r)["id"]
if id != "" {
container, _, err := s.findContainer(id)
if err != nil {
http.Error(w, err.Error(), http.StatusBadRequest)
return
}
if spec.ContainerPath != "" && container.Path != spec.ContainerPath {
continue
}
if spec.ContainerArgs != nil && reflect.DeepEqual(container.Args, spec.ContainerArgs) {
continue
}
}
http.Error(w, errorId, http.StatusBadRequest)
http.Error(w, errorID, http.StatusBadRequest)
return
}
f(w, r)
@ -272,6 +303,7 @@ func (s *DockerServer) createContainer(w http.ResponseWriter, r *http.Request) {
s.cMut.Lock()
s.containers = append(s.containers, &container)
s.cMut.Unlock()
s.notify(&container)
var c = struct{ ID string }{ID: container.ID}
json.NewEncoder(w).Encode(c)
}
@ -308,6 +340,7 @@ func (s *DockerServer) startContainer(w http.ResponseWriter, r *http.Request) {
return
}
container.State.Running = true
s.notify(container)
}
func (s *DockerServer) stopContainer(w http.ResponseWriter, r *http.Request) {
@ -325,6 +358,41 @@ func (s *DockerServer) stopContainer(w http.ResponseWriter, r *http.Request) {
}
w.WriteHeader(http.StatusNoContent)
container.State.Running = false
s.notify(container)
}
func (s *DockerServer) pauseContainer(w http.ResponseWriter, r *http.Request) {
id := mux.Vars(r)["id"]
container, _, err := s.findContainer(id)
if err != nil {
http.Error(w, err.Error(), http.StatusNotFound)
return
}
s.cMut.Lock()
defer s.cMut.Unlock()
if container.State.Paused {
http.Error(w, "Container already paused", http.StatusBadRequest)
return
}
w.WriteHeader(http.StatusNoContent)
container.State.Paused = true
}
func (s *DockerServer) unpauseContainer(w http.ResponseWriter, r *http.Request) {
id := mux.Vars(r)["id"]
container, _, err := s.findContainer(id)
if err != nil {
http.Error(w, err.Error(), http.StatusNotFound)
return
}
s.cMut.Lock()
defer s.cMut.Unlock()
if !container.State.Paused {
http.Error(w, "Container not paused", http.StatusBadRequest)
return
}
w.WriteHeader(http.StatusNoContent)
container.State.Paused = false
}
func (s *DockerServer) attachContainer(w http.ResponseWriter, r *http.Request) {
@ -361,7 +429,8 @@ func (s *DockerServer) waitContainer(w http.ResponseWriter, r *http.Request) {
}
s.cMut.RUnlock()
}
w.Write([]byte(`{"StatusCode":0}`))
result := map[string]int{"StatusCode": container.State.ExitCode}
json.NewEncoder(w).Encode(result)
}
func (s *DockerServer) removeContainer(w http.ResponseWriter, r *http.Request) {
@ -547,6 +616,10 @@ func (s *DockerServer) listEvents(w http.ResponseWriter, r *http.Request) {
}
}
func (s *DockerServer) pingDocker(w http.ResponseWriter, r *http.Request) {
w.WriteHeader(http.StatusOK)
}
func (s *DockerServer) generateEvent() *docker.APIEvents {
var eventType string
switch mathrand.Intn(4) {

View File

@ -20,7 +20,7 @@ import (
)
func TestNewServer(t *testing.T) {
server, err := NewServer("127.0.0.1:0", nil)
server, err := NewServer("127.0.0.1:0", nil, nil)
if err != nil {
t.Fatal(err)
}
@ -33,7 +33,7 @@ func TestNewServer(t *testing.T) {
}
func TestServerStop(t *testing.T) {
server, err := NewServer("127.0.0.1:0", nil)
server, err := NewServer("127.0.0.1:0", nil, nil)
if err != nil {
t.Fatal(err)
}
@ -50,7 +50,7 @@ func TestServerStopNoListener(t *testing.T) {
}
func TestServerURL(t *testing.T) {
server, err := NewServer("127.0.0.1:0", nil)
server, err := NewServer("127.0.0.1:0", nil, nil)
if err != nil {
t.Fatal(err)
}
@ -71,7 +71,7 @@ func TestServerURLNoListener(t *testing.T) {
func TestHandleWithHook(t *testing.T) {
var called bool
server, _ := NewServer("127.0.0.1:0", func(*http.Request) { called = true })
server, _ := NewServer("127.0.0.1:0", nil, func(*http.Request) { called = true })
defer server.Stop()
recorder := httptest.NewRecorder()
request, _ := http.NewRequest("GET", "/containers/json?all=1", nil)
@ -81,6 +81,25 @@ func TestHandleWithHook(t *testing.T) {
}
}
func TestCustomHandler(t *testing.T) {
var called bool
server, _ := NewServer("127.0.0.1:0", nil, nil)
addContainers(server, 2)
server.CustomHandler("/containers/json", http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
called = true
fmt.Fprint(w, "Hello world")
}))
recorder := httptest.NewRecorder()
request, _ := http.NewRequest("GET", "/containers/json?all=1", nil)
server.ServeHTTP(recorder, request)
if !called {
t.Error("Did not call the custom handler")
}
if got := recorder.Body.String(); got != "Hello world" {
t.Errorf("Wrong output for custom handler: want %q. Got %q.", "Hello world", got)
}
}
func TestListContainers(t *testing.T) {
server := DockerServer{}
addContainers(&server, 2)
@ -158,6 +177,25 @@ func TestCreateContainer(t *testing.T) {
}
}
func TestCreateContainerWithNotifyChannel(t *testing.T) {
ch := make(chan *docker.Container, 1)
server := DockerServer{}
server.imgIDs = map[string]string{"base": "a1234"}
server.cChan = ch
server.buildMuxer()
recorder := httptest.NewRecorder()
body := `{"Hostname":"", "User":"", "Memory":0, "MemorySwap":0, "AttachStdin":false, "AttachStdout":true, "AttachStderr":true,
"PortSpecs":null, "Tty":false, "OpenStdin":false, "StdinOnce":false, "Env":null, "Cmd":["date"], "Image":"base", "Volumes":{}, "VolumesFrom":""}`
request, _ := http.NewRequest("POST", "/containers/create", strings.NewReader(body))
server.ServeHTTP(recorder, request)
if recorder.Code != http.StatusCreated {
t.Errorf("CreateContainer: wrong status. Want %d. Got %d.", http.StatusCreated, recorder.Code)
}
if notified := <-ch; notified != server.containers[0] {
t.Errorf("CreateContainer: did not notify the proper container. Want %q. Got %q.", server.containers[0].ID, notified.ID)
}
}
func TestCreateContainerInvalidBody(t *testing.T) {
server := DockerServer{}
server.buildMuxer()
@ -320,6 +358,25 @@ func TestStartContainer(t *testing.T) {
}
}
func TestStartContainerWithNotifyChannel(t *testing.T) {
ch := make(chan *docker.Container, 1)
server := DockerServer{}
server.cChan = ch
addContainers(&server, 1)
addContainers(&server, 1)
server.buildMuxer()
recorder := httptest.NewRecorder()
path := fmt.Sprintf("/containers/%s/start", server.containers[1].ID)
request, _ := http.NewRequest("POST", path, nil)
server.ServeHTTP(recorder, request)
if recorder.Code != http.StatusOK {
t.Errorf("StartContainer: wrong status code. Want %d. Got %d.", http.StatusOK, recorder.Code)
}
if notified := <-ch; notified != server.containers[1] {
t.Errorf("StartContainer: did not notify the proper container. Want %q. Got %q.", server.containers[1].ID, notified.ID)
}
}
func TestStartContainerNotFound(t *testing.T) {
server := DockerServer{}
server.buildMuxer()
@ -363,6 +420,26 @@ func TestStopContainer(t *testing.T) {
}
}
func TestStopContainerWithNotifyChannel(t *testing.T) {
ch := make(chan *docker.Container, 1)
server := DockerServer{}
server.cChan = ch
addContainers(&server, 1)
addContainers(&server, 1)
server.containers[1].State.Running = true
server.buildMuxer()
recorder := httptest.NewRecorder()
path := fmt.Sprintf("/containers/%s/stop", server.containers[1].ID)
request, _ := http.NewRequest("POST", path, nil)
server.ServeHTTP(recorder, request)
if recorder.Code != http.StatusNoContent {
t.Errorf("StopContainer: wrong status code. Want %d. Got %d.", http.StatusNoContent, recorder.Code)
}
if notified := <-ch; notified != server.containers[1] {
t.Errorf("StopContainer: did not notify the proper container. Want %q. Got %q.", server.containers[1].ID, notified.ID)
}
}
func TestStopContainerNotFound(t *testing.T) {
server := DockerServer{}
server.buildMuxer()
@ -388,6 +465,90 @@ func TestStopContainerNotRunning(t *testing.T) {
}
}
func TestPauseContainer(t *testing.T) {
server := DockerServer{}
addContainers(&server, 1)
server.buildMuxer()
recorder := httptest.NewRecorder()
path := fmt.Sprintf("/containers/%s/pause", server.containers[0].ID)
request, _ := http.NewRequest("POST", path, nil)
server.ServeHTTP(recorder, request)
if recorder.Code != http.StatusNoContent {
t.Errorf("PauseContainer: wrong status code. Want %d. Got %d.", http.StatusNoContent, recorder.Code)
}
if !server.containers[0].State.Paused {
t.Error("PauseContainer: did not pause the container")
}
}
func TestPauseContainerAlreadyPaused(t *testing.T) {
server := DockerServer{}
addContainers(&server, 1)
server.containers[0].State.Paused = true
server.buildMuxer()
recorder := httptest.NewRecorder()
path := fmt.Sprintf("/containers/%s/pause", server.containers[0].ID)
request, _ := http.NewRequest("POST", path, nil)
server.ServeHTTP(recorder, request)
if recorder.Code != http.StatusBadRequest {
t.Errorf("PauseContainer: wrong status code. Want %d. Got %d.", http.StatusBadRequest, recorder.Code)
}
}
func TestPauseContainerNotFound(t *testing.T) {
server := DockerServer{}
server.buildMuxer()
recorder := httptest.NewRecorder()
path := "/containers/abc123/pause"
request, _ := http.NewRequest("POST", path, nil)
server.ServeHTTP(recorder, request)
if recorder.Code != http.StatusNotFound {
t.Errorf("PauseContainer: wrong status code. Want %d. Got %d.", http.StatusNotFound, recorder.Code)
}
}
func TestUnpauseContainer(t *testing.T) {
server := DockerServer{}
addContainers(&server, 1)
server.containers[0].State.Paused = true
server.buildMuxer()
recorder := httptest.NewRecorder()
path := fmt.Sprintf("/containers/%s/unpause", server.containers[0].ID)
request, _ := http.NewRequest("POST", path, nil)
server.ServeHTTP(recorder, request)
if recorder.Code != http.StatusNoContent {
t.Errorf("UnpauseContainer: wrong status code. Want %d. Got %d.", http.StatusNoContent, recorder.Code)
}
if server.containers[0].State.Paused {
t.Error("UnpauseContainer: did not unpause the container")
}
}
func TestUnpauseContainerNotPaused(t *testing.T) {
server := DockerServer{}
addContainers(&server, 1)
server.buildMuxer()
recorder := httptest.NewRecorder()
path := fmt.Sprintf("/containers/%s/unpause", server.containers[0].ID)
request, _ := http.NewRequest("POST", path, nil)
server.ServeHTTP(recorder, request)
if recorder.Code != http.StatusBadRequest {
t.Errorf("UnpauseContainer: wrong status code. Want %d. Got %d.", http.StatusBadRequest, recorder.Code)
}
}
func TestUnpauseContainerNotFound(t *testing.T) {
server := DockerServer{}
server.buildMuxer()
recorder := httptest.NewRecorder()
path := "/containers/abc123/unpause"
request, _ := http.NewRequest("POST", path, nil)
server.ServeHTTP(recorder, request)
if recorder.Code != http.StatusNotFound {
t.Errorf("UnpauseContainer: wrong status code. Want %d. Got %d.", http.StatusNotFound, recorder.Code)
}
}
func TestWaitContainer(t *testing.T) {
server := DockerServer{}
addContainers(&server, 1)
@ -405,7 +566,25 @@ func TestWaitContainer(t *testing.T) {
if recorder.Code != http.StatusOK {
t.Errorf("WaitContainer: wrong status. Want %d. Got %d.", http.StatusOK, recorder.Code)
}
expected := `{"StatusCode":0}`
expected := `{"StatusCode":0}` + "\n"
if body := recorder.Body.String(); body != expected {
t.Errorf("WaitContainer: wrong body. Want %q. Got %q.", expected, body)
}
}
func TestWaitContainerStatus(t *testing.T) {
server := DockerServer{}
addContainers(&server, 1)
server.buildMuxer()
server.containers[0].State.ExitCode = 63
recorder := httptest.NewRecorder()
path := fmt.Sprintf("/containers/%s/wait", server.containers[0].ID)
request, _ := http.NewRequest("POST", path, nil)
server.ServeHTTP(recorder, request)
if recorder.Code != http.StatusOK {
t.Errorf("WaitContainer: wrong status. Want %d. Got %d.", http.StatusOK, recorder.Code)
}
expected := `{"StatusCode":63}` + "\n"
if body := recorder.Body.String(); body != expected {
t.Errorf("WaitContainer: wrong body. Want %q. Got %q.", expected, body)
}
@ -659,70 +838,33 @@ func TestRemoveImageByName(t *testing.T) {
}
func TestPrepareFailure(t *testing.T) {
server := DockerServer{failures: make(map[string]FailureSpec)}
server := DockerServer{failures: make(map[string]string)}
server.buildMuxer()
errorId := "my_error"
failure := FailureSpec{UrlRegex: "containers/json"}
server.PrepareFailure(errorId, failure)
errorID := "my_error"
server.PrepareFailure(errorID, "containers/json")
recorder := httptest.NewRecorder()
request, _ := http.NewRequest("GET", "/containers/json?all=1", nil)
server.ServeHTTP(recorder, request)
if recorder.Code != http.StatusBadRequest {
t.Errorf("PrepareFailure: wrong status. Want %d. Got %d.", http.StatusBadRequest, recorder.Code)
}
if recorder.Body.String() != errorId+"\n" {
t.Errorf("PrepareFailure: wrong message. Want %s. Got %s.", errorId, recorder.Body.String())
}
}
func TestPrepareFailureUsingContainerPath(t *testing.T) {
server := DockerServer{failures: make(map[string]FailureSpec)}
addContainers(&server, 1)
server.buildMuxer()
errorId := "my_path_error"
failure := FailureSpec{UrlRegex: "containers/.*?/start", ContainerPath: "ls"}
server.PrepareFailure(errorId, failure)
recorder := httptest.NewRecorder()
path := fmt.Sprintf("/containers/%s/start", server.containers[0].ID)
request, _ := http.NewRequest("POST", path, nil)
server.ServeHTTP(recorder, request)
if recorder.Code != http.StatusBadRequest {
t.Errorf("TestPrepareFailureUsingContainerPath: wrong status. Want %d. Got %d.", http.StatusBadRequest, recorder.Code)
}
if recorder.Body.String() != errorId+"\n" {
t.Errorf("TestPrepareFailureUsingContainerPath: wrong message. Want %s. Got %s.", errorId, recorder.Body.String())
}
}
func TestPrepareFailureUsingContainerPathWithWrongPath(t *testing.T) {
server := DockerServer{failures: make(map[string]FailureSpec)}
addContainers(&server, 1)
server.buildMuxer()
errorId := "my_path_error"
failure := FailureSpec{UrlRegex: "containers/.*?/start", ContainerPath: "xxx"}
server.PrepareFailure(errorId, failure)
recorder := httptest.NewRecorder()
path := fmt.Sprintf("/containers/%s/start", server.containers[0].ID)
request, _ := http.NewRequest("POST", path, nil)
server.ServeHTTP(recorder, request)
if recorder.Code != http.StatusOK {
t.Errorf("StartContainer: wrong status code. Want %d. Got %d.", http.StatusOK, recorder.Code)
if recorder.Body.String() != errorID+"\n" {
t.Errorf("PrepareFailure: wrong message. Want %s. Got %s.", errorID, recorder.Body.String())
}
}
func TestRemoveFailure(t *testing.T) {
server := DockerServer{failures: make(map[string]FailureSpec)}
server := DockerServer{failures: make(map[string]string)}
server.buildMuxer()
errorId := "my_error"
failure := FailureSpec{UrlRegex: "containers/json"}
server.PrepareFailure(errorId, failure)
errorID := "my_error"
server.PrepareFailure(errorID, "containers/json")
recorder := httptest.NewRecorder()
request, _ := http.NewRequest("GET", "/containers/json?all=1", nil)
server.ServeHTTP(recorder, request)
if recorder.Code != http.StatusBadRequest {
t.Errorf("PrepareFailure: wrong status. Want %d. Got %d.", http.StatusBadRequest, recorder.Code)
}
server.ResetFailure(errorId)
server.ResetFailure(errorID)
recorder = httptest.NewRecorder()
request, _ = http.NewRequest("GET", "/containers/json?all=1", nil)
server.ServeHTTP(recorder, request)
@ -731,6 +873,34 @@ func TestRemoveFailure(t *testing.T) {
}
}
func TestMutateContainer(t *testing.T) {
server := DockerServer{failures: make(map[string]string)}
server.buildMuxer()
server.containers = append(server.containers, &docker.Container{ID: "id123"})
state := docker.State{Running: false, ExitCode: 1}
err := server.MutateContainer("id123", state)
if err != nil {
t.Fatal(err)
}
if !reflect.DeepEqual(server.containers[0].State, state) {
t.Errorf("Wrong state after mutation.\nWant %#v.\nGot %#v.",
state, server.containers[0].State)
}
}
func TestMutateContainerNotFound(t *testing.T) {
server := DockerServer{failures: make(map[string]string)}
server.buildMuxer()
state := docker.State{Running: false, ExitCode: 1}
err := server.MutateContainer("id123", state)
if err == nil {
t.Error("Unexpected <nil> error")
}
if err.Error() != "container not found" {
t.Errorf("wrong error message. Want %q. Got %q.", "container not found", err)
}
}
func TestBuildImageWithContentTypeTar(t *testing.T) {
server := DockerServer{imgIDs: make(map[string]string)}
imageName := "teste"
@ -762,3 +932,16 @@ func TestBuildImageWithRemoteDockerfile(t *testing.T) {
t.Errorf("BuildImage: image %s not builded", imageName)
}
}
func TestPing(t *testing.T) {
server := DockerServer{}
recorder := httptest.NewRecorder()
request, _ := http.NewRequest("GET", "/_ping", nil)
server.pingDocker(recorder, request)
if recorder.Body.String() != "" {
t.Errorf("Ping: Unexpected body: %s", recorder.Body.String())
}
if recorder.Code != http.StatusOK {
t.Errorf("Ping: Expected code %d, got: %d", http.StatusOK, recorder.Code)
}
}

View File

@ -87,9 +87,12 @@ func StdCopy(dstout, dsterr io.Writer, src io.Reader) (written int64, err error)
var nr2 int
nr2, er = src.Read(buf[nr:])
if er == io.EOF {
return written, nil
}
if er != nil {
if nr < StdWriterPrefixLen && nr2 < StdWriterPrefixLen {
return written, nil
}
nr += nr2
break
} else if er != nil {
return 0, er
}
nr += nr2
@ -117,7 +120,7 @@ func StdCopy(dstout, dsterr io.Writer, src io.Reader) (written int64, err error)
// Extend it if necessary.
if frameSize+StdWriterPrefixLen > bufLen {
Debugf("Extending buffer cap.")
buf = append(buf, make([]byte, frameSize-len(buf)+1)...)
buf = append(buf, make([]byte, frameSize+StdWriterPrefixLen-len(buf)+1)...)
bufLen = len(buf)
}
@ -126,9 +129,12 @@ func StdCopy(dstout, dsterr io.Writer, src io.Reader) (written int64, err error)
var nr2 int
nr2, er = src.Read(buf[nr:])
if er == io.EOF {
return written, nil
}
if er != nil {
if nr == 0 {
return written, nil
}
nr += nr2
break
} else if er != nil {
Debugf("Error reading frame: %s", er)
return 0, er
}
@ -136,7 +142,11 @@ func StdCopy(dstout, dsterr io.Writer, src io.Reader) (written int64, err error)
}
// Write the retrieved frame (without header)
nw, ew = out.Write(buf[StdWriterPrefixLen : frameSize+StdWriterPrefixLen])
bound := frameSize + StdWriterPrefixLen
if bound > nr {
bound = nr
}
nw, ew = out.Write(buf[StdWriterPrefixLen:bound])
if nw > 0 {
written += int64(nw)
}
@ -147,7 +157,7 @@ func StdCopy(dstout, dsterr io.Writer, src io.Reader) (written int64, err error)
// If the frame has not been fully written: error
if nw != frameSize {
Debugf("Error Short Write: (%d on %d)", nw, frameSize)
return 0, io.ErrShortWrite
return written, io.ErrShortWrite
}
// Move the rest of the buffer to the beginning

View File

@ -0,0 +1,217 @@
// Copyright 2014 go-dockerclient authors. All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the DOCKER-LICENSE file.
package utils
import (
"bytes"
"errors"
"io"
"strings"
"testing"
"testing/iotest"
)
type errorWriter struct {
}
func (errorWriter) Write([]byte) (int, error) {
return 0, errors.New("something went wrong")
}
func TestStdCopy(t *testing.T) {
var input, stdout, stderr bytes.Buffer
input.Write([]byte{2, 0, 0, 0, 0, 0, 0, 19})
input.Write([]byte("something happened!"))
input.Write([]byte{1, 0, 0, 0, 0, 0, 0, 12})
input.Write([]byte("just kidding"))
input.Write([]byte{0, 0, 0, 0, 0, 0, 0, 6})
input.Write([]byte("\nyeah!"))
n, err := StdCopy(&stdout, &stderr, &input)
if err != nil {
t.Fatal(err)
}
if expected := int64(19 + 12 + 6); n != expected {
t.Errorf("Wrong number of bytes. Want %d. Got %d.", expected, n)
}
if got := stderr.String(); got != "something happened!" {
t.Errorf("StdCopy: wrong stderr. Want %q. Got %q.", "something happened!", got)
}
if got := stdout.String(); got != "just kidding\nyeah!" {
t.Errorf("StdCopy: wrong stdout. Want %q. Got %q.", "just kidding\nyeah!", got)
}
}
func TestStdCopyStress(t *testing.T) {
var input, stdout, stderr bytes.Buffer
value := strings.Repeat("something ", 4096)
writer := NewStdWriter(&input, Stdout)
writer.Write([]byte(value))
n, err := StdCopy(&stdout, &stderr, &input)
if err != nil {
t.Fatal(err)
}
if n != 40960 {
t.Errorf("Wrong number of bytes. Want 40960. Got %d.", n)
}
if got := stderr.String(); got != "" {
t.Errorf("StdCopy: wrong stderr. Want empty string. Got %q", got)
}
if got := stdout.String(); got != value {
t.Errorf("StdCopy: wrong stdout. Want %q. Got %q", value, got)
}
}
func TestStdCopyInvalidStdHeader(t *testing.T) {
var input, stdout, stderr bytes.Buffer
input.Write([]byte{3, 0, 0, 0, 0, 0, 0, 19})
n, err := StdCopy(&stdout, &stderr, &input)
if n != 0 {
t.Errorf("StdCopy: wrong number of bytes. Want 0. Got %d", n)
}
if err != ErrInvalidStdHeader {
t.Errorf("StdCopy: wrong error. Want ErrInvalidStdHeader. Got %#v", err)
}
}
func TestStdCopyBigFrame(t *testing.T) {
var input, stdout, stderr bytes.Buffer
input.Write([]byte{2, 0, 0, 0, 0, 0, 0, 18})
input.Write([]byte("something happened!"))
n, err := StdCopy(&stdout, &stderr, &input)
if err != nil {
t.Fatal(err)
}
if expected := int64(18); n != expected {
t.Errorf("Wrong number of bytes. Want %d. Got %d.", expected, n)
}
if got := stderr.String(); got != "something happened" {
t.Errorf("StdCopy: wrong stderr. Want %q. Got %q.", "something happened", got)
}
if got := stdout.String(); got != "" {
t.Errorf("StdCopy: wrong stdout. Want %q. Got %q.", "", got)
}
}
func TestStdCopySmallFrame(t *testing.T) {
var input, stdout, stderr bytes.Buffer
input.Write([]byte{2, 0, 0, 0, 0, 0, 0, 20})
input.Write([]byte("something happened!"))
n, err := StdCopy(&stdout, &stderr, &input)
if err != io.ErrShortWrite {
t.Errorf("StdCopy: wrong error. Want ShortWrite. Got %#v", err)
}
if expected := int64(19); n != expected {
t.Errorf("Wrong number of bytes. Want %d. Got %d.", expected, n)
}
if got := stderr.String(); got != "something happened!" {
t.Errorf("StdCopy: wrong stderr. Want %q. Got %q.", "something happened", got)
}
if got := stdout.String(); got != "" {
t.Errorf("StdCopy: wrong stdout. Want %q. Got %q.", "", got)
}
}
func TestStdCopyEmpty(t *testing.T) {
var input, stdout, stderr bytes.Buffer
n, err := StdCopy(&stdout, &stderr, &input)
if err != nil {
t.Fatal(err)
}
if n != 0 {
t.Errorf("StdCopy: wrong number of bytes. Want 0. Got %d.", n)
}
}
func TestStdCopyCorruptedHeader(t *testing.T) {
var input, stdout, stderr bytes.Buffer
input.Write([]byte{2, 0, 0, 0, 0})
n, err := StdCopy(&stdout, &stderr, &input)
if err != nil {
t.Fatal(err)
}
if n != 0 {
t.Errorf("StdCopy: wrong number of bytes. Want 0. Got %d.", n)
}
}
func TestStdCopyTruncateWriter(t *testing.T) {
var input, stdout, stderr bytes.Buffer
input.Write([]byte{2, 0, 0, 0, 0, 0, 0, 19})
input.Write([]byte("something happened!"))
n, err := StdCopy(&stdout, iotest.TruncateWriter(&stderr, 7), &input)
if err != nil {
t.Fatal(err)
}
if expected := int64(19); n != expected {
t.Errorf("Wrong number of bytes. Want %d. Got %d.", expected, n)
}
if got := stderr.String(); got != "somethi" {
t.Errorf("StdCopy: wrong stderr. Want %q. Got %q.", "somethi", got)
}
if got := stdout.String(); got != "" {
t.Errorf("StdCopy: wrong stdout. Want %q. Got %q.", "", got)
}
}
func TestStdCopyHeaderOnly(t *testing.T) {
var input, stdout, stderr bytes.Buffer
input.Write([]byte{2, 0, 0, 0, 0, 0, 0, 19})
n, err := StdCopy(&stdout, iotest.TruncateWriter(&stderr, 7), &input)
if err != io.ErrShortWrite {
t.Errorf("StdCopy: wrong error. Want ShortWrite. Got %#v", err)
}
if n != 0 {
t.Errorf("Wrong number of bytes. Want 0. Got %d.", n)
}
if got := stderr.String(); got != "" {
t.Errorf("StdCopy: wrong stderr. Want %q. Got %q.", "", got)
}
if got := stdout.String(); got != "" {
t.Errorf("StdCopy: wrong stdout. Want %q. Got %q.", "", got)
}
}
func TestStdCopyDataErrReader(t *testing.T) {
var input, stdout, stderr bytes.Buffer
input.Write([]byte{2, 0, 0, 0, 0, 0, 0, 19})
input.Write([]byte("something happened!"))
n, err := StdCopy(&stdout, &stderr, iotest.DataErrReader(&input))
if err != nil {
t.Fatal(err)
}
if expected := int64(19); n != expected {
t.Errorf("Wrong number of bytes. Want %d. Got %d.", expected, n)
}
if got := stderr.String(); got != "something happened!" {
t.Errorf("StdCopy: wrong stderr. Want %q. Got %q.", "something happened!", got)
}
if got := stdout.String(); got != "" {
t.Errorf("StdCopy: wrong stdout. Want %q. Got %q.", "", got)
}
}
func TestStdCopyTimeoutReader(t *testing.T) {
var input, stdout, stderr bytes.Buffer
input.Write([]byte{2, 0, 0, 0, 0, 0, 0, 19})
input.Write([]byte("something happened!"))
_, err := StdCopy(&stdout, &stderr, iotest.TimeoutReader(&input))
if err != iotest.ErrTimeout {
t.Errorf("StdCopy: wrong error. Want ErrTimeout. Got %#v.", err)
}
}
func TestStdCopyWriteError(t *testing.T) {
var input bytes.Buffer
input.Write([]byte{2, 0, 0, 0, 0, 0, 0, 19})
input.Write([]byte("something happened!"))
var stdout, stderr errorWriter
n, err := StdCopy(stdout, stderr, &input)
if err.Error() != "something went wrong" {
t.Errorf("StdCopy: wrong error. Want %q. Got %q", "something went wrong", err)
}
if n != 0 {
t.Errorf("StdCopy: wrong number of bytes. Want 0. Got %d.", n)
}
}