From f5594142a8cb8f1bd3136eb6919779c8e47a5f1a Mon Sep 17 00:00:00 2001 From: Solomon Hykes Date: Wed, 13 Feb 2013 17:10:00 -0800 Subject: [PATCH 001/138] Moved server and client logic into sub-packages docker/server and docker/client, respectively. The UI is not affected. --- client.go | 123 +++++++++++++++++++++++++++++++++++++++ term.go | 145 ++++++++++++++++++++++++++++++++++++++++++++++ termios_darwin.go | 8 +++ termios_linux.go | 8 +++ 4 files changed, 284 insertions(+) create mode 100644 client.go create mode 100644 term.go create mode 100644 termios_darwin.go create mode 100644 termios_linux.go diff --git a/client.go b/client.go new file mode 100644 index 0000000000..6c5e6c4c99 --- /dev/null +++ b/client.go @@ -0,0 +1,123 @@ +package client + +import ( + "github.com/dotcloud/docker/rcli" + "github.com/dotcloud/docker/future" + "io" + "io/ioutil" + "log" + "os" + "os/exec" + "path" + "path/filepath" +) + +// Run docker in "simple mode": run a single command and return. +func SimpleMode(args []string) error { + var oldState *State + var err error + if IsTerminal(0) && os.Getenv("NORAW") == "" { + oldState, err = MakeRaw(0) + if err != nil { + return err + } + defer Restore(0, oldState) + } + // FIXME: we want to use unix sockets here, but net.UnixConn doesn't expose + // CloseWrite(), which we need to cleanly signal that stdin is closed without + // closing the connection. + // See http://code.google.com/p/go/issues/detail?id=3345 + conn, err := rcli.Call("tcp", "127.0.0.1:4242", args...) + if err != nil { + return err + } + receive_stdout := future.Go(func() error { + _, err := io.Copy(os.Stdout, conn) + return err + }) + send_stdin := future.Go(func() error { + _, err := io.Copy(conn, os.Stdin) + if err := conn.CloseWrite(); err != nil { + log.Printf("Couldn't send EOF: " + err.Error()) + } + return err + }) + if err := <-receive_stdout; err != nil { + return err + } + if oldState != nil { + Restore(0, oldState) + } + if !IsTerminal(0) { + if err := <-send_stdin; err != nil { + return err + } + } + return nil +} + +// Run docker in "interactive mode": run a bash-compatible shell capable of running docker commands. +func InteractiveMode(scripts ...string) error { + // Determine path of current docker binary + dockerPath, err := exec.LookPath(os.Args[0]) + if err != nil { + return err + } + dockerPath, err = filepath.Abs(dockerPath) + if err != nil { + return err + } + + // Create a temp directory + tmp, err := ioutil.TempDir("", "docker-shell") + if err != nil { + return err + } + defer os.RemoveAll(tmp) + + // For each command, create an alias in temp directory + // FIXME: generate this list dynamically with introspection of some sort + // It might make sense to merge docker and dockerd to keep that introspection + // within a single binary. + for _, cmd := range []string{ + "help", + "run", + "ps", + "pull", + "put", + "rm", + "kill", + "wait", + "stop", + "logs", + "diff", + "commit", + "attach", + "info", + "tar", + "web", + "images", + "docker", + } { + if err := os.Symlink(dockerPath, path.Join(tmp, cmd)); err != nil { + return err + } + } + + // Run $SHELL with PATH set to temp directory + rcfile, err := ioutil.TempFile("", "docker-shell-rc") + if err != nil { + return err + } + io.WriteString(rcfile, "enable -n help\n") + os.Setenv("PATH", tmp) + os.Setenv("PS1", "\\h docker> ") + shell := exec.Command("/bin/bash", append([]string{"--rcfile", rcfile.Name()}, scripts...)...) + shell.Stdin = os.Stdin + shell.Stdout = os.Stdout + shell.Stderr = os.Stderr + if err := shell.Run(); err != nil { + return err + } + return nil +} diff --git a/term.go b/term.go new file mode 100644 index 0000000000..8b58611cd9 --- /dev/null +++ b/term.go @@ -0,0 +1,145 @@ +package client + +import ( + "syscall" + "unsafe" +) + +type Termios struct { + Iflag uintptr + Oflag uintptr + Cflag uintptr + Lflag uintptr + Cc [20]byte + Ispeed uintptr + Ospeed uintptr +} + + +const ( + // Input flags + inpck = 0x010 + istrip = 0x020 + icrnl = 0x100 + ixon = 0x200 + + // Output flags + opost = 0x1 + + // Control flags + cs8 = 0x300 + + // Local flags + icanon = 0x100 + iexten = 0x400 +) + +const ( + HUPCL = 0x4000 + ICANON = 0x100 + ICRNL = 0x100 + IEXTEN = 0x400 + BRKINT = 0x2 + CFLUSH = 0xf + CLOCAL = 0x8000 + CREAD = 0x800 + CS5 = 0x0 + CS6 = 0x100 + CS7 = 0x200 + CS8 = 0x300 + CSIZE = 0x300 + CSTART = 0x11 + CSTATUS = 0x14 + CSTOP = 0x13 + CSTOPB = 0x400 + CSUSP = 0x1a + IGNBRK = 0x1 + IGNCR = 0x80 + IGNPAR = 0x4 + IMAXBEL = 0x2000 + INLCR = 0x40 + INPCK = 0x10 + ISIG = 0x80 + ISTRIP = 0x20 + IUTF8 = 0x4000 + IXANY = 0x800 + IXOFF = 0x400 + IXON = 0x200 + NOFLSH = 0x80000000 + OCRNL = 0x10 + OFDEL = 0x20000 + OFILL = 0x80 + ONLCR = 0x2 + ONLRET = 0x40 + ONOCR = 0x20 + ONOEOT = 0x8 + OPOST = 0x1 +RENB = 0x1000 + PARMRK = 0x8 + PARODD = 0x2000 + + TOSTOP = 0x400000 + VDISCARD = 0xf + VDSUSP = 0xb + VEOF = 0x0 + VEOL = 0x1 + VEOL2 = 0x2 + VERASE = 0x3 + VINTR = 0x8 + VKILL = 0x5 + VLNEXT = 0xe + VMIN = 0x10 + VQUIT = 0x9 + VREPRINT = 0x6 + VSTART = 0xc + VSTATUS = 0x12 + VSTOP = 0xd + VSUSP = 0xa + VT0 = 0x0 + VT1 = 0x10000 + VTDLY = 0x10000 + VTIME = 0x11 + ECHO = 0x00000008 + + PENDIN = 0x20000000 +) + +type State struct { + termios Termios +} + +// IsTerminal returns true if the given file descriptor is a terminal. +func IsTerminal(fd int) bool { + var termios Termios + _, _, err := syscall.Syscall6(syscall.SYS_IOCTL, uintptr(fd), uintptr(getTermios), uintptr(unsafe.Pointer(&termios)), 0, 0, 0) + return err == 0 +} + +// MakeRaw put the terminal connected to the given file descriptor into raw +// mode and returns the previous state of the terminal so that it can be +// restored. +func MakeRaw(fd int) (*State, error) { + var oldState State + if _, _, err := syscall.Syscall6(syscall.SYS_IOCTL, uintptr(fd), uintptr(getTermios), uintptr(unsafe.Pointer(&oldState.termios)), 0, 0, 0); err != 0 { + return nil, err + } + + newState := oldState.termios + newState.Iflag &^= istrip | INLCR | ICRNL | IGNCR | IXON | IXOFF + newState.Lflag &^= ECHO | ICANON | ISIG + if _, _, err := syscall.Syscall6(syscall.SYS_IOCTL, uintptr(fd), uintptr(setTermios), uintptr(unsafe.Pointer(&newState)), 0, 0, 0); err != 0 { + return nil, err + } + + return &oldState, nil +} + + +// Restore restores the terminal connected to the given file descriptor to a +// previous state. +func Restore(fd int, state *State) error { + _, _, err := syscall.Syscall6(syscall.SYS_IOCTL, uintptr(fd), uintptr(setTermios), uintptr(unsafe.Pointer(&state.termios)), 0, 0, 0) + return err +} + + diff --git a/termios_darwin.go b/termios_darwin.go new file mode 100644 index 0000000000..185687920c --- /dev/null +++ b/termios_darwin.go @@ -0,0 +1,8 @@ +package client + +import "syscall" + +const ( + getTermios = syscall.TIOCGETA + setTermios = syscall.TIOCSETA +) diff --git a/termios_linux.go b/termios_linux.go new file mode 100644 index 0000000000..36957c44a1 --- /dev/null +++ b/termios_linux.go @@ -0,0 +1,8 @@ +package client + +import "syscall" + +const ( + getTermios = syscall.TCGETS + setTermios = syscall.TCSETS +) From 29aab0e4bf614ff426977b5ecb081367c0e256f2 Mon Sep 17 00:00:00 2001 From: Solomon Hykes Date: Thu, 14 Feb 2013 13:49:05 -0800 Subject: [PATCH 002/138] 'docker start' and 'docker restart': start or restart a container --- client.go | 2 ++ 1 file changed, 2 insertions(+) diff --git a/client.go b/client.go index 6c5e6c4c99..164e1be321 100644 --- a/client.go +++ b/client.go @@ -89,6 +89,8 @@ func InteractiveMode(scripts ...string) error { "kill", "wait", "stop", + "start", + "restart", "logs", "diff", "commit", From 4e24b235c30a47b05920871596131beca3d4dcd0 Mon Sep 17 00:00:00 2001 From: Jeff Lindsay Date: Wed, 20 Feb 2013 12:19:22 +0000 Subject: [PATCH 003/138] make sure the standard "replace input carriage returns with line feeds" and "replace output line feeds with carriage return and line feed" flags are set, even on raw (I dont think it should actually be raw) --- term.go | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/term.go b/term.go index 8b58611cd9..7606cbd42b 100644 --- a/term.go +++ b/term.go @@ -123,9 +123,11 @@ func MakeRaw(fd int) (*State, error) { if _, _, err := syscall.Syscall6(syscall.SYS_IOCTL, uintptr(fd), uintptr(getTermios), uintptr(unsafe.Pointer(&oldState.termios)), 0, 0, 0); err != 0 { return nil, err } - + newState := oldState.termios - newState.Iflag &^= istrip | INLCR | ICRNL | IGNCR | IXON | IXOFF + newState.Iflag &^= ISTRIP | INLCR | IGNCR | IXON | IXOFF + newState.Iflag |= ICRNL + newState.Oflag |= ONLCR newState.Lflag &^= ECHO | ICANON | ISIG if _, _, err := syscall.Syscall6(syscall.SYS_IOCTL, uintptr(fd), uintptr(setTermios), uintptr(unsafe.Pointer(&newState)), 0, 0, 0); err != 0 { return nil, err From 52fa34605ca1fe604ec483aad864366ef21a1c9d Mon Sep 17 00:00:00 2001 From: Jeff Lindsay Date: Wed, 20 Feb 2013 12:23:47 +0000 Subject: [PATCH 004/138] white space --- term.go | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/term.go b/term.go index 7606cbd42b..ed52be96b4 100644 --- a/term.go +++ b/term.go @@ -123,11 +123,11 @@ func MakeRaw(fd int) (*State, error) { if _, _, err := syscall.Syscall6(syscall.SYS_IOCTL, uintptr(fd), uintptr(getTermios), uintptr(unsafe.Pointer(&oldState.termios)), 0, 0, 0); err != 0 { return nil, err } - + newState := oldState.termios newState.Iflag &^= ISTRIP | INLCR | IGNCR | IXON | IXOFF - newState.Iflag |= ICRNL - newState.Oflag |= ONLCR + newState.Iflag |= ICRNL + newState.Oflag |= ONLCR newState.Lflag &^= ECHO | ICANON | ISIG if _, _, err := syscall.Syscall6(syscall.SYS_IOCTL, uintptr(fd), uintptr(setTermios), uintptr(unsafe.Pointer(&newState)), 0, 0, 0); err != 0 { return nil, err From 754cf30b0e88d67a0e04a8e4b1792669788a2c1b Mon Sep 17 00:00:00 2001 From: Solomon Hykes Date: Wed, 20 Feb 2013 14:45:43 -0800 Subject: [PATCH 005/138] Interactive mode preserves existing PATH, to facilitate scripting --- client.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/client.go b/client.go index 164e1be321..5a8aac3807 100644 --- a/client.go +++ b/client.go @@ -112,7 +112,7 @@ func InteractiveMode(scripts ...string) error { return err } io.WriteString(rcfile, "enable -n help\n") - os.Setenv("PATH", tmp) + os.Setenv("PATH", tmp + ":" + os.Getenv("PATH")) os.Setenv("PS1", "\\h docker> ") shell := exec.Command("/bin/bash", append([]string{"--rcfile", rcfile.Name()}, scripts...)...) shell.Stdin = os.Stdin From df5134f46e7169fb7bec63eb5c33ed20115d3539 Mon Sep 17 00:00:00 2001 From: Solomon Hykes Date: Tue, 26 Feb 2013 17:26:46 -0800 Subject: [PATCH 006/138] go fmt --- client.go | 4 +- term.go | 172 ++++++++++++++++++++++++++---------------------------- 2 files changed, 86 insertions(+), 90 deletions(-) diff --git a/client.go b/client.go index 5a8aac3807..4c4ea1c5e3 100644 --- a/client.go +++ b/client.go @@ -1,8 +1,8 @@ package client import ( - "github.com/dotcloud/docker/rcli" "github.com/dotcloud/docker/future" + "github.com/dotcloud/docker/rcli" "io" "io/ioutil" "log" @@ -112,7 +112,7 @@ func InteractiveMode(scripts ...string) error { return err } io.WriteString(rcfile, "enable -n help\n") - os.Setenv("PATH", tmp + ":" + os.Getenv("PATH")) + os.Setenv("PATH", tmp+":"+os.Getenv("PATH")) os.Setenv("PS1", "\\h docker> ") shell := exec.Command("/bin/bash", append([]string{"--rcfile", rcfile.Name()}, scripts...)...) shell.Stdin = os.Stdin diff --git a/term.go b/term.go index ed52be96b4..a988d0d796 100644 --- a/term.go +++ b/term.go @@ -15,7 +15,6 @@ type Termios struct { Ospeed uintptr } - const ( // Input flags inpck = 0x010 @@ -35,113 +34,110 @@ const ( ) const ( - HUPCL = 0x4000 - ICANON = 0x100 - ICRNL = 0x100 - IEXTEN = 0x400 - BRKINT = 0x2 - CFLUSH = 0xf - CLOCAL = 0x8000 - CREAD = 0x800 - CS5 = 0x0 - CS6 = 0x100 - CS7 = 0x200 - CS8 = 0x300 - CSIZE = 0x300 - CSTART = 0x11 - CSTATUS = 0x14 - CSTOP = 0x13 - CSTOPB = 0x400 - CSUSP = 0x1a - IGNBRK = 0x1 - IGNCR = 0x80 - IGNPAR = 0x4 - IMAXBEL = 0x2000 - INLCR = 0x40 - INPCK = 0x10 - ISIG = 0x80 - ISTRIP = 0x20 - IUTF8 = 0x4000 - IXANY = 0x800 - IXOFF = 0x400 - IXON = 0x200 - NOFLSH = 0x80000000 - OCRNL = 0x10 - OFDEL = 0x20000 - OFILL = 0x80 - ONLCR = 0x2 - ONLRET = 0x40 - ONOCR = 0x20 - ONOEOT = 0x8 - OPOST = 0x1 -RENB = 0x1000 - PARMRK = 0x8 - PARODD = 0x2000 + HUPCL = 0x4000 + ICANON = 0x100 + ICRNL = 0x100 + IEXTEN = 0x400 + BRKINT = 0x2 + CFLUSH = 0xf + CLOCAL = 0x8000 + CREAD = 0x800 + CS5 = 0x0 + CS6 = 0x100 + CS7 = 0x200 + CS8 = 0x300 + CSIZE = 0x300 + CSTART = 0x11 + CSTATUS = 0x14 + CSTOP = 0x13 + CSTOPB = 0x400 + CSUSP = 0x1a + IGNBRK = 0x1 + IGNCR = 0x80 + IGNPAR = 0x4 + IMAXBEL = 0x2000 + INLCR = 0x40 + INPCK = 0x10 + ISIG = 0x80 + ISTRIP = 0x20 + IUTF8 = 0x4000 + IXANY = 0x800 + IXOFF = 0x400 + IXON = 0x200 + NOFLSH = 0x80000000 + OCRNL = 0x10 + OFDEL = 0x20000 + OFILL = 0x80 + ONLCR = 0x2 + ONLRET = 0x40 + ONOCR = 0x20 + ONOEOT = 0x8 + OPOST = 0x1 + RENB = 0x1000 + PARMRK = 0x8 + PARODD = 0x2000 - TOSTOP = 0x400000 - VDISCARD = 0xf - VDSUSP = 0xb - VEOF = 0x0 - VEOL = 0x1 - VEOL2 = 0x2 - VERASE = 0x3 - VINTR = 0x8 - VKILL = 0x5 - VLNEXT = 0xe - VMIN = 0x10 - VQUIT = 0x9 - VREPRINT = 0x6 - VSTART = 0xc - VSTATUS = 0x12 - VSTOP = 0xd - VSUSP = 0xa - VT0 = 0x0 - VT1 = 0x10000 - VTDLY = 0x10000 - VTIME = 0x11 - ECHO = 0x00000008 + TOSTOP = 0x400000 + VDISCARD = 0xf + VDSUSP = 0xb + VEOF = 0x0 + VEOL = 0x1 + VEOL2 = 0x2 + VERASE = 0x3 + VINTR = 0x8 + VKILL = 0x5 + VLNEXT = 0xe + VMIN = 0x10 + VQUIT = 0x9 + VREPRINT = 0x6 + VSTART = 0xc + VSTATUS = 0x12 + VSTOP = 0xd + VSUSP = 0xa + VT0 = 0x0 + VT1 = 0x10000 + VTDLY = 0x10000 + VTIME = 0x11 + ECHO = 0x00000008 - PENDIN = 0x20000000 + PENDIN = 0x20000000 ) type State struct { - termios Termios + termios Termios } // IsTerminal returns true if the given file descriptor is a terminal. func IsTerminal(fd int) bool { - var termios Termios - _, _, err := syscall.Syscall6(syscall.SYS_IOCTL, uintptr(fd), uintptr(getTermios), uintptr(unsafe.Pointer(&termios)), 0, 0, 0) - return err == 0 + var termios Termios + _, _, err := syscall.Syscall6(syscall.SYS_IOCTL, uintptr(fd), uintptr(getTermios), uintptr(unsafe.Pointer(&termios)), 0, 0, 0) + return err == 0 } // MakeRaw put the terminal connected to the given file descriptor into raw // mode and returns the previous state of the terminal so that it can be // restored. func MakeRaw(fd int) (*State, error) { - var oldState State - if _, _, err := syscall.Syscall6(syscall.SYS_IOCTL, uintptr(fd), uintptr(getTermios), uintptr(unsafe.Pointer(&oldState.termios)), 0, 0, 0); err != 0 { - return nil, err - } + var oldState State + if _, _, err := syscall.Syscall6(syscall.SYS_IOCTL, uintptr(fd), uintptr(getTermios), uintptr(unsafe.Pointer(&oldState.termios)), 0, 0, 0); err != 0 { + return nil, err + } - newState := oldState.termios - newState.Iflag &^= ISTRIP | INLCR | IGNCR | IXON | IXOFF - newState.Iflag |= ICRNL - newState.Oflag |= ONLCR - newState.Lflag &^= ECHO | ICANON | ISIG - if _, _, err := syscall.Syscall6(syscall.SYS_IOCTL, uintptr(fd), uintptr(setTermios), uintptr(unsafe.Pointer(&newState)), 0, 0, 0); err != 0 { - return nil, err - } + newState := oldState.termios + newState.Iflag &^= ISTRIP | INLCR | IGNCR | IXON | IXOFF + newState.Iflag |= ICRNL + newState.Oflag |= ONLCR + newState.Lflag &^= ECHO | ICANON | ISIG + if _, _, err := syscall.Syscall6(syscall.SYS_IOCTL, uintptr(fd), uintptr(setTermios), uintptr(unsafe.Pointer(&newState)), 0, 0, 0); err != 0 { + return nil, err + } - return &oldState, nil + return &oldState, nil } - // Restore restores the terminal connected to the given file descriptor to a // previous state. func Restore(fd int, state *State) error { - _, _, err := syscall.Syscall6(syscall.SYS_IOCTL, uintptr(fd), uintptr(setTermios), uintptr(unsafe.Pointer(&state.termios)), 0, 0, 0) - return err + _, _, err := syscall.Syscall6(syscall.SYS_IOCTL, uintptr(fd), uintptr(setTermios), uintptr(unsafe.Pointer(&state.termios)), 0, 0, 0) + return err } - - From 3de7ff271caa84f02f2c68e0d9122fdfa113bdb7 Mon Sep 17 00:00:00 2001 From: Solomon Hykes Date: Sat, 9 Mar 2013 19:44:09 -0800 Subject: [PATCH 007/138] gofmt --- client.go | 4 +- term.go | 172 ++++++++++++++++++++++++++---------------------------- 2 files changed, 86 insertions(+), 90 deletions(-) diff --git a/client.go b/client.go index 5a8aac3807..4c4ea1c5e3 100644 --- a/client.go +++ b/client.go @@ -1,8 +1,8 @@ package client import ( - "github.com/dotcloud/docker/rcli" "github.com/dotcloud/docker/future" + "github.com/dotcloud/docker/rcli" "io" "io/ioutil" "log" @@ -112,7 +112,7 @@ func InteractiveMode(scripts ...string) error { return err } io.WriteString(rcfile, "enable -n help\n") - os.Setenv("PATH", tmp + ":" + os.Getenv("PATH")) + os.Setenv("PATH", tmp+":"+os.Getenv("PATH")) os.Setenv("PS1", "\\h docker> ") shell := exec.Command("/bin/bash", append([]string{"--rcfile", rcfile.Name()}, scripts...)...) shell.Stdin = os.Stdin diff --git a/term.go b/term.go index ed52be96b4..a988d0d796 100644 --- a/term.go +++ b/term.go @@ -15,7 +15,6 @@ type Termios struct { Ospeed uintptr } - const ( // Input flags inpck = 0x010 @@ -35,113 +34,110 @@ const ( ) const ( - HUPCL = 0x4000 - ICANON = 0x100 - ICRNL = 0x100 - IEXTEN = 0x400 - BRKINT = 0x2 - CFLUSH = 0xf - CLOCAL = 0x8000 - CREAD = 0x800 - CS5 = 0x0 - CS6 = 0x100 - CS7 = 0x200 - CS8 = 0x300 - CSIZE = 0x300 - CSTART = 0x11 - CSTATUS = 0x14 - CSTOP = 0x13 - CSTOPB = 0x400 - CSUSP = 0x1a - IGNBRK = 0x1 - IGNCR = 0x80 - IGNPAR = 0x4 - IMAXBEL = 0x2000 - INLCR = 0x40 - INPCK = 0x10 - ISIG = 0x80 - ISTRIP = 0x20 - IUTF8 = 0x4000 - IXANY = 0x800 - IXOFF = 0x400 - IXON = 0x200 - NOFLSH = 0x80000000 - OCRNL = 0x10 - OFDEL = 0x20000 - OFILL = 0x80 - ONLCR = 0x2 - ONLRET = 0x40 - ONOCR = 0x20 - ONOEOT = 0x8 - OPOST = 0x1 -RENB = 0x1000 - PARMRK = 0x8 - PARODD = 0x2000 + HUPCL = 0x4000 + ICANON = 0x100 + ICRNL = 0x100 + IEXTEN = 0x400 + BRKINT = 0x2 + CFLUSH = 0xf + CLOCAL = 0x8000 + CREAD = 0x800 + CS5 = 0x0 + CS6 = 0x100 + CS7 = 0x200 + CS8 = 0x300 + CSIZE = 0x300 + CSTART = 0x11 + CSTATUS = 0x14 + CSTOP = 0x13 + CSTOPB = 0x400 + CSUSP = 0x1a + IGNBRK = 0x1 + IGNCR = 0x80 + IGNPAR = 0x4 + IMAXBEL = 0x2000 + INLCR = 0x40 + INPCK = 0x10 + ISIG = 0x80 + ISTRIP = 0x20 + IUTF8 = 0x4000 + IXANY = 0x800 + IXOFF = 0x400 + IXON = 0x200 + NOFLSH = 0x80000000 + OCRNL = 0x10 + OFDEL = 0x20000 + OFILL = 0x80 + ONLCR = 0x2 + ONLRET = 0x40 + ONOCR = 0x20 + ONOEOT = 0x8 + OPOST = 0x1 + RENB = 0x1000 + PARMRK = 0x8 + PARODD = 0x2000 - TOSTOP = 0x400000 - VDISCARD = 0xf - VDSUSP = 0xb - VEOF = 0x0 - VEOL = 0x1 - VEOL2 = 0x2 - VERASE = 0x3 - VINTR = 0x8 - VKILL = 0x5 - VLNEXT = 0xe - VMIN = 0x10 - VQUIT = 0x9 - VREPRINT = 0x6 - VSTART = 0xc - VSTATUS = 0x12 - VSTOP = 0xd - VSUSP = 0xa - VT0 = 0x0 - VT1 = 0x10000 - VTDLY = 0x10000 - VTIME = 0x11 - ECHO = 0x00000008 + TOSTOP = 0x400000 + VDISCARD = 0xf + VDSUSP = 0xb + VEOF = 0x0 + VEOL = 0x1 + VEOL2 = 0x2 + VERASE = 0x3 + VINTR = 0x8 + VKILL = 0x5 + VLNEXT = 0xe + VMIN = 0x10 + VQUIT = 0x9 + VREPRINT = 0x6 + VSTART = 0xc + VSTATUS = 0x12 + VSTOP = 0xd + VSUSP = 0xa + VT0 = 0x0 + VT1 = 0x10000 + VTDLY = 0x10000 + VTIME = 0x11 + ECHO = 0x00000008 - PENDIN = 0x20000000 + PENDIN = 0x20000000 ) type State struct { - termios Termios + termios Termios } // IsTerminal returns true if the given file descriptor is a terminal. func IsTerminal(fd int) bool { - var termios Termios - _, _, err := syscall.Syscall6(syscall.SYS_IOCTL, uintptr(fd), uintptr(getTermios), uintptr(unsafe.Pointer(&termios)), 0, 0, 0) - return err == 0 + var termios Termios + _, _, err := syscall.Syscall6(syscall.SYS_IOCTL, uintptr(fd), uintptr(getTermios), uintptr(unsafe.Pointer(&termios)), 0, 0, 0) + return err == 0 } // MakeRaw put the terminal connected to the given file descriptor into raw // mode and returns the previous state of the terminal so that it can be // restored. func MakeRaw(fd int) (*State, error) { - var oldState State - if _, _, err := syscall.Syscall6(syscall.SYS_IOCTL, uintptr(fd), uintptr(getTermios), uintptr(unsafe.Pointer(&oldState.termios)), 0, 0, 0); err != 0 { - return nil, err - } + var oldState State + if _, _, err := syscall.Syscall6(syscall.SYS_IOCTL, uintptr(fd), uintptr(getTermios), uintptr(unsafe.Pointer(&oldState.termios)), 0, 0, 0); err != 0 { + return nil, err + } - newState := oldState.termios - newState.Iflag &^= ISTRIP | INLCR | IGNCR | IXON | IXOFF - newState.Iflag |= ICRNL - newState.Oflag |= ONLCR - newState.Lflag &^= ECHO | ICANON | ISIG - if _, _, err := syscall.Syscall6(syscall.SYS_IOCTL, uintptr(fd), uintptr(setTermios), uintptr(unsafe.Pointer(&newState)), 0, 0, 0); err != 0 { - return nil, err - } + newState := oldState.termios + newState.Iflag &^= ISTRIP | INLCR | IGNCR | IXON | IXOFF + newState.Iflag |= ICRNL + newState.Oflag |= ONLCR + newState.Lflag &^= ECHO | ICANON | ISIG + if _, _, err := syscall.Syscall6(syscall.SYS_IOCTL, uintptr(fd), uintptr(setTermios), uintptr(unsafe.Pointer(&newState)), 0, 0, 0); err != 0 { + return nil, err + } - return &oldState, nil + return &oldState, nil } - // Restore restores the terminal connected to the given file descriptor to a // previous state. func Restore(fd int, state *State) error { - _, _, err := syscall.Syscall6(syscall.SYS_IOCTL, uintptr(fd), uintptr(setTermios), uintptr(unsafe.Pointer(&state.termios)), 0, 0, 0) - return err + _, _, err := syscall.Syscall6(syscall.SYS_IOCTL, uintptr(fd), uintptr(setTermios), uintptr(unsafe.Pointer(&state.termios)), 0, 0, 0) + return err } - - From b4b078c5ae8f9f34d6f4af682a2455cd1ef6596b Mon Sep 17 00:00:00 2001 From: shin- Date: Mon, 11 Mar 2013 07:39:06 -0700 Subject: [PATCH 008/138] post-merge repairs --- client.go | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/client.go b/client.go index 4c4ea1c5e3..a277a4b18f 100644 --- a/client.go +++ b/client.go @@ -1,8 +1,8 @@ package client import ( - "github.com/dotcloud/docker/future" - "github.com/dotcloud/docker/rcli" + "../future" + "../rcli" "io" "io/ioutil" "log" From 39ad2cf8d371ec0fbcc810a399d35a8d4a017536 Mon Sep 17 00:00:00 2001 From: "Guillaume J. Charmes" Date: Mon, 11 Mar 2013 02:59:52 -0700 Subject: [PATCH 009/138] Change relative paths to absolute --- client.go | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/client.go b/client.go index a277a4b18f..4c4ea1c5e3 100644 --- a/client.go +++ b/client.go @@ -1,8 +1,8 @@ package client import ( - "../future" - "../rcli" + "github.com/dotcloud/docker/future" + "github.com/dotcloud/docker/rcli" "io" "io/ioutil" "log" From ab1211bcb8f346ec371a9225e6a534b926504dc4 Mon Sep 17 00:00:00 2001 From: "Guillaume J. Charmes" Date: Tue, 12 Mar 2013 05:17:51 -0700 Subject: [PATCH 010/138] Put back the relative paths for dev purpose --- client.go | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/client.go b/client.go index 4c4ea1c5e3..a277a4b18f 100644 --- a/client.go +++ b/client.go @@ -1,8 +1,8 @@ package client import ( - "github.com/dotcloud/docker/future" - "github.com/dotcloud/docker/rcli" + "../future" + "../rcli" "io" "io/ioutil" "log" From d895b3a7f8e824453b7ba63c102721ef66ab69b9 Mon Sep 17 00:00:00 2001 From: Louis Opter Date: Tue, 12 Mar 2013 12:12:40 -0700 Subject: [PATCH 011/138] Automatically remove the rcfile generated by docker -i from /tmp --- client.go | 1 + 1 file changed, 1 insertion(+) diff --git a/client.go b/client.go index 4c4ea1c5e3..814aed2f18 100644 --- a/client.go +++ b/client.go @@ -111,6 +111,7 @@ func InteractiveMode(scripts ...string) error { if err != nil { return err } + defer os.Remove(rcfile.Name()) io.WriteString(rcfile, "enable -n help\n") os.Setenv("PATH", tmp+":"+os.Getenv("PATH")) os.Setenv("PS1", "\\h docker> ") From 20c2c684b20748eeb688fa9aa33df2d9efe3f136 Mon Sep 17 00:00:00 2001 From: creack Date: Tue, 12 Mar 2013 11:59:27 -0700 Subject: [PATCH 012/138] Put back the github.com path for the import --- client.go | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/client.go b/client.go index 073fe02b0b..814aed2f18 100644 --- a/client.go +++ b/client.go @@ -1,8 +1,8 @@ package client import ( - "../future" - "../rcli" + "github.com/dotcloud/docker/future" + "github.com/dotcloud/docker/rcli" "io" "io/ioutil" "log" From ae5f2d9a567ea9e45a9cc9e82b7759cc725d777a Mon Sep 17 00:00:00 2001 From: Solomon Hykes Date: Tue, 12 Mar 2013 15:05:41 -0700 Subject: [PATCH 013/138] Removed interactive mode ('docker -i'). Cool UI experiment but seems more trouble than it's worth --- client.go | 73 ------------------------------------------------------- 1 file changed, 73 deletions(-) diff --git a/client.go b/client.go index 073fe02b0b..815e20a048 100644 --- a/client.go +++ b/client.go @@ -4,12 +4,8 @@ import ( "../future" "../rcli" "io" - "io/ioutil" "log" "os" - "os/exec" - "path" - "path/filepath" ) // Run docker in "simple mode": run a single command and return. @@ -55,72 +51,3 @@ func SimpleMode(args []string) error { } return nil } - -// Run docker in "interactive mode": run a bash-compatible shell capable of running docker commands. -func InteractiveMode(scripts ...string) error { - // Determine path of current docker binary - dockerPath, err := exec.LookPath(os.Args[0]) - if err != nil { - return err - } - dockerPath, err = filepath.Abs(dockerPath) - if err != nil { - return err - } - - // Create a temp directory - tmp, err := ioutil.TempDir("", "docker-shell") - if err != nil { - return err - } - defer os.RemoveAll(tmp) - - // For each command, create an alias in temp directory - // FIXME: generate this list dynamically with introspection of some sort - // It might make sense to merge docker and dockerd to keep that introspection - // within a single binary. - for _, cmd := range []string{ - "help", - "run", - "ps", - "pull", - "put", - "rm", - "kill", - "wait", - "stop", - "start", - "restart", - "logs", - "diff", - "commit", - "attach", - "info", - "tar", - "web", - "images", - "docker", - } { - if err := os.Symlink(dockerPath, path.Join(tmp, cmd)); err != nil { - return err - } - } - - // Run $SHELL with PATH set to temp directory - rcfile, err := ioutil.TempFile("", "docker-shell-rc") - if err != nil { - return err - } - defer os.Remove(rcfile.Name()) - io.WriteString(rcfile, "enable -n help\n") - os.Setenv("PATH", tmp+":"+os.Getenv("PATH")) - os.Setenv("PS1", "\\h docker> ") - shell := exec.Command("/bin/bash", append([]string{"--rcfile", rcfile.Name()}, scripts...)...) - shell.Stdin = os.Stdin - shell.Stdout = os.Stdout - shell.Stderr = os.Stderr - if err := shell.Run(); err != nil { - return err - } - return nil -} From 7f13a9cf3a1673c42868192a85a60f7614c9a85f Mon Sep 17 00:00:00 2001 From: Solomon Hykes Date: Wed, 13 Mar 2013 00:29:40 -0700 Subject: [PATCH 014/138] Merge dockerd into docker. 'docker -d' runs in daemon mode. For all other commands, docker auto-detects whether to run standalone or to remote-control the daemon --- client.go | 53 ----------------- term.go | 143 ---------------------------------------------- termios_darwin.go | 8 --- termios_linux.go | 8 --- 4 files changed, 212 deletions(-) delete mode 100644 client.go delete mode 100644 term.go delete mode 100644 termios_darwin.go delete mode 100644 termios_linux.go diff --git a/client.go b/client.go deleted file mode 100644 index 30d741f8bd..0000000000 --- a/client.go +++ /dev/null @@ -1,53 +0,0 @@ -package client - -import ( - "github.com/dotcloud/docker/future" - "github.com/dotcloud/docker/rcli" - "io" - "log" - "os" -) - -// Run docker in "simple mode": run a single command and return. -func SimpleMode(args []string) error { - var oldState *State - var err error - if IsTerminal(0) && os.Getenv("NORAW") == "" { - oldState, err = MakeRaw(0) - if err != nil { - return err - } - defer Restore(0, oldState) - } - // FIXME: we want to use unix sockets here, but net.UnixConn doesn't expose - // CloseWrite(), which we need to cleanly signal that stdin is closed without - // closing the connection. - // See http://code.google.com/p/go/issues/detail?id=3345 - conn, err := rcli.Call("tcp", "127.0.0.1:4242", args...) - if err != nil { - return err - } - receive_stdout := future.Go(func() error { - _, err := io.Copy(os.Stdout, conn) - return err - }) - send_stdin := future.Go(func() error { - _, err := io.Copy(conn, os.Stdin) - if err := conn.CloseWrite(); err != nil { - log.Printf("Couldn't send EOF: " + err.Error()) - } - return err - }) - if err := <-receive_stdout; err != nil { - return err - } - if oldState != nil { - Restore(0, oldState) - } - if !IsTerminal(0) { - if err := <-send_stdin; err != nil { - return err - } - } - return nil -} diff --git a/term.go b/term.go deleted file mode 100644 index a988d0d796..0000000000 --- a/term.go +++ /dev/null @@ -1,143 +0,0 @@ -package client - -import ( - "syscall" - "unsafe" -) - -type Termios struct { - Iflag uintptr - Oflag uintptr - Cflag uintptr - Lflag uintptr - Cc [20]byte - Ispeed uintptr - Ospeed uintptr -} - -const ( - // Input flags - inpck = 0x010 - istrip = 0x020 - icrnl = 0x100 - ixon = 0x200 - - // Output flags - opost = 0x1 - - // Control flags - cs8 = 0x300 - - // Local flags - icanon = 0x100 - iexten = 0x400 -) - -const ( - HUPCL = 0x4000 - ICANON = 0x100 - ICRNL = 0x100 - IEXTEN = 0x400 - BRKINT = 0x2 - CFLUSH = 0xf - CLOCAL = 0x8000 - CREAD = 0x800 - CS5 = 0x0 - CS6 = 0x100 - CS7 = 0x200 - CS8 = 0x300 - CSIZE = 0x300 - CSTART = 0x11 - CSTATUS = 0x14 - CSTOP = 0x13 - CSTOPB = 0x400 - CSUSP = 0x1a - IGNBRK = 0x1 - IGNCR = 0x80 - IGNPAR = 0x4 - IMAXBEL = 0x2000 - INLCR = 0x40 - INPCK = 0x10 - ISIG = 0x80 - ISTRIP = 0x20 - IUTF8 = 0x4000 - IXANY = 0x800 - IXOFF = 0x400 - IXON = 0x200 - NOFLSH = 0x80000000 - OCRNL = 0x10 - OFDEL = 0x20000 - OFILL = 0x80 - ONLCR = 0x2 - ONLRET = 0x40 - ONOCR = 0x20 - ONOEOT = 0x8 - OPOST = 0x1 - RENB = 0x1000 - PARMRK = 0x8 - PARODD = 0x2000 - - TOSTOP = 0x400000 - VDISCARD = 0xf - VDSUSP = 0xb - VEOF = 0x0 - VEOL = 0x1 - VEOL2 = 0x2 - VERASE = 0x3 - VINTR = 0x8 - VKILL = 0x5 - VLNEXT = 0xe - VMIN = 0x10 - VQUIT = 0x9 - VREPRINT = 0x6 - VSTART = 0xc - VSTATUS = 0x12 - VSTOP = 0xd - VSUSP = 0xa - VT0 = 0x0 - VT1 = 0x10000 - VTDLY = 0x10000 - VTIME = 0x11 - ECHO = 0x00000008 - - PENDIN = 0x20000000 -) - -type State struct { - termios Termios -} - -// IsTerminal returns true if the given file descriptor is a terminal. -func IsTerminal(fd int) bool { - var termios Termios - _, _, err := syscall.Syscall6(syscall.SYS_IOCTL, uintptr(fd), uintptr(getTermios), uintptr(unsafe.Pointer(&termios)), 0, 0, 0) - return err == 0 -} - -// MakeRaw put the terminal connected to the given file descriptor into raw -// mode and returns the previous state of the terminal so that it can be -// restored. -func MakeRaw(fd int) (*State, error) { - var oldState State - if _, _, err := syscall.Syscall6(syscall.SYS_IOCTL, uintptr(fd), uintptr(getTermios), uintptr(unsafe.Pointer(&oldState.termios)), 0, 0, 0); err != 0 { - return nil, err - } - - newState := oldState.termios - newState.Iflag &^= ISTRIP | INLCR | IGNCR | IXON | IXOFF - newState.Iflag |= ICRNL - newState.Oflag |= ONLCR - newState.Lflag &^= ECHO | ICANON | ISIG - if _, _, err := syscall.Syscall6(syscall.SYS_IOCTL, uintptr(fd), uintptr(setTermios), uintptr(unsafe.Pointer(&newState)), 0, 0, 0); err != 0 { - return nil, err - } - - return &oldState, nil -} - -// Restore restores the terminal connected to the given file descriptor to a -// previous state. -func Restore(fd int, state *State) error { - _, _, err := syscall.Syscall6(syscall.SYS_IOCTL, uintptr(fd), uintptr(setTermios), uintptr(unsafe.Pointer(&state.termios)), 0, 0, 0) - return err -} diff --git a/termios_darwin.go b/termios_darwin.go deleted file mode 100644 index 185687920c..0000000000 --- a/termios_darwin.go +++ /dev/null @@ -1,8 +0,0 @@ -package client - -import "syscall" - -const ( - getTermios = syscall.TIOCGETA - setTermios = syscall.TIOCSETA -) diff --git a/termios_linux.go b/termios_linux.go deleted file mode 100644 index 36957c44a1..0000000000 --- a/termios_linux.go +++ /dev/null @@ -1,8 +0,0 @@ -package client - -import "syscall" - -const ( - getTermios = syscall.TCGETS - setTermios = syscall.TCSETS -) From 91dd0c0c6984240eced455a20d0842da72f8b9d0 Mon Sep 17 00:00:00 2001 From: Daniel Nephin Date: Fri, 19 Feb 2016 17:42:51 -0500 Subject: [PATCH 015/138] Build two binaries client and daemon. Add a proxy to support 'docker daemon' Fix configFile option, and remove a test that is no longer relevant. Remove daemon build tag. Remove DOCKER_CLIENTONLY from build scripts. Signed-off-by: Daniel Nephin Change docker-daemon to dockerd. Signed-off-by: Daniel Nephin --- client.go | 38 ++++++++++++++++++++++ client_test.go | 23 +++++++++++++ daemon.go | 43 +++++++++++++++++++++++++ docker.go | 82 +++++++++++++++++++++++++++++++++++++++++++++++ docker_windows.go | 5 +++ flags.go | 30 +++++++++++++++++ flags_test.go | 13 ++++++++ 7 files changed, 234 insertions(+) create mode 100644 client.go create mode 100644 client_test.go create mode 100644 daemon.go create mode 100644 docker.go create mode 100644 docker_windows.go create mode 100644 flags.go create mode 100644 flags_test.go diff --git a/client.go b/client.go new file mode 100644 index 0000000000..e8c7f889f8 --- /dev/null +++ b/client.go @@ -0,0 +1,38 @@ +package main + +import ( + "path/filepath" + + "github.com/docker/docker/cli" + cliflags "github.com/docker/docker/cli/flags" + "github.com/docker/docker/cliconfig" + flag "github.com/docker/docker/pkg/mflag" + "github.com/docker/docker/utils" +) + +var ( + commonFlags = cliflags.InitCommonFlags() + clientFlags = &cli.ClientFlags{FlagSet: new(flag.FlagSet), Common: commonFlags} +) + +func init() { + + client := clientFlags.FlagSet + client.StringVar(&clientFlags.ConfigDir, []string{"-config"}, cliconfig.ConfigDir(), "Location of client config files") + + clientFlags.PostParse = func() { + clientFlags.Common.PostParse() + + if clientFlags.ConfigDir != "" { + cliconfig.SetConfigDir(clientFlags.ConfigDir) + } + + if clientFlags.Common.TrustKey == "" { + clientFlags.Common.TrustKey = filepath.Join(cliconfig.ConfigDir(), cliflags.DefaultTrustKeyFile) + } + + if clientFlags.Common.Debug { + utils.EnableDebug() + } + } +} diff --git a/client_test.go b/client_test.go new file mode 100644 index 0000000000..5708c96cb5 --- /dev/null +++ b/client_test.go @@ -0,0 +1,23 @@ +package main + +import ( + "os" + "testing" + + "github.com/Sirupsen/logrus" + "github.com/docker/docker/utils" +) + +func TestClientDebugEnabled(t *testing.T) { + defer utils.DisableDebug() + + clientFlags.Common.FlagSet.Parse([]string{"-D"}) + clientFlags.PostParse() + + if os.Getenv("DEBUG") != "1" { + t.Fatal("expected debug enabled, got false") + } + if logrus.GetLevel() != logrus.DebugLevel { + t.Fatalf("expected logrus debug level, got %v", logrus.GetLevel()) + } +} diff --git a/daemon.go b/daemon.go new file mode 100644 index 0000000000..48064b4cf6 --- /dev/null +++ b/daemon.go @@ -0,0 +1,43 @@ +package main + +import ( + "os" + "os/exec" + "syscall" +) + +const daemonBinary = "dockerd" + +// DaemonProxy acts as a cli.Handler to proxy calls to the daemon binary +type DaemonProxy struct{} + +// NewDaemonProxy returns a new handler +func NewDaemonProxy() DaemonProxy { + return DaemonProxy{} +} + +// CmdDaemon execs dockerd with the same flags +// TODO: add a deprecation warning? +func (p DaemonProxy) CmdDaemon(args ...string) error { + args = stripDaemonArg(os.Args[1:]) + + binaryAbsPath, err := exec.LookPath(daemonBinary) + if err != nil { + return err + } + + return syscall.Exec( + binaryAbsPath, + append([]string{daemonBinary}, args...), + os.Environ()) +} + +// stripDaemonArg removes the `daemon` argument from the list +func stripDaemonArg(args []string) []string { + for i, arg := range args { + if arg == "daemon" { + return append(args[:i], args[i+1:]...) + } + } + return args +} diff --git a/docker.go b/docker.go new file mode 100644 index 0000000000..5641f12b12 --- /dev/null +++ b/docker.go @@ -0,0 +1,82 @@ +package main + +import ( + "fmt" + "os" + + "github.com/Sirupsen/logrus" + "github.com/docker/docker/api/client" + "github.com/docker/docker/cli" + "github.com/docker/docker/dockerversion" + flag "github.com/docker/docker/pkg/mflag" + "github.com/docker/docker/pkg/reexec" + "github.com/docker/docker/pkg/term" + "github.com/docker/docker/utils" +) + +func main() { + if reexec.Init() { + return + } + + // Set terminal emulation based on platform as required. + stdin, stdout, stderr := term.StdStreams() + + logrus.SetOutput(stderr) + + flag.Merge(flag.CommandLine, clientFlags.FlagSet, commonFlags.FlagSet) + + flag.Usage = func() { + fmt.Fprint(stdout, "Usage: docker [OPTIONS] COMMAND [arg...]\n docker [ --help | -v | --version ]\n\n") + fmt.Fprint(stdout, "A self-sufficient runtime for containers.\n\nOptions:\n") + + flag.CommandLine.SetOutput(stdout) + flag.PrintDefaults() + + help := "\nCommands:\n" + + for _, cmd := range dockerCommands { + help += fmt.Sprintf(" %-10.10s%s\n", cmd.Name, cmd.Description) + } + + help += "\nRun 'docker COMMAND --help' for more information on a command." + fmt.Fprintf(stdout, "%s\n", help) + } + + flag.Parse() + + if *flVersion { + showVersion() + return + } + + if *flHelp { + // if global flag --help is present, regardless of what other options and commands there are, + // just print the usage. + flag.Usage() + return + } + + clientCli := client.NewDockerCli(stdin, stdout, stderr, clientFlags) + + c := cli.New(clientCli, NewDaemonProxy()) + if err := c.Run(flag.Args()...); err != nil { + if sterr, ok := err.(cli.StatusError); ok { + if sterr.Status != "" { + fmt.Fprintln(stderr, sterr.Status) + os.Exit(1) + } + os.Exit(sterr.StatusCode) + } + fmt.Fprintln(stderr, err) + os.Exit(1) + } +} + +func showVersion() { + if utils.ExperimentalBuild() { + fmt.Printf("Docker version %s, build %s, experimental\n", dockerversion.Version, dockerversion.GitCommit) + } else { + fmt.Printf("Docker version %s, build %s\n", dockerversion.Version, dockerversion.GitCommit) + } +} diff --git a/docker_windows.go b/docker_windows.go new file mode 100644 index 0000000000..a31dffc95c --- /dev/null +++ b/docker_windows.go @@ -0,0 +1,5 @@ +package main + +import ( + _ "github.com/docker/docker/autogen/winresources" +) diff --git a/flags.go b/flags.go new file mode 100644 index 0000000000..35a8108880 --- /dev/null +++ b/flags.go @@ -0,0 +1,30 @@ +package main + +import ( + "sort" + + "github.com/docker/docker/cli" + flag "github.com/docker/docker/pkg/mflag" +) + +var ( + flHelp = flag.Bool([]string{"h", "-help"}, false, "Print usage") + flVersion = flag.Bool([]string{"v", "-version"}, false, "Print version information and quit") +) + +type byName []cli.Command + +func (a byName) Len() int { return len(a) } +func (a byName) Swap(i, j int) { a[i], a[j] = a[j], a[i] } +func (a byName) Less(i, j int) bool { return a[i].Name < a[j].Name } + +var dockerCommands []cli.Command + +// TODO(tiborvass): do not show 'daemon' on client-only binaries + +func init() { + for _, cmd := range cli.DockerCommands { + dockerCommands = append(dockerCommands, cmd) + } + sort.Sort(byName(dockerCommands)) +} diff --git a/flags_test.go b/flags_test.go new file mode 100644 index 0000000000..28021ba4c9 --- /dev/null +++ b/flags_test.go @@ -0,0 +1,13 @@ +package main + +import ( + "sort" + "testing" +) + +// Tests if the subcommands of docker are sorted +func TestDockerSubcommandsAreSorted(t *testing.T) { + if !sort.IsSorted(byName(dockerCommands)) { + t.Fatal("Docker subcommands are not in sorted order") + } +} From ef9ad854299d36241115e58f84f13d509ed98b7d Mon Sep 17 00:00:00 2001 From: Daniel Nephin Date: Fri, 22 Apr 2016 12:37:48 -0400 Subject: [PATCH 016/138] Cleanup from CR. Signed-off-by: Daniel Nephin --- daemon.go | 32 -------------------------------- daemon_unix.go | 37 +++++++++++++++++++++++++++++++++++++ daemon_windows.go | 11 +++++++++++ daemon_windows_test.go | 18 ++++++++++++++++++ docker.go | 5 ----- 5 files changed, 66 insertions(+), 37 deletions(-) create mode 100644 daemon_unix.go create mode 100644 daemon_windows.go create mode 100644 daemon_windows_test.go diff --git a/daemon.go b/daemon.go index 48064b4cf6..15dffbaefb 100644 --- a/daemon.go +++ b/daemon.go @@ -1,11 +1,5 @@ package main -import ( - "os" - "os/exec" - "syscall" -) - const daemonBinary = "dockerd" // DaemonProxy acts as a cli.Handler to proxy calls to the daemon binary @@ -15,29 +9,3 @@ type DaemonProxy struct{} func NewDaemonProxy() DaemonProxy { return DaemonProxy{} } - -// CmdDaemon execs dockerd with the same flags -// TODO: add a deprecation warning? -func (p DaemonProxy) CmdDaemon(args ...string) error { - args = stripDaemonArg(os.Args[1:]) - - binaryAbsPath, err := exec.LookPath(daemonBinary) - if err != nil { - return err - } - - return syscall.Exec( - binaryAbsPath, - append([]string{daemonBinary}, args...), - os.Environ()) -} - -// stripDaemonArg removes the `daemon` argument from the list -func stripDaemonArg(args []string) []string { - for i, arg := range args { - if arg == "daemon" { - return append(args[:i], args[i+1:]...) - } - } - return args -} diff --git a/daemon_unix.go b/daemon_unix.go new file mode 100644 index 0000000000..abe9ebfc51 --- /dev/null +++ b/daemon_unix.go @@ -0,0 +1,37 @@ +// +build !windows + +package main + +import ( + "os" + "os/exec" + "syscall" +) + +// CmdDaemon execs dockerd with the same flags +// TODO: add a deprecation warning? +func (p DaemonProxy) CmdDaemon(args ...string) error { + // Use os.Args[1:] so that "global" args are passed to dockerd + args = stripDaemonArg(os.Args[1:]) + + // TODO: check dirname args[0] first + binaryAbsPath, err := exec.LookPath(daemonBinary) + if err != nil { + return err + } + + return syscall.Exec( + binaryAbsPath, + append([]string{daemonBinary}, args...), + os.Environ()) +} + +// stripDaemonArg removes the `daemon` argument from the list +func stripDaemonArg(args []string) []string { + for i, arg := range args { + if arg == "daemon" { + return append(args[:i], args[i+1:]...) + } + } + return args +} diff --git a/daemon_windows.go b/daemon_windows.go new file mode 100644 index 0000000000..41c0133b67 --- /dev/null +++ b/daemon_windows.go @@ -0,0 +1,11 @@ +package main + +import ( + "fmt" +) + +// CmdDaemon reports on an error on windows, because there is no exec +func (p DaemonProxy) CmdDaemon(args ...string) error { + return fmt.Errorf( + "`docker daemon` does not exist on windows. Please run `dockerd` directly") +} diff --git a/daemon_windows_test.go b/daemon_windows_test.go new file mode 100644 index 0000000000..3da4e5d7cc --- /dev/null +++ b/daemon_windows_test.go @@ -0,0 +1,18 @@ +package main + +import ( + "strings" + "testing" +) + +func TestCmdDaemon(t *testing.T) { + proxy := NewDaemonProxy() + err := proxy.CmdDaemon("--help") + if err == nil { + t.Fatal("Expected CmdDaemon to fail in Windows.") + } + + if !strings.Contains(err.Error(), "Please run `dockerd`") { + t.Fatalf("Expected an error about running dockerd, got %s", err) + } +} diff --git a/docker.go b/docker.go index 5641f12b12..838602164d 100644 --- a/docker.go +++ b/docker.go @@ -9,16 +9,11 @@ import ( "github.com/docker/docker/cli" "github.com/docker/docker/dockerversion" flag "github.com/docker/docker/pkg/mflag" - "github.com/docker/docker/pkg/reexec" "github.com/docker/docker/pkg/term" "github.com/docker/docker/utils" ) func main() { - if reexec.Init() { - return - } - // Set terminal emulation based on platform as required. stdin, stdout, stderr := term.StdStreams() From 5c252a7914378dbd535d2d098a3af1474bbc55fb Mon Sep 17 00:00:00 2001 From: John Starks Date: Sat, 23 Apr 2016 15:11:08 -0700 Subject: [PATCH 017/138] Windows: Add file version information This change adds file version information to docker.exe and dockerd.exe by adding a Windows version resource with the windres tool. This change adds a dependency to binutils-mingw-w64 on Linux, but removes a dependency on rsrc. Most Windows build environments should already have windres if they have gcc (which is necessary to build dockerd). Signed-off-by: John Starks --- docker_windows.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docker_windows.go b/docker_windows.go index a31dffc95c..de32257586 100644 --- a/docker_windows.go +++ b/docker_windows.go @@ -1,5 +1,5 @@ package main import ( - _ "github.com/docker/docker/autogen/winresources" + _ "github.com/docker/docker/autogen/winresources/docker" ) From 969302c16934ae253627ed77dda4ed05f56182ab Mon Sep 17 00:00:00 2001 From: John Howard Date: Sat, 23 Apr 2016 18:31:57 -0700 Subject: [PATCH 018/138] Make dockerd debuggable Signed-off-by: John Howard --- client.go | 38 --------------------- client_test.go | 23 ------------- daemon.go | 11 ------ daemon_unix.go | 37 -------------------- daemon_windows.go | 11 ------ daemon_windows_test.go | 18 ---------- docker.go | 77 ------------------------------------------ docker_windows.go | 5 --- flags.go | 30 ---------------- flags_test.go | 13 ------- 10 files changed, 263 deletions(-) delete mode 100644 client.go delete mode 100644 client_test.go delete mode 100644 daemon.go delete mode 100644 daemon_unix.go delete mode 100644 daemon_windows.go delete mode 100644 daemon_windows_test.go delete mode 100644 docker.go delete mode 100644 docker_windows.go delete mode 100644 flags.go delete mode 100644 flags_test.go diff --git a/client.go b/client.go deleted file mode 100644 index e8c7f889f8..0000000000 --- a/client.go +++ /dev/null @@ -1,38 +0,0 @@ -package main - -import ( - "path/filepath" - - "github.com/docker/docker/cli" - cliflags "github.com/docker/docker/cli/flags" - "github.com/docker/docker/cliconfig" - flag "github.com/docker/docker/pkg/mflag" - "github.com/docker/docker/utils" -) - -var ( - commonFlags = cliflags.InitCommonFlags() - clientFlags = &cli.ClientFlags{FlagSet: new(flag.FlagSet), Common: commonFlags} -) - -func init() { - - client := clientFlags.FlagSet - client.StringVar(&clientFlags.ConfigDir, []string{"-config"}, cliconfig.ConfigDir(), "Location of client config files") - - clientFlags.PostParse = func() { - clientFlags.Common.PostParse() - - if clientFlags.ConfigDir != "" { - cliconfig.SetConfigDir(clientFlags.ConfigDir) - } - - if clientFlags.Common.TrustKey == "" { - clientFlags.Common.TrustKey = filepath.Join(cliconfig.ConfigDir(), cliflags.DefaultTrustKeyFile) - } - - if clientFlags.Common.Debug { - utils.EnableDebug() - } - } -} diff --git a/client_test.go b/client_test.go deleted file mode 100644 index 5708c96cb5..0000000000 --- a/client_test.go +++ /dev/null @@ -1,23 +0,0 @@ -package main - -import ( - "os" - "testing" - - "github.com/Sirupsen/logrus" - "github.com/docker/docker/utils" -) - -func TestClientDebugEnabled(t *testing.T) { - defer utils.DisableDebug() - - clientFlags.Common.FlagSet.Parse([]string{"-D"}) - clientFlags.PostParse() - - if os.Getenv("DEBUG") != "1" { - t.Fatal("expected debug enabled, got false") - } - if logrus.GetLevel() != logrus.DebugLevel { - t.Fatalf("expected logrus debug level, got %v", logrus.GetLevel()) - } -} diff --git a/daemon.go b/daemon.go deleted file mode 100644 index 15dffbaefb..0000000000 --- a/daemon.go +++ /dev/null @@ -1,11 +0,0 @@ -package main - -const daemonBinary = "dockerd" - -// DaemonProxy acts as a cli.Handler to proxy calls to the daemon binary -type DaemonProxy struct{} - -// NewDaemonProxy returns a new handler -func NewDaemonProxy() DaemonProxy { - return DaemonProxy{} -} diff --git a/daemon_unix.go b/daemon_unix.go deleted file mode 100644 index abe9ebfc51..0000000000 --- a/daemon_unix.go +++ /dev/null @@ -1,37 +0,0 @@ -// +build !windows - -package main - -import ( - "os" - "os/exec" - "syscall" -) - -// CmdDaemon execs dockerd with the same flags -// TODO: add a deprecation warning? -func (p DaemonProxy) CmdDaemon(args ...string) error { - // Use os.Args[1:] so that "global" args are passed to dockerd - args = stripDaemonArg(os.Args[1:]) - - // TODO: check dirname args[0] first - binaryAbsPath, err := exec.LookPath(daemonBinary) - if err != nil { - return err - } - - return syscall.Exec( - binaryAbsPath, - append([]string{daemonBinary}, args...), - os.Environ()) -} - -// stripDaemonArg removes the `daemon` argument from the list -func stripDaemonArg(args []string) []string { - for i, arg := range args { - if arg == "daemon" { - return append(args[:i], args[i+1:]...) - } - } - return args -} diff --git a/daemon_windows.go b/daemon_windows.go deleted file mode 100644 index 41c0133b67..0000000000 --- a/daemon_windows.go +++ /dev/null @@ -1,11 +0,0 @@ -package main - -import ( - "fmt" -) - -// CmdDaemon reports on an error on windows, because there is no exec -func (p DaemonProxy) CmdDaemon(args ...string) error { - return fmt.Errorf( - "`docker daemon` does not exist on windows. Please run `dockerd` directly") -} diff --git a/daemon_windows_test.go b/daemon_windows_test.go deleted file mode 100644 index 3da4e5d7cc..0000000000 --- a/daemon_windows_test.go +++ /dev/null @@ -1,18 +0,0 @@ -package main - -import ( - "strings" - "testing" -) - -func TestCmdDaemon(t *testing.T) { - proxy := NewDaemonProxy() - err := proxy.CmdDaemon("--help") - if err == nil { - t.Fatal("Expected CmdDaemon to fail in Windows.") - } - - if !strings.Contains(err.Error(), "Please run `dockerd`") { - t.Fatalf("Expected an error about running dockerd, got %s", err) - } -} diff --git a/docker.go b/docker.go deleted file mode 100644 index 838602164d..0000000000 --- a/docker.go +++ /dev/null @@ -1,77 +0,0 @@ -package main - -import ( - "fmt" - "os" - - "github.com/Sirupsen/logrus" - "github.com/docker/docker/api/client" - "github.com/docker/docker/cli" - "github.com/docker/docker/dockerversion" - flag "github.com/docker/docker/pkg/mflag" - "github.com/docker/docker/pkg/term" - "github.com/docker/docker/utils" -) - -func main() { - // Set terminal emulation based on platform as required. - stdin, stdout, stderr := term.StdStreams() - - logrus.SetOutput(stderr) - - flag.Merge(flag.CommandLine, clientFlags.FlagSet, commonFlags.FlagSet) - - flag.Usage = func() { - fmt.Fprint(stdout, "Usage: docker [OPTIONS] COMMAND [arg...]\n docker [ --help | -v | --version ]\n\n") - fmt.Fprint(stdout, "A self-sufficient runtime for containers.\n\nOptions:\n") - - flag.CommandLine.SetOutput(stdout) - flag.PrintDefaults() - - help := "\nCommands:\n" - - for _, cmd := range dockerCommands { - help += fmt.Sprintf(" %-10.10s%s\n", cmd.Name, cmd.Description) - } - - help += "\nRun 'docker COMMAND --help' for more information on a command." - fmt.Fprintf(stdout, "%s\n", help) - } - - flag.Parse() - - if *flVersion { - showVersion() - return - } - - if *flHelp { - // if global flag --help is present, regardless of what other options and commands there are, - // just print the usage. - flag.Usage() - return - } - - clientCli := client.NewDockerCli(stdin, stdout, stderr, clientFlags) - - c := cli.New(clientCli, NewDaemonProxy()) - if err := c.Run(flag.Args()...); err != nil { - if sterr, ok := err.(cli.StatusError); ok { - if sterr.Status != "" { - fmt.Fprintln(stderr, sterr.Status) - os.Exit(1) - } - os.Exit(sterr.StatusCode) - } - fmt.Fprintln(stderr, err) - os.Exit(1) - } -} - -func showVersion() { - if utils.ExperimentalBuild() { - fmt.Printf("Docker version %s, build %s, experimental\n", dockerversion.Version, dockerversion.GitCommit) - } else { - fmt.Printf("Docker version %s, build %s\n", dockerversion.Version, dockerversion.GitCommit) - } -} diff --git a/docker_windows.go b/docker_windows.go deleted file mode 100644 index de32257586..0000000000 --- a/docker_windows.go +++ /dev/null @@ -1,5 +0,0 @@ -package main - -import ( - _ "github.com/docker/docker/autogen/winresources/docker" -) diff --git a/flags.go b/flags.go deleted file mode 100644 index 35a8108880..0000000000 --- a/flags.go +++ /dev/null @@ -1,30 +0,0 @@ -package main - -import ( - "sort" - - "github.com/docker/docker/cli" - flag "github.com/docker/docker/pkg/mflag" -) - -var ( - flHelp = flag.Bool([]string{"h", "-help"}, false, "Print usage") - flVersion = flag.Bool([]string{"v", "-version"}, false, "Print version information and quit") -) - -type byName []cli.Command - -func (a byName) Len() int { return len(a) } -func (a byName) Swap(i, j int) { a[i], a[j] = a[j], a[i] } -func (a byName) Less(i, j int) bool { return a[i].Name < a[j].Name } - -var dockerCommands []cli.Command - -// TODO(tiborvass): do not show 'daemon' on client-only binaries - -func init() { - for _, cmd := range cli.DockerCommands { - dockerCommands = append(dockerCommands, cmd) - } - sort.Sort(byName(dockerCommands)) -} diff --git a/flags_test.go b/flags_test.go deleted file mode 100644 index 28021ba4c9..0000000000 --- a/flags_test.go +++ /dev/null @@ -1,13 +0,0 @@ -package main - -import ( - "sort" - "testing" -) - -// Tests if the subcommands of docker are sorted -func TestDockerSubcommandsAreSorted(t *testing.T) { - if !sort.IsSorted(byName(dockerCommands)) { - t.Fatal("Docker subcommands are not in sorted order") - } -} From 3fff6acaa19d7f2812a68f7fe49cfac5fa8dbade Mon Sep 17 00:00:00 2001 From: Michael Crosby Date: Tue, 6 Sep 2016 11:46:37 -0700 Subject: [PATCH 019/138] Move engine-api client package This moves the engine-api client package to `/docker/docker/client`. Signed-off-by: Michael Crosby --- checkpoint_create.go | 13 ++ checkpoint_create_test.go | 73 ++++++++ checkpoint_delete.go | 12 ++ checkpoint_delete_test.go | 47 +++++ checkpoint_list.go | 22 +++ checkpoint_list_test.go | 57 ++++++ client.go | 156 ++++++++++++++++ client_mock_test.go | 76 ++++++++ client_test.go | 249 +++++++++++++++++++++++++ client_unix.go | 6 + client_windows.go | 4 + container_attach.go | 34 ++++ container_commit.go | 53 ++++++ container_commit_test.go | 96 ++++++++++ container_copy.go | 97 ++++++++++ container_copy_test.go | 244 ++++++++++++++++++++++++ container_create.go | 46 +++++ container_create_test.go | 77 ++++++++ container_diff.go | 23 +++ container_diff_test.go | 61 ++++++ container_exec.go | 49 +++++ container_exec_test.go | 157 ++++++++++++++++ container_export.go | 20 ++ container_export_test.go | 50 +++++ container_inspect.go | 54 ++++++ container_inspect_test.go | 125 +++++++++++++ container_kill.go | 17 ++ container_kill_test.go | 46 +++++ container_list.go | 56 ++++++ container_list_test.go | 96 ++++++++++ container_logs.go | 52 ++++++ container_logs_test.go | 133 +++++++++++++ container_pause.go | 10 + container_pause_test.go | 41 ++++ container_remove.go | 27 +++ container_remove_test.go | 59 ++++++ container_rename.go | 16 ++ container_rename_test.go | 46 +++++ container_resize.go | 29 +++ container_resize_test.go | 82 ++++++++ container_restart.go | 22 +++ container_restart_test.go | 48 +++++ container_start.go | 21 +++ container_start_test.go | 58 ++++++ container_stats.go | 24 +++ container_stats_test.go | 70 +++++++ container_stop.go | 21 +++ container_stop_test.go | 48 +++++ container_top.go | 28 +++ container_top_test.go | 74 ++++++++ container_unpause.go | 10 + container_unpause_test.go | 41 ++++ container_update.go | 23 +++ container_update_test.go | 59 ++++++ container_wait.go | 26 +++ container_wait_test.go | 70 +++++++ errors.go | 208 +++++++++++++++++++++ events.go | 48 +++++ events_test.go | 126 +++++++++++++ hijack.go | 174 +++++++++++++++++ image_build.go | 123 ++++++++++++ image_build_test.go | 230 +++++++++++++++++++++++ image_create.go | 34 ++++ image_create_test.go | 76 ++++++++ image_history.go | 22 +++ image_history_test.go | 60 ++++++ image_import.go | 37 ++++ image_import_test.go | 81 ++++++++ image_inspect.go | 33 ++++ image_inspect_test.go | 71 +++++++ image_list.go | 40 ++++ image_list_test.go | 122 ++++++++++++ image_load.go | 30 +++ image_load_test.go | 95 ++++++++++ image_pull.go | 46 +++++ image_pull_test.go | 199 ++++++++++++++++++++ image_push.go | 54 ++++++ image_push_test.go | 180 ++++++++++++++++++ image_remove.go | 31 +++ image_remove_test.go | 95 ++++++++++ image_save.go | 22 +++ image_save_test.go | 58 ++++++ image_search.go | 51 +++++ image_search_test.go | 165 ++++++++++++++++ image_tag.go | 34 ++++ image_tag_test.go | 121 ++++++++++++ info.go | 26 +++ info_test.go | 76 ++++++++ interface.go | 135 ++++++++++++++ interface_experimental.go | 37 ++++ interface_stable.go | 11 ++ login.go | 28 +++ network_connect.go | 18 ++ network_connect_test.go | 107 +++++++++++ network_create.go | 25 +++ network_create_test.go | 72 +++++++ network_disconnect.go | 14 ++ network_disconnect_test.go | 64 +++++++ network_inspect.go | 38 ++++ network_inspect_test.go | 69 +++++++ network_list.go | 31 +++ network_list_test.go | 108 +++++++++++ network_remove.go | 10 + network_remove_test.go | 47 +++++ node_inspect.go | 33 ++++ node_inspect_test.go | 65 +++++++ node_list.go | 36 ++++ node_list_test.go | 94 ++++++++++ node_remove.go | 21 +++ node_remove_test.go | 69 +++++++ node_update.go | 18 ++ node_update_test.go | 49 +++++ plugin_disable.go | 14 ++ plugin_disable_test.go | 49 +++++ plugin_enable.go | 14 ++ plugin_enable_test.go | 49 +++++ plugin_inspect.go | 30 +++ plugin_inspect_test.go | 56 ++++++ plugin_install.go | 59 ++++++ plugin_list.go | 23 +++ plugin_list_test.go | 61 ++++++ plugin_push.go | 15 ++ plugin_push_test.go | 53 ++++++ plugin_remove.go | 22 +++ plugin_remove_test.go | 51 +++++ plugin_set.go | 14 ++ plugin_set_test.go | 49 +++++ request.go | 208 +++++++++++++++++++++ request_test.go | 91 +++++++++ service_create.go | 30 +++ service_create_test.go | 57 ++++++ service_inspect.go | 33 ++++ service_inspect_test.go | 65 +++++++ service_list.go | 35 ++++ service_list_test.go | 94 ++++++++++ service_remove.go | 10 + service_remove_test.go | 47 +++++ service_update.go | 30 +++ service_update_test.go | 77 ++++++++ swarm_init.go | 21 +++ swarm_init_test.go | 54 ++++++ swarm_inspect.go | 21 +++ swarm_inspect_test.go | 56 ++++++ swarm_join.go | 13 ++ swarm_join_test.go | 51 +++++ swarm_leave.go | 18 ++ swarm_leave_test.go | 66 +++++++ swarm_update.go | 21 +++ swarm_update_test.go | 49 +++++ task_inspect.go | 34 ++++ task_inspect_test.go | 54 ++++++ task_list.go | 35 ++++ task_list_test.go | 94 ++++++++++ testdata/ca.pem | 18 ++ testdata/cert.pem | 18 ++ testdata/key.pem | 27 +++ transport/cancellable/LICENSE | 27 +++ transport/cancellable/canceler.go | 23 +++ transport/cancellable/canceler_go14.go | 27 +++ transport/cancellable/cancellable.go | 115 ++++++++++++ transport/client.go | 47 +++++ transport/tlsconfig_clone.go | 11 ++ transport/tlsconfig_clone_go17.go | 33 ++++ transport/transport.go | 57 ++++++ version.go | 21 +++ volume_create.go | 20 ++ volume_create_test.go | 74 ++++++++ volume_inspect.go | 38 ++++ volume_inspect_test.go | 76 ++++++++ volume_list.go | 32 ++++ volume_list_test.go | 97 ++++++++++ volume_remove.go | 18 ++ volume_remove_test.go | 47 +++++ 173 files changed, 9970 insertions(+) create mode 100644 checkpoint_create.go create mode 100644 checkpoint_create_test.go create mode 100644 checkpoint_delete.go create mode 100644 checkpoint_delete_test.go create mode 100644 checkpoint_list.go create mode 100644 checkpoint_list_test.go create mode 100644 client.go create mode 100644 client_mock_test.go create mode 100644 client_test.go create mode 100644 client_unix.go create mode 100644 client_windows.go create mode 100644 container_attach.go create mode 100644 container_commit.go create mode 100644 container_commit_test.go create mode 100644 container_copy.go create mode 100644 container_copy_test.go create mode 100644 container_create.go create mode 100644 container_create_test.go create mode 100644 container_diff.go create mode 100644 container_diff_test.go create mode 100644 container_exec.go create mode 100644 container_exec_test.go create mode 100644 container_export.go create mode 100644 container_export_test.go create mode 100644 container_inspect.go create mode 100644 container_inspect_test.go create mode 100644 container_kill.go create mode 100644 container_kill_test.go create mode 100644 container_list.go create mode 100644 container_list_test.go create mode 100644 container_logs.go create mode 100644 container_logs_test.go create mode 100644 container_pause.go create mode 100644 container_pause_test.go create mode 100644 container_remove.go create mode 100644 container_remove_test.go create mode 100644 container_rename.go create mode 100644 container_rename_test.go create mode 100644 container_resize.go create mode 100644 container_resize_test.go create mode 100644 container_restart.go create mode 100644 container_restart_test.go create mode 100644 container_start.go create mode 100644 container_start_test.go create mode 100644 container_stats.go create mode 100644 container_stats_test.go create mode 100644 container_stop.go create mode 100644 container_stop_test.go create mode 100644 container_top.go create mode 100644 container_top_test.go create mode 100644 container_unpause.go create mode 100644 container_unpause_test.go create mode 100644 container_update.go create mode 100644 container_update_test.go create mode 100644 container_wait.go create mode 100644 container_wait_test.go create mode 100644 errors.go create mode 100644 events.go create mode 100644 events_test.go create mode 100644 hijack.go create mode 100644 image_build.go create mode 100644 image_build_test.go create mode 100644 image_create.go create mode 100644 image_create_test.go create mode 100644 image_history.go create mode 100644 image_history_test.go create mode 100644 image_import.go create mode 100644 image_import_test.go create mode 100644 image_inspect.go create mode 100644 image_inspect_test.go create mode 100644 image_list.go create mode 100644 image_list_test.go create mode 100644 image_load.go create mode 100644 image_load_test.go create mode 100644 image_pull.go create mode 100644 image_pull_test.go create mode 100644 image_push.go create mode 100644 image_push_test.go create mode 100644 image_remove.go create mode 100644 image_remove_test.go create mode 100644 image_save.go create mode 100644 image_save_test.go create mode 100644 image_search.go create mode 100644 image_search_test.go create mode 100644 image_tag.go create mode 100644 image_tag_test.go create mode 100644 info.go create mode 100644 info_test.go create mode 100644 interface.go create mode 100644 interface_experimental.go create mode 100644 interface_stable.go create mode 100644 login.go create mode 100644 network_connect.go create mode 100644 network_connect_test.go create mode 100644 network_create.go create mode 100644 network_create_test.go create mode 100644 network_disconnect.go create mode 100644 network_disconnect_test.go create mode 100644 network_inspect.go create mode 100644 network_inspect_test.go create mode 100644 network_list.go create mode 100644 network_list_test.go create mode 100644 network_remove.go create mode 100644 network_remove_test.go create mode 100644 node_inspect.go create mode 100644 node_inspect_test.go create mode 100644 node_list.go create mode 100644 node_list_test.go create mode 100644 node_remove.go create mode 100644 node_remove_test.go create mode 100644 node_update.go create mode 100644 node_update_test.go create mode 100644 plugin_disable.go create mode 100644 plugin_disable_test.go create mode 100644 plugin_enable.go create mode 100644 plugin_enable_test.go create mode 100644 plugin_inspect.go create mode 100644 plugin_inspect_test.go create mode 100644 plugin_install.go create mode 100644 plugin_list.go create mode 100644 plugin_list_test.go create mode 100644 plugin_push.go create mode 100644 plugin_push_test.go create mode 100644 plugin_remove.go create mode 100644 plugin_remove_test.go create mode 100644 plugin_set.go create mode 100644 plugin_set_test.go create mode 100644 request.go create mode 100644 request_test.go create mode 100644 service_create.go create mode 100644 service_create_test.go create mode 100644 service_inspect.go create mode 100644 service_inspect_test.go create mode 100644 service_list.go create mode 100644 service_list_test.go create mode 100644 service_remove.go create mode 100644 service_remove_test.go create mode 100644 service_update.go create mode 100644 service_update_test.go create mode 100644 swarm_init.go create mode 100644 swarm_init_test.go create mode 100644 swarm_inspect.go create mode 100644 swarm_inspect_test.go create mode 100644 swarm_join.go create mode 100644 swarm_join_test.go create mode 100644 swarm_leave.go create mode 100644 swarm_leave_test.go create mode 100644 swarm_update.go create mode 100644 swarm_update_test.go create mode 100644 task_inspect.go create mode 100644 task_inspect_test.go create mode 100644 task_list.go create mode 100644 task_list_test.go create mode 100644 testdata/ca.pem create mode 100644 testdata/cert.pem create mode 100644 testdata/key.pem create mode 100644 transport/cancellable/LICENSE create mode 100644 transport/cancellable/canceler.go create mode 100644 transport/cancellable/canceler_go14.go create mode 100644 transport/cancellable/cancellable.go create mode 100644 transport/client.go create mode 100644 transport/tlsconfig_clone.go create mode 100644 transport/tlsconfig_clone_go17.go create mode 100644 transport/transport.go create mode 100644 version.go create mode 100644 volume_create.go create mode 100644 volume_create_test.go create mode 100644 volume_inspect.go create mode 100644 volume_inspect_test.go create mode 100644 volume_list.go create mode 100644 volume_list_test.go create mode 100644 volume_remove.go create mode 100644 volume_remove_test.go diff --git a/checkpoint_create.go b/checkpoint_create.go new file mode 100644 index 0000000000..0effe498be --- /dev/null +++ b/checkpoint_create.go @@ -0,0 +1,13 @@ +package client + +import ( + "github.com/docker/docker/api/types" + "golang.org/x/net/context" +) + +// CheckpointCreate creates a checkpoint from the given container with the given name +func (cli *Client) CheckpointCreate(ctx context.Context, container string, options types.CheckpointCreateOptions) error { + resp, err := cli.post(ctx, "/containers/"+container+"/checkpoints", nil, options, nil) + ensureReaderClosed(resp) + return err +} diff --git a/checkpoint_create_test.go b/checkpoint_create_test.go new file mode 100644 index 0000000000..e2ae36e1e0 --- /dev/null +++ b/checkpoint_create_test.go @@ -0,0 +1,73 @@ +package client + +import ( + "bytes" + "encoding/json" + "fmt" + "io/ioutil" + "net/http" + "strings" + "testing" + + "github.com/docker/docker/api/types" + "golang.org/x/net/context" +) + +func TestCheckpointCreateError(t *testing.T) { + client := &Client{ + transport: newMockClient(nil, errorMock(http.StatusInternalServerError, "Server error")), + } + err := client.CheckpointCreate(context.Background(), "nothing", types.CheckpointCreateOptions{ + CheckpointID: "noting", + Exit: true, + }) + + if err == nil || err.Error() != "Error response from daemon: Server error" { + t.Fatalf("expected a Server Error, got %v", err) + } +} + +func TestCheckpointCreate(t *testing.T) { + expectedContainerID := "container_id" + expectedCheckpointID := "checkpoint_id" + expectedURL := "/containers/container_id/checkpoints" + + client := &Client{ + transport: newMockClient(nil, func(req *http.Request) (*http.Response, error) { + if !strings.HasPrefix(req.URL.Path, expectedURL) { + return nil, fmt.Errorf("Expected URL '%s', got '%s'", expectedURL, req.URL) + } + + if req.Method != "POST" { + return nil, fmt.Errorf("expected POST method, got %s", req.Method) + } + + createOptions := &types.CheckpointCreateOptions{} + if err := json.NewDecoder(req.Body).Decode(createOptions); err != nil { + return nil, err + } + + if createOptions.CheckpointID != expectedCheckpointID { + return nil, fmt.Errorf("expected CheckpointID to be 'checkpoint_id', got %v", createOptions.CheckpointID) + } + + if !createOptions.Exit { + return nil, fmt.Errorf("expected Exit to be true") + } + + return &http.Response{ + StatusCode: http.StatusOK, + Body: ioutil.NopCloser(bytes.NewReader([]byte(""))), + }, nil + }), + } + + err := client.CheckpointCreate(context.Background(), expectedContainerID, types.CheckpointCreateOptions{ + CheckpointID: expectedCheckpointID, + Exit: true, + }) + + if err != nil { + t.Fatal(err) + } +} diff --git a/checkpoint_delete.go b/checkpoint_delete.go new file mode 100644 index 0000000000..a4e9ed0c06 --- /dev/null +++ b/checkpoint_delete.go @@ -0,0 +1,12 @@ +package client + +import ( + "golang.org/x/net/context" +) + +// CheckpointDelete deletes the checkpoint with the given name from the given container +func (cli *Client) CheckpointDelete(ctx context.Context, containerID string, checkpointID string) error { + resp, err := cli.delete(ctx, "/containers/"+containerID+"/checkpoints/"+checkpointID, nil, nil) + ensureReaderClosed(resp) + return err +} diff --git a/checkpoint_delete_test.go b/checkpoint_delete_test.go new file mode 100644 index 0000000000..097ab37693 --- /dev/null +++ b/checkpoint_delete_test.go @@ -0,0 +1,47 @@ +package client + +import ( + "bytes" + "fmt" + "io/ioutil" + "net/http" + "strings" + "testing" + + "golang.org/x/net/context" +) + +func TestCheckpointDeleteError(t *testing.T) { + client := &Client{ + transport: newMockClient(nil, errorMock(http.StatusInternalServerError, "Server error")), + } + + err := client.CheckpointDelete(context.Background(), "container_id", "checkpoint_id") + if err == nil || err.Error() != "Error response from daemon: Server error" { + t.Fatalf("expected a Server Error, got %v", err) + } +} + +func TestCheckpointDelete(t *testing.T) { + expectedURL := "/containers/container_id/checkpoints/checkpoint_id" + + client := &Client{ + transport: newMockClient(nil, func(req *http.Request) (*http.Response, error) { + if !strings.HasPrefix(req.URL.Path, expectedURL) { + return nil, fmt.Errorf("Expected URL '%s', got '%s'", expectedURL, req.URL) + } + if req.Method != "DELETE" { + return nil, fmt.Errorf("expected DELETE method, got %s", req.Method) + } + return &http.Response{ + StatusCode: http.StatusOK, + Body: ioutil.NopCloser(bytes.NewReader([]byte(""))), + }, nil + }), + } + + err := client.CheckpointDelete(context.Background(), "container_id", "checkpoint_id") + if err != nil { + t.Fatal(err) + } +} diff --git a/checkpoint_list.go b/checkpoint_list.go new file mode 100644 index 0000000000..bb471e0056 --- /dev/null +++ b/checkpoint_list.go @@ -0,0 +1,22 @@ +package client + +import ( + "encoding/json" + + "github.com/docker/docker/api/types" + "golang.org/x/net/context" +) + +// CheckpointList returns the volumes configured in the docker host. +func (cli *Client) CheckpointList(ctx context.Context, container string) ([]types.Checkpoint, error) { + var checkpoints []types.Checkpoint + + resp, err := cli.get(ctx, "/containers/"+container+"/checkpoints", nil, nil) + if err != nil { + return checkpoints, err + } + + err = json.NewDecoder(resp.body).Decode(&checkpoints) + ensureReaderClosed(resp) + return checkpoints, err +} diff --git a/checkpoint_list_test.go b/checkpoint_list_test.go new file mode 100644 index 0000000000..5960436eb1 --- /dev/null +++ b/checkpoint_list_test.go @@ -0,0 +1,57 @@ +package client + +import ( + "bytes" + "encoding/json" + "fmt" + "io/ioutil" + "net/http" + "strings" + "testing" + + "github.com/docker/docker/api/types" + "golang.org/x/net/context" +) + +func TestCheckpointListError(t *testing.T) { + client := &Client{ + transport: newMockClient(nil, errorMock(http.StatusInternalServerError, "Server error")), + } + + _, err := client.CheckpointList(context.Background(), "container_id") + if err == nil || err.Error() != "Error response from daemon: Server error" { + t.Fatalf("expected a Server Error, got %v", err) + } +} + +func TestCheckpointList(t *testing.T) { + expectedURL := "/containers/container_id/checkpoints" + + client := &Client{ + transport: newMockClient(nil, func(req *http.Request) (*http.Response, error) { + if !strings.HasPrefix(req.URL.Path, expectedURL) { + return nil, fmt.Errorf("Expected URL '%s', got '%s'", expectedURL, req.URL) + } + content, err := json.Marshal([]types.Checkpoint{ + { + Name: "checkpoint", + }, + }) + if err != nil { + return nil, err + } + return &http.Response{ + StatusCode: http.StatusOK, + Body: ioutil.NopCloser(bytes.NewReader(content)), + }, nil + }), + } + + checkpoints, err := client.CheckpointList(context.Background(), "container_id") + if err != nil { + t.Fatal(err) + } + if len(checkpoints) != 1 { + t.Fatalf("expected 1 checkpoint, got %v", checkpoints) + } +} diff --git a/client.go b/client.go new file mode 100644 index 0000000000..6a85121c6d --- /dev/null +++ b/client.go @@ -0,0 +1,156 @@ +package client + +import ( + "fmt" + "net/http" + "net/url" + "os" + "path/filepath" + "strings" + + "github.com/docker/docker/client/transport" + "github.com/docker/go-connections/tlsconfig" +) + +// DefaultVersion is the version of the current stable API +const DefaultVersion string = "1.23" + +// Client is the API client that performs all operations +// against a docker server. +type Client struct { + // host holds the server address to connect to + host string + // proto holds the client protocol i.e. unix. + proto string + // addr holds the client address. + addr string + // basePath holds the path to prepend to the requests. + basePath string + // transport is the interface to send request with, it implements transport.Client. + transport transport.Client + // version of the server to talk to. + version string + // custom http headers configured by users. + customHTTPHeaders map[string]string +} + +// NewEnvClient initializes a new API client based on environment variables. +// Use DOCKER_HOST to set the url to the docker server. +// Use DOCKER_API_VERSION to set the version of the API to reach, leave empty for latest. +// Use DOCKER_CERT_PATH to load the tls certificates from. +// Use DOCKER_TLS_VERIFY to enable or disable TLS verification, off by default. +func NewEnvClient() (*Client, error) { + var client *http.Client + if dockerCertPath := os.Getenv("DOCKER_CERT_PATH"); dockerCertPath != "" { + options := tlsconfig.Options{ + CAFile: filepath.Join(dockerCertPath, "ca.pem"), + CertFile: filepath.Join(dockerCertPath, "cert.pem"), + KeyFile: filepath.Join(dockerCertPath, "key.pem"), + InsecureSkipVerify: os.Getenv("DOCKER_TLS_VERIFY") == "", + } + tlsc, err := tlsconfig.Client(options) + if err != nil { + return nil, err + } + + client = &http.Client{ + Transport: &http.Transport{ + TLSClientConfig: tlsc, + }, + } + } + + host := os.Getenv("DOCKER_HOST") + if host == "" { + host = DefaultDockerHost + } + + version := os.Getenv("DOCKER_API_VERSION") + if version == "" { + version = DefaultVersion + } + + return NewClient(host, version, client, nil) +} + +// NewClient initializes a new API client for the given host and API version. +// It uses the given http client as transport. +// It also initializes the custom http headers to add to each request. +// +// It won't send any version information if the version number is empty. It is +// highly recommended that you set a version or your client may break if the +// server is upgraded. +func NewClient(host string, version string, client *http.Client, httpHeaders map[string]string) (*Client, error) { + proto, addr, basePath, err := ParseHost(host) + if err != nil { + return nil, err + } + + transport, err := transport.NewTransportWithHTTP(proto, addr, client) + if err != nil { + return nil, err + } + + return &Client{ + host: host, + proto: proto, + addr: addr, + basePath: basePath, + transport: transport, + version: version, + customHTTPHeaders: httpHeaders, + }, nil +} + +// getAPIPath returns the versioned request path to call the api. +// It appends the query parameters to the path if they are not empty. +func (cli *Client) getAPIPath(p string, query url.Values) string { + var apiPath string + if cli.version != "" { + v := strings.TrimPrefix(cli.version, "v") + apiPath = fmt.Sprintf("%s/v%s%s", cli.basePath, v, p) + } else { + apiPath = fmt.Sprintf("%s%s", cli.basePath, p) + } + + u := &url.URL{ + Path: apiPath, + } + if len(query) > 0 { + u.RawQuery = query.Encode() + } + return u.String() +} + +// ClientVersion returns the version string associated with this +// instance of the Client. Note that this value can be changed +// via the DOCKER_API_VERSION env var. +func (cli *Client) ClientVersion() string { + return cli.version +} + +// UpdateClientVersion updates the version string associated with this +// instance of the Client. +func (cli *Client) UpdateClientVersion(v string) { + cli.version = v +} + +// ParseHost verifies that the given host strings is valid. +func ParseHost(host string) (string, string, string, error) { + protoAddrParts := strings.SplitN(host, "://", 2) + if len(protoAddrParts) == 1 { + return "", "", "", fmt.Errorf("unable to parse docker host `%s`", host) + } + + var basePath string + proto, addr := protoAddrParts[0], protoAddrParts[1] + if proto == "tcp" { + parsed, err := url.Parse("tcp://" + addr) + if err != nil { + return "", "", "", err + } + addr = parsed.Host + basePath = parsed.Path + } + return proto, addr, basePath, nil +} diff --git a/client_mock_test.go b/client_mock_test.go new file mode 100644 index 0000000000..33c247266c --- /dev/null +++ b/client_mock_test.go @@ -0,0 +1,76 @@ +package client + +import ( + "bytes" + "crypto/tls" + "encoding/json" + "io/ioutil" + "net/http" + + "github.com/docker/docker/api/types" + "github.com/docker/docker/client/transport" +) + +type mockClient struct { + do func(*http.Request) (*http.Response, error) +} + +// TLSConfig returns the TLS configuration. +func (m *mockClient) TLSConfig() *tls.Config { + return &tls.Config{} +} + +// Scheme returns protocol scheme to use. +func (m *mockClient) Scheme() string { + return "http" +} + +// Secure returns true if there is a TLS configuration. +func (m *mockClient) Secure() bool { + return false +} + +// NewMockClient returns a mocked client that runs the function supplied as `client.Do` call +func newMockClient(tlsConfig *tls.Config, doer func(*http.Request) (*http.Response, error)) transport.Client { + if tlsConfig != nil { + panic("this actually gets set!") + } + + return &mockClient{ + do: doer, + } +} + +// Do executes the supplied function for the mock. +func (m mockClient) Do(req *http.Request) (*http.Response, error) { + return m.do(req) +} + +func errorMock(statusCode int, message string) func(req *http.Request) (*http.Response, error) { + return func(req *http.Request) (*http.Response, error) { + header := http.Header{} + header.Set("Content-Type", "application/json") + + body, err := json.Marshal(&types.ErrorResponse{ + Message: message, + }) + if err != nil { + return nil, err + } + + return &http.Response{ + StatusCode: statusCode, + Body: ioutil.NopCloser(bytes.NewReader(body)), + Header: header, + }, nil + } +} + +func plainTextErrorMock(statusCode int, message string) func(req *http.Request) (*http.Response, error) { + return func(req *http.Request) (*http.Response, error) { + return &http.Response{ + StatusCode: statusCode, + Body: ioutil.NopCloser(bytes.NewReader([]byte(message))), + }, nil + } +} diff --git a/client_test.go b/client_test.go new file mode 100644 index 0000000000..60af3db029 --- /dev/null +++ b/client_test.go @@ -0,0 +1,249 @@ +package client + +import ( + "bytes" + "encoding/json" + "io/ioutil" + "net/http" + "net/url" + "os" + "runtime" + "strings" + "testing" + + "github.com/docker/docker/api/types" + "golang.org/x/net/context" +) + +func TestNewEnvClient(t *testing.T) { + if runtime.GOOS == "windows" { + t.Skip("skipping unix only test for windows") + } + cases := []struct { + envs map[string]string + expectedError string + expectedVersion string + }{ + { + envs: map[string]string{}, + expectedVersion: DefaultVersion, + }, + { + envs: map[string]string{ + "DOCKER_CERT_PATH": "invalid/path", + }, + expectedError: "Could not load X509 key pair: open invalid/path/cert.pem: no such file or directory. Make sure the key is not encrypted", + }, + { + envs: map[string]string{ + "DOCKER_CERT_PATH": "testdata/", + }, + expectedVersion: DefaultVersion, + }, + { + envs: map[string]string{ + "DOCKER_HOST": "host", + }, + expectedError: "unable to parse docker host `host`", + }, + { + envs: map[string]string{ + "DOCKER_HOST": "invalid://url", + }, + expectedVersion: DefaultVersion, + }, + { + envs: map[string]string{ + "DOCKER_API_VERSION": "anything", + }, + expectedVersion: "anything", + }, + { + envs: map[string]string{ + "DOCKER_API_VERSION": "1.22", + }, + expectedVersion: "1.22", + }, + } + for _, c := range cases { + recoverEnvs := setupEnvs(t, c.envs) + apiclient, err := NewEnvClient() + if c.expectedError != "" { + if err == nil || err.Error() != c.expectedError { + t.Errorf("expected an error %s, got %s, for %v", c.expectedError, err.Error(), c) + } + } else { + if err != nil { + t.Error(err) + } + version := apiclient.ClientVersion() + if version != c.expectedVersion { + t.Errorf("expected %s, got %s, for %v", c.expectedVersion, version, c) + } + } + recoverEnvs(t) + } +} + +func setupEnvs(t *testing.T, envs map[string]string) func(*testing.T) { + oldEnvs := map[string]string{} + for key, value := range envs { + oldEnv := os.Getenv(key) + oldEnvs[key] = oldEnv + err := os.Setenv(key, value) + if err != nil { + t.Error(err) + } + } + return func(t *testing.T) { + for key, value := range oldEnvs { + err := os.Setenv(key, value) + if err != nil { + t.Error(err) + } + } + } +} + +func TestGetAPIPath(t *testing.T) { + cases := []struct { + v string + p string + q url.Values + e string + }{ + {"", "/containers/json", nil, "/containers/json"}, + {"", "/containers/json", url.Values{}, "/containers/json"}, + {"", "/containers/json", url.Values{"s": []string{"c"}}, "/containers/json?s=c"}, + {"1.22", "/containers/json", nil, "/v1.22/containers/json"}, + {"1.22", "/containers/json", url.Values{}, "/v1.22/containers/json"}, + {"1.22", "/containers/json", url.Values{"s": []string{"c"}}, "/v1.22/containers/json?s=c"}, + {"v1.22", "/containers/json", nil, "/v1.22/containers/json"}, + {"v1.22", "/containers/json", url.Values{}, "/v1.22/containers/json"}, + {"v1.22", "/containers/json", url.Values{"s": []string{"c"}}, "/v1.22/containers/json?s=c"}, + {"v1.22", "/networks/kiwl$%^", nil, "/v1.22/networks/kiwl$%25%5E"}, + } + + for _, cs := range cases { + c, err := NewClient("unix:///var/run/docker.sock", cs.v, nil, nil) + if err != nil { + t.Fatal(err) + } + g := c.getAPIPath(cs.p, cs.q) + if g != cs.e { + t.Fatalf("Expected %s, got %s", cs.e, g) + } + } +} + +func TestParseHost(t *testing.T) { + cases := []struct { + host string + proto string + addr string + base string + err bool + }{ + {"", "", "", "", true}, + {"foobar", "", "", "", true}, + {"foo://bar", "foo", "bar", "", false}, + {"tcp://localhost:2476", "tcp", "localhost:2476", "", false}, + {"tcp://localhost:2476/path", "tcp", "localhost:2476", "/path", false}, + } + + for _, cs := range cases { + p, a, b, e := ParseHost(cs.host) + if cs.err && e == nil { + t.Fatalf("expected error, got nil") + } + if !cs.err && e != nil { + t.Fatal(e) + } + if cs.proto != p { + t.Fatalf("expected proto %s, got %s", cs.proto, p) + } + if cs.addr != a { + t.Fatalf("expected addr %s, got %s", cs.addr, a) + } + if cs.base != b { + t.Fatalf("expected base %s, got %s", cs.base, b) + } + } +} + +func TestUpdateClientVersion(t *testing.T) { + client := &Client{ + transport: newMockClient(nil, func(req *http.Request) (*http.Response, error) { + splitQuery := strings.Split(req.URL.Path, "/") + queryVersion := splitQuery[1] + b, err := json.Marshal(types.Version{ + APIVersion: queryVersion, + }) + if err != nil { + return nil, err + } + return &http.Response{ + StatusCode: http.StatusOK, + Body: ioutil.NopCloser(bytes.NewReader(b)), + }, nil + }), + } + + cases := []struct { + v string + }{ + {"1.20"}, + {"v1.21"}, + {"1.22"}, + {"v1.22"}, + } + + for _, cs := range cases { + client.UpdateClientVersion(cs.v) + r, err := client.ServerVersion(context.Background()) + if err != nil { + t.Fatal(err) + } + if strings.TrimPrefix(r.APIVersion, "v") != strings.TrimPrefix(cs.v, "v") { + t.Fatalf("Expected %s, got %s", cs.v, r.APIVersion) + } + } +} + +func TestNewEnvClientSetsDefaultVersion(t *testing.T) { + // Unset environment variables + envVarKeys := []string{ + "DOCKER_HOST", + "DOCKER_API_VERSION", + "DOCKER_TLS_VERIFY", + "DOCKER_CERT_PATH", + } + envVarValues := make(map[string]string) + for _, key := range envVarKeys { + envVarValues[key] = os.Getenv(key) + os.Setenv(key, "") + } + + client, err := NewEnvClient() + if err != nil { + t.Fatal(err) + } + if client.version != DefaultVersion { + t.Fatalf("Expected %s, got %s", DefaultVersion, client.version) + } + + expected := "1.22" + os.Setenv("DOCKER_API_VERSION", expected) + client, err = NewEnvClient() + if err != nil { + t.Fatal(err) + } + if client.version != expected { + t.Fatalf("Expected %s, got %s", expected, client.version) + } + + // Restore environment variables + for _, key := range envVarKeys { + os.Setenv(key, envVarValues[key]) + } +} diff --git a/client_unix.go b/client_unix.go new file mode 100644 index 0000000000..89de892c85 --- /dev/null +++ b/client_unix.go @@ -0,0 +1,6 @@ +// +build linux freebsd solaris openbsd darwin + +package client + +// DefaultDockerHost defines os specific default if DOCKER_HOST is unset +const DefaultDockerHost = "unix:///var/run/docker.sock" diff --git a/client_windows.go b/client_windows.go new file mode 100644 index 0000000000..07c0c7a774 --- /dev/null +++ b/client_windows.go @@ -0,0 +1,4 @@ +package client + +// DefaultDockerHost defines os specific default if DOCKER_HOST is unset +const DefaultDockerHost = "npipe:////./pipe/docker_engine" diff --git a/container_attach.go b/container_attach.go new file mode 100644 index 0000000000..7cfc860fcc --- /dev/null +++ b/container_attach.go @@ -0,0 +1,34 @@ +package client + +import ( + "net/url" + + "github.com/docker/docker/api/types" + "golang.org/x/net/context" +) + +// ContainerAttach attaches a connection to a container in the server. +// It returns a types.HijackedConnection with the hijacked connection +// and the a reader to get output. It's up to the called to close +// the hijacked connection by calling types.HijackedResponse.Close. +func (cli *Client) ContainerAttach(ctx context.Context, container string, options types.ContainerAttachOptions) (types.HijackedResponse, error) { + query := url.Values{} + if options.Stream { + query.Set("stream", "1") + } + if options.Stdin { + query.Set("stdin", "1") + } + if options.Stdout { + query.Set("stdout", "1") + } + if options.Stderr { + query.Set("stderr", "1") + } + if options.DetachKeys != "" { + query.Set("detachKeys", options.DetachKeys) + } + + headers := map[string][]string{"Content-Type": {"text/plain"}} + return cli.postHijacked(ctx, "/containers/"+container+"/attach", query, nil, headers) +} diff --git a/container_commit.go b/container_commit.go new file mode 100644 index 0000000000..363950cc24 --- /dev/null +++ b/container_commit.go @@ -0,0 +1,53 @@ +package client + +import ( + "encoding/json" + "errors" + "net/url" + + distreference "github.com/docker/distribution/reference" + "github.com/docker/docker/api/types" + "github.com/docker/docker/api/types/reference" + "golang.org/x/net/context" +) + +// ContainerCommit applies changes into a container and creates a new tagged image. +func (cli *Client) ContainerCommit(ctx context.Context, container string, options types.ContainerCommitOptions) (types.ContainerCommitResponse, error) { + var repository, tag string + if options.Reference != "" { + distributionRef, err := distreference.ParseNamed(options.Reference) + if err != nil { + return types.ContainerCommitResponse{}, err + } + + if _, isCanonical := distributionRef.(distreference.Canonical); isCanonical { + return types.ContainerCommitResponse{}, errors.New("refusing to create a tag with a digest reference") + } + + tag = reference.GetTagFromNamedRef(distributionRef) + repository = distributionRef.Name() + } + + query := url.Values{} + query.Set("container", container) + query.Set("repo", repository) + query.Set("tag", tag) + query.Set("comment", options.Comment) + query.Set("author", options.Author) + for _, change := range options.Changes { + query.Add("changes", change) + } + if options.Pause != true { + query.Set("pause", "0") + } + + var response types.ContainerCommitResponse + resp, err := cli.post(ctx, "/commit", query, options.Config, nil) + if err != nil { + return response, err + } + + err = json.NewDecoder(resp.body).Decode(&response) + ensureReaderClosed(resp) + return response, err +} diff --git a/container_commit_test.go b/container_commit_test.go new file mode 100644 index 0000000000..3fc3e5cfd0 --- /dev/null +++ b/container_commit_test.go @@ -0,0 +1,96 @@ +package client + +import ( + "bytes" + "encoding/json" + "fmt" + "io/ioutil" + "net/http" + "strings" + "testing" + + "github.com/docker/docker/api/types" + "golang.org/x/net/context" +) + +func TestContainerCommitError(t *testing.T) { + client := &Client{ + transport: newMockClient(nil, errorMock(http.StatusInternalServerError, "Server error")), + } + _, err := client.ContainerCommit(context.Background(), "nothing", types.ContainerCommitOptions{}) + if err == nil || err.Error() != "Error response from daemon: Server error" { + t.Fatalf("expected a Server Error, got %v", err) + } +} + +func TestContainerCommit(t *testing.T) { + expectedURL := "/commit" + expectedContainerID := "container_id" + specifiedReference := "repository_name:tag" + expectedRepositoryName := "repository_name" + expectedTag := "tag" + expectedComment := "comment" + expectedAuthor := "author" + expectedChanges := []string{"change1", "change2"} + + client := &Client{ + transport: newMockClient(nil, func(req *http.Request) (*http.Response, error) { + if !strings.HasPrefix(req.URL.Path, expectedURL) { + return nil, fmt.Errorf("Expected URL '%s', got '%s'", expectedURL, req.URL) + } + query := req.URL.Query() + containerID := query.Get("container") + if containerID != expectedContainerID { + return nil, fmt.Errorf("container id not set in URL query properly. Expected '%s', got %s", expectedContainerID, containerID) + } + repo := query.Get("repo") + if repo != expectedRepositoryName { + return nil, fmt.Errorf("container repo not set in URL query properly. Expected '%s', got %s", expectedRepositoryName, repo) + } + tag := query.Get("tag") + if tag != expectedTag { + return nil, fmt.Errorf("container tag not set in URL query properly. Expected '%s', got %s'", expectedTag, tag) + } + comment := query.Get("comment") + if comment != expectedComment { + return nil, fmt.Errorf("container comment not set in URL query properly. Expected '%s', got %s'", expectedComment, comment) + } + author := query.Get("author") + if author != expectedAuthor { + return nil, fmt.Errorf("container author not set in URL query properly. Expected '%s', got %s'", expectedAuthor, author) + } + pause := query.Get("pause") + if pause != "0" { + return nil, fmt.Errorf("container pause not set in URL query properly. Expected 'true', got %v'", pause) + } + changes := query["changes"] + if len(changes) != len(expectedChanges) { + return nil, fmt.Errorf("expected container changes size to be '%d', got %d", len(expectedChanges), len(changes)) + } + b, err := json.Marshal(types.ContainerCommitResponse{ + ID: "new_container_id", + }) + if err != nil { + return nil, err + } + return &http.Response{ + StatusCode: http.StatusOK, + Body: ioutil.NopCloser(bytes.NewReader(b)), + }, nil + }), + } + + r, err := client.ContainerCommit(context.Background(), expectedContainerID, types.ContainerCommitOptions{ + Reference: specifiedReference, + Comment: expectedComment, + Author: expectedAuthor, + Changes: expectedChanges, + Pause: false, + }) + if err != nil { + t.Fatal(err) + } + if r.ID != "new_container_id" { + t.Fatalf("expected `container_id`, got %s", r.ID) + } +} diff --git a/container_copy.go b/container_copy.go new file mode 100644 index 0000000000..8380eeabc9 --- /dev/null +++ b/container_copy.go @@ -0,0 +1,97 @@ +package client + +import ( + "encoding/base64" + "encoding/json" + "fmt" + "io" + "net/http" + "net/url" + "path/filepath" + "strings" + + "golang.org/x/net/context" + + "github.com/docker/docker/api/types" +) + +// ContainerStatPath returns Stat information about a path inside the container filesystem. +func (cli *Client) ContainerStatPath(ctx context.Context, containerID, path string) (types.ContainerPathStat, error) { + query := url.Values{} + query.Set("path", filepath.ToSlash(path)) // Normalize the paths used in the API. + + urlStr := fmt.Sprintf("/containers/%s/archive", containerID) + response, err := cli.head(ctx, urlStr, query, nil) + if err != nil { + return types.ContainerPathStat{}, err + } + defer ensureReaderClosed(response) + return getContainerPathStatFromHeader(response.header) +} + +// CopyToContainer copies content into the container filesystem. +func (cli *Client) CopyToContainer(ctx context.Context, container, path string, content io.Reader, options types.CopyToContainerOptions) error { + query := url.Values{} + query.Set("path", filepath.ToSlash(path)) // Normalize the paths used in the API. + // Do not allow for an existing directory to be overwritten by a non-directory and vice versa. + if !options.AllowOverwriteDirWithFile { + query.Set("noOverwriteDirNonDir", "true") + } + + apiPath := fmt.Sprintf("/containers/%s/archive", container) + + response, err := cli.putRaw(ctx, apiPath, query, content, nil) + if err != nil { + return err + } + defer ensureReaderClosed(response) + + if response.statusCode != http.StatusOK { + return fmt.Errorf("unexpected status code from daemon: %d", response.statusCode) + } + + return nil +} + +// CopyFromContainer gets the content from the container and returns it as a Reader +// to manipulate it in the host. It's up to the caller to close the reader. +func (cli *Client) CopyFromContainer(ctx context.Context, container, srcPath string) (io.ReadCloser, types.ContainerPathStat, error) { + query := make(url.Values, 1) + query.Set("path", filepath.ToSlash(srcPath)) // Normalize the paths used in the API. + + apiPath := fmt.Sprintf("/containers/%s/archive", container) + response, err := cli.get(ctx, apiPath, query, nil) + if err != nil { + return nil, types.ContainerPathStat{}, err + } + + if response.statusCode != http.StatusOK { + return nil, types.ContainerPathStat{}, fmt.Errorf("unexpected status code from daemon: %d", response.statusCode) + } + + // In order to get the copy behavior right, we need to know information + // about both the source and the destination. The response headers include + // stat info about the source that we can use in deciding exactly how to + // copy it locally. Along with the stat info about the local destination, + // we have everything we need to handle the multiple possibilities there + // can be when copying a file/dir from one location to another file/dir. + stat, err := getContainerPathStatFromHeader(response.header) + if err != nil { + return nil, stat, fmt.Errorf("unable to get resource stat from response: %s", err) + } + return response.body, stat, err +} + +func getContainerPathStatFromHeader(header http.Header) (types.ContainerPathStat, error) { + var stat types.ContainerPathStat + + encodedStat := header.Get("X-Docker-Container-Path-Stat") + statDecoder := base64.NewDecoder(base64.StdEncoding, strings.NewReader(encodedStat)) + + err := json.NewDecoder(statDecoder).Decode(&stat) + if err != nil { + err = fmt.Errorf("unable to decode container path stat header: %s", err) + } + + return stat, err +} diff --git a/container_copy_test.go b/container_copy_test.go new file mode 100644 index 0000000000..39cd05ac2d --- /dev/null +++ b/container_copy_test.go @@ -0,0 +1,244 @@ +package client + +import ( + "bytes" + "encoding/base64" + "encoding/json" + "fmt" + "io/ioutil" + "net/http" + "strings" + "testing" + + "golang.org/x/net/context" + + "github.com/docker/docker/api/types" +) + +func TestContainerStatPathError(t *testing.T) { + client := &Client{ + transport: newMockClient(nil, errorMock(http.StatusInternalServerError, "Server error")), + } + _, err := client.ContainerStatPath(context.Background(), "container_id", "path") + if err == nil || err.Error() != "Error response from daemon: Server error" { + t.Fatalf("expected a Server error, got %v", err) + } +} + +func TestContainerStatPathNoHeaderError(t *testing.T) { + client := &Client{ + transport: newMockClient(nil, func(req *http.Request) (*http.Response, error) { + return &http.Response{ + StatusCode: http.StatusOK, + Body: ioutil.NopCloser(bytes.NewReader([]byte(""))), + }, nil + }), + } + _, err := client.ContainerStatPath(context.Background(), "container_id", "path/to/file") + if err == nil { + t.Fatalf("expected an error, got nothing") + } +} + +func TestContainerStatPath(t *testing.T) { + expectedURL := "/containers/container_id/archive" + expectedPath := "path/to/file" + client := &Client{ + transport: newMockClient(nil, func(req *http.Request) (*http.Response, error) { + if !strings.HasPrefix(req.URL.Path, expectedURL) { + return nil, fmt.Errorf("Expected URL '%s', got '%s'", expectedURL, req.URL) + } + if req.Method != "HEAD" { + return nil, fmt.Errorf("expected HEAD method, got %s", req.Method) + } + query := req.URL.Query() + path := query.Get("path") + if path != expectedPath { + return nil, fmt.Errorf("path not set in URL query properly") + } + content, err := json.Marshal(types.ContainerPathStat{ + Name: "name", + Mode: 0700, + }) + if err != nil { + return nil, err + } + base64PathStat := base64.StdEncoding.EncodeToString(content) + return &http.Response{ + StatusCode: http.StatusOK, + Body: ioutil.NopCloser(bytes.NewReader([]byte(""))), + Header: http.Header{ + "X-Docker-Container-Path-Stat": []string{base64PathStat}, + }, + }, nil + }), + } + stat, err := client.ContainerStatPath(context.Background(), "container_id", expectedPath) + if err != nil { + t.Fatal(err) + } + if stat.Name != "name" { + t.Fatalf("expected container path stat name to be 'name', was '%s'", stat.Name) + } + if stat.Mode != 0700 { + t.Fatalf("expected container path stat mode to be 0700, was '%v'", stat.Mode) + } +} + +func TestCopyToContainerError(t *testing.T) { + client := &Client{ + transport: newMockClient(nil, errorMock(http.StatusInternalServerError, "Server error")), + } + err := client.CopyToContainer(context.Background(), "container_id", "path/to/file", bytes.NewReader([]byte("")), types.CopyToContainerOptions{}) + if err == nil || err.Error() != "Error response from daemon: Server error" { + t.Fatalf("expected a Server error, got %v", err) + } +} + +func TestCopyToContainerNotStatusOKError(t *testing.T) { + client := &Client{ + transport: newMockClient(nil, errorMock(http.StatusNoContent, "No content")), + } + err := client.CopyToContainer(context.Background(), "container_id", "path/to/file", bytes.NewReader([]byte("")), types.CopyToContainerOptions{}) + if err == nil || err.Error() != "unexpected status code from daemon: 204" { + t.Fatalf("expected an unexpected status code error, got %v", err) + } +} + +func TestCopyToContainer(t *testing.T) { + expectedURL := "/containers/container_id/archive" + expectedPath := "path/to/file" + client := &Client{ + transport: newMockClient(nil, func(req *http.Request) (*http.Response, error) { + if !strings.HasPrefix(req.URL.Path, expectedURL) { + return nil, fmt.Errorf("Expected URL '%s', got '%s'", expectedURL, req.URL) + } + if req.Method != "PUT" { + return nil, fmt.Errorf("expected PUT method, got %s", req.Method) + } + query := req.URL.Query() + path := query.Get("path") + if path != expectedPath { + return nil, fmt.Errorf("path not set in URL query properly, expected '%s', got %s", expectedPath, path) + } + noOverwriteDirNonDir := query.Get("noOverwriteDirNonDir") + if noOverwriteDirNonDir != "true" { + return nil, fmt.Errorf("noOverwriteDirNonDir not set in URL query properly, expected true, got %s", noOverwriteDirNonDir) + } + + content, err := ioutil.ReadAll(req.Body) + if err != nil { + return nil, err + } + if err := req.Body.Close(); err != nil { + return nil, err + } + if string(content) != "content" { + return nil, fmt.Errorf("expected content to be 'content', got %s", string(content)) + } + + return &http.Response{ + StatusCode: http.StatusOK, + Body: ioutil.NopCloser(bytes.NewReader([]byte(""))), + }, nil + }), + } + err := client.CopyToContainer(context.Background(), "container_id", expectedPath, bytes.NewReader([]byte("content")), types.CopyToContainerOptions{ + AllowOverwriteDirWithFile: false, + }) + if err != nil { + t.Fatal(err) + } +} + +func TestCopyFromContainerError(t *testing.T) { + client := &Client{ + transport: newMockClient(nil, errorMock(http.StatusInternalServerError, "Server error")), + } + _, _, err := client.CopyFromContainer(context.Background(), "container_id", "path/to/file") + if err == nil || err.Error() != "Error response from daemon: Server error" { + t.Fatalf("expected a Server error, got %v", err) + } +} + +func TestCopyFromContainerNotStatusOKError(t *testing.T) { + client := &Client{ + transport: newMockClient(nil, errorMock(http.StatusNoContent, "No content")), + } + _, _, err := client.CopyFromContainer(context.Background(), "container_id", "path/to/file") + if err == nil || err.Error() != "unexpected status code from daemon: 204" { + t.Fatalf("expected an unexpected status code error, got %v", err) + } +} + +func TestCopyFromContainerNoHeaderError(t *testing.T) { + client := &Client{ + transport: newMockClient(nil, func(req *http.Request) (*http.Response, error) { + return &http.Response{ + StatusCode: http.StatusOK, + Body: ioutil.NopCloser(bytes.NewReader([]byte(""))), + }, nil + }), + } + _, _, err := client.CopyFromContainer(context.Background(), "container_id", "path/to/file") + if err == nil { + t.Fatalf("expected an error, got nothing") + } +} + +func TestCopyFromContainer(t *testing.T) { + expectedURL := "/containers/container_id/archive" + expectedPath := "path/to/file" + client := &Client{ + transport: newMockClient(nil, func(req *http.Request) (*http.Response, error) { + if !strings.HasPrefix(req.URL.Path, expectedURL) { + return nil, fmt.Errorf("Expected URL '%s', got '%s'", expectedURL, req.URL) + } + if req.Method != "GET" { + return nil, fmt.Errorf("expected PUT method, got %s", req.Method) + } + query := req.URL.Query() + path := query.Get("path") + if path != expectedPath { + return nil, fmt.Errorf("path not set in URL query properly, expected '%s', got %s", expectedPath, path) + } + + headercontent, err := json.Marshal(types.ContainerPathStat{ + Name: "name", + Mode: 0700, + }) + if err != nil { + return nil, err + } + base64PathStat := base64.StdEncoding.EncodeToString(headercontent) + + return &http.Response{ + StatusCode: http.StatusOK, + Body: ioutil.NopCloser(bytes.NewReader([]byte("content"))), + Header: http.Header{ + "X-Docker-Container-Path-Stat": []string{base64PathStat}, + }, + }, nil + }), + } + r, stat, err := client.CopyFromContainer(context.Background(), "container_id", expectedPath) + if err != nil { + t.Fatal(err) + } + if stat.Name != "name" { + t.Fatalf("expected container path stat name to be 'name', was '%s'", stat.Name) + } + if stat.Mode != 0700 { + t.Fatalf("expected container path stat mode to be 0700, was '%v'", stat.Mode) + } + content, err := ioutil.ReadAll(r) + if err != nil { + t.Fatal(err) + } + if err := r.Close(); err != nil { + t.Fatal(err) + } + if string(content) != "content" { + t.Fatalf("expected content to be 'content', got %s", string(content)) + } +} diff --git a/container_create.go b/container_create.go new file mode 100644 index 0000000000..a862172956 --- /dev/null +++ b/container_create.go @@ -0,0 +1,46 @@ +package client + +import ( + "encoding/json" + "net/url" + "strings" + + "github.com/docker/docker/api/types" + "github.com/docker/docker/api/types/container" + "github.com/docker/docker/api/types/network" + "golang.org/x/net/context" +) + +type configWrapper struct { + *container.Config + HostConfig *container.HostConfig + NetworkingConfig *network.NetworkingConfig +} + +// ContainerCreate creates a new container based in the given configuration. +// It can be associated with a name, but it's not mandatory. +func (cli *Client) ContainerCreate(ctx context.Context, config *container.Config, hostConfig *container.HostConfig, networkingConfig *network.NetworkingConfig, containerName string) (types.ContainerCreateResponse, error) { + var response types.ContainerCreateResponse + query := url.Values{} + if containerName != "" { + query.Set("name", containerName) + } + + body := configWrapper{ + Config: config, + HostConfig: hostConfig, + NetworkingConfig: networkingConfig, + } + + serverResp, err := cli.post(ctx, "/containers/create", query, body, nil) + if err != nil { + if serverResp.statusCode == 404 && strings.Contains(err.Error(), "No such image") { + return response, imageNotFoundError{config.Image} + } + return response, err + } + + err = json.NewDecoder(serverResp.body).Decode(&response) + ensureReaderClosed(serverResp) + return response, err +} diff --git a/container_create_test.go b/container_create_test.go new file mode 100644 index 0000000000..4c14cdc5d1 --- /dev/null +++ b/container_create_test.go @@ -0,0 +1,77 @@ +package client + +import ( + "bytes" + "encoding/json" + "fmt" + "io/ioutil" + "net/http" + "strings" + "testing" + + "github.com/docker/docker/api/types" + "github.com/docker/docker/api/types/container" + "golang.org/x/net/context" +) + +func TestContainerCreateError(t *testing.T) { + client := &Client{ + transport: newMockClient(nil, errorMock(http.StatusInternalServerError, "Server error")), + } + _, err := client.ContainerCreate(context.Background(), nil, nil, nil, "nothing") + if err == nil || err.Error() != "Error response from daemon: Server error" { + t.Fatalf("expected a Server Error, got %v", err) + } + + // 404 doesn't automagitally means an unknown image + client = &Client{ + transport: newMockClient(nil, errorMock(http.StatusNotFound, "Server error")), + } + _, err = client.ContainerCreate(context.Background(), nil, nil, nil, "nothing") + if err == nil || err.Error() != "Error response from daemon: Server error" { + t.Fatalf("expected a Server Error, got %v", err) + } +} + +func TestContainerCreateImageNotFound(t *testing.T) { + client := &Client{ + transport: newMockClient(nil, errorMock(http.StatusNotFound, "No such image")), + } + _, err := client.ContainerCreate(context.Background(), &container.Config{Image: "unknown_image"}, nil, nil, "unknown") + if err == nil || !IsErrImageNotFound(err) { + t.Fatalf("expected an imageNotFound error, got %v", err) + } +} + +func TestContainerCreateWithName(t *testing.T) { + expectedURL := "/containers/create" + client := &Client{ + transport: newMockClient(nil, func(req *http.Request) (*http.Response, error) { + if !strings.HasPrefix(req.URL.Path, expectedURL) { + return nil, fmt.Errorf("Expected URL '%s', got '%s'", expectedURL, req.URL) + } + name := req.URL.Query().Get("name") + if name != "container_name" { + return nil, fmt.Errorf("container name not set in URL query properly. Expected `container_name`, got %s", name) + } + b, err := json.Marshal(types.ContainerCreateResponse{ + ID: "container_id", + }) + if err != nil { + return nil, err + } + return &http.Response{ + StatusCode: http.StatusOK, + Body: ioutil.NopCloser(bytes.NewReader(b)), + }, nil + }), + } + + r, err := client.ContainerCreate(context.Background(), nil, nil, nil, "container_name") + if err != nil { + t.Fatal(err) + } + if r.ID != "container_id" { + t.Fatalf("expected `container_id`, got %s", r.ID) + } +} diff --git a/container_diff.go b/container_diff.go new file mode 100644 index 0000000000..1e3e554fc5 --- /dev/null +++ b/container_diff.go @@ -0,0 +1,23 @@ +package client + +import ( + "encoding/json" + "net/url" + + "github.com/docker/docker/api/types" + "golang.org/x/net/context" +) + +// ContainerDiff shows differences in a container filesystem since it was started. +func (cli *Client) ContainerDiff(ctx context.Context, containerID string) ([]types.ContainerChange, error) { + var changes []types.ContainerChange + + serverResp, err := cli.get(ctx, "/containers/"+containerID+"/changes", url.Values{}, nil) + if err != nil { + return changes, err + } + + err = json.NewDecoder(serverResp.body).Decode(&changes) + ensureReaderClosed(serverResp) + return changes, err +} diff --git a/container_diff_test.go b/container_diff_test.go new file mode 100644 index 0000000000..03ea3354d2 --- /dev/null +++ b/container_diff_test.go @@ -0,0 +1,61 @@ +package client + +import ( + "bytes" + "encoding/json" + "fmt" + "io/ioutil" + "net/http" + "strings" + "testing" + + "github.com/docker/docker/api/types" + "golang.org/x/net/context" +) + +func TestContainerDiffError(t *testing.T) { + client := &Client{ + transport: newMockClient(nil, errorMock(http.StatusInternalServerError, "Server error")), + } + _, err := client.ContainerDiff(context.Background(), "nothing") + if err == nil || err.Error() != "Error response from daemon: Server error" { + t.Fatalf("expected a Server Error, got %v", err) + } + +} + +func TestContainerDiff(t *testing.T) { + expectedURL := "/containers/container_id/changes" + client := &Client{ + transport: newMockClient(nil, func(req *http.Request) (*http.Response, error) { + if !strings.HasPrefix(req.URL.Path, expectedURL) { + return nil, fmt.Errorf("Expected URL '%s', got '%s'", expectedURL, req.URL) + } + b, err := json.Marshal([]types.ContainerChange{ + { + Kind: 0, + Path: "/path/1", + }, + { + Kind: 1, + Path: "/path/2", + }, + }) + if err != nil { + return nil, err + } + return &http.Response{ + StatusCode: http.StatusOK, + Body: ioutil.NopCloser(bytes.NewReader(b)), + }, nil + }), + } + + changes, err := client.ContainerDiff(context.Background(), "container_id") + if err != nil { + t.Fatal(err) + } + if len(changes) != 2 { + t.Fatalf("expected an array of 2 changes, got %v", changes) + } +} diff --git a/container_exec.go b/container_exec.go new file mode 100644 index 0000000000..34173d3194 --- /dev/null +++ b/container_exec.go @@ -0,0 +1,49 @@ +package client + +import ( + "encoding/json" + + "github.com/docker/docker/api/types" + "golang.org/x/net/context" +) + +// ContainerExecCreate creates a new exec configuration to run an exec process. +func (cli *Client) ContainerExecCreate(ctx context.Context, container string, config types.ExecConfig) (types.ContainerExecCreateResponse, error) { + var response types.ContainerExecCreateResponse + resp, err := cli.post(ctx, "/containers/"+container+"/exec", nil, config, nil) + if err != nil { + return response, err + } + err = json.NewDecoder(resp.body).Decode(&response) + ensureReaderClosed(resp) + return response, err +} + +// ContainerExecStart starts an exec process already created in the docker host. +func (cli *Client) ContainerExecStart(ctx context.Context, execID string, config types.ExecStartCheck) error { + resp, err := cli.post(ctx, "/exec/"+execID+"/start", nil, config, nil) + ensureReaderClosed(resp) + return err +} + +// ContainerExecAttach attaches a connection to an exec process in the server. +// It returns a types.HijackedConnection with the hijacked connection +// and the a reader to get output. It's up to the called to close +// the hijacked connection by calling types.HijackedResponse.Close. +func (cli *Client) ContainerExecAttach(ctx context.Context, execID string, config types.ExecConfig) (types.HijackedResponse, error) { + headers := map[string][]string{"Content-Type": {"application/json"}} + return cli.postHijacked(ctx, "/exec/"+execID+"/start", nil, config, headers) +} + +// ContainerExecInspect returns information about a specific exec process on the docker host. +func (cli *Client) ContainerExecInspect(ctx context.Context, execID string) (types.ContainerExecInspect, error) { + var response types.ContainerExecInspect + resp, err := cli.get(ctx, "/exec/"+execID+"/json", nil, nil) + if err != nil { + return response, err + } + + err = json.NewDecoder(resp.body).Decode(&response) + ensureReaderClosed(resp) + return response, err +} diff --git a/container_exec_test.go b/container_exec_test.go new file mode 100644 index 0000000000..abe824e47b --- /dev/null +++ b/container_exec_test.go @@ -0,0 +1,157 @@ +package client + +import ( + "bytes" + "encoding/json" + "fmt" + "io/ioutil" + "net/http" + "strings" + "testing" + + "golang.org/x/net/context" + + "github.com/docker/docker/api/types" +) + +func TestContainerExecCreateError(t *testing.T) { + client := &Client{ + transport: newMockClient(nil, errorMock(http.StatusInternalServerError, "Server error")), + } + _, err := client.ContainerExecCreate(context.Background(), "container_id", types.ExecConfig{}) + if err == nil || err.Error() != "Error response from daemon: Server error" { + t.Fatalf("expected a Server Error, got %v", err) + } +} + +func TestContainerExecCreate(t *testing.T) { + expectedURL := "/containers/container_id/exec" + client := &Client{ + transport: newMockClient(nil, func(req *http.Request) (*http.Response, error) { + if !strings.HasPrefix(req.URL.Path, expectedURL) { + return nil, fmt.Errorf("expected URL '%s', got '%s'", expectedURL, req.URL) + } + if req.Method != "POST" { + return nil, fmt.Errorf("expected POST method, got %s", req.Method) + } + // FIXME validate the content is the given ExecConfig ? + if err := req.ParseForm(); err != nil { + return nil, err + } + execConfig := &types.ExecConfig{} + if err := json.NewDecoder(req.Body).Decode(execConfig); err != nil { + return nil, err + } + if execConfig.User != "user" { + return nil, fmt.Errorf("expected an execConfig with User == 'user', got %v", execConfig) + } + b, err := json.Marshal(types.ContainerExecCreateResponse{ + ID: "exec_id", + }) + if err != nil { + return nil, err + } + return &http.Response{ + StatusCode: http.StatusOK, + Body: ioutil.NopCloser(bytes.NewReader(b)), + }, nil + }), + } + + r, err := client.ContainerExecCreate(context.Background(), "container_id", types.ExecConfig{ + User: "user", + }) + if err != nil { + t.Fatal(err) + } + if r.ID != "exec_id" { + t.Fatalf("expected `exec_id`, got %s", r.ID) + } +} + +func TestContainerExecStartError(t *testing.T) { + client := &Client{ + transport: newMockClient(nil, errorMock(http.StatusInternalServerError, "Server error")), + } + err := client.ContainerExecStart(context.Background(), "nothing", types.ExecStartCheck{}) + if err == nil || err.Error() != "Error response from daemon: Server error" { + t.Fatalf("expected a Server Error, got %v", err) + } +} + +func TestContainerExecStart(t *testing.T) { + expectedURL := "/exec/exec_id/start" + client := &Client{ + transport: newMockClient(nil, func(req *http.Request) (*http.Response, error) { + if !strings.HasPrefix(req.URL.Path, expectedURL) { + return nil, fmt.Errorf("Expected URL '%s', got '%s'", expectedURL, req.URL) + } + if err := req.ParseForm(); err != nil { + return nil, err + } + execStartCheck := &types.ExecStartCheck{} + if err := json.NewDecoder(req.Body).Decode(execStartCheck); err != nil { + return nil, err + } + if execStartCheck.Tty || !execStartCheck.Detach { + return nil, fmt.Errorf("expected execStartCheck{Detach:true,Tty:false}, got %v", execStartCheck) + } + + return &http.Response{ + StatusCode: http.StatusOK, + Body: ioutil.NopCloser(bytes.NewReader([]byte(""))), + }, nil + }), + } + + err := client.ContainerExecStart(context.Background(), "exec_id", types.ExecStartCheck{ + Detach: true, + Tty: false, + }) + if err != nil { + t.Fatal(err) + } +} + +func TestContainerExecInspectError(t *testing.T) { + client := &Client{ + transport: newMockClient(nil, errorMock(http.StatusInternalServerError, "Server error")), + } + _, err := client.ContainerExecInspect(context.Background(), "nothing") + if err == nil || err.Error() != "Error response from daemon: Server error" { + t.Fatalf("expected a Server Error, got %v", err) + } +} + +func TestContainerExecInspect(t *testing.T) { + expectedURL := "/exec/exec_id/json" + client := &Client{ + transport: newMockClient(nil, func(req *http.Request) (*http.Response, error) { + if !strings.HasPrefix(req.URL.Path, expectedURL) { + return nil, fmt.Errorf("Expected URL '%s', got '%s'", expectedURL, req.URL) + } + b, err := json.Marshal(types.ContainerExecInspect{ + ExecID: "exec_id", + ContainerID: "container_id", + }) + if err != nil { + return nil, err + } + return &http.Response{ + StatusCode: http.StatusOK, + Body: ioutil.NopCloser(bytes.NewReader(b)), + }, nil + }), + } + + inspect, err := client.ContainerExecInspect(context.Background(), "exec_id") + if err != nil { + t.Fatal(err) + } + if inspect.ExecID != "exec_id" { + t.Fatalf("expected ExecID to be `exec_id`, got %s", inspect.ExecID) + } + if inspect.ContainerID != "container_id" { + t.Fatalf("expected ContainerID `container_id`, got %s", inspect.ContainerID) + } +} diff --git a/container_export.go b/container_export.go new file mode 100644 index 0000000000..52194f3d34 --- /dev/null +++ b/container_export.go @@ -0,0 +1,20 @@ +package client + +import ( + "io" + "net/url" + + "golang.org/x/net/context" +) + +// ContainerExport retrieves the raw contents of a container +// and returns them as an io.ReadCloser. It's up to the caller +// to close the stream. +func (cli *Client) ContainerExport(ctx context.Context, containerID string) (io.ReadCloser, error) { + serverResp, err := cli.get(ctx, "/containers/"+containerID+"/export", url.Values{}, nil) + if err != nil { + return nil, err + } + + return serverResp.body, nil +} diff --git a/container_export_test.go b/container_export_test.go new file mode 100644 index 0000000000..10eba33d2f --- /dev/null +++ b/container_export_test.go @@ -0,0 +1,50 @@ +package client + +import ( + "bytes" + "fmt" + "io/ioutil" + "net/http" + "strings" + "testing" + + "golang.org/x/net/context" +) + +func TestContainerExportError(t *testing.T) { + client := &Client{ + transport: newMockClient(nil, errorMock(http.StatusInternalServerError, "Server error")), + } + _, err := client.ContainerExport(context.Background(), "nothing") + if err == nil || err.Error() != "Error response from daemon: Server error" { + t.Fatalf("expected a Server Error, got %v", err) + } +} + +func TestContainerExport(t *testing.T) { + expectedURL := "/containers/container_id/export" + client := &Client{ + transport: newMockClient(nil, func(r *http.Request) (*http.Response, error) { + if !strings.HasPrefix(r.URL.Path, expectedURL) { + return nil, fmt.Errorf("Expected URL '%s', got '%s'", expectedURL, r.URL) + } + + return &http.Response{ + StatusCode: http.StatusOK, + Body: ioutil.NopCloser(bytes.NewReader([]byte("response"))), + }, nil + }), + } + body, err := client.ContainerExport(context.Background(), "container_id") + if err != nil { + t.Fatal(err) + } + defer body.Close() + content, err := ioutil.ReadAll(body) + if err != nil { + t.Fatal(err) + } + if string(content) != "response" { + t.Fatalf("expected response to contain 'response', got %s", string(content)) + } +} diff --git a/container_inspect.go b/container_inspect.go new file mode 100644 index 0000000000..17f1809747 --- /dev/null +++ b/container_inspect.go @@ -0,0 +1,54 @@ +package client + +import ( + "bytes" + "encoding/json" + "io/ioutil" + "net/http" + "net/url" + + "github.com/docker/docker/api/types" + "golang.org/x/net/context" +) + +// ContainerInspect returns the container information. +func (cli *Client) ContainerInspect(ctx context.Context, containerID string) (types.ContainerJSON, error) { + serverResp, err := cli.get(ctx, "/containers/"+containerID+"/json", nil, nil) + if err != nil { + if serverResp.statusCode == http.StatusNotFound { + return types.ContainerJSON{}, containerNotFoundError{containerID} + } + return types.ContainerJSON{}, err + } + + var response types.ContainerJSON + err = json.NewDecoder(serverResp.body).Decode(&response) + ensureReaderClosed(serverResp) + return response, err +} + +// ContainerInspectWithRaw returns the container information and its raw representation. +func (cli *Client) ContainerInspectWithRaw(ctx context.Context, containerID string, getSize bool) (types.ContainerJSON, []byte, error) { + query := url.Values{} + if getSize { + query.Set("size", "1") + } + serverResp, err := cli.get(ctx, "/containers/"+containerID+"/json", query, nil) + if err != nil { + if serverResp.statusCode == http.StatusNotFound { + return types.ContainerJSON{}, nil, containerNotFoundError{containerID} + } + return types.ContainerJSON{}, nil, err + } + defer ensureReaderClosed(serverResp) + + body, err := ioutil.ReadAll(serverResp.body) + if err != nil { + return types.ContainerJSON{}, nil, err + } + + var response types.ContainerJSON + rdr := bytes.NewReader(body) + err = json.NewDecoder(rdr).Decode(&response) + return response, body, err +} diff --git a/container_inspect_test.go b/container_inspect_test.go new file mode 100644 index 0000000000..0dc8ac3753 --- /dev/null +++ b/container_inspect_test.go @@ -0,0 +1,125 @@ +package client + +import ( + "bytes" + "encoding/json" + "fmt" + "io/ioutil" + "net/http" + "strings" + "testing" + + "github.com/docker/docker/api/types" + "golang.org/x/net/context" +) + +func TestContainerInspectError(t *testing.T) { + client := &Client{ + transport: newMockClient(nil, errorMock(http.StatusInternalServerError, "Server error")), + } + + _, err := client.ContainerInspect(context.Background(), "nothing") + if err == nil || err.Error() != "Error response from daemon: Server error" { + t.Fatalf("expected a Server Error, got %v", err) + } +} + +func TestContainerInspectContainerNotFound(t *testing.T) { + client := &Client{ + transport: newMockClient(nil, errorMock(http.StatusNotFound, "Server error")), + } + + _, err := client.ContainerInspect(context.Background(), "unknown") + if err == nil || !IsErrContainerNotFound(err) { + t.Fatalf("expected a containerNotFound error, got %v", err) + } +} + +func TestContainerInspect(t *testing.T) { + expectedURL := "/containers/container_id/json" + client := &Client{ + transport: newMockClient(nil, func(req *http.Request) (*http.Response, error) { + if !strings.HasPrefix(req.URL.Path, expectedURL) { + return nil, fmt.Errorf("Expected URL '%s', got '%s'", expectedURL, req.URL) + } + content, err := json.Marshal(types.ContainerJSON{ + ContainerJSONBase: &types.ContainerJSONBase{ + ID: "container_id", + Image: "image", + Name: "name", + }, + }) + if err != nil { + return nil, err + } + return &http.Response{ + StatusCode: http.StatusOK, + Body: ioutil.NopCloser(bytes.NewReader(content)), + }, nil + }), + } + + r, err := client.ContainerInspect(context.Background(), "container_id") + if err != nil { + t.Fatal(err) + } + if r.ID != "container_id" { + t.Fatalf("expected `container_id`, got %s", r.ID) + } + if r.Image != "image" { + t.Fatalf("expected `image`, got %s", r.ID) + } + if r.Name != "name" { + t.Fatalf("expected `name`, got %s", r.ID) + } +} + +func TestContainerInspectNode(t *testing.T) { + client := &Client{ + transport: newMockClient(nil, func(req *http.Request) (*http.Response, error) { + content, err := json.Marshal(types.ContainerJSON{ + ContainerJSONBase: &types.ContainerJSONBase{ + ID: "container_id", + Image: "image", + Name: "name", + Node: &types.ContainerNode{ + ID: "container_node_id", + Addr: "container_node", + Labels: map[string]string{"foo": "bar"}, + }, + }, + }) + if err != nil { + return nil, err + } + return &http.Response{ + StatusCode: http.StatusOK, + Body: ioutil.NopCloser(bytes.NewReader(content)), + }, nil + }), + } + + r, err := client.ContainerInspect(context.Background(), "container_id") + if err != nil { + t.Fatal(err) + } + if r.ID != "container_id" { + t.Fatalf("expected `container_id`, got %s", r.ID) + } + if r.Image != "image" { + t.Fatalf("expected `image`, got %s", r.ID) + } + if r.Name != "name" { + t.Fatalf("expected `name`, got %s", r.ID) + } + if r.Node.ID != "container_node_id" { + t.Fatalf("expected `container_node_id`, got %s", r.Node.ID) + } + if r.Node.Addr != "container_node" { + t.Fatalf("expected `container_node`, got %s", r.Node.Addr) + } + foo, ok := r.Node.Labels["foo"] + if foo != "bar" || !ok { + t.Fatalf("expected `bar` for label `foo`") + } +} diff --git a/container_kill.go b/container_kill.go new file mode 100644 index 0000000000..29f80c73ad --- /dev/null +++ b/container_kill.go @@ -0,0 +1,17 @@ +package client + +import ( + "net/url" + + "golang.org/x/net/context" +) + +// ContainerKill terminates the container process but does not remove the container from the docker host. +func (cli *Client) ContainerKill(ctx context.Context, containerID, signal string) error { + query := url.Values{} + query.Set("signal", signal) + + resp, err := cli.post(ctx, "/containers/"+containerID+"/kill", query, nil, nil) + ensureReaderClosed(resp) + return err +} diff --git a/container_kill_test.go b/container_kill_test.go new file mode 100644 index 0000000000..a34a7b5b11 --- /dev/null +++ b/container_kill_test.go @@ -0,0 +1,46 @@ +package client + +import ( + "bytes" + "fmt" + "io/ioutil" + "net/http" + "strings" + "testing" + + "golang.org/x/net/context" +) + +func TestContainerKillError(t *testing.T) { + client := &Client{ + transport: newMockClient(nil, errorMock(http.StatusInternalServerError, "Server error")), + } + err := client.ContainerKill(context.Background(), "nothing", "SIGKILL") + if err == nil || err.Error() != "Error response from daemon: Server error" { + t.Fatalf("expected a Server Error, got %v", err) + } +} + +func TestContainerKill(t *testing.T) { + expectedURL := "/containers/container_id/kill" + client := &Client{ + transport: newMockClient(nil, func(req *http.Request) (*http.Response, error) { + if !strings.HasPrefix(req.URL.Path, expectedURL) { + return nil, fmt.Errorf("Expected URL '%s', got '%s'", expectedURL, req.URL) + } + signal := req.URL.Query().Get("signal") + if signal != "SIGKILL" { + return nil, fmt.Errorf("signal not set in URL query properly. Expected 'SIGKILL', got %s", signal) + } + return &http.Response{ + StatusCode: http.StatusOK, + Body: ioutil.NopCloser(bytes.NewReader([]byte(""))), + }, nil + }), + } + + err := client.ContainerKill(context.Background(), "container_id", "SIGKILL") + if err != nil { + t.Fatal(err) + } +} diff --git a/container_list.go b/container_list.go new file mode 100644 index 0000000000..a8945d84f1 --- /dev/null +++ b/container_list.go @@ -0,0 +1,56 @@ +package client + +import ( + "encoding/json" + "net/url" + "strconv" + + "github.com/docker/docker/api/types" + "github.com/docker/docker/api/types/filters" + "golang.org/x/net/context" +) + +// ContainerList returns the list of containers in the docker host. +func (cli *Client) ContainerList(ctx context.Context, options types.ContainerListOptions) ([]types.Container, error) { + query := url.Values{} + + if options.All { + query.Set("all", "1") + } + + if options.Limit != -1 { + query.Set("limit", strconv.Itoa(options.Limit)) + } + + if options.Since != "" { + query.Set("since", options.Since) + } + + if options.Before != "" { + query.Set("before", options.Before) + } + + if options.Size { + query.Set("size", "1") + } + + if options.Filter.Len() > 0 { + filterJSON, err := filters.ToParamWithVersion(cli.version, options.Filter) + + if err != nil { + return nil, err + } + + query.Set("filters", filterJSON) + } + + resp, err := cli.get(ctx, "/containers/json", query, nil) + if err != nil { + return nil, err + } + + var containers []types.Container + err = json.NewDecoder(resp.body).Decode(&containers) + ensureReaderClosed(resp) + return containers, err +} diff --git a/container_list_test.go b/container_list_test.go new file mode 100644 index 0000000000..3aa2101f27 --- /dev/null +++ b/container_list_test.go @@ -0,0 +1,96 @@ +package client + +import ( + "bytes" + "encoding/json" + "fmt" + "io/ioutil" + "net/http" + "strings" + "testing" + + "github.com/docker/docker/api/types" + "github.com/docker/docker/api/types/filters" + "golang.org/x/net/context" +) + +func TestContainerListError(t *testing.T) { + client := &Client{ + transport: newMockClient(nil, errorMock(http.StatusInternalServerError, "Server error")), + } + _, err := client.ContainerList(context.Background(), types.ContainerListOptions{}) + if err == nil || err.Error() != "Error response from daemon: Server error" { + t.Fatalf("expected a Server Error, got %v", err) + } +} + +func TestContainerList(t *testing.T) { + expectedURL := "/containers/json" + expectedFilters := `{"before":{"container":true},"label":{"label1":true,"label2":true}}` + client := &Client{ + transport: newMockClient(nil, func(req *http.Request) (*http.Response, error) { + if !strings.HasPrefix(req.URL.Path, expectedURL) { + return nil, fmt.Errorf("Expected URL '%s', got '%s'", expectedURL, req.URL) + } + query := req.URL.Query() + all := query.Get("all") + if all != "1" { + return nil, fmt.Errorf("all not set in URL query properly. Expected '1', got %s", all) + } + limit := query.Get("limit") + if limit != "0" { + return nil, fmt.Errorf("limit should have not be present in query. Expected '0', got %s", limit) + } + since := query.Get("since") + if since != "container" { + return nil, fmt.Errorf("since not set in URL query properly. Expected 'container', got %s", since) + } + before := query.Get("before") + if before != "" { + return nil, fmt.Errorf("before should have not be present in query, go %s", before) + } + size := query.Get("size") + if size != "1" { + return nil, fmt.Errorf("size not set in URL query properly. Expected '1', got %s", size) + } + filters := query.Get("filters") + if filters != expectedFilters { + return nil, fmt.Errorf("expected filters incoherent '%v' with actual filters %v", expectedFilters, filters) + } + + b, err := json.Marshal([]types.Container{ + { + ID: "container_id1", + }, + { + ID: "container_id2", + }, + }) + if err != nil { + return nil, err + } + + return &http.Response{ + StatusCode: http.StatusOK, + Body: ioutil.NopCloser(bytes.NewReader(b)), + }, nil + }), + } + + filters := filters.NewArgs() + filters.Add("label", "label1") + filters.Add("label", "label2") + filters.Add("before", "container") + containers, err := client.ContainerList(context.Background(), types.ContainerListOptions{ + Size: true, + All: true, + Since: "container", + Filter: filters, + }) + if err != nil { + t.Fatal(err) + } + if len(containers) != 2 { + t.Fatalf("expected 2 containers, got %v", containers) + } +} diff --git a/container_logs.go b/container_logs.go new file mode 100644 index 0000000000..69056b6321 --- /dev/null +++ b/container_logs.go @@ -0,0 +1,52 @@ +package client + +import ( + "io" + "net/url" + "time" + + "golang.org/x/net/context" + + "github.com/docker/docker/api/types" + timetypes "github.com/docker/docker/api/types/time" +) + +// ContainerLogs returns the logs generated by a container in an io.ReadCloser. +// It's up to the caller to close the stream. +func (cli *Client) ContainerLogs(ctx context.Context, container string, options types.ContainerLogsOptions) (io.ReadCloser, error) { + query := url.Values{} + if options.ShowStdout { + query.Set("stdout", "1") + } + + if options.ShowStderr { + query.Set("stderr", "1") + } + + if options.Since != "" { + ts, err := timetypes.GetTimestamp(options.Since, time.Now()) + if err != nil { + return nil, err + } + query.Set("since", ts) + } + + if options.Timestamps { + query.Set("timestamps", "1") + } + + if options.Details { + query.Set("details", "1") + } + + if options.Follow { + query.Set("follow", "1") + } + query.Set("tail", options.Tail) + + resp, err := cli.get(ctx, "/containers/"+container+"/logs", query, nil) + if err != nil { + return nil, err + } + return resp.body, nil +} diff --git a/container_logs_test.go b/container_logs_test.go new file mode 100644 index 0000000000..d7f0adc9c0 --- /dev/null +++ b/container_logs_test.go @@ -0,0 +1,133 @@ +package client + +import ( + "bytes" + "fmt" + "io" + "io/ioutil" + "log" + "net/http" + "os" + "strings" + "testing" + "time" + + "github.com/docker/docker/api/types" + + "golang.org/x/net/context" +) + +func TestContainerLogsError(t *testing.T) { + client := &Client{ + transport: newMockClient(nil, errorMock(http.StatusInternalServerError, "Server error")), + } + _, err := client.ContainerLogs(context.Background(), "container_id", types.ContainerLogsOptions{}) + if err == nil || err.Error() != "Error response from daemon: Server error" { + t.Fatalf("expected a Server Error, got %v", err) + } + _, err = client.ContainerLogs(context.Background(), "container_id", types.ContainerLogsOptions{ + Since: "2006-01-02TZ", + }) + if err == nil || !strings.Contains(err.Error(), `parsing time "2006-01-02TZ"`) { + t.Fatalf("expected a 'parsing time' error, got %v", err) + } +} + +func TestContainerLogs(t *testing.T) { + expectedURL := "/containers/container_id/logs" + cases := []struct { + options types.ContainerLogsOptions + expectedQueryParams map[string]string + }{ + { + expectedQueryParams: map[string]string{ + "tail": "", + }, + }, + { + options: types.ContainerLogsOptions{ + Tail: "any", + }, + expectedQueryParams: map[string]string{ + "tail": "any", + }, + }, + { + options: types.ContainerLogsOptions{ + ShowStdout: true, + ShowStderr: true, + Timestamps: true, + Details: true, + Follow: true, + }, + expectedQueryParams: map[string]string{ + "tail": "", + "stdout": "1", + "stderr": "1", + "timestamps": "1", + "details": "1", + "follow": "1", + }, + }, + { + options: types.ContainerLogsOptions{ + // An complete invalid date, timestamp or go duration will be + // passed as is + Since: "invalid but valid", + }, + expectedQueryParams: map[string]string{ + "tail": "", + "since": "invalid but valid", + }, + }, + } + for _, logCase := range cases { + client := &Client{ + transport: newMockClient(nil, func(r *http.Request) (*http.Response, error) { + if !strings.HasPrefix(r.URL.Path, expectedURL) { + return nil, fmt.Errorf("Expected URL '%s', got '%s'", expectedURL, r.URL) + } + // Check query parameters + query := r.URL.Query() + for key, expected := range logCase.expectedQueryParams { + actual := query.Get(key) + if actual != expected { + return nil, fmt.Errorf("%s not set in URL query properly. Expected '%s', got %s", key, expected, actual) + } + } + return &http.Response{ + StatusCode: http.StatusOK, + Body: ioutil.NopCloser(bytes.NewReader([]byte("response"))), + }, nil + }), + } + body, err := client.ContainerLogs(context.Background(), "container_id", logCase.options) + if err != nil { + t.Fatal(err) + } + defer body.Close() + content, err := ioutil.ReadAll(body) + if err != nil { + t.Fatal(err) + } + if string(content) != "response" { + t.Fatalf("expected response to contain 'response', got %s", string(content)) + } + } +} + +func ExampleClient_ContainerLogs_withTimeout() { + ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second) + defer cancel() + + client, _ := NewEnvClient() + reader, err := client.ContainerLogs(ctx, "container_id", types.ContainerLogsOptions{}) + if err != nil { + log.Fatal(err) + } + + _, err = io.Copy(os.Stdout, reader) + if err != nil && err != io.EOF { + log.Fatal(err) + } +} diff --git a/container_pause.go b/container_pause.go new file mode 100644 index 0000000000..412067a782 --- /dev/null +++ b/container_pause.go @@ -0,0 +1,10 @@ +package client + +import "golang.org/x/net/context" + +// ContainerPause pauses the main process of a given container without terminating it. +func (cli *Client) ContainerPause(ctx context.Context, containerID string) error { + resp, err := cli.post(ctx, "/containers/"+containerID+"/pause", nil, nil, nil) + ensureReaderClosed(resp) + return err +} diff --git a/container_pause_test.go b/container_pause_test.go new file mode 100644 index 0000000000..ebd12a6ac7 --- /dev/null +++ b/container_pause_test.go @@ -0,0 +1,41 @@ +package client + +import ( + "bytes" + "fmt" + "io/ioutil" + "net/http" + "strings" + "testing" + + "golang.org/x/net/context" +) + +func TestContainerPauseError(t *testing.T) { + client := &Client{ + transport: newMockClient(nil, errorMock(http.StatusInternalServerError, "Server error")), + } + err := client.ContainerPause(context.Background(), "nothing") + if err == nil || err.Error() != "Error response from daemon: Server error" { + t.Fatalf("expected a Server Error, got %v", err) + } +} + +func TestContainerPause(t *testing.T) { + expectedURL := "/containers/container_id/pause" + client := &Client{ + transport: newMockClient(nil, func(req *http.Request) (*http.Response, error) { + if !strings.HasPrefix(req.URL.Path, expectedURL) { + return nil, fmt.Errorf("Expected URL '%s', got '%s'", expectedURL, req.URL) + } + return &http.Response{ + StatusCode: http.StatusOK, + Body: ioutil.NopCloser(bytes.NewReader([]byte(""))), + }, nil + }), + } + err := client.ContainerPause(context.Background(), "container_id") + if err != nil { + t.Fatal(err) + } +} diff --git a/container_remove.go b/container_remove.go new file mode 100644 index 0000000000..3a79590ced --- /dev/null +++ b/container_remove.go @@ -0,0 +1,27 @@ +package client + +import ( + "net/url" + + "github.com/docker/docker/api/types" + "golang.org/x/net/context" +) + +// ContainerRemove kills and removes a container from the docker host. +func (cli *Client) ContainerRemove(ctx context.Context, containerID string, options types.ContainerRemoveOptions) error { + query := url.Values{} + if options.RemoveVolumes { + query.Set("v", "1") + } + if options.RemoveLinks { + query.Set("link", "1") + } + + if options.Force { + query.Set("force", "1") + } + + resp, err := cli.delete(ctx, "/containers/"+containerID, query, nil) + ensureReaderClosed(resp) + return err +} diff --git a/container_remove_test.go b/container_remove_test.go new file mode 100644 index 0000000000..6e135d6ef2 --- /dev/null +++ b/container_remove_test.go @@ -0,0 +1,59 @@ +package client + +import ( + "bytes" + "fmt" + "io/ioutil" + "net/http" + "strings" + "testing" + + "github.com/docker/docker/api/types" + "golang.org/x/net/context" +) + +func TestContainerRemoveError(t *testing.T) { + client := &Client{ + transport: newMockClient(nil, errorMock(http.StatusInternalServerError, "Server error")), + } + err := client.ContainerRemove(context.Background(), "container_id", types.ContainerRemoveOptions{}) + if err == nil || err.Error() != "Error response from daemon: Server error" { + t.Fatalf("expected a Server Error, got %v", err) + } +} + +func TestContainerRemove(t *testing.T) { + expectedURL := "/containers/container_id" + client := &Client{ + transport: newMockClient(nil, func(req *http.Request) (*http.Response, error) { + if !strings.HasPrefix(req.URL.Path, expectedURL) { + return nil, fmt.Errorf("Expected URL '%s', got '%s'", expectedURL, req.URL) + } + query := req.URL.Query() + volume := query.Get("v") + if volume != "1" { + return nil, fmt.Errorf("v (volume) not set in URL query properly. Expected '1', got %s", volume) + } + force := query.Get("force") + if force != "1" { + return nil, fmt.Errorf("force not set in URL query properly. Expected '1', got %s", force) + } + link := query.Get("link") + if link != "" { + return nil, fmt.Errorf("link should have not be present in query, go %s", link) + } + return &http.Response{ + StatusCode: http.StatusOK, + Body: ioutil.NopCloser(bytes.NewReader([]byte(""))), + }, nil + }), + } + + err := client.ContainerRemove(context.Background(), "container_id", types.ContainerRemoveOptions{ + RemoveVolumes: true, + Force: true, + }) + if err != nil { + t.Fatal(err) + } +} diff --git a/container_rename.go b/container_rename.go new file mode 100644 index 0000000000..0e718da7c6 --- /dev/null +++ b/container_rename.go @@ -0,0 +1,16 @@ +package client + +import ( + "net/url" + + "golang.org/x/net/context" +) + +// ContainerRename changes the name of a given container. +func (cli *Client) ContainerRename(ctx context.Context, containerID, newContainerName string) error { + query := url.Values{} + query.Set("name", newContainerName) + resp, err := cli.post(ctx, "/containers/"+containerID+"/rename", query, nil, nil) + ensureReaderClosed(resp) + return err +} diff --git a/container_rename_test.go b/container_rename_test.go new file mode 100644 index 0000000000..9344bab7db --- /dev/null +++ b/container_rename_test.go @@ -0,0 +1,46 @@ +package client + +import ( + "bytes" + "fmt" + "io/ioutil" + "net/http" + "strings" + "testing" + + "golang.org/x/net/context" +) + +func TestContainerRenameError(t *testing.T) { + client := &Client{ + transport: newMockClient(nil, errorMock(http.StatusInternalServerError, "Server error")), + } + err := client.ContainerRename(context.Background(), "nothing", "newNothing") + if err == nil || err.Error() != "Error response from daemon: Server error" { + t.Fatalf("expected a Server Error, got %v", err) + } +} + +func TestContainerRename(t *testing.T) { + expectedURL := "/containers/container_id/rename" + client := &Client{ + transport: newMockClient(nil, func(req *http.Request) (*http.Response, error) { + if !strings.HasPrefix(req.URL.Path, expectedURL) { + return nil, fmt.Errorf("Expected URL '%s', got '%s'", expectedURL, req.URL) + } + name := req.URL.Query().Get("name") + if name != "newName" { + return nil, fmt.Errorf("name not set in URL query properly. Expected 'newName', got %s", name) + } + return &http.Response{ + StatusCode: http.StatusOK, + Body: ioutil.NopCloser(bytes.NewReader([]byte(""))), + }, nil + }), + } + + err := client.ContainerRename(context.Background(), "container_id", "newName") + if err != nil { + t.Fatal(err) + } +} diff --git a/container_resize.go b/container_resize.go new file mode 100644 index 0000000000..a7f38b024b --- /dev/null +++ b/container_resize.go @@ -0,0 +1,29 @@ +package client + +import ( + "net/url" + "strconv" + + "github.com/docker/docker/api/types" + "golang.org/x/net/context" +) + +// ContainerResize changes the size of the tty for a container. +func (cli *Client) ContainerResize(ctx context.Context, containerID string, options types.ResizeOptions) error { + return cli.resize(ctx, "/containers/"+containerID, options.Height, options.Width) +} + +// ContainerExecResize changes the size of the tty for an exec process running inside a container. +func (cli *Client) ContainerExecResize(ctx context.Context, execID string, options types.ResizeOptions) error { + return cli.resize(ctx, "/exec/"+execID, options.Height, options.Width) +} + +func (cli *Client) resize(ctx context.Context, basePath string, height, width int) error { + query := url.Values{} + query.Set("h", strconv.Itoa(height)) + query.Set("w", strconv.Itoa(width)) + + resp, err := cli.post(ctx, basePath+"/resize", query, nil, nil) + ensureReaderClosed(resp) + return err +} diff --git a/container_resize_test.go b/container_resize_test.go new file mode 100644 index 0000000000..e0056c88d1 --- /dev/null +++ b/container_resize_test.go @@ -0,0 +1,82 @@ +package client + +import ( + "bytes" + "fmt" + "io/ioutil" + "net/http" + "strings" + "testing" + + "github.com/docker/docker/api/types" + "golang.org/x/net/context" +) + +func TestContainerResizeError(t *testing.T) { + client := &Client{ + transport: newMockClient(nil, errorMock(http.StatusInternalServerError, "Server error")), + } + err := client.ContainerResize(context.Background(), "container_id", types.ResizeOptions{}) + if err == nil || err.Error() != "Error response from daemon: Server error" { + t.Fatalf("expected a Server Error, got %v", err) + } +} + +func TestContainerExecResizeError(t *testing.T) { + client := &Client{ + transport: newMockClient(nil, errorMock(http.StatusInternalServerError, "Server error")), + } + err := client.ContainerExecResize(context.Background(), "exec_id", types.ResizeOptions{}) + if err == nil || err.Error() != "Error response from daemon: Server error" { + t.Fatalf("expected a Server Error, got %v", err) + } +} + +func TestContainerResize(t *testing.T) { + client := &Client{ + transport: newMockClient(nil, resizeTransport("/containers/container_id/resize")), + } + + err := client.ContainerResize(context.Background(), "container_id", types.ResizeOptions{ + Height: 500, + Width: 600, + }) + if err != nil { + t.Fatal(err) + } +} + +func TestContainerExecResize(t *testing.T) { + client := &Client{ + transport: newMockClient(nil, resizeTransport("/exec/exec_id/resize")), + } + + err := client.ContainerExecResize(context.Background(), "exec_id", types.ResizeOptions{ + Height: 500, + Width: 600, + }) + if err != nil { + t.Fatal(err) + } +} + +func resizeTransport(expectedURL string) func(req *http.Request) (*http.Response, error) { + return func(req *http.Request) (*http.Response, error) { + if !strings.HasPrefix(req.URL.Path, expectedURL) { + return nil, fmt.Errorf("Expected URL '%s', got '%s'", expectedURL, req.URL) + } + query := req.URL.Query() + h := query.Get("h") + if h != "500" { + return nil, fmt.Errorf("h not set in URL query properly. Expected '500', got %s", h) + } + w := query.Get("w") + if w != "600" { + return nil, fmt.Errorf("w not set in URL query properly. Expected '600', got %s", w) + } + return &http.Response{ + StatusCode: http.StatusOK, + Body: ioutil.NopCloser(bytes.NewReader([]byte(""))), + }, nil + } +} diff --git a/container_restart.go b/container_restart.go new file mode 100644 index 0000000000..74d7455f02 --- /dev/null +++ b/container_restart.go @@ -0,0 +1,22 @@ +package client + +import ( + "net/url" + "time" + + timetypes "github.com/docker/docker/api/types/time" + "golang.org/x/net/context" +) + +// ContainerRestart stops and starts a container again. +// It makes the daemon to wait for the container to be up again for +// a specific amount of time, given the timeout. +func (cli *Client) ContainerRestart(ctx context.Context, containerID string, timeout *time.Duration) error { + query := url.Values{} + if timeout != nil { + query.Set("t", timetypes.DurationToSecondsString(*timeout)) + } + resp, err := cli.post(ctx, "/containers/"+containerID+"/restart", query, nil, nil) + ensureReaderClosed(resp) + return err +} diff --git a/container_restart_test.go b/container_restart_test.go new file mode 100644 index 0000000000..080656d368 --- /dev/null +++ b/container_restart_test.go @@ -0,0 +1,48 @@ +package client + +import ( + "bytes" + "fmt" + "io/ioutil" + "net/http" + "strings" + "testing" + "time" + + "golang.org/x/net/context" +) + +func TestContainerRestartError(t *testing.T) { + client := &Client{ + transport: newMockClient(nil, errorMock(http.StatusInternalServerError, "Server error")), + } + timeout := 0 * time.Second + err := client.ContainerRestart(context.Background(), "nothing", &timeout) + if err == nil || err.Error() != "Error response from daemon: Server error" { + t.Fatalf("expected a Server Error, got %v", err) + } +} + +func TestContainerRestart(t *testing.T) { + expectedURL := "/containers/container_id/restart" + client := &Client{ + transport: newMockClient(nil, func(req *http.Request) (*http.Response, error) { + if !strings.HasPrefix(req.URL.Path, expectedURL) { + return nil, fmt.Errorf("Expected URL '%s', got '%s'", expectedURL, req.URL) + } + t := req.URL.Query().Get("t") + if t != "100" { + return nil, fmt.Errorf("t (timeout) not set in URL query properly. Expected '100', got %s", t) + } + return &http.Response{ + StatusCode: http.StatusOK, + Body: ioutil.NopCloser(bytes.NewReader([]byte(""))), + }, nil + }), + } + timeout := 100 * time.Second + err := client.ContainerRestart(context.Background(), "container_id", &timeout) + if err != nil { + t.Fatal(err) + } +} diff --git a/container_start.go b/container_start.go new file mode 100644 index 0000000000..44bb0080c0 --- /dev/null +++ b/container_start.go @@ -0,0 +1,21 @@ +package client + +import ( + "net/url" + + "golang.org/x/net/context" + + "github.com/docker/docker/api/types" +) + +// ContainerStart sends a request to the docker daemon to start a container. +func (cli *Client) ContainerStart(ctx context.Context, containerID string, options types.ContainerStartOptions) error { + query := url.Values{} + if len(options.CheckpointID) != 0 { + query.Set("checkpoint", options.CheckpointID) + } + + resp, err := cli.post(ctx, "/containers/"+containerID+"/start", query, nil, nil) + ensureReaderClosed(resp) + return err +} diff --git a/container_start_test.go b/container_start_test.go new file mode 100644 index 0000000000..79f85b332a --- /dev/null +++ b/container_start_test.go @@ -0,0 +1,58 @@ +package client + +import ( + "bytes" + "encoding/json" + "fmt" + "io/ioutil" + "net/http" + "strings" + "testing" + + "golang.org/x/net/context" + + "github.com/docker/docker/api/types" +) + +func TestContainerStartError(t *testing.T) { + client := &Client{ + transport: newMockClient(nil, errorMock(http.StatusInternalServerError, "Server error")), + } + err := client.ContainerStart(context.Background(), "nothing", types.ContainerStartOptions{}) + if err == nil || err.Error() != "Error response from daemon: Server error" { + t.Fatalf("expected a Server Error, got %v", err) + } +} + +func TestContainerStart(t *testing.T) { + expectedURL := "/containers/container_id/start" + client := &Client{ + transport: newMockClient(nil, func(req *http.Request) (*http.Response, error) { + if !strings.HasPrefix(req.URL.Path, expectedURL) { + return nil, fmt.Errorf("Expected URL '%s', got '%s'", expectedURL, req.URL) + } + // we're not expecting any payload, but if one is supplied, check it is valid. + if req.Header.Get("Content-Type") == "application/json" { + var startConfig interface{} + if err := json.NewDecoder(req.Body).Decode(&startConfig); err != nil { + return nil, fmt.Errorf("Unable to parse json: %s", err) + } + } + + checkpoint := req.URL.Query().Get("checkpoint") + if checkpoint != "checkpoint_id" { + return nil, fmt.Errorf("checkpoint not set in URL query properly. Expected 'checkpoint_id', got %s", checkpoint) + } + + return &http.Response{ + StatusCode: http.StatusOK, + Body: ioutil.NopCloser(bytes.NewReader([]byte(""))), + }, nil + }), + } + + err := client.ContainerStart(context.Background(), "container_id", types.ContainerStartOptions{CheckpointID: "checkpoint_id"}) + if err != nil { + t.Fatal(err) + } +} diff --git a/container_stats.go b/container_stats.go new file mode 100644 index 0000000000..2cc67c3af1 --- /dev/null +++ b/container_stats.go @@ -0,0 +1,24 @@ +package client + +import ( + "io" + "net/url" + + "golang.org/x/net/context" +) + +// ContainerStats returns near realtime stats for a given container. +// It's up to the caller to close the io.ReadCloser returned. +func (cli *Client) ContainerStats(ctx context.Context, containerID string, stream bool) (io.ReadCloser, error) { + query := url.Values{} + query.Set("stream", "0") + if stream { + query.Set("stream", "1") + } + + resp, err := cli.get(ctx, "/containers/"+containerID+"/stats", query, nil) + if err != nil { + return nil, err + } + return resp.body, err +} diff --git a/container_stats_test.go b/container_stats_test.go new file mode 100644 index 0000000000..22ecd6170f --- /dev/null +++ b/container_stats_test.go @@ -0,0 +1,70 @@ +package client + +import ( + "bytes" + "fmt" + "io/ioutil" + "net/http" + "strings" + "testing" + + "golang.org/x/net/context" +) + +func TestContainerStatsError(t *testing.T) { + client := &Client{ + transport: newMockClient(nil, errorMock(http.StatusInternalServerError, "Server error")), + } + _, err := client.ContainerStats(context.Background(), "nothing", false) + if err == nil || err.Error() != "Error response from daemon: Server error" { + t.Fatalf("expected a Server Error, got %v", err) + } +} + +func TestContainerStats(t *testing.T) { + expectedURL := "/containers/container_id/stats" + cases := []struct { + stream bool + expectedStream string + }{ + { + expectedStream: "0", + }, + { + stream: true, + expectedStream: "1", + }, + } + for _, c := range cases { + client := &Client{ + transport: newMockClient(nil, func(r *http.Request) (*http.Response, error) { + if !strings.HasPrefix(r.URL.Path, expectedURL) { + return nil, fmt.Errorf("Expected URL '%s', got '%s'", expectedURL, r.URL) + } + + query := r.URL.Query() + stream := query.Get("stream") + if stream != c.expectedStream { + return nil, fmt.Errorf("stream not set in URL query properly. Expected '%s', got %s", c.expectedStream, stream) + } + + return &http.Response{ + StatusCode: http.StatusOK, + Body: ioutil.NopCloser(bytes.NewReader([]byte("response"))), + }, nil + }), + } + body, err := client.ContainerStats(context.Background(), "container_id", c.stream) + if err != nil { + t.Fatal(err) + } + defer body.Close() + content, err := ioutil.ReadAll(body) + if err != nil { + t.Fatal(err) + } + if string(content) != "response" { + t.Fatalf("expected response to contain 'response', got %s", string(content)) + } + } +} diff --git a/container_stop.go b/container_stop.go new file mode 100644 index 0000000000..b5418ae8c8 --- /dev/null +++ b/container_stop.go @@ -0,0 +1,21 @@ +package client + +import ( + "net/url" + "time" + + timetypes "github.com/docker/docker/api/types/time" + "golang.org/x/net/context" +) + +// ContainerStop stops a container without terminating the process. +// The process is blocked until the container stops or the timeout expires. +func (cli *Client) ContainerStop(ctx context.Context, containerID string, timeout *time.Duration) error { + query := url.Values{} + if timeout != nil { + query.Set("t", timetypes.DurationToSecondsString(*timeout)) + } + resp, err := cli.post(ctx, "/containers/"+containerID+"/stop", query, nil, nil) + ensureReaderClosed(resp) + return err +} diff --git a/container_stop_test.go b/container_stop_test.go new file mode 100644 index 0000000000..4b052f9908 --- /dev/null +++ b/container_stop_test.go @@ -0,0 +1,48 @@ +package client + +import ( + "bytes" + "fmt" + "io/ioutil" + "net/http" + "strings" + "testing" + "time" + + "golang.org/x/net/context" +) + +func TestContainerStopError(t *testing.T) { + client := &Client{ + transport: newMockClient(nil, errorMock(http.StatusInternalServerError, "Server error")), + } + timeout := 0 * time.Second + err := client.ContainerStop(context.Background(), "nothing", &timeout) + if err == nil || err.Error() != "Error response from daemon: Server error" { + t.Fatalf("expected a Server Error, got %v", err) + } +} + +func TestContainerStop(t *testing.T) { + expectedURL := "/containers/container_id/stop" + client := &Client{ + transport: newMockClient(nil, func(req *http.Request) (*http.Response, error) { + if !strings.HasPrefix(req.URL.Path, expectedURL) { + return nil, fmt.Errorf("Expected URL '%s', got '%s'", expectedURL, req.URL) + } + t := req.URL.Query().Get("t") + if t != "100" { + return nil, fmt.Errorf("t (timeout) not set in URL query properly. Expected '100', got %s", t) + } + return &http.Response{ + StatusCode: http.StatusOK, + Body: ioutil.NopCloser(bytes.NewReader([]byte(""))), + }, nil + }), + } + timeout := 100 * time.Second + err := client.ContainerStop(context.Background(), "container_id", &timeout) + if err != nil { + t.Fatal(err) + } +} diff --git a/container_top.go b/container_top.go new file mode 100644 index 0000000000..4e7270ea22 --- /dev/null +++ b/container_top.go @@ -0,0 +1,28 @@ +package client + +import ( + "encoding/json" + "net/url" + "strings" + + "github.com/docker/docker/api/types" + "golang.org/x/net/context" +) + +// ContainerTop shows process information from within a container. +func (cli *Client) ContainerTop(ctx context.Context, containerID string, arguments []string) (types.ContainerProcessList, error) { + var response types.ContainerProcessList + query := url.Values{} + if len(arguments) > 0 { + query.Set("ps_args", strings.Join(arguments, " ")) + } + + resp, err := cli.get(ctx, "/containers/"+containerID+"/top", query, nil) + if err != nil { + return response, err + } + + err = json.NewDecoder(resp.body).Decode(&response) + ensureReaderClosed(resp) + return response, err +} diff --git a/container_top_test.go b/container_top_test.go new file mode 100644 index 0000000000..4df7d82d84 --- /dev/null +++ b/container_top_test.go @@ -0,0 +1,74 @@ +package client + +import ( + "bytes" + "encoding/json" + "fmt" + "io/ioutil" + "net/http" + "reflect" + "strings" + "testing" + + "github.com/docker/docker/api/types" + "golang.org/x/net/context" +) + +func TestContainerTopError(t *testing.T) { + client := &Client{ + transport: newMockClient(nil, errorMock(http.StatusInternalServerError, "Server error")), + } + _, err := client.ContainerTop(context.Background(), "nothing", []string{}) + if err == nil || err.Error() != "Error response from daemon: Server error" { + t.Fatalf("expected a Server Error, got %v", err) + } +} + +func TestContainerTop(t *testing.T) { + expectedURL := "/containers/container_id/top" + expectedProcesses := [][]string{ + {"p1", "p2"}, + {"p3"}, + } + expectedTitles := []string{"title1", "title2"} + + client := &Client{ + transport: newMockClient(nil, func(req *http.Request) (*http.Response, error) { + if !strings.HasPrefix(req.URL.Path, expectedURL) { + return nil, fmt.Errorf("Expected URL '%s', got '%s'", expectedURL, req.URL) + } + query := req.URL.Query() + args := query.Get("ps_args") + if args != "arg1 arg2" { + return nil, fmt.Errorf("args not set in URL query properly. Expected 'arg1 arg2', got %v", args) + } + + b, err := json.Marshal(types.ContainerProcessList{ + Processes: [][]string{ + {"p1", "p2"}, + {"p3"}, + }, + Titles: []string{"title1", "title2"}, + }) + if err != nil { + return nil, err + } + + return &http.Response{ + StatusCode: http.StatusOK, + Body: ioutil.NopCloser(bytes.NewReader(b)), + }, nil + }), + } + + processList, err := client.ContainerTop(context.Background(), "container_id", []string{"arg1", "arg2"}) + if err != nil { + t.Fatal(err) + } + if !reflect.DeepEqual(expectedProcesses, processList.Processes) { + t.Fatalf("Processes: expected %v, got %v", expectedProcesses, processList.Processes) + } + if !reflect.DeepEqual(expectedTitles, processList.Titles) { + t.Fatalf("Titles: expected %v, got %v", expectedTitles, processList.Titles) + } +} diff --git a/container_unpause.go b/container_unpause.go new file mode 100644 index 0000000000..5c76211256 --- /dev/null +++ b/container_unpause.go @@ -0,0 +1,10 @@ +package client + +import "golang.org/x/net/context" + +// ContainerUnpause resumes the process execution within a container +func (cli *Client) ContainerUnpause(ctx context.Context, containerID string) error { + resp, err := cli.post(ctx, "/containers/"+containerID+"/unpause", nil, nil, nil) + ensureReaderClosed(resp) + return err +} diff --git a/container_unpause_test.go b/container_unpause_test.go new file mode 100644 index 0000000000..a5b21bf56c --- /dev/null +++ b/container_unpause_test.go @@ -0,0 +1,41 @@ +package client + +import ( + "bytes" + "fmt" + "io/ioutil" + "net/http" + "strings" + "testing" + + "golang.org/x/net/context" +) + +func TestContainerUnpauseError(t *testing.T) { + client := &Client{ + transport: newMockClient(nil, errorMock(http.StatusInternalServerError, "Server error")), + } + err := client.ContainerUnpause(context.Background(), "nothing") + if err == nil || err.Error() != "Error response from daemon: Server error" { + t.Fatalf("expected a Server Error, got %v", err) + } +} + +func TestContainerUnpause(t *testing.T) { + expectedURL := "/containers/container_id/unpause" + client := &Client{ + transport: newMockClient(nil, func(req *http.Request) (*http.Response, error) { + if !strings.HasPrefix(req.URL.Path, expectedURL) { + return nil, fmt.Errorf("Expected URL '%s', got '%s'", expectedURL, req.URL) + } + return &http.Response{ + StatusCode: http.StatusOK, + Body: ioutil.NopCloser(bytes.NewReader([]byte(""))), + }, nil + }), + } + err := client.ContainerUnpause(context.Background(), "container_id") + if err != nil { + t.Fatal(err) + } +} diff --git a/container_update.go b/container_update.go new file mode 100644 index 0000000000..48b75bee30 --- /dev/null +++ b/container_update.go @@ -0,0 +1,23 @@ +package client + +import ( + "encoding/json" + + "github.com/docker/docker/api/types" + "github.com/docker/docker/api/types/container" + "golang.org/x/net/context" +) + +// ContainerUpdate updates resources of a container +func (cli *Client) ContainerUpdate(ctx context.Context, containerID string, updateConfig container.UpdateConfig) (types.ContainerUpdateResponse, error) { + var response types.ContainerUpdateResponse + serverResp, err := cli.post(ctx, "/containers/"+containerID+"/update", nil, updateConfig, nil) + if err != nil { + return response, err + } + + err = json.NewDecoder(serverResp.body).Decode(&response) + + ensureReaderClosed(serverResp) + return response, err +} diff --git a/container_update_test.go b/container_update_test.go new file mode 100644 index 0000000000..46e34d6936 --- /dev/null +++ b/container_update_test.go @@ -0,0 +1,59 @@ +package client + +import ( + "bytes" + "encoding/json" + "fmt" + "io/ioutil" + "net/http" + "strings" + "testing" + + "github.com/docker/docker/api/types" + "github.com/docker/docker/api/types/container" + "golang.org/x/net/context" +) + +func TestContainerUpdateError(t *testing.T) { + client := &Client{ + transport: newMockClient(nil, errorMock(http.StatusInternalServerError, "Server error")), + } + _, err := client.ContainerUpdate(context.Background(), "nothing", container.UpdateConfig{}) + if err == nil || err.Error() != "Error response from daemon: Server error" { + t.Fatalf("expected a Server Error, got %v", err) + } +} + +func TestContainerUpdate(t *testing.T) { + expectedURL := "/containers/container_id/update" + + client := &Client{ + transport: newMockClient(nil, func(req *http.Request) (*http.Response, error) { + if !strings.HasPrefix(req.URL.Path, expectedURL) { + return nil, fmt.Errorf("Expected URL '%s', got '%s'", expectedURL, req.URL) + } + + b, err := json.Marshal(types.ContainerUpdateResponse{}) + if err != nil { + return nil, err + } + + return &http.Response{ + StatusCode: http.StatusOK, + Body: ioutil.NopCloser(bytes.NewReader(b)), + }, nil + }), + } + + _, err := client.ContainerUpdate(context.Background(), "container_id", container.UpdateConfig{ + Resources: container.Resources{ + CPUPeriod: 1, + }, + RestartPolicy: container.RestartPolicy{ + Name: "always", + }, + }) + if err != nil { + t.Fatal(err) + } +} diff --git a/container_wait.go b/container_wait.go new file mode 100644 index 0000000000..8a858f0ea3 --- /dev/null +++ b/container_wait.go @@ -0,0 +1,26 @@ +package client + +import ( + "encoding/json" + + "golang.org/x/net/context" + + "github.com/docker/docker/api/types" +) + +// ContainerWait pauses execution until a container exits. +// It returns the API status code as response of its readiness. +func (cli *Client) ContainerWait(ctx context.Context, containerID string) (int, error) { + resp, err := cli.post(ctx, "/containers/"+containerID+"/wait", nil, nil, nil) + if err != nil { + return -1, err + } + defer ensureReaderClosed(resp) + + var res types.ContainerWaitResponse + if err := json.NewDecoder(resp.body).Decode(&res); err != nil { + return -1, err + } + + return res.StatusCode, nil +} diff --git a/container_wait_test.go b/container_wait_test.go new file mode 100644 index 0000000000..bf2ba6b925 --- /dev/null +++ b/container_wait_test.go @@ -0,0 +1,70 @@ +package client + +import ( + "bytes" + "encoding/json" + "fmt" + "io/ioutil" + "log" + "net/http" + "strings" + "testing" + "time" + + "github.com/docker/docker/api/types" + + "golang.org/x/net/context" +) + +func TestContainerWaitError(t *testing.T) { + client := &Client{ + transport: newMockClient(nil, errorMock(http.StatusInternalServerError, "Server error")), + } + code, err := client.ContainerWait(context.Background(), "nothing") + if err == nil || err.Error() != "Error response from daemon: Server error" { + t.Fatalf("expected a Server Error, got %v", err) + } + if code != -1 { + t.Fatalf("expected a status code equal to '-1', got %d", code) + } +} + +func TestContainerWait(t *testing.T) { + expectedURL := "/containers/container_id/wait" + client := &Client{ + transport: newMockClient(nil, func(req *http.Request) (*http.Response, error) { + if !strings.HasPrefix(req.URL.Path, expectedURL) { + return nil, fmt.Errorf("Expected URL '%s', got '%s'", expectedURL, req.URL) + } + b, err := json.Marshal(types.ContainerWaitResponse{ + StatusCode: 15, + }) + if err != nil { + return nil, err + } + return &http.Response{ + StatusCode: http.StatusOK, + Body: ioutil.NopCloser(bytes.NewReader(b)), + }, nil + }), + } + + code, err := client.ContainerWait(context.Background(), "container_id") + if err != nil { + t.Fatal(err) + } + if code != 15 { + t.Fatalf("expected a status code equal to '15', got %d", code) + } +} + +func ExampleClient_ContainerWait_withTimeout() { + ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second) + defer cancel() + + client, _ := NewEnvClient() + _, err := client.ContainerWait(ctx, "container_id") + if err != nil { + log.Fatal(err) + } +} diff --git a/errors.go b/errors.go new file mode 100644 index 0000000000..71e25a7ae1 --- /dev/null +++ b/errors.go @@ -0,0 +1,208 @@ +package client + +import ( + "errors" + "fmt" +) + +// ErrConnectionFailed is an error raised when the connection between the client and the server failed. +var ErrConnectionFailed = errors.New("Cannot connect to the Docker daemon. Is the docker daemon running on this host?") + +// ErrorConnectionFailed returns an error with host in the error message when connection to docker daemon failed. +func ErrorConnectionFailed(host string) error { + return fmt.Errorf("Cannot connect to the Docker daemon at %s. Is the docker daemon running?", host) +} + +type notFound interface { + error + NotFound() bool // Is the error a NotFound error +} + +// IsErrNotFound returns true if the error is caused with an +// object (image, container, network, volume, …) is not found in the docker host. +func IsErrNotFound(err error) bool { + te, ok := err.(notFound) + return ok && te.NotFound() +} + +// imageNotFoundError implements an error returned when an image is not in the docker host. +type imageNotFoundError struct { + imageID string +} + +// NoFound indicates that this error type is of NotFound +func (e imageNotFoundError) NotFound() bool { + return true +} + +// Error returns a string representation of an imageNotFoundError +func (e imageNotFoundError) Error() string { + return fmt.Sprintf("Error: No such image: %s", e.imageID) +} + +// IsErrImageNotFound returns true if the error is caused +// when an image is not found in the docker host. +func IsErrImageNotFound(err error) bool { + return IsErrNotFound(err) +} + +// containerNotFoundError implements an error returned when a container is not in the docker host. +type containerNotFoundError struct { + containerID string +} + +// NoFound indicates that this error type is of NotFound +func (e containerNotFoundError) NotFound() bool { + return true +} + +// Error returns a string representation of a containerNotFoundError +func (e containerNotFoundError) Error() string { + return fmt.Sprintf("Error: No such container: %s", e.containerID) +} + +// IsErrContainerNotFound returns true if the error is caused +// when a container is not found in the docker host. +func IsErrContainerNotFound(err error) bool { + return IsErrNotFound(err) +} + +// networkNotFoundError implements an error returned when a network is not in the docker host. +type networkNotFoundError struct { + networkID string +} + +// NoFound indicates that this error type is of NotFound +func (e networkNotFoundError) NotFound() bool { + return true +} + +// Error returns a string representation of a networkNotFoundError +func (e networkNotFoundError) Error() string { + return fmt.Sprintf("Error: No such network: %s", e.networkID) +} + +// IsErrNetworkNotFound returns true if the error is caused +// when a network is not found in the docker host. +func IsErrNetworkNotFound(err error) bool { + return IsErrNotFound(err) +} + +// volumeNotFoundError implements an error returned when a volume is not in the docker host. +type volumeNotFoundError struct { + volumeID string +} + +// NoFound indicates that this error type is of NotFound +func (e volumeNotFoundError) NotFound() bool { + return true +} + +// Error returns a string representation of a networkNotFoundError +func (e volumeNotFoundError) Error() string { + return fmt.Sprintf("Error: No such volume: %s", e.volumeID) +} + +// IsErrVolumeNotFound returns true if the error is caused +// when a volume is not found in the docker host. +func IsErrVolumeNotFound(err error) bool { + return IsErrNotFound(err) +} + +// unauthorizedError represents an authorization error in a remote registry. +type unauthorizedError struct { + cause error +} + +// Error returns a string representation of an unauthorizedError +func (u unauthorizedError) Error() string { + return u.cause.Error() +} + +// IsErrUnauthorized returns true if the error is caused +// when a remote registry authentication fails +func IsErrUnauthorized(err error) bool { + _, ok := err.(unauthorizedError) + return ok +} + +// nodeNotFoundError implements an error returned when a node is not found. +type nodeNotFoundError struct { + nodeID string +} + +// Error returns a string representation of a nodeNotFoundError +func (e nodeNotFoundError) Error() string { + return fmt.Sprintf("Error: No such node: %s", e.nodeID) +} + +// NoFound indicates that this error type is of NotFound +func (e nodeNotFoundError) NotFound() bool { + return true +} + +// IsErrNodeNotFound returns true if the error is caused +// when a node is not found. +func IsErrNodeNotFound(err error) bool { + _, ok := err.(nodeNotFoundError) + return ok +} + +// serviceNotFoundError implements an error returned when a service is not found. +type serviceNotFoundError struct { + serviceID string +} + +// Error returns a string representation of a serviceNotFoundError +func (e serviceNotFoundError) Error() string { + return fmt.Sprintf("Error: No such service: %s", e.serviceID) +} + +// NoFound indicates that this error type is of NotFound +func (e serviceNotFoundError) NotFound() bool { + return true +} + +// IsErrServiceNotFound returns true if the error is caused +// when a service is not found. +func IsErrServiceNotFound(err error) bool { + _, ok := err.(serviceNotFoundError) + return ok +} + +// taskNotFoundError implements an error returned when a task is not found. +type taskNotFoundError struct { + taskID string +} + +// Error returns a string representation of a taskNotFoundError +func (e taskNotFoundError) Error() string { + return fmt.Sprintf("Error: No such task: %s", e.taskID) +} + +// NoFound indicates that this error type is of NotFound +func (e taskNotFoundError) NotFound() bool { + return true +} + +// IsErrTaskNotFound returns true if the error is caused +// when a task is not found. +func IsErrTaskNotFound(err error) bool { + _, ok := err.(taskNotFoundError) + return ok +} + +type pluginPermissionDenied struct { + name string +} + +func (e pluginPermissionDenied) Error() string { + return "Permission denied while installing plugin " + e.name +} + +// IsErrPluginPermissionDenied returns true if the error is caused +// when a user denies a plugin's permissions +func IsErrPluginPermissionDenied(err error) bool { + _, ok := err.(pluginPermissionDenied) + return ok +} diff --git a/events.go b/events.go new file mode 100644 index 0000000000..0ba7114f94 --- /dev/null +++ b/events.go @@ -0,0 +1,48 @@ +package client + +import ( + "io" + "net/url" + "time" + + "golang.org/x/net/context" + + "github.com/docker/docker/api/types" + "github.com/docker/docker/api/types/filters" + timetypes "github.com/docker/docker/api/types/time" +) + +// Events returns a stream of events in the daemon in a ReadCloser. +// It's up to the caller to close the stream. +func (cli *Client) Events(ctx context.Context, options types.EventsOptions) (io.ReadCloser, error) { + query := url.Values{} + ref := time.Now() + + if options.Since != "" { + ts, err := timetypes.GetTimestamp(options.Since, ref) + if err != nil { + return nil, err + } + query.Set("since", ts) + } + if options.Until != "" { + ts, err := timetypes.GetTimestamp(options.Until, ref) + if err != nil { + return nil, err + } + query.Set("until", ts) + } + if options.Filters.Len() > 0 { + filterJSON, err := filters.ToParamWithVersion(cli.version, options.Filters) + if err != nil { + return nil, err + } + query.Set("filters", filterJSON) + } + + serverResponse, err := cli.get(ctx, "/events", query, nil) + if err != nil { + return nil, err + } + return serverResponse.body, nil +} diff --git a/events_test.go b/events_test.go new file mode 100644 index 0000000000..f7cb33f611 --- /dev/null +++ b/events_test.go @@ -0,0 +1,126 @@ +package client + +import ( + "bytes" + "fmt" + "io/ioutil" + "net/http" + "strings" + "testing" + + "golang.org/x/net/context" + + "github.com/docker/docker/api/types" + "github.com/docker/docker/api/types/filters" +) + +func TestEventsErrorInOptions(t *testing.T) { + errorCases := []struct { + options types.EventsOptions + expectedError string + }{ + { + options: types.EventsOptions{ + Since: "2006-01-02TZ", + }, + expectedError: `parsing time "2006-01-02TZ"`, + }, + { + options: types.EventsOptions{ + Until: "2006-01-02TZ", + }, + expectedError: `parsing time "2006-01-02TZ"`, + }, + } + for _, e := range errorCases { + client := &Client{ + transport: newMockClient(nil, errorMock(http.StatusInternalServerError, "Server error")), + } + _, err := client.Events(context.Background(), e.options) + if err == nil || !strings.Contains(err.Error(), e.expectedError) { + t.Fatalf("expected a error %q, got %v", e.expectedError, err) + } + } +} + +func TestEventsErrorFromServer(t *testing.T) { + client := &Client{ + transport: newMockClient(nil, errorMock(http.StatusInternalServerError, "Server error")), + } + _, err := client.Events(context.Background(), types.EventsOptions{}) + if err == nil || err.Error() != "Error response from daemon: Server error" { + t.Fatalf("expected a Server Error, got %v", err) + } +} + +func TestEvents(t *testing.T) { + expectedURL := "/events" + + filters := filters.NewArgs() + filters.Add("label", "label1") + filters.Add("label", "label2") + expectedFiltersJSON := `{"label":{"label1":true,"label2":true}}` + + eventsCases := []struct { + options types.EventsOptions + expectedQueryParams map[string]string + }{ + { + options: types.EventsOptions{ + Since: "invalid but valid", + }, + expectedQueryParams: map[string]string{ + "since": "invalid but valid", + }, + }, + { + options: types.EventsOptions{ + Until: "invalid but valid", + }, + expectedQueryParams: map[string]string{ + "until": "invalid but valid", + }, + }, + { + options: types.EventsOptions{ + Filters: filters, + }, + expectedQueryParams: map[string]string{ + "filters": expectedFiltersJSON, + }, + }, + } + + for _, eventsCase := range eventsCases { + client := &Client{ + transport: newMockClient(nil, func(req *http.Request) (*http.Response, error) { + if !strings.HasPrefix(req.URL.Path, expectedURL) { + return nil, fmt.Errorf("Expected URL '%s', got '%s'", expectedURL, req.URL) + } + query := req.URL.Query() + for key, expected := range eventsCase.expectedQueryParams { + actual := query.Get(key) + if actual != expected { + return nil, fmt.Errorf("%s not set in URL query properly. Expected '%s', got %s", key, expected, actual) + } + } + return &http.Response{ + StatusCode: http.StatusOK, + Body: ioutil.NopCloser(bytes.NewReader([]byte("response"))), + }, nil + }), + } + body, err := client.Events(context.Background(), eventsCase.options) + if err != nil { + t.Fatal(err) + } + defer body.Close() + content, err := ioutil.ReadAll(body) + if err != nil { + t.Fatal(err) + } + if string(content) != "response" { + t.Fatalf("expected response to contain 'response', got %s", string(content)) + } + } +} diff --git a/hijack.go b/hijack.go new file mode 100644 index 0000000000..9376d21b97 --- /dev/null +++ b/hijack.go @@ -0,0 +1,174 @@ +package client + +import ( + "crypto/tls" + "errors" + "fmt" + "net" + "net/http/httputil" + "net/url" + "strings" + "time" + + "github.com/docker/docker/api/types" + "github.com/docker/docker/client/transport" + "github.com/docker/go-connections/sockets" + "golang.org/x/net/context" +) + +// tlsClientCon holds tls information and a dialed connection. +type tlsClientCon struct { + *tls.Conn + rawConn net.Conn +} + +func (c *tlsClientCon) CloseWrite() error { + // Go standard tls.Conn doesn't provide the CloseWrite() method so we do it + // on its underlying connection. + if conn, ok := c.rawConn.(types.CloseWriter); ok { + return conn.CloseWrite() + } + return nil +} + +// postHijacked sends a POST request and hijacks the connection. +func (cli *Client) postHijacked(ctx context.Context, path string, query url.Values, body interface{}, headers map[string][]string) (types.HijackedResponse, error) { + bodyEncoded, err := encodeData(body) + if err != nil { + return types.HijackedResponse{}, err + } + + req, err := cli.newRequest("POST", path, query, bodyEncoded, headers) + if err != nil { + return types.HijackedResponse{}, err + } + req.Host = cli.addr + + req.Header.Set("Connection", "Upgrade") + req.Header.Set("Upgrade", "tcp") + + conn, err := dial(cli.proto, cli.addr, cli.transport.TLSConfig()) + if err != nil { + if strings.Contains(err.Error(), "connection refused") { + return types.HijackedResponse{}, fmt.Errorf("Cannot connect to the Docker daemon. Is 'docker daemon' running on this host?") + } + return types.HijackedResponse{}, err + } + + // When we set up a TCP connection for hijack, there could be long periods + // of inactivity (a long running command with no output) that in certain + // network setups may cause ECONNTIMEOUT, leaving the client in an unknown + // state. Setting TCP KeepAlive on the socket connection will prohibit + // ECONNTIMEOUT unless the socket connection truly is broken + if tcpConn, ok := conn.(*net.TCPConn); ok { + tcpConn.SetKeepAlive(true) + tcpConn.SetKeepAlivePeriod(30 * time.Second) + } + + clientconn := httputil.NewClientConn(conn, nil) + defer clientconn.Close() + + // Server hijacks the connection, error 'connection closed' expected + _, err = clientconn.Do(req) + + rwc, br := clientconn.Hijack() + + return types.HijackedResponse{Conn: rwc, Reader: br}, err +} + +func tlsDial(network, addr string, config *tls.Config) (net.Conn, error) { + return tlsDialWithDialer(new(net.Dialer), network, addr, config) +} + +// We need to copy Go's implementation of tls.Dial (pkg/cryptor/tls/tls.go) in +// order to return our custom tlsClientCon struct which holds both the tls.Conn +// object _and_ its underlying raw connection. The rationale for this is that +// we need to be able to close the write end of the connection when attaching, +// which tls.Conn does not provide. +func tlsDialWithDialer(dialer *net.Dialer, network, addr string, config *tls.Config) (net.Conn, error) { + // We want the Timeout and Deadline values from dialer to cover the + // whole process: TCP connection and TLS handshake. This means that we + // also need to start our own timers now. + timeout := dialer.Timeout + + if !dialer.Deadline.IsZero() { + deadlineTimeout := dialer.Deadline.Sub(time.Now()) + if timeout == 0 || deadlineTimeout < timeout { + timeout = deadlineTimeout + } + } + + var errChannel chan error + + if timeout != 0 { + errChannel = make(chan error, 2) + time.AfterFunc(timeout, func() { + errChannel <- errors.New("") + }) + } + + proxyDialer, err := sockets.DialerFromEnvironment(dialer) + if err != nil { + return nil, err + } + + rawConn, err := proxyDialer.Dial(network, addr) + if err != nil { + return nil, err + } + // When we set up a TCP connection for hijack, there could be long periods + // of inactivity (a long running command with no output) that in certain + // network setups may cause ECONNTIMEOUT, leaving the client in an unknown + // state. Setting TCP KeepAlive on the socket connection will prohibit + // ECONNTIMEOUT unless the socket connection truly is broken + if tcpConn, ok := rawConn.(*net.TCPConn); ok { + tcpConn.SetKeepAlive(true) + tcpConn.SetKeepAlivePeriod(30 * time.Second) + } + + colonPos := strings.LastIndex(addr, ":") + if colonPos == -1 { + colonPos = len(addr) + } + hostname := addr[:colonPos] + + // If no ServerName is set, infer the ServerName + // from the hostname we're connecting to. + if config.ServerName == "" { + // Make a copy to avoid polluting argument or default. + config = transport.TLSConfigClone(config) + config.ServerName = hostname + } + + conn := tls.Client(rawConn, config) + + if timeout == 0 { + err = conn.Handshake() + } else { + go func() { + errChannel <- conn.Handshake() + }() + + err = <-errChannel + } + + if err != nil { + rawConn.Close() + return nil, err + } + + // This is Docker difference with standard's crypto/tls package: returned a + // wrapper which holds both the TLS and raw connections. + return &tlsClientCon{conn, rawConn}, nil +} + +func dial(proto, addr string, tlsConfig *tls.Config) (net.Conn, error) { + if tlsConfig != nil && proto != "unix" && proto != "npipe" { + // Notice this isn't Go standard's tls.Dial function + return tlsDial(proto, addr, tlsConfig) + } + if proto == "npipe" { + return sockets.DialPipe(addr, 32*time.Second) + } + return net.Dial(proto, addr) +} diff --git a/image_build.go b/image_build.go new file mode 100644 index 0000000000..8dd6744859 --- /dev/null +++ b/image_build.go @@ -0,0 +1,123 @@ +package client + +import ( + "encoding/base64" + "encoding/json" + "io" + "net/http" + "net/url" + "regexp" + "strconv" + + "golang.org/x/net/context" + + "github.com/docker/docker/api/types" + "github.com/docker/docker/api/types/container" +) + +var headerRegexp = regexp.MustCompile(`\ADocker/.+\s\((.+)\)\z`) + +// ImageBuild sends request to the daemon to build images. +// The Body in the response implement an io.ReadCloser and it's up to the caller to +// close it. +func (cli *Client) ImageBuild(ctx context.Context, buildContext io.Reader, options types.ImageBuildOptions) (types.ImageBuildResponse, error) { + query, err := imageBuildOptionsToQuery(options) + if err != nil { + return types.ImageBuildResponse{}, err + } + + headers := http.Header(make(map[string][]string)) + buf, err := json.Marshal(options.AuthConfigs) + if err != nil { + return types.ImageBuildResponse{}, err + } + headers.Add("X-Registry-Config", base64.URLEncoding.EncodeToString(buf)) + headers.Set("Content-Type", "application/tar") + + serverResp, err := cli.postRaw(ctx, "/build", query, buildContext, headers) + if err != nil { + return types.ImageBuildResponse{}, err + } + + osType := getDockerOS(serverResp.header.Get("Server")) + + return types.ImageBuildResponse{ + Body: serverResp.body, + OSType: osType, + }, nil +} + +func imageBuildOptionsToQuery(options types.ImageBuildOptions) (url.Values, error) { + query := url.Values{ + "t": options.Tags, + } + if options.SuppressOutput { + query.Set("q", "1") + } + if options.RemoteContext != "" { + query.Set("remote", options.RemoteContext) + } + if options.NoCache { + query.Set("nocache", "1") + } + if options.Remove { + query.Set("rm", "1") + } else { + query.Set("rm", "0") + } + + if options.ForceRemove { + query.Set("forcerm", "1") + } + + if options.PullParent { + query.Set("pull", "1") + } + + if options.Squash { + query.Set("squash", "1") + } + + if !container.Isolation.IsDefault(options.Isolation) { + query.Set("isolation", string(options.Isolation)) + } + + query.Set("cpusetcpus", options.CPUSetCPUs) + query.Set("cpusetmems", options.CPUSetMems) + query.Set("cpushares", strconv.FormatInt(options.CPUShares, 10)) + query.Set("cpuquota", strconv.FormatInt(options.CPUQuota, 10)) + query.Set("cpuperiod", strconv.FormatInt(options.CPUPeriod, 10)) + query.Set("memory", strconv.FormatInt(options.Memory, 10)) + query.Set("memswap", strconv.FormatInt(options.MemorySwap, 10)) + query.Set("cgroupparent", options.CgroupParent) + query.Set("shmsize", strconv.FormatInt(options.ShmSize, 10)) + query.Set("dockerfile", options.Dockerfile) + + ulimitsJSON, err := json.Marshal(options.Ulimits) + if err != nil { + return query, err + } + query.Set("ulimits", string(ulimitsJSON)) + + buildArgsJSON, err := json.Marshal(options.BuildArgs) + if err != nil { + return query, err + } + query.Set("buildargs", string(buildArgsJSON)) + + labelsJSON, err := json.Marshal(options.Labels) + if err != nil { + return query, err + } + query.Set("labels", string(labelsJSON)) + return query, nil +} + +func getDockerOS(serverHeader string) string { + var osType string + matches := headerRegexp.FindStringSubmatch(serverHeader) + if len(matches) > 0 { + osType = matches[1] + } + return osType +} diff --git a/image_build_test.go b/image_build_test.go new file mode 100644 index 0000000000..8261c54854 --- /dev/null +++ b/image_build_test.go @@ -0,0 +1,230 @@ +package client + +import ( + "bytes" + "fmt" + "io/ioutil" + "net/http" + "reflect" + "strings" + "testing" + + "golang.org/x/net/context" + + "github.com/docker/docker/api/types" + "github.com/docker/docker/api/types/container" + "github.com/docker/go-units" +) + +func TestImageBuildError(t *testing.T) { + client := &Client{ + transport: newMockClient(nil, errorMock(http.StatusInternalServerError, "Server error")), + } + _, err := client.ImageBuild(context.Background(), nil, types.ImageBuildOptions{}) + if err == nil || err.Error() != "Error response from daemon: Server error" { + t.Fatalf("expected a Server Error, got %v", err) + } +} + +func TestImageBuild(t *testing.T) { + emptyRegistryConfig := "bnVsbA==" + buildCases := []struct { + buildOptions types.ImageBuildOptions + expectedQueryParams map[string]string + expectedTags []string + expectedRegistryConfig string + }{ + { + buildOptions: types.ImageBuildOptions{ + SuppressOutput: true, + NoCache: true, + Remove: true, + ForceRemove: true, + PullParent: true, + }, + expectedQueryParams: map[string]string{ + "q": "1", + "nocache": "1", + "rm": "1", + "forcerm": "1", + "pull": "1", + }, + expectedTags: []string{}, + expectedRegistryConfig: emptyRegistryConfig, + }, + { + buildOptions: types.ImageBuildOptions{ + SuppressOutput: false, + NoCache: false, + Remove: false, + ForceRemove: false, + PullParent: false, + }, + expectedQueryParams: map[string]string{ + "q": "", + "nocache": "", + "rm": "0", + "forcerm": "", + "pull": "", + }, + expectedTags: []string{}, + expectedRegistryConfig: emptyRegistryConfig, + }, + { + buildOptions: types.ImageBuildOptions{ + RemoteContext: "remoteContext", + Isolation: container.Isolation("isolation"), + CPUSetCPUs: "2", + CPUSetMems: "12", + CPUShares: 20, + CPUQuota: 10, + CPUPeriod: 30, + Memory: 256, + MemorySwap: 512, + ShmSize: 10, + CgroupParent: "cgroup_parent", + Dockerfile: "Dockerfile", + }, + expectedQueryParams: map[string]string{ + "remote": "remoteContext", + "isolation": "isolation", + "cpusetcpus": "2", + "cpusetmems": "12", + "cpushares": "20", + "cpuquota": "10", + "cpuperiod": "30", + "memory": "256", + "memswap": "512", + "shmsize": "10", + "cgroupparent": "cgroup_parent", + "dockerfile": "Dockerfile", + "rm": "0", + }, + expectedTags: []string{}, + expectedRegistryConfig: emptyRegistryConfig, + }, + { + buildOptions: types.ImageBuildOptions{ + BuildArgs: map[string]string{ + "ARG1": "value1", + "ARG2": "value2", + }, + }, + expectedQueryParams: map[string]string{ + "buildargs": `{"ARG1":"value1","ARG2":"value2"}`, + "rm": "0", + }, + expectedTags: []string{}, + expectedRegistryConfig: emptyRegistryConfig, + }, + { + buildOptions: types.ImageBuildOptions{ + Ulimits: []*units.Ulimit{ + { + Name: "nproc", + Hard: 65557, + Soft: 65557, + }, + { + Name: "nofile", + Hard: 20000, + Soft: 40000, + }, + }, + }, + expectedQueryParams: map[string]string{ + "ulimits": `[{"Name":"nproc","Hard":65557,"Soft":65557},{"Name":"nofile","Hard":20000,"Soft":40000}]`, + "rm": "0", + }, + expectedTags: []string{}, + expectedRegistryConfig: emptyRegistryConfig, + }, + { + buildOptions: types.ImageBuildOptions{ + AuthConfigs: map[string]types.AuthConfig{ + "https://index.docker.io/v1/": { + Auth: "dG90bwo=", + }, + }, + }, + expectedQueryParams: map[string]string{ + "rm": "0", + }, + expectedTags: []string{}, + expectedRegistryConfig: "eyJodHRwczovL2luZGV4LmRvY2tlci5pby92MS8iOnsiYXV0aCI6ImRHOTBid289In19", + }, + } + for _, buildCase := range buildCases { + expectedURL := "/build" + client := &Client{ + transport: newMockClient(nil, func(r *http.Request) (*http.Response, error) { + if !strings.HasPrefix(r.URL.Path, expectedURL) { + return nil, fmt.Errorf("Expected URL '%s', got '%s'", expectedURL, r.URL) + } + // Check request headers + registryConfig := r.Header.Get("X-Registry-Config") + if registryConfig != buildCase.expectedRegistryConfig { + return nil, fmt.Errorf("X-Registry-Config header not properly set in the request. Expected '%s', got %s", buildCase.expectedRegistryConfig, registryConfig) + } + contentType := r.Header.Get("Content-Type") + if contentType != "application/tar" { + return nil, fmt.Errorf("Content-type header not properly set in the request. Expected 'application/tar', got %s", contentType) + } + + // Check query parameters + query := r.URL.Query() + for key, expected := range buildCase.expectedQueryParams { + actual := query.Get(key) + if actual != expected { + return nil, fmt.Errorf("%s not set in URL query properly. Expected '%s', got %s", key, expected, actual) + } + } + + // Check tags + if len(buildCase.expectedTags) > 0 { + tags := query["t"] + if !reflect.DeepEqual(tags, buildCase.expectedTags) { + return nil, fmt.Errorf("t (tags) not set in URL query properly. Expected '%s', got %s", buildCase.expectedTags, tags) + } + } + + headers := http.Header{} + headers.Add("Server", "Docker/v1.23 (MyOS)") + return &http.Response{ + StatusCode: http.StatusOK, + Body: ioutil.NopCloser(bytes.NewReader([]byte("body"))), + Header: headers, + }, nil + }), + } + buildResponse, err := client.ImageBuild(context.Background(), nil, buildCase.buildOptions) + if err != nil { + t.Fatal(err) + } + if buildResponse.OSType != "MyOS" { + t.Fatalf("expected OSType to be 'MyOS', got %s", buildResponse.OSType) + } + response, err := ioutil.ReadAll(buildResponse.Body) + if err != nil { + t.Fatal(err) + } + buildResponse.Body.Close() + if string(response) != "body" { + t.Fatalf("expected Body to contain 'body' string, got %s", response) + } + } +} + +func TestGetDockerOS(t *testing.T) { + cases := map[string]string{ + "Docker/v1.22 (linux)": "linux", + "Docker/v1.22 (windows)": "windows", + "Foo/v1.22 (bar)": "", + } + for header, os := range cases { + g := getDockerOS(header) + if g != os { + t.Fatalf("Expected %s, got %s", os, g) + } + } +} diff --git a/image_create.go b/image_create.go new file mode 100644 index 0000000000..cf023a7186 --- /dev/null +++ b/image_create.go @@ -0,0 +1,34 @@ +package client + +import ( + "io" + "net/url" + + "golang.org/x/net/context" + + "github.com/docker/docker/api/types" + "github.com/docker/docker/api/types/reference" +) + +// ImageCreate creates a new image based in the parent options. +// It returns the JSON content in the response body. +func (cli *Client) ImageCreate(ctx context.Context, parentReference string, options types.ImageCreateOptions) (io.ReadCloser, error) { + repository, tag, err := reference.Parse(parentReference) + if err != nil { + return nil, err + } + + query := url.Values{} + query.Set("fromImage", repository) + query.Set("tag", tag) + resp, err := cli.tryImageCreate(ctx, query, options.RegistryAuth) + if err != nil { + return nil, err + } + return resp.body, nil +} + +func (cli *Client) tryImageCreate(ctx context.Context, query url.Values, registryAuth string) (serverResponse, error) { + headers := map[string][]string{"X-Registry-Auth": {registryAuth}} + return cli.post(ctx, "/images/create", query, nil, headers) +} diff --git a/image_create_test.go b/image_create_test.go new file mode 100644 index 0000000000..a2e001be5d --- /dev/null +++ b/image_create_test.go @@ -0,0 +1,76 @@ +package client + +import ( + "bytes" + "fmt" + "io/ioutil" + "net/http" + "strings" + "testing" + + "golang.org/x/net/context" + + "github.com/docker/docker/api/types" +) + +func TestImageCreateError(t *testing.T) { + client := &Client{ + transport: newMockClient(nil, errorMock(http.StatusInternalServerError, "Server error")), + } + _, err := client.ImageCreate(context.Background(), "reference", types.ImageCreateOptions{}) + if err == nil || err.Error() != "Error response from daemon: Server error" { + t.Fatalf("expected a Server error, got %v", err) + } +} + +func TestImageCreate(t *testing.T) { + expectedURL := "/images/create" + expectedImage := "test:5000/my_image" + expectedTag := "sha256:ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff" + expectedReference := fmt.Sprintf("%s@%s", expectedImage, expectedTag) + expectedRegistryAuth := "eyJodHRwczovL2luZGV4LmRvY2tlci5pby92MS8iOnsiYXV0aCI6ImRHOTBid289IiwiZW1haWwiOiJqb2huQGRvZS5jb20ifX0=" + client := &Client{ + transport: newMockClient(nil, func(r *http.Request) (*http.Response, error) { + if !strings.HasPrefix(r.URL.Path, expectedURL) { + return nil, fmt.Errorf("Expected URL '%s', got '%s'", expectedURL, r.URL) + } + registryAuth := r.Header.Get("X-Registry-Auth") + if registryAuth != expectedRegistryAuth { + return nil, fmt.Errorf("X-Registry-Auth header not properly set in the request. Expected '%s', got %s", expectedRegistryAuth, registryAuth) + } + + query := r.URL.Query() + fromImage := query.Get("fromImage") + if fromImage != expectedImage { + return nil, fmt.Errorf("fromImage not set in URL query properly. Expected '%s', got %s", expectedImage, fromImage) + } + + tag := query.Get("tag") + if tag != expectedTag { + return nil, fmt.Errorf("tag not set in URL query properly. Expected '%s', got %s", expectedTag, tag) + } + + return &http.Response{ + StatusCode: http.StatusOK, + Body: ioutil.NopCloser(bytes.NewReader([]byte("body"))), + }, nil + }), + } + + createResponse, err := client.ImageCreate(context.Background(), expectedReference, types.ImageCreateOptions{ + RegistryAuth: expectedRegistryAuth, + }) + if err != nil { + t.Fatal(err) + } + response, err := ioutil.ReadAll(createResponse) + if err != nil { + t.Fatal(err) + } + if err = createResponse.Close(); err != nil { + t.Fatal(err) + } + if string(response) != "body" { + t.Fatalf("expected Body to contain 'body' string, got %s", response) + } +} diff --git a/image_history.go b/image_history.go new file mode 100644 index 0000000000..acb1ee9278 --- /dev/null +++ b/image_history.go @@ -0,0 +1,22 @@ +package client + +import ( + "encoding/json" + "net/url" + + "github.com/docker/docker/api/types" + "golang.org/x/net/context" +) + +// ImageHistory returns the changes in an image in history format. +func (cli *Client) ImageHistory(ctx context.Context, imageID string) ([]types.ImageHistory, error) { + var history []types.ImageHistory + serverResp, err := cli.get(ctx, "/images/"+imageID+"/history", url.Values{}, nil) + if err != nil { + return history, err + } + + err = json.NewDecoder(serverResp.body).Decode(&history) + ensureReaderClosed(serverResp) + return history, err +} diff --git a/image_history_test.go b/image_history_test.go new file mode 100644 index 0000000000..c9516151b7 --- /dev/null +++ b/image_history_test.go @@ -0,0 +1,60 @@ +package client + +import ( + "bytes" + "encoding/json" + "fmt" + "io/ioutil" + "net/http" + "strings" + "testing" + + "github.com/docker/docker/api/types" + "golang.org/x/net/context" +) + +func TestImageHistoryError(t *testing.T) { + client := &Client{ + transport: newMockClient(nil, errorMock(http.StatusInternalServerError, "Server error")), + } + _, err := client.ImageHistory(context.Background(), "nothing") + if err == nil || err.Error() != "Error response from daemon: Server error" { + t.Fatalf("expected a Server error, got %v", err) + } +} + +func TestImageHistory(t *testing.T) { + expectedURL := "/images/image_id/history" + client := &Client{ + transport: newMockClient(nil, func(r *http.Request) (*http.Response, error) { + if !strings.HasPrefix(r.URL.Path, expectedURL) { + return nil, fmt.Errorf("Expected URL '%s', got '%s'", expectedURL, r.URL) + } + b, err := json.Marshal([]types.ImageHistory{ + { + ID: "image_id1", + Tags: []string{"tag1", "tag2"}, + }, + { + ID: "image_id2", + Tags: []string{"tag1", "tag2"}, + }, + }) + if err != nil { + return nil, err + } + + return &http.Response{ + StatusCode: http.StatusOK, + Body: ioutil.NopCloser(bytes.NewReader(b)), + }, nil + }), + } + imageHistories, err := client.ImageHistory(context.Background(), "image_id") + if err != nil { + t.Fatal(err) + } + if len(imageHistories) != 2 { + t.Fatalf("expected 2 containers, got %v", imageHistories) + } +} diff --git a/image_import.go b/image_import.go new file mode 100644 index 0000000000..c6f154b249 --- /dev/null +++ b/image_import.go @@ -0,0 +1,37 @@ +package client + +import ( + "io" + "net/url" + + "golang.org/x/net/context" + + "github.com/docker/distribution/reference" + "github.com/docker/docker/api/types" +) + +// ImageImport creates a new image based in the source options. +// It returns the JSON content in the response body. +func (cli *Client) ImageImport(ctx context.Context, source types.ImageImportSource, ref string, options types.ImageImportOptions) (io.ReadCloser, error) { + if ref != "" { + //Check if the given image name can be resolved + if _, err := reference.ParseNamed(ref); err != nil { + return nil, err + } + } + + query := url.Values{} + query.Set("fromSrc", source.SourceName) + query.Set("repo", ref) + query.Set("tag", options.Tag) + query.Set("message", options.Message) + for _, change := range options.Changes { + query.Add("changes", change) + } + + resp, err := cli.postRaw(ctx, "/images/create", query, source.Source, nil) + if err != nil { + return nil, err + } + return resp.body, nil +} diff --git a/image_import_test.go b/image_import_test.go new file mode 100644 index 0000000000..b64ca74d7b --- /dev/null +++ b/image_import_test.go @@ -0,0 +1,81 @@ +package client + +import ( + "bytes" + "fmt" + "io/ioutil" + "net/http" + "reflect" + "strings" + "testing" + + "github.com/docker/docker/api/types" + "golang.org/x/net/context" +) + +func TestImageImportError(t *testing.T) { + client := &Client{ + transport: newMockClient(nil, errorMock(http.StatusInternalServerError, "Server error")), + } + _, err := client.ImageImport(context.Background(), types.ImageImportSource{}, "image:tag", types.ImageImportOptions{}) + if err == nil || err.Error() != "Error response from daemon: Server error" { + t.Fatalf("expected a Server error, got %v", err) + } +} + +func TestImageImport(t *testing.T) { + expectedURL := "/images/create" + client := &Client{ + transport: newMockClient(nil, func(r *http.Request) (*http.Response, error) { + if !strings.HasPrefix(r.URL.Path, expectedURL) { + return nil, fmt.Errorf("Expected URL '%s', got '%s'", expectedURL, r.URL) + } + query := r.URL.Query() + fromSrc := query.Get("fromSrc") + if fromSrc != "image_source" { + return nil, fmt.Errorf("fromSrc not set in URL query properly. Expected 'image_source', got %s", fromSrc) + } + repo := query.Get("repo") + if repo != "repository_name:imported" { + return nil, fmt.Errorf("repo not set in URL query properly. Expected 'repository_name', got %s", repo) + } + tag := query.Get("tag") + if tag != "imported" { + return nil, fmt.Errorf("tag not set in URL query properly. Expected 'imported', got %s", tag) + } + message := query.Get("message") + if message != "A message" { + return nil, fmt.Errorf("message not set in URL query properly. Expected 'A message', got %s", message) + } + changes := query["changes"] + expectedChanges := []string{"change1", "change2"} + if !reflect.DeepEqual(expectedChanges, changes) { + return nil, fmt.Errorf("changes not set in URL query properly. Expected %v, got %v", expectedChanges, changes) + } + + return &http.Response{ + StatusCode: http.StatusOK, + Body: ioutil.NopCloser(bytes.NewReader([]byte("response"))), + }, nil + }), + } + importResponse, err := client.ImageImport(context.Background(), types.ImageImportSource{ + Source: strings.NewReader("source"), + SourceName: "image_source", + }, "repository_name:imported", types.ImageImportOptions{ + Tag: "imported", + Message: "A message", + Changes: []string{"change1", "change2"}, + }) + if err != nil { + t.Fatal(err) + } + response, err := ioutil.ReadAll(importResponse) + if err != nil { + t.Fatal(err) + } + importResponse.Close() + if string(response) != "response" { + t.Fatalf("expected response to contain 'response', got %s", string(response)) + } +} diff --git a/image_inspect.go b/image_inspect.go new file mode 100644 index 0000000000..b3a64ce2f8 --- /dev/null +++ b/image_inspect.go @@ -0,0 +1,33 @@ +package client + +import ( + "bytes" + "encoding/json" + "io/ioutil" + "net/http" + + "github.com/docker/docker/api/types" + "golang.org/x/net/context" +) + +// ImageInspectWithRaw returns the image information and its raw representation. +func (cli *Client) ImageInspectWithRaw(ctx context.Context, imageID string) (types.ImageInspect, []byte, error) { + serverResp, err := cli.get(ctx, "/images/"+imageID+"/json", nil, nil) + if err != nil { + if serverResp.statusCode == http.StatusNotFound { + return types.ImageInspect{}, nil, imageNotFoundError{imageID} + } + return types.ImageInspect{}, nil, err + } + defer ensureReaderClosed(serverResp) + + body, err := ioutil.ReadAll(serverResp.body) + if err != nil { + return types.ImageInspect{}, nil, err + } + + var response types.ImageInspect + rdr := bytes.NewReader(body) + err = json.NewDecoder(rdr).Decode(&response) + return response, body, err +} diff --git a/image_inspect_test.go b/image_inspect_test.go new file mode 100644 index 0000000000..5c7ca2721f --- /dev/null +++ b/image_inspect_test.go @@ -0,0 +1,71 @@ +package client + +import ( + "bytes" + "encoding/json" + "fmt" + "io/ioutil" + "net/http" + "reflect" + "strings" + "testing" + + "github.com/docker/docker/api/types" + "golang.org/x/net/context" +) + +func TestImageInspectError(t *testing.T) { + client := &Client{ + transport: newMockClient(nil, errorMock(http.StatusInternalServerError, "Server error")), + } + + _, _, err := client.ImageInspectWithRaw(context.Background(), "nothing") + if err == nil || err.Error() != "Error response from daemon: Server error" { + t.Fatalf("expected a Server Error, got %v", err) + } +} + +func TestImageInspectImageNotFound(t *testing.T) { + client := &Client{ + transport: newMockClient(nil, errorMock(http.StatusNotFound, "Server error")), + } + + _, _, err := client.ImageInspectWithRaw(context.Background(), "unknown") + if err == nil || !IsErrImageNotFound(err) { + t.Fatalf("expected an imageNotFound error, got %v", err) + } +} + +func TestImageInspect(t *testing.T) { + expectedURL := "/images/image_id/json" + expectedTags := []string{"tag1", "tag2"} + client := &Client{ + transport: newMockClient(nil, func(req *http.Request) (*http.Response, error) { + if !strings.HasPrefix(req.URL.Path, expectedURL) { + return nil, fmt.Errorf("Expected URL '%s', got '%s'", expectedURL, req.URL) + } + content, err := json.Marshal(types.ImageInspect{ + ID: "image_id", + RepoTags: expectedTags, + }) + if err != nil { + return nil, err + } + return &http.Response{ + StatusCode: http.StatusOK, + Body: ioutil.NopCloser(bytes.NewReader(content)), + }, nil + }), + } + + imageInspect, _, err := client.ImageInspectWithRaw(context.Background(), "image_id") + if err != nil { + t.Fatal(err) + } + if imageInspect.ID != "image_id" { + t.Fatalf("expected `image_id`, got %s", imageInspect.ID) + } + if !reflect.DeepEqual(imageInspect.RepoTags, expectedTags) { + t.Fatalf("expected `%v`, got %v", expectedTags, imageInspect.RepoTags) + } +} diff --git a/image_list.go b/image_list.go new file mode 100644 index 0000000000..00f27dc0c9 --- /dev/null +++ b/image_list.go @@ -0,0 +1,40 @@ +package client + +import ( + "encoding/json" + "net/url" + + "github.com/docker/docker/api/types" + "github.com/docker/docker/api/types/filters" + "golang.org/x/net/context" +) + +// ImageList returns a list of images in the docker host. +func (cli *Client) ImageList(ctx context.Context, options types.ImageListOptions) ([]types.Image, error) { + var images []types.Image + query := url.Values{} + + if options.Filters.Len() > 0 { + filterJSON, err := filters.ToParamWithVersion(cli.version, options.Filters) + if err != nil { + return images, err + } + query.Set("filters", filterJSON) + } + if options.MatchName != "" { + // FIXME rename this parameter, to not be confused with the filters flag + query.Set("filter", options.MatchName) + } + if options.All { + query.Set("all", "1") + } + + serverResp, err := cli.get(ctx, "/images/json", query, nil) + if err != nil { + return images, err + } + + err = json.NewDecoder(serverResp.body).Decode(&images) + ensureReaderClosed(serverResp) + return images, err +} diff --git a/image_list_test.go b/image_list_test.go new file mode 100644 index 0000000000..99ed1964a2 --- /dev/null +++ b/image_list_test.go @@ -0,0 +1,122 @@ +package client + +import ( + "bytes" + "encoding/json" + "fmt" + "io/ioutil" + "net/http" + "strings" + "testing" + + "github.com/docker/docker/api/types" + "github.com/docker/docker/api/types/filters" + "golang.org/x/net/context" +) + +func TestImageListError(t *testing.T) { + client := &Client{ + transport: newMockClient(nil, errorMock(http.StatusInternalServerError, "Server error")), + } + + _, err := client.ImageList(context.Background(), types.ImageListOptions{}) + if err == nil || err.Error() != "Error response from daemon: Server error" { + t.Fatalf("expected a Server Error, got %v", err) + } +} + +func TestImageList(t *testing.T) { + expectedURL := "/images/json" + + noDanglingfilters := filters.NewArgs() + noDanglingfilters.Add("dangling", "false") + + filters := filters.NewArgs() + filters.Add("label", "label1") + filters.Add("label", "label2") + filters.Add("dangling", "true") + + listCases := []struct { + options types.ImageListOptions + expectedQueryParams map[string]string + }{ + { + options: types.ImageListOptions{}, + expectedQueryParams: map[string]string{ + "all": "", + "filter": "", + "filters": "", + }, + }, + { + options: types.ImageListOptions{ + All: true, + MatchName: "image_name", + }, + expectedQueryParams: map[string]string{ + "all": "1", + "filter": "image_name", + "filters": "", + }, + }, + { + options: types.ImageListOptions{ + Filters: filters, + }, + expectedQueryParams: map[string]string{ + "all": "", + "filter": "", + "filters": `{"dangling":{"true":true},"label":{"label1":true,"label2":true}}`, + }, + }, + { + options: types.ImageListOptions{ + Filters: noDanglingfilters, + }, + expectedQueryParams: map[string]string{ + "all": "", + "filter": "", + "filters": `{"dangling":{"false":true}}`, + }, + }, + } + for _, listCase := range listCases { + client := &Client{ + transport: newMockClient(nil, func(req *http.Request) (*http.Response, error) { + if !strings.HasPrefix(req.URL.Path, expectedURL) { + return nil, fmt.Errorf("Expected URL '%s', got '%s'", expectedURL, req.URL) + } + query := req.URL.Query() + for key, expected := range listCase.expectedQueryParams { + actual := query.Get(key) + if actual != expected { + return nil, fmt.Errorf("%s not set in URL query properly. Expected '%s', got %s", key, expected, actual) + } + } + content, err := json.Marshal([]types.Image{ + { + ID: "image_id2", + }, + { + ID: "image_id2", + }, + }) + if err != nil { + return nil, err + } + return &http.Response{ + StatusCode: http.StatusOK, + Body: ioutil.NopCloser(bytes.NewReader(content)), + }, nil + }), + } + + images, err := client.ImageList(context.Background(), listCase.options) + if err != nil { + t.Fatal(err) + } + if len(images) != 2 { + t.Fatalf("expected 2 images, got %v", images) + } + } +} diff --git a/image_load.go b/image_load.go new file mode 100644 index 0000000000..77aaf1af36 --- /dev/null +++ b/image_load.go @@ -0,0 +1,30 @@ +package client + +import ( + "io" + "net/url" + + "golang.org/x/net/context" + + "github.com/docker/docker/api/types" +) + +// ImageLoad loads an image in the docker host from the client host. +// It's up to the caller to close the io.ReadCloser in the +// ImageLoadResponse returned by this function. +func (cli *Client) ImageLoad(ctx context.Context, input io.Reader, quiet bool) (types.ImageLoadResponse, error) { + v := url.Values{} + v.Set("quiet", "0") + if quiet { + v.Set("quiet", "1") + } + headers := map[string][]string{"Content-Type": {"application/x-tar"}} + resp, err := cli.postRaw(ctx, "/images/load", v, input, headers) + if err != nil { + return types.ImageLoadResponse{}, err + } + return types.ImageLoadResponse{ + Body: resp.body, + JSON: resp.header.Get("Content-Type") == "application/json", + }, nil +} diff --git a/image_load_test.go b/image_load_test.go new file mode 100644 index 0000000000..0ee7cf35a6 --- /dev/null +++ b/image_load_test.go @@ -0,0 +1,95 @@ +package client + +import ( + "bytes" + "fmt" + "io/ioutil" + "net/http" + "strings" + "testing" + + "golang.org/x/net/context" +) + +func TestImageLoadError(t *testing.T) { + client := &Client{ + transport: newMockClient(nil, errorMock(http.StatusInternalServerError, "Server error")), + } + + _, err := client.ImageLoad(context.Background(), nil, true) + if err == nil || err.Error() != "Error response from daemon: Server error" { + t.Fatalf("expected a Server Error, got %v", err) + } +} + +func TestImageLoad(t *testing.T) { + expectedURL := "/images/load" + expectedInput := "inputBody" + expectedOutput := "outputBody" + loadCases := []struct { + quiet bool + responseContentType string + expectedResponseJSON bool + expectedQueryParams map[string]string + }{ + { + quiet: false, + responseContentType: "text/plain", + expectedResponseJSON: false, + expectedQueryParams: map[string]string{ + "quiet": "0", + }, + }, + { + quiet: true, + responseContentType: "application/json", + expectedResponseJSON: true, + expectedQueryParams: map[string]string{ + "quiet": "1", + }, + }, + } + for _, loadCase := range loadCases { + client := &Client{ + transport: newMockClient(nil, func(req *http.Request) (*http.Response, error) { + if !strings.HasPrefix(req.URL.Path, expectedURL) { + return nil, fmt.Errorf("Expected URL '%s', got '%s'", expectedURL, req.URL) + } + contentType := req.Header.Get("Content-Type") + if contentType != "application/x-tar" { + return nil, fmt.Errorf("content-type not set in URL headers properly. Expected 'application/x-tar', got %s", contentType) + } + query := req.URL.Query() + for key, expected := range loadCase.expectedQueryParams { + actual := query.Get(key) + if actual != expected { + return nil, fmt.Errorf("%s not set in URL query properly. Expected '%s', got %s", key, expected, actual) + } + } + headers := http.Header{} + headers.Add("Content-Type", loadCase.responseContentType) + return &http.Response{ + StatusCode: http.StatusOK, + Body: ioutil.NopCloser(bytes.NewReader([]byte(expectedOutput))), + Header: headers, + }, nil + }), + } + + input := bytes.NewReader([]byte(expectedInput)) + imageLoadResponse, err := client.ImageLoad(context.Background(), input, loadCase.quiet) + if err != nil { + t.Fatal(err) + } + if imageLoadResponse.JSON != loadCase.expectedResponseJSON { + t.Fatalf("expected a JSON response, was not.") + } + body, err := ioutil.ReadAll(imageLoadResponse.Body) + if err != nil { + t.Fatal(err) + } + if string(body) != expectedOutput { + t.Fatalf("expected %s, got %s", expectedOutput, string(body)) + } + } +} diff --git a/image_pull.go b/image_pull.go new file mode 100644 index 0000000000..3bffdb70e8 --- /dev/null +++ b/image_pull.go @@ -0,0 +1,46 @@ +package client + +import ( + "io" + "net/http" + "net/url" + + "golang.org/x/net/context" + + "github.com/docker/docker/api/types" + "github.com/docker/docker/api/types/reference" +) + +// ImagePull requests the docker host to pull an image from a remote registry. +// It executes the privileged function if the operation is unauthorized +// and it tries one more time. +// It's up to the caller to handle the io.ReadCloser and close it properly. +// +// FIXME(vdemeester): there is currently used in a few way in docker/docker +// - if not in trusted content, ref is used to pass the whole reference, and tag is empty +// - if in trusted content, ref is used to pass the reference name, and tag for the digest +func (cli *Client) ImagePull(ctx context.Context, ref string, options types.ImagePullOptions) (io.ReadCloser, error) { + repository, tag, err := reference.Parse(ref) + if err != nil { + return nil, err + } + + query := url.Values{} + query.Set("fromImage", repository) + if tag != "" && !options.All { + query.Set("tag", tag) + } + + resp, err := cli.tryImageCreate(ctx, query, options.RegistryAuth) + if resp.statusCode == http.StatusUnauthorized && options.PrivilegeFunc != nil { + newAuthHeader, privilegeErr := options.PrivilegeFunc() + if privilegeErr != nil { + return nil, privilegeErr + } + resp, err = cli.tryImageCreate(ctx, query, newAuthHeader) + } + if err != nil { + return nil, err + } + return resp.body, nil +} diff --git a/image_pull_test.go b/image_pull_test.go new file mode 100644 index 0000000000..c33a6dcc8a --- /dev/null +++ b/image_pull_test.go @@ -0,0 +1,199 @@ +package client + +import ( + "bytes" + "fmt" + "io/ioutil" + "net/http" + "strings" + "testing" + + "golang.org/x/net/context" + + "github.com/docker/docker/api/types" +) + +func TestImagePullReferenceParseError(t *testing.T) { + client := &Client{ + transport: newMockClient(nil, func(req *http.Request) (*http.Response, error) { + return nil, nil + }), + } + // An empty reference is an invalid reference + _, err := client.ImagePull(context.Background(), "", types.ImagePullOptions{}) + if err == nil || err.Error() != "repository name must have at least one component" { + t.Fatalf("expected an error, got %v", err) + } +} + +func TestImagePullAnyError(t *testing.T) { + client := &Client{ + transport: newMockClient(nil, errorMock(http.StatusInternalServerError, "Server error")), + } + _, err := client.ImagePull(context.Background(), "myimage", types.ImagePullOptions{}) + if err == nil || err.Error() != "Error response from daemon: Server error" { + t.Fatalf("expected a Server Error, got %v", err) + } +} + +func TestImagePullStatusUnauthorizedError(t *testing.T) { + client := &Client{ + transport: newMockClient(nil, errorMock(http.StatusUnauthorized, "Unauthorized error")), + } + _, err := client.ImagePull(context.Background(), "myimage", types.ImagePullOptions{}) + if err == nil || err.Error() != "Error response from daemon: Unauthorized error" { + t.Fatalf("expected an Unauthorized Error, got %v", err) + } +} + +func TestImagePullWithUnauthorizedErrorAndPrivilegeFuncError(t *testing.T) { + client := &Client{ + transport: newMockClient(nil, errorMock(http.StatusUnauthorized, "Unauthorized error")), + } + privilegeFunc := func() (string, error) { + return "", fmt.Errorf("Error requesting privilege") + } + _, err := client.ImagePull(context.Background(), "myimage", types.ImagePullOptions{ + PrivilegeFunc: privilegeFunc, + }) + if err == nil || err.Error() != "Error requesting privilege" { + t.Fatalf("expected an error requesting privilege, got %v", err) + } +} + +func TestImagePullWithUnauthorizedErrorAndAnotherUnauthorizedError(t *testing.T) { + client := &Client{ + transport: newMockClient(nil, errorMock(http.StatusUnauthorized, "Unauthorized error")), + } + privilegeFunc := func() (string, error) { + return "a-auth-header", nil + } + _, err := client.ImagePull(context.Background(), "myimage", types.ImagePullOptions{ + PrivilegeFunc: privilegeFunc, + }) + if err == nil || err.Error() != "Error response from daemon: Unauthorized error" { + t.Fatalf("expected an Unauthorized Error, got %v", err) + } +} + +func TestImagePullWithPrivilegedFuncNoError(t *testing.T) { + expectedURL := "/images/create" + client := &Client{ + transport: newMockClient(nil, func(req *http.Request) (*http.Response, error) { + if !strings.HasPrefix(req.URL.Path, expectedURL) { + return nil, fmt.Errorf("Expected URL '%s', got '%s'", expectedURL, req.URL) + } + auth := req.Header.Get("X-Registry-Auth") + if auth == "NotValid" { + return &http.Response{ + StatusCode: http.StatusUnauthorized, + Body: ioutil.NopCloser(bytes.NewReader([]byte("Invalid credentials"))), + }, nil + } + if auth != "IAmValid" { + return nil, fmt.Errorf("Invalid auth header : expected %s, got %s", "IAmValid", auth) + } + query := req.URL.Query() + fromImage := query.Get("fromImage") + if fromImage != "myimage" { + return nil, fmt.Errorf("fromimage not set in URL query properly. Expected '%s', got %s", "myimage", fromImage) + } + tag := query.Get("tag") + if tag != "latest" { + return nil, fmt.Errorf("tag not set in URL query properly. Expected '%s', got %s", "latest", tag) + } + return &http.Response{ + StatusCode: http.StatusOK, + Body: ioutil.NopCloser(bytes.NewReader([]byte("hello world"))), + }, nil + }), + } + privilegeFunc := func() (string, error) { + return "IAmValid", nil + } + resp, err := client.ImagePull(context.Background(), "myimage", types.ImagePullOptions{ + RegistryAuth: "NotValid", + PrivilegeFunc: privilegeFunc, + }) + if err != nil { + t.Fatal(err) + } + body, err := ioutil.ReadAll(resp) + if err != nil { + t.Fatal(err) + } + if string(body) != "hello world" { + t.Fatalf("expected 'hello world', got %s", string(body)) + } +} + +func TestImagePullWithoutErrors(t *testing.T) { + expectedURL := "/images/create" + expectedOutput := "hello world" + pullCases := []struct { + all bool + reference string + expectedImage string + expectedTag string + }{ + { + all: false, + reference: "myimage", + expectedImage: "myimage", + expectedTag: "latest", + }, + { + all: false, + reference: "myimage:tag", + expectedImage: "myimage", + expectedTag: "tag", + }, + { + all: true, + reference: "myimage", + expectedImage: "myimage", + expectedTag: "", + }, + { + all: true, + reference: "myimage:anything", + expectedImage: "myimage", + expectedTag: "", + }, + } + for _, pullCase := range pullCases { + client := &Client{ + transport: newMockClient(nil, func(req *http.Request) (*http.Response, error) { + if !strings.HasPrefix(req.URL.Path, expectedURL) { + return nil, fmt.Errorf("Expected URL '%s', got '%s'", expectedURL, req.URL) + } + query := req.URL.Query() + fromImage := query.Get("fromImage") + if fromImage != pullCase.expectedImage { + return nil, fmt.Errorf("fromimage not set in URL query properly. Expected '%s', got %s", pullCase.expectedImage, fromImage) + } + tag := query.Get("tag") + if tag != pullCase.expectedTag { + return nil, fmt.Errorf("tag not set in URL query properly. Expected '%s', got %s", pullCase.expectedTag, tag) + } + return &http.Response{ + StatusCode: http.StatusOK, + Body: ioutil.NopCloser(bytes.NewReader([]byte(expectedOutput))), + }, nil + }), + } + resp, err := client.ImagePull(context.Background(), pullCase.reference, types.ImagePullOptions{ + All: pullCase.all, + }) + if err != nil { + t.Fatal(err) + } + body, err := ioutil.ReadAll(resp) + if err != nil { + t.Fatal(err) + } + if string(body) != expectedOutput { + t.Fatalf("expected '%s', got %s", expectedOutput, string(body)) + } + } +} diff --git a/image_push.go b/image_push.go new file mode 100644 index 0000000000..8e73d28f56 --- /dev/null +++ b/image_push.go @@ -0,0 +1,54 @@ +package client + +import ( + "errors" + "io" + "net/http" + "net/url" + + "golang.org/x/net/context" + + distreference "github.com/docker/distribution/reference" + "github.com/docker/docker/api/types" +) + +// ImagePush requests the docker host to push an image to a remote registry. +// It executes the privileged function if the operation is unauthorized +// and it tries one more time. +// It's up to the caller to handle the io.ReadCloser and close it properly. +func (cli *Client) ImagePush(ctx context.Context, ref string, options types.ImagePushOptions) (io.ReadCloser, error) { + distributionRef, err := distreference.ParseNamed(ref) + if err != nil { + return nil, err + } + + if _, isCanonical := distributionRef.(distreference.Canonical); isCanonical { + return nil, errors.New("cannot push a digest reference") + } + + var tag = "" + if nameTaggedRef, isNamedTagged := distributionRef.(distreference.NamedTagged); isNamedTagged { + tag = nameTaggedRef.Tag() + } + + query := url.Values{} + query.Set("tag", tag) + + resp, err := cli.tryImagePush(ctx, distributionRef.Name(), query, options.RegistryAuth) + if resp.statusCode == http.StatusUnauthorized && options.PrivilegeFunc != nil { + newAuthHeader, privilegeErr := options.PrivilegeFunc() + if privilegeErr != nil { + return nil, privilegeErr + } + resp, err = cli.tryImagePush(ctx, distributionRef.Name(), query, newAuthHeader) + } + if err != nil { + return nil, err + } + return resp.body, nil +} + +func (cli *Client) tryImagePush(ctx context.Context, imageID string, query url.Values, registryAuth string) (serverResponse, error) { + headers := map[string][]string{"X-Registry-Auth": {registryAuth}} + return cli.post(ctx, "/images/"+imageID+"/push", query, nil, headers) +} diff --git a/image_push_test.go b/image_push_test.go new file mode 100644 index 0000000000..d32f3ef3c7 --- /dev/null +++ b/image_push_test.go @@ -0,0 +1,180 @@ +package client + +import ( + "bytes" + "fmt" + "io/ioutil" + "net/http" + "strings" + "testing" + + "golang.org/x/net/context" + + "github.com/docker/docker/api/types" +) + +func TestImagePushReferenceError(t *testing.T) { + client := &Client{ + transport: newMockClient(nil, func(req *http.Request) (*http.Response, error) { + return nil, nil + }), + } + // An empty reference is an invalid reference + _, err := client.ImagePush(context.Background(), "", types.ImagePushOptions{}) + if err == nil || err.Error() != "repository name must have at least one component" { + t.Fatalf("expected an error, got %v", err) + } + // An canonical reference cannot be pushed + _, err = client.ImagePush(context.Background(), "repo@sha256:ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff", types.ImagePushOptions{}) + if err == nil || err.Error() != "cannot push a digest reference" { + t.Fatalf("expected an error, got %v", err) + } +} + +func TestImagePushAnyError(t *testing.T) { + client := &Client{ + transport: newMockClient(nil, errorMock(http.StatusInternalServerError, "Server error")), + } + _, err := client.ImagePush(context.Background(), "myimage", types.ImagePushOptions{}) + if err == nil || err.Error() != "Error response from daemon: Server error" { + t.Fatalf("expected a Server Error, got %v", err) + } +} + +func TestImagePushStatusUnauthorizedError(t *testing.T) { + client := &Client{ + transport: newMockClient(nil, errorMock(http.StatusUnauthorized, "Unauthorized error")), + } + _, err := client.ImagePush(context.Background(), "myimage", types.ImagePushOptions{}) + if err == nil || err.Error() != "Error response from daemon: Unauthorized error" { + t.Fatalf("expected an Unauthorized Error, got %v", err) + } +} + +func TestImagePushWithUnauthorizedErrorAndPrivilegeFuncError(t *testing.T) { + client := &Client{ + transport: newMockClient(nil, errorMock(http.StatusUnauthorized, "Unauthorized error")), + } + privilegeFunc := func() (string, error) { + return "", fmt.Errorf("Error requesting privilege") + } + _, err := client.ImagePush(context.Background(), "myimage", types.ImagePushOptions{ + PrivilegeFunc: privilegeFunc, + }) + if err == nil || err.Error() != "Error requesting privilege" { + t.Fatalf("expected an error requesting privilege, got %v", err) + } +} + +func TestImagePushWithUnauthorizedErrorAndAnotherUnauthorizedError(t *testing.T) { + client := &Client{ + transport: newMockClient(nil, errorMock(http.StatusUnauthorized, "Unauthorized error")), + } + privilegeFunc := func() (string, error) { + return "a-auth-header", nil + } + _, err := client.ImagePush(context.Background(), "myimage", types.ImagePushOptions{ + PrivilegeFunc: privilegeFunc, + }) + if err == nil || err.Error() != "Error response from daemon: Unauthorized error" { + t.Fatalf("expected an Unauthorized Error, got %v", err) + } +} + +func TestImagePushWithPrivilegedFuncNoError(t *testing.T) { + expectedURL := "/images/myimage/push" + client := &Client{ + transport: newMockClient(nil, func(req *http.Request) (*http.Response, error) { + if !strings.HasPrefix(req.URL.Path, expectedURL) { + return nil, fmt.Errorf("Expected URL '%s', got '%s'", expectedURL, req.URL) + } + auth := req.Header.Get("X-Registry-Auth") + if auth == "NotValid" { + return &http.Response{ + StatusCode: http.StatusUnauthorized, + Body: ioutil.NopCloser(bytes.NewReader([]byte("Invalid credentials"))), + }, nil + } + if auth != "IAmValid" { + return nil, fmt.Errorf("Invalid auth header : expected %s, got %s", "IAmValid", auth) + } + query := req.URL.Query() + tag := query.Get("tag") + if tag != "tag" { + return nil, fmt.Errorf("tag not set in URL query properly. Expected '%s', got %s", "tag", tag) + } + return &http.Response{ + StatusCode: http.StatusOK, + Body: ioutil.NopCloser(bytes.NewReader([]byte("hello world"))), + }, nil + }), + } + privilegeFunc := func() (string, error) { + return "IAmValid", nil + } + resp, err := client.ImagePush(context.Background(), "myimage:tag", types.ImagePushOptions{ + RegistryAuth: "NotValid", + PrivilegeFunc: privilegeFunc, + }) + if err != nil { + t.Fatal(err) + } + body, err := ioutil.ReadAll(resp) + if err != nil { + t.Fatal(err) + } + if string(body) != "hello world" { + t.Fatalf("expected 'hello world', got %s", string(body)) + } +} + +func TestImagePushWithoutErrors(t *testing.T) { + expectedOutput := "hello world" + expectedURLFormat := "/images/%s/push" + pullCases := []struct { + reference string + expectedImage string + expectedTag string + }{ + { + reference: "myimage", + expectedImage: "myimage", + expectedTag: "", + }, + { + reference: "myimage:tag", + expectedImage: "myimage", + expectedTag: "tag", + }, + } + for _, pullCase := range pullCases { + client := &Client{ + transport: newMockClient(nil, func(req *http.Request) (*http.Response, error) { + expectedURL := fmt.Sprintf(expectedURLFormat, pullCase.expectedImage) + if !strings.HasPrefix(req.URL.Path, expectedURL) { + return nil, fmt.Errorf("Expected URL '%s', got '%s'", expectedURL, req.URL) + } + query := req.URL.Query() + tag := query.Get("tag") + if tag != pullCase.expectedTag { + return nil, fmt.Errorf("tag not set in URL query properly. Expected '%s', got %s", pullCase.expectedTag, tag) + } + return &http.Response{ + StatusCode: http.StatusOK, + Body: ioutil.NopCloser(bytes.NewReader([]byte(expectedOutput))), + }, nil + }), + } + resp, err := client.ImagePush(context.Background(), pullCase.reference, types.ImagePushOptions{}) + if err != nil { + t.Fatal(err) + } + body, err := ioutil.ReadAll(resp) + if err != nil { + t.Fatal(err) + } + if string(body) != expectedOutput { + t.Fatalf("expected '%s', got %s", expectedOutput, string(body)) + } + } +} diff --git a/image_remove.go b/image_remove.go new file mode 100644 index 0000000000..839e5311c4 --- /dev/null +++ b/image_remove.go @@ -0,0 +1,31 @@ +package client + +import ( + "encoding/json" + "net/url" + + "github.com/docker/docker/api/types" + "golang.org/x/net/context" +) + +// ImageRemove removes an image from the docker host. +func (cli *Client) ImageRemove(ctx context.Context, imageID string, options types.ImageRemoveOptions) ([]types.ImageDelete, error) { + query := url.Values{} + + if options.Force { + query.Set("force", "1") + } + if !options.PruneChildren { + query.Set("noprune", "1") + } + + resp, err := cli.delete(ctx, "/images/"+imageID, query, nil) + if err != nil { + return nil, err + } + + var dels []types.ImageDelete + err = json.NewDecoder(resp.body).Decode(&dels) + ensureReaderClosed(resp) + return dels, err +} diff --git a/image_remove_test.go b/image_remove_test.go new file mode 100644 index 0000000000..696d06729d --- /dev/null +++ b/image_remove_test.go @@ -0,0 +1,95 @@ +package client + +import ( + "bytes" + "encoding/json" + "fmt" + "io/ioutil" + "net/http" + "strings" + "testing" + + "github.com/docker/docker/api/types" + "golang.org/x/net/context" +) + +func TestImageRemoveError(t *testing.T) { + client := &Client{ + transport: newMockClient(nil, errorMock(http.StatusInternalServerError, "Server error")), + } + + _, err := client.ImageRemove(context.Background(), "image_id", types.ImageRemoveOptions{}) + if err == nil || err.Error() != "Error response from daemon: Server error" { + t.Fatalf("expected a Server Error, got %v", err) + } +} + +func TestImageRemove(t *testing.T) { + expectedURL := "/images/image_id" + removeCases := []struct { + force bool + pruneChildren bool + expectedQueryParams map[string]string + }{ + { + force: false, + pruneChildren: false, + expectedQueryParams: map[string]string{ + "force": "", + "noprune": "1", + }, + }, { + force: true, + pruneChildren: true, + expectedQueryParams: map[string]string{ + "force": "1", + "noprune": "", + }, + }, + } + for _, removeCase := range removeCases { + client := &Client{ + transport: newMockClient(nil, func(req *http.Request) (*http.Response, error) { + if !strings.HasPrefix(req.URL.Path, expectedURL) { + return nil, fmt.Errorf("expected URL '%s', got '%s'", expectedURL, req.URL) + } + if req.Method != "DELETE" { + return nil, fmt.Errorf("expected DELETE method, got %s", req.Method) + } + query := req.URL.Query() + for key, expected := range removeCase.expectedQueryParams { + actual := query.Get(key) + if actual != expected { + return nil, fmt.Errorf("%s not set in URL query properly. Expected '%s', got %s", key, expected, actual) + } + } + b, err := json.Marshal([]types.ImageDelete{ + { + Untagged: "image_id1", + }, + { + Deleted: "image_id", + }, + }) + if err != nil { + return nil, err + } + + return &http.Response{ + StatusCode: http.StatusOK, + Body: ioutil.NopCloser(bytes.NewReader(b)), + }, nil + }), + } + imageDeletes, err := client.ImageRemove(context.Background(), "image_id", types.ImageRemoveOptions{ + Force: removeCase.force, + PruneChildren: removeCase.pruneChildren, + }) + if err != nil { + t.Fatal(err) + } + if len(imageDeletes) != 2 { + t.Fatalf("expected 2 deleted images, got %v", imageDeletes) + } + } +} diff --git a/image_save.go b/image_save.go new file mode 100644 index 0000000000..ecac880a32 --- /dev/null +++ b/image_save.go @@ -0,0 +1,22 @@ +package client + +import ( + "io" + "net/url" + + "golang.org/x/net/context" +) + +// ImageSave retrieves one or more images from the docker host as an io.ReadCloser. +// It's up to the caller to store the images and close the stream. +func (cli *Client) ImageSave(ctx context.Context, imageIDs []string) (io.ReadCloser, error) { + query := url.Values{ + "names": imageIDs, + } + + resp, err := cli.get(ctx, "/images/get", query, nil) + if err != nil { + return nil, err + } + return resp.body, nil +} diff --git a/image_save_test.go b/image_save_test.go new file mode 100644 index 0000000000..8ee40c43ae --- /dev/null +++ b/image_save_test.go @@ -0,0 +1,58 @@ +package client + +import ( + "bytes" + "fmt" + "io/ioutil" + "net/http" + "reflect" + "testing" + + "golang.org/x/net/context" + + "strings" +) + +func TestImageSaveError(t *testing.T) { + client := &Client{ + transport: newMockClient(nil, errorMock(http.StatusInternalServerError, "Server error")), + } + _, err := client.ImageSave(context.Background(), []string{"nothing"}) + if err == nil || err.Error() != "Error response from daemon: Server error" { + t.Fatalf("expected a Server error, got %v", err) + } +} + +func TestImageSave(t *testing.T) { + expectedURL := "/images/get" + client := &Client{ + transport: newMockClient(nil, func(r *http.Request) (*http.Response, error) { + if !strings.HasPrefix(r.URL.Path, expectedURL) { + return nil, fmt.Errorf("Expected URL '%s', got '%s'", expectedURL, r.URL) + } + query := r.URL.Query() + names := query["names"] + expectedNames := []string{"image_id1", "image_id2"} + if !reflect.DeepEqual(names, expectedNames) { + return nil, fmt.Errorf("names not set in URL query properly. Expected %v, got %v", names, expectedNames) + } + + return &http.Response{ + StatusCode: http.StatusOK, + Body: ioutil.NopCloser(bytes.NewReader([]byte("response"))), + }, nil + }), + } + saveResponse, err := client.ImageSave(context.Background(), []string{"image_id1", "image_id2"}) + if err != nil { + t.Fatal(err) + } + response, err := ioutil.ReadAll(saveResponse) + if err != nil { + t.Fatal(err) + } + saveResponse.Close() + if string(response) != "response" { + t.Fatalf("expected response to contain 'response', got %s", string(response)) + } +} diff --git a/image_search.go b/image_search.go new file mode 100644 index 0000000000..b0fcd5c23d --- /dev/null +++ b/image_search.go @@ -0,0 +1,51 @@ +package client + +import ( + "encoding/json" + "fmt" + "net/http" + "net/url" + + "github.com/docker/docker/api/types" + "github.com/docker/docker/api/types/filters" + "github.com/docker/docker/api/types/registry" + "golang.org/x/net/context" +) + +// ImageSearch makes the docker host to search by a term in a remote registry. +// The list of results is not sorted in any fashion. +func (cli *Client) ImageSearch(ctx context.Context, term string, options types.ImageSearchOptions) ([]registry.SearchResult, error) { + var results []registry.SearchResult + query := url.Values{} + query.Set("term", term) + query.Set("limit", fmt.Sprintf("%d", options.Limit)) + + if options.Filters.Len() > 0 { + filterJSON, err := filters.ToParam(options.Filters) + if err != nil { + return results, err + } + query.Set("filters", filterJSON) + } + + resp, err := cli.tryImageSearch(ctx, query, options.RegistryAuth) + if resp.statusCode == http.StatusUnauthorized && options.PrivilegeFunc != nil { + newAuthHeader, privilegeErr := options.PrivilegeFunc() + if privilegeErr != nil { + return results, privilegeErr + } + resp, err = cli.tryImageSearch(ctx, query, newAuthHeader) + } + if err != nil { + return results, err + } + + err = json.NewDecoder(resp.body).Decode(&results) + ensureReaderClosed(resp) + return results, err +} + +func (cli *Client) tryImageSearch(ctx context.Context, query url.Values, registryAuth string) (serverResponse, error) { + headers := map[string][]string{"X-Registry-Auth": {registryAuth}} + return cli.get(ctx, "/images/search", query, headers) +} diff --git a/image_search_test.go b/image_search_test.go new file mode 100644 index 0000000000..2f21b2cc14 --- /dev/null +++ b/image_search_test.go @@ -0,0 +1,165 @@ +package client + +import ( + "bytes" + "fmt" + "io/ioutil" + "net/http" + "strings" + "testing" + + "golang.org/x/net/context" + + "encoding/json" + "github.com/docker/docker/api/types" + "github.com/docker/docker/api/types/filters" + "github.com/docker/docker/api/types/registry" +) + +func TestImageSearchAnyError(t *testing.T) { + client := &Client{ + transport: newMockClient(nil, errorMock(http.StatusInternalServerError, "Server error")), + } + _, err := client.ImageSearch(context.Background(), "some-image", types.ImageSearchOptions{}) + if err == nil || err.Error() != "Error response from daemon: Server error" { + t.Fatalf("expected a Server Error, got %v", err) + } +} + +func TestImageSearchStatusUnauthorizedError(t *testing.T) { + client := &Client{ + transport: newMockClient(nil, errorMock(http.StatusUnauthorized, "Unauthorized error")), + } + _, err := client.ImageSearch(context.Background(), "some-image", types.ImageSearchOptions{}) + if err == nil || err.Error() != "Error response from daemon: Unauthorized error" { + t.Fatalf("expected an Unauthorized Error, got %v", err) + } +} + +func TestImageSearchWithUnauthorizedErrorAndPrivilegeFuncError(t *testing.T) { + client := &Client{ + transport: newMockClient(nil, errorMock(http.StatusUnauthorized, "Unauthorized error")), + } + privilegeFunc := func() (string, error) { + return "", fmt.Errorf("Error requesting privilege") + } + _, err := client.ImageSearch(context.Background(), "some-image", types.ImageSearchOptions{ + PrivilegeFunc: privilegeFunc, + }) + if err == nil || err.Error() != "Error requesting privilege" { + t.Fatalf("expected an error requesting privilege, got %v", err) + } +} + +func TestImageSearchWithUnauthorizedErrorAndAnotherUnauthorizedError(t *testing.T) { + client := &Client{ + transport: newMockClient(nil, errorMock(http.StatusUnauthorized, "Unauthorized error")), + } + privilegeFunc := func() (string, error) { + return "a-auth-header", nil + } + _, err := client.ImageSearch(context.Background(), "some-image", types.ImageSearchOptions{ + PrivilegeFunc: privilegeFunc, + }) + if err == nil || err.Error() != "Error response from daemon: Unauthorized error" { + t.Fatalf("expected an Unauthorized Error, got %v", err) + } +} + +func TestImageSearchWithPrivilegedFuncNoError(t *testing.T) { + expectedURL := "/images/search" + client := &Client{ + transport: newMockClient(nil, func(req *http.Request) (*http.Response, error) { + if !strings.HasPrefix(req.URL.Path, expectedURL) { + return nil, fmt.Errorf("Expected URL '%s', got '%s'", expectedURL, req.URL) + } + auth := req.Header.Get("X-Registry-Auth") + if auth == "NotValid" { + return &http.Response{ + StatusCode: http.StatusUnauthorized, + Body: ioutil.NopCloser(bytes.NewReader([]byte("Invalid credentials"))), + }, nil + } + if auth != "IAmValid" { + return nil, fmt.Errorf("Invalid auth header : expected %s, got %s", "IAmValid", auth) + } + query := req.URL.Query() + term := query.Get("term") + if term != "some-image" { + return nil, fmt.Errorf("tag not set in URL query properly. Expected '%s', got %s", "some-image", term) + } + content, err := json.Marshal([]registry.SearchResult{ + { + Name: "anything", + }, + }) + if err != nil { + return nil, err + } + return &http.Response{ + StatusCode: http.StatusOK, + Body: ioutil.NopCloser(bytes.NewReader(content)), + }, nil + }), + } + privilegeFunc := func() (string, error) { + return "IAmValid", nil + } + results, err := client.ImageSearch(context.Background(), "some-image", types.ImageSearchOptions{ + RegistryAuth: "NotValid", + PrivilegeFunc: privilegeFunc, + }) + if err != nil { + t.Fatal(err) + } + if len(results) != 1 { + t.Fatalf("expected a result, got %v", results) + } +} + +func TestImageSearchWithoutErrors(t *testing.T) { + expectedURL := "/images/search" + filterArgs := filters.NewArgs() + filterArgs.Add("is-automated", "true") + filterArgs.Add("stars", "3") + + expectedFilters := `{"is-automated":{"true":true},"stars":{"3":true}}` + + client := &Client{ + transport: newMockClient(nil, func(req *http.Request) (*http.Response, error) { + if !strings.HasPrefix(req.URL.Path, expectedURL) { + return nil, fmt.Errorf("Expected URL '%s', got '%s'", expectedURL, req.URL) + } + query := req.URL.Query() + term := query.Get("term") + if term != "some-image" { + return nil, fmt.Errorf("tag not set in URL query properly. Expected '%s', got %s", "some-image", term) + } + filters := query.Get("filters") + if filters != expectedFilters { + return nil, fmt.Errorf("filters not set in URL query properly. Expected '%s', got %s", expectedFilters, filters) + } + content, err := json.Marshal([]registry.SearchResult{ + { + Name: "anything", + }, + }) + if err != nil { + return nil, err + } + return &http.Response{ + StatusCode: http.StatusOK, + Body: ioutil.NopCloser(bytes.NewReader(content)), + }, nil + }), + } + results, err := client.ImageSearch(context.Background(), "some-image", types.ImageSearchOptions{ + Filters: filterArgs, + }) + if err != nil { + t.Fatal(err) + } + if len(results) != 1 { + t.Fatalf("expected a result, got %v", results) + } +} diff --git a/image_tag.go b/image_tag.go new file mode 100644 index 0000000000..bdbf94add2 --- /dev/null +++ b/image_tag.go @@ -0,0 +1,34 @@ +package client + +import ( + "errors" + "fmt" + "net/url" + + "golang.org/x/net/context" + + distreference "github.com/docker/distribution/reference" + "github.com/docker/docker/api/types/reference" +) + +// ImageTag tags an image in the docker host +func (cli *Client) ImageTag(ctx context.Context, imageID, ref string) error { + distributionRef, err := distreference.ParseNamed(ref) + if err != nil { + return fmt.Errorf("Error parsing reference: %q is not a valid repository/tag", ref) + } + + if _, isCanonical := distributionRef.(distreference.Canonical); isCanonical { + return errors.New("refusing to create a tag with a digest reference") + } + + tag := reference.GetTagFromNamedRef(distributionRef) + + query := url.Values{} + query.Set("repo", distributionRef.Name()) + query.Set("tag", tag) + + resp, err := cli.post(ctx, "/images/"+imageID+"/tag", query, nil, nil) + ensureReaderClosed(resp) + return err +} diff --git a/image_tag_test.go b/image_tag_test.go new file mode 100644 index 0000000000..f3571dfdd3 --- /dev/null +++ b/image_tag_test.go @@ -0,0 +1,121 @@ +package client + +import ( + "bytes" + "fmt" + "io/ioutil" + "net/http" + "strings" + "testing" + + "golang.org/x/net/context" +) + +func TestImageTagError(t *testing.T) { + client := &Client{ + transport: newMockClient(nil, errorMock(http.StatusInternalServerError, "Server error")), + } + + err := client.ImageTag(context.Background(), "image_id", "repo:tag") + if err == nil || err.Error() != "Error response from daemon: Server error" { + t.Fatalf("expected a Server Error, got %v", err) + } +} + +// Note: this is not testing all the InvalidReference as it's the reponsability +// of distribution/reference package. +func TestImageTagInvalidReference(t *testing.T) { + client := &Client{ + transport: newMockClient(nil, errorMock(http.StatusInternalServerError, "Server error")), + } + + err := client.ImageTag(context.Background(), "image_id", "aa/asdf$$^/aa") + if err == nil || err.Error() != `Error parsing reference: "aa/asdf$$^/aa" is not a valid repository/tag` { + t.Fatalf("expected ErrReferenceInvalidFormat, got %v", err) + } +} + +func TestImageTag(t *testing.T) { + expectedURL := "/images/image_id/tag" + tagCases := []struct { + reference string + expectedQueryParams map[string]string + }{ + { + reference: "repository:tag1", + expectedQueryParams: map[string]string{ + "repo": "repository", + "tag": "tag1", + }, + }, { + reference: "another_repository:latest", + expectedQueryParams: map[string]string{ + "repo": "another_repository", + "tag": "latest", + }, + }, { + reference: "another_repository", + expectedQueryParams: map[string]string{ + "repo": "another_repository", + "tag": "latest", + }, + }, { + reference: "test/another_repository", + expectedQueryParams: map[string]string{ + "repo": "test/another_repository", + "tag": "latest", + }, + }, { + reference: "test/another_repository:tag1", + expectedQueryParams: map[string]string{ + "repo": "test/another_repository", + "tag": "tag1", + }, + }, { + reference: "test/test/another_repository:tag1", + expectedQueryParams: map[string]string{ + "repo": "test/test/another_repository", + "tag": "tag1", + }, + }, { + reference: "test:5000/test/another_repository:tag1", + expectedQueryParams: map[string]string{ + "repo": "test:5000/test/another_repository", + "tag": "tag1", + }, + }, { + reference: "test:5000/test/another_repository", + expectedQueryParams: map[string]string{ + "repo": "test:5000/test/another_repository", + "tag": "latest", + }, + }, + } + for _, tagCase := range tagCases { + client := &Client{ + transport: newMockClient(nil, func(req *http.Request) (*http.Response, error) { + if !strings.HasPrefix(req.URL.Path, expectedURL) { + return nil, fmt.Errorf("expected URL '%s', got '%s'", expectedURL, req.URL) + } + if req.Method != "POST" { + return nil, fmt.Errorf("expected POST method, got %s", req.Method) + } + query := req.URL.Query() + for key, expected := range tagCase.expectedQueryParams { + actual := query.Get(key) + if actual != expected { + return nil, fmt.Errorf("%s not set in URL query properly. Expected '%s', got %s", key, expected, actual) + } + } + return &http.Response{ + StatusCode: http.StatusOK, + Body: ioutil.NopCloser(bytes.NewReader([]byte(""))), + }, nil + }), + } + err := client.ImageTag(context.Background(), "image_id", tagCase.reference) + if err != nil { + t.Fatal(err) + } + } +} diff --git a/info.go b/info.go new file mode 100644 index 0000000000..ac07961224 --- /dev/null +++ b/info.go @@ -0,0 +1,26 @@ +package client + +import ( + "encoding/json" + "fmt" + "net/url" + + "github.com/docker/docker/api/types" + "golang.org/x/net/context" +) + +// Info returns information about the docker server. +func (cli *Client) Info(ctx context.Context) (types.Info, error) { + var info types.Info + serverResp, err := cli.get(ctx, "/info", url.Values{}, nil) + if err != nil { + return info, err + } + defer ensureReaderClosed(serverResp) + + if err := json.NewDecoder(serverResp.body).Decode(&info); err != nil { + return info, fmt.Errorf("Error reading remote info: %v", err) + } + + return info, nil +} diff --git a/info_test.go b/info_test.go new file mode 100644 index 0000000000..9d51b1a78b --- /dev/null +++ b/info_test.go @@ -0,0 +1,76 @@ +package client + +import ( + "bytes" + "encoding/json" + "fmt" + "io/ioutil" + "net/http" + "strings" + "testing" + + "github.com/docker/docker/api/types" + "golang.org/x/net/context" +) + +func TestInfoServerError(t *testing.T) { + client := &Client{ + transport: newMockClient(nil, errorMock(http.StatusInternalServerError, "Server error")), + } + _, err := client.Info(context.Background()) + if err == nil || err.Error() != "Error response from daemon: Server error" { + t.Fatalf("expected a Server Error, got %v", err) + } +} + +func TestInfoInvalidResponseJSONError(t *testing.T) { + client := &Client{ + transport: newMockClient(nil, func(req *http.Request) (*http.Response, error) { + return &http.Response{ + StatusCode: http.StatusOK, + Body: ioutil.NopCloser(bytes.NewReader([]byte("invalid json"))), + }, nil + }), + } + _, err := client.Info(context.Background()) + if err == nil || !strings.Contains(err.Error(), "invalid character") { + t.Fatalf("expected a 'invalid character' error, got %v", err) + } +} + +func TestInfo(t *testing.T) { + expectedURL := "/info" + client := &Client{ + transport: newMockClient(nil, func(req *http.Request) (*http.Response, error) { + if !strings.HasPrefix(req.URL.Path, expectedURL) { + return nil, fmt.Errorf("Expected URL '%s', got '%s'", expectedURL, req.URL) + } + info := &types.Info{ + ID: "daemonID", + Containers: 3, + } + b, err := json.Marshal(info) + if err != nil { + return nil, err + } + + return &http.Response{ + StatusCode: http.StatusOK, + Body: ioutil.NopCloser(bytes.NewReader(b)), + }, nil + }), + } + + info, err := client.Info(context.Background()) + if err != nil { + t.Fatal(err) + } + + if info.ID != "daemonID" { + t.Fatalf("expected daemonID, got %s", info.ID) + } + + if info.Containers != 3 { + t.Fatalf("expected 3 containers, got %d", info.Containers) + } +} diff --git a/interface.go b/interface.go new file mode 100644 index 0000000000..1bfeb6aeb6 --- /dev/null +++ b/interface.go @@ -0,0 +1,135 @@ +package client + +import ( + "io" + "time" + + "github.com/docker/docker/api/types" + "github.com/docker/docker/api/types/container" + "github.com/docker/docker/api/types/filters" + "github.com/docker/docker/api/types/network" + "github.com/docker/docker/api/types/registry" + "github.com/docker/docker/api/types/swarm" + "golang.org/x/net/context" +) + +// CommonAPIClient is the common methods between stable and experimental versions of APIClient. +type CommonAPIClient interface { + ContainerAPIClient + ImageAPIClient + NodeAPIClient + NetworkAPIClient + ServiceAPIClient + SwarmAPIClient + SystemAPIClient + VolumeAPIClient + ClientVersion() string + ServerVersion(ctx context.Context) (types.Version, error) + UpdateClientVersion(v string) +} + +// ContainerAPIClient defines API client methods for the containers +type ContainerAPIClient interface { + ContainerAttach(ctx context.Context, container string, options types.ContainerAttachOptions) (types.HijackedResponse, error) + ContainerCommit(ctx context.Context, container string, options types.ContainerCommitOptions) (types.ContainerCommitResponse, error) + ContainerCreate(ctx context.Context, config *container.Config, hostConfig *container.HostConfig, networkingConfig *network.NetworkingConfig, containerName string) (types.ContainerCreateResponse, error) + ContainerDiff(ctx context.Context, container string) ([]types.ContainerChange, error) + ContainerExecAttach(ctx context.Context, execID string, config types.ExecConfig) (types.HijackedResponse, error) + ContainerExecCreate(ctx context.Context, container string, config types.ExecConfig) (types.ContainerExecCreateResponse, error) + ContainerExecInspect(ctx context.Context, execID string) (types.ContainerExecInspect, error) + ContainerExecResize(ctx context.Context, execID string, options types.ResizeOptions) error + ContainerExecStart(ctx context.Context, execID string, config types.ExecStartCheck) error + ContainerExport(ctx context.Context, container string) (io.ReadCloser, error) + ContainerInspect(ctx context.Context, container string) (types.ContainerJSON, error) + ContainerInspectWithRaw(ctx context.Context, container string, getSize bool) (types.ContainerJSON, []byte, error) + ContainerKill(ctx context.Context, container, signal string) error + ContainerList(ctx context.Context, options types.ContainerListOptions) ([]types.Container, error) + ContainerLogs(ctx context.Context, container string, options types.ContainerLogsOptions) (io.ReadCloser, error) + ContainerPause(ctx context.Context, container string) error + ContainerRemove(ctx context.Context, container string, options types.ContainerRemoveOptions) error + ContainerRename(ctx context.Context, container, newContainerName string) error + ContainerResize(ctx context.Context, container string, options types.ResizeOptions) error + ContainerRestart(ctx context.Context, container string, timeout *time.Duration) error + ContainerStatPath(ctx context.Context, container, path string) (types.ContainerPathStat, error) + ContainerStats(ctx context.Context, container string, stream bool) (io.ReadCloser, error) + ContainerStart(ctx context.Context, container string, options types.ContainerStartOptions) error + ContainerStop(ctx context.Context, container string, timeout *time.Duration) error + ContainerTop(ctx context.Context, container string, arguments []string) (types.ContainerProcessList, error) + ContainerUnpause(ctx context.Context, container string) error + ContainerUpdate(ctx context.Context, container string, updateConfig container.UpdateConfig) (types.ContainerUpdateResponse, error) + ContainerWait(ctx context.Context, container string) (int, error) + CopyFromContainer(ctx context.Context, container, srcPath string) (io.ReadCloser, types.ContainerPathStat, error) + CopyToContainer(ctx context.Context, container, path string, content io.Reader, options types.CopyToContainerOptions) error +} + +// ImageAPIClient defines API client methods for the images +type ImageAPIClient interface { + ImageBuild(ctx context.Context, context io.Reader, options types.ImageBuildOptions) (types.ImageBuildResponse, error) + ImageCreate(ctx context.Context, parentReference string, options types.ImageCreateOptions) (io.ReadCloser, error) + ImageHistory(ctx context.Context, image string) ([]types.ImageHistory, error) + ImageImport(ctx context.Context, source types.ImageImportSource, ref string, options types.ImageImportOptions) (io.ReadCloser, error) + ImageInspectWithRaw(ctx context.Context, image string) (types.ImageInspect, []byte, error) + ImageList(ctx context.Context, options types.ImageListOptions) ([]types.Image, error) + ImageLoad(ctx context.Context, input io.Reader, quiet bool) (types.ImageLoadResponse, error) + ImagePull(ctx context.Context, ref string, options types.ImagePullOptions) (io.ReadCloser, error) + ImagePush(ctx context.Context, ref string, options types.ImagePushOptions) (io.ReadCloser, error) + ImageRemove(ctx context.Context, image string, options types.ImageRemoveOptions) ([]types.ImageDelete, error) + ImageSearch(ctx context.Context, term string, options types.ImageSearchOptions) ([]registry.SearchResult, error) + ImageSave(ctx context.Context, images []string) (io.ReadCloser, error) + ImageTag(ctx context.Context, image, ref string) error +} + +// NetworkAPIClient defines API client methods for the networks +type NetworkAPIClient interface { + NetworkConnect(ctx context.Context, networkID, container string, config *network.EndpointSettings) error + NetworkCreate(ctx context.Context, name string, options types.NetworkCreate) (types.NetworkCreateResponse, error) + NetworkDisconnect(ctx context.Context, networkID, container string, force bool) error + NetworkInspect(ctx context.Context, networkID string) (types.NetworkResource, error) + NetworkInspectWithRaw(ctx context.Context, networkID string) (types.NetworkResource, []byte, error) + NetworkList(ctx context.Context, options types.NetworkListOptions) ([]types.NetworkResource, error) + NetworkRemove(ctx context.Context, networkID string) error +} + +// NodeAPIClient defines API client methods for the nodes +type NodeAPIClient interface { + NodeInspectWithRaw(ctx context.Context, nodeID string) (swarm.Node, []byte, error) + NodeList(ctx context.Context, options types.NodeListOptions) ([]swarm.Node, error) + NodeRemove(ctx context.Context, nodeID string, options types.NodeRemoveOptions) error + NodeUpdate(ctx context.Context, nodeID string, version swarm.Version, node swarm.NodeSpec) error +} + +// ServiceAPIClient defines API client methods for the services +type ServiceAPIClient interface { + ServiceCreate(ctx context.Context, service swarm.ServiceSpec, options types.ServiceCreateOptions) (types.ServiceCreateResponse, error) + ServiceInspectWithRaw(ctx context.Context, serviceID string) (swarm.Service, []byte, error) + ServiceList(ctx context.Context, options types.ServiceListOptions) ([]swarm.Service, error) + ServiceRemove(ctx context.Context, serviceID string) error + ServiceUpdate(ctx context.Context, serviceID string, version swarm.Version, service swarm.ServiceSpec, options types.ServiceUpdateOptions) error + TaskInspectWithRaw(ctx context.Context, taskID string) (swarm.Task, []byte, error) + TaskList(ctx context.Context, options types.TaskListOptions) ([]swarm.Task, error) +} + +// SwarmAPIClient defines API client methods for the swarm +type SwarmAPIClient interface { + SwarmInit(ctx context.Context, req swarm.InitRequest) (string, error) + SwarmJoin(ctx context.Context, req swarm.JoinRequest) error + SwarmLeave(ctx context.Context, force bool) error + SwarmInspect(ctx context.Context) (swarm.Swarm, error) + SwarmUpdate(ctx context.Context, version swarm.Version, swarm swarm.Spec, flags swarm.UpdateFlags) error +} + +// SystemAPIClient defines API client methods for the system +type SystemAPIClient interface { + Events(ctx context.Context, options types.EventsOptions) (io.ReadCloser, error) + Info(ctx context.Context) (types.Info, error) + RegistryLogin(ctx context.Context, auth types.AuthConfig) (types.AuthResponse, error) +} + +// VolumeAPIClient defines API client methods for the volumes +type VolumeAPIClient interface { + VolumeCreate(ctx context.Context, options types.VolumeCreateRequest) (types.Volume, error) + VolumeInspect(ctx context.Context, volumeID string) (types.Volume, error) + VolumeInspectWithRaw(ctx context.Context, volumeID string) (types.Volume, []byte, error) + VolumeList(ctx context.Context, filter filters.Args) (types.VolumesListResponse, error) + VolumeRemove(ctx context.Context, volumeID string, force bool) error +} diff --git a/interface_experimental.go b/interface_experimental.go new file mode 100644 index 0000000000..1ddc517c9a --- /dev/null +++ b/interface_experimental.go @@ -0,0 +1,37 @@ +// +build experimental + +package client + +import ( + "github.com/docker/docker/api/types" + "golang.org/x/net/context" +) + +// APIClient is an interface that clients that talk with a docker server must implement. +type APIClient interface { + CommonAPIClient + CheckpointAPIClient + PluginAPIClient +} + +// CheckpointAPIClient defines API client methods for the checkpoints +type CheckpointAPIClient interface { + CheckpointCreate(ctx context.Context, container string, options types.CheckpointCreateOptions) error + CheckpointDelete(ctx context.Context, container string, checkpointID string) error + CheckpointList(ctx context.Context, container string) ([]types.Checkpoint, error) +} + +// PluginAPIClient defines API client methods for the plugins +type PluginAPIClient interface { + PluginList(ctx context.Context) (types.PluginsListResponse, error) + PluginRemove(ctx context.Context, name string, options types.PluginRemoveOptions) error + PluginEnable(ctx context.Context, name string) error + PluginDisable(ctx context.Context, name string) error + PluginInstall(ctx context.Context, name string, options types.PluginInstallOptions) error + PluginPush(ctx context.Context, name string, registryAuth string) error + PluginSet(ctx context.Context, name string, args []string) error + PluginInspectWithRaw(ctx context.Context, name string) (*types.Plugin, []byte, error) +} + +// Ensure that Client always implements APIClient. +var _ APIClient = &Client{} diff --git a/interface_stable.go b/interface_stable.go new file mode 100644 index 0000000000..496f522d51 --- /dev/null +++ b/interface_stable.go @@ -0,0 +1,11 @@ +// +build !experimental + +package client + +// APIClient is an interface that clients that talk with a docker server must implement. +type APIClient interface { + CommonAPIClient +} + +// Ensure that Client always implements APIClient. +var _ APIClient = &Client{} diff --git a/login.go b/login.go new file mode 100644 index 0000000000..d8d277ccba --- /dev/null +++ b/login.go @@ -0,0 +1,28 @@ +package client + +import ( + "encoding/json" + "net/http" + "net/url" + + "github.com/docker/docker/api/types" + "golang.org/x/net/context" +) + +// RegistryLogin authenticates the docker server with a given docker registry. +// It returns UnauthorizerError when the authentication fails. +func (cli *Client) RegistryLogin(ctx context.Context, auth types.AuthConfig) (types.AuthResponse, error) { + resp, err := cli.post(ctx, "/auth", url.Values{}, auth, nil) + + if resp.statusCode == http.StatusUnauthorized { + return types.AuthResponse{}, unauthorizedError{err} + } + if err != nil { + return types.AuthResponse{}, err + } + + var response types.AuthResponse + err = json.NewDecoder(resp.body).Decode(&response) + ensureReaderClosed(resp) + return response, err +} diff --git a/network_connect.go b/network_connect.go new file mode 100644 index 0000000000..c022c17b5b --- /dev/null +++ b/network_connect.go @@ -0,0 +1,18 @@ +package client + +import ( + "github.com/docker/docker/api/types" + "github.com/docker/docker/api/types/network" + "golang.org/x/net/context" +) + +// NetworkConnect connects a container to an existent network in the docker host. +func (cli *Client) NetworkConnect(ctx context.Context, networkID, containerID string, config *network.EndpointSettings) error { + nc := types.NetworkConnect{ + Container: containerID, + EndpointConfig: config, + } + resp, err := cli.post(ctx, "/networks/"+networkID+"/connect", nil, nc, nil) + ensureReaderClosed(resp) + return err +} diff --git a/network_connect_test.go b/network_connect_test.go new file mode 100644 index 0000000000..95b149e685 --- /dev/null +++ b/network_connect_test.go @@ -0,0 +1,107 @@ +package client + +import ( + "bytes" + "encoding/json" + "fmt" + "io/ioutil" + "net/http" + "strings" + "testing" + + "golang.org/x/net/context" + + "github.com/docker/docker/api/types" + "github.com/docker/docker/api/types/network" +) + +func TestNetworkConnectError(t *testing.T) { + client := &Client{ + transport: newMockClient(nil, errorMock(http.StatusInternalServerError, "Server error")), + } + + err := client.NetworkConnect(context.Background(), "network_id", "container_id", nil) + if err == nil || err.Error() != "Error response from daemon: Server error" { + t.Fatalf("expected a Server Error, got %v", err) + } +} + +func TestNetworkConnectEmptyNilEndpointSettings(t *testing.T) { + expectedURL := "/networks/network_id/connect" + + client := &Client{ + transport: newMockClient(nil, func(req *http.Request) (*http.Response, error) { + if !strings.HasPrefix(req.URL.Path, expectedURL) { + return nil, fmt.Errorf("Expected URL '%s', got '%s'", expectedURL, req.URL) + } + + if req.Method != "POST" { + return nil, fmt.Errorf("expected POST method, got %s", req.Method) + } + + var connect types.NetworkConnect + if err := json.NewDecoder(req.Body).Decode(&connect); err != nil { + return nil, err + } + + if connect.Container != "container_id" { + return nil, fmt.Errorf("expected 'container_id', got %s", connect.Container) + } + + if connect.EndpointConfig != nil { + return nil, fmt.Errorf("expected connect.EndpointConfig to be nil, got %v", connect.EndpointConfig) + } + + return &http.Response{ + StatusCode: http.StatusOK, + Body: ioutil.NopCloser(bytes.NewReader([]byte(""))), + }, nil + }), + } + + err := client.NetworkConnect(context.Background(), "network_id", "container_id", nil) + if err != nil { + t.Fatal(err) + } +} + +func TestNetworkConnect(t *testing.T) { + expectedURL := "/networks/network_id/connect" + + client := &Client{ + transport: newMockClient(nil, func(req *http.Request) (*http.Response, error) { + if !strings.HasPrefix(req.URL.Path, expectedURL) { + return nil, fmt.Errorf("Expected URL '%s', got '%s'", expectedURL, req.URL) + } + + if req.Method != "POST" { + return nil, fmt.Errorf("expected POST method, got %s", req.Method) + } + + var connect types.NetworkConnect + if err := json.NewDecoder(req.Body).Decode(&connect); err != nil { + return nil, err + } + + if connect.Container != "container_id" { + return nil, fmt.Errorf("expected 'container_id', got %s", connect.Container) + } + + if connect.EndpointConfig.NetworkID != "NetworkID" { + return nil, fmt.Errorf("expected 'NetworkID', got %s", connect.EndpointConfig.NetworkID) + } + + return &http.Response{ + StatusCode: http.StatusOK, + Body: ioutil.NopCloser(bytes.NewReader([]byte(""))), + }, nil + }), + } + + err := client.NetworkConnect(context.Background(), "network_id", "container_id", &network.EndpointSettings{ + NetworkID: "NetworkID", + }) + if err != nil { + t.Fatal(err) + } +} diff --git a/network_create.go b/network_create.go new file mode 100644 index 0000000000..4067a541ff --- /dev/null +++ b/network_create.go @@ -0,0 +1,25 @@ +package client + +import ( + "encoding/json" + + "github.com/docker/docker/api/types" + "golang.org/x/net/context" +) + +// NetworkCreate creates a new network in the docker host. +func (cli *Client) NetworkCreate(ctx context.Context, name string, options types.NetworkCreate) (types.NetworkCreateResponse, error) { + networkCreateRequest := types.NetworkCreateRequest{ + NetworkCreate: options, + Name: name, + } + var response types.NetworkCreateResponse + serverResp, err := cli.post(ctx, "/networks/create", nil, networkCreateRequest, nil) + if err != nil { + return response, err + } + + json.NewDecoder(serverResp.body).Decode(&response) + ensureReaderClosed(serverResp) + return response, err +} diff --git a/network_create_test.go b/network_create_test.go new file mode 100644 index 0000000000..611ed8173e --- /dev/null +++ b/network_create_test.go @@ -0,0 +1,72 @@ +package client + +import ( + "bytes" + "encoding/json" + "fmt" + "io/ioutil" + "net/http" + "strings" + "testing" + + "github.com/docker/docker/api/types" + "golang.org/x/net/context" +) + +func TestNetworkCreateError(t *testing.T) { + client := &Client{ + transport: newMockClient(nil, errorMock(http.StatusInternalServerError, "Server error")), + } + + _, err := client.NetworkCreate(context.Background(), "mynetwork", types.NetworkCreate{}) + if err == nil || err.Error() != "Error response from daemon: Server error" { + t.Fatalf("expected a Server Error, got %v", err) + } +} + +func TestNetworkCreate(t *testing.T) { + expectedURL := "/networks/create" + + client := &Client{ + transport: newMockClient(nil, func(req *http.Request) (*http.Response, error) { + if !strings.HasPrefix(req.URL.Path, expectedURL) { + return nil, fmt.Errorf("Expected URL '%s', got '%s'", expectedURL, req.URL) + } + + if req.Method != "POST" { + return nil, fmt.Errorf("expected POST method, got %s", req.Method) + } + + content, err := json.Marshal(types.NetworkCreateResponse{ + ID: "network_id", + Warning: "warning", + }) + if err != nil { + return nil, err + } + return &http.Response{ + StatusCode: http.StatusOK, + Body: ioutil.NopCloser(bytes.NewReader(content)), + }, nil + }), + } + + networkResponse, err := client.NetworkCreate(context.Background(), "mynetwork", types.NetworkCreate{ + CheckDuplicate: true, + Driver: "mydriver", + EnableIPv6: true, + Internal: true, + Options: map[string]string{ + "opt-key": "opt-value", + }, + }) + if err != nil { + t.Fatal(err) + } + if networkResponse.ID != "network_id" { + t.Fatalf("expected networkResponse.ID to be 'network_id', got %s", networkResponse.ID) + } + if networkResponse.Warning != "warning" { + t.Fatalf("expected networkResponse.Warning to be 'warning', got %s", networkResponse.Warning) + } +} diff --git a/network_disconnect.go b/network_disconnect.go new file mode 100644 index 0000000000..24b58e3c12 --- /dev/null +++ b/network_disconnect.go @@ -0,0 +1,14 @@ +package client + +import ( + "github.com/docker/docker/api/types" + "golang.org/x/net/context" +) + +// NetworkDisconnect disconnects a container from an existent network in the docker host. +func (cli *Client) NetworkDisconnect(ctx context.Context, networkID, containerID string, force bool) error { + nd := types.NetworkDisconnect{Container: containerID, Force: force} + resp, err := cli.post(ctx, "/networks/"+networkID+"/disconnect", nil, nd, nil) + ensureReaderClosed(resp) + return err +} diff --git a/network_disconnect_test.go b/network_disconnect_test.go new file mode 100644 index 0000000000..d9dbb67159 --- /dev/null +++ b/network_disconnect_test.go @@ -0,0 +1,64 @@ +package client + +import ( + "bytes" + "encoding/json" + "fmt" + "io/ioutil" + "net/http" + "strings" + "testing" + + "github.com/docker/docker/api/types" + "golang.org/x/net/context" +) + +func TestNetworkDisconnectError(t *testing.T) { + client := &Client{ + transport: newMockClient(nil, errorMock(http.StatusInternalServerError, "Server error")), + } + + err := client.NetworkDisconnect(context.Background(), "network_id", "container_id", false) + if err == nil || err.Error() != "Error response from daemon: Server error" { + t.Fatalf("expected a Server Error, got %v", err) + } +} + +func TestNetworkDisconnect(t *testing.T) { + expectedURL := "/networks/network_id/disconnect" + + client := &Client{ + transport: newMockClient(nil, func(req *http.Request) (*http.Response, error) { + if !strings.HasPrefix(req.URL.Path, expectedURL) { + return nil, fmt.Errorf("Expected URL '%s', got '%s'", expectedURL, req.URL) + } + + if req.Method != "POST" { + return nil, fmt.Errorf("expected POST method, got %s", req.Method) + } + + var disconnect types.NetworkDisconnect + if err := json.NewDecoder(req.Body).Decode(&disconnect); err != nil { + return nil, err + } + + if disconnect.Container != "container_id" { + return nil, fmt.Errorf("expected 'container_id', got %s", disconnect.Container) + } + + if !disconnect.Force { + return nil, fmt.Errorf("expected Force to be true, got %v", disconnect.Force) + } + + return &http.Response{ + StatusCode: http.StatusOK, + Body: ioutil.NopCloser(bytes.NewReader([]byte(""))), + }, nil + }), + } + + err := client.NetworkDisconnect(context.Background(), "network_id", "container_id", true) + if err != nil { + t.Fatal(err) + } +} diff --git a/network_inspect.go b/network_inspect.go new file mode 100644 index 0000000000..5ad4ea5bf3 --- /dev/null +++ b/network_inspect.go @@ -0,0 +1,38 @@ +package client + +import ( + "bytes" + "encoding/json" + "io/ioutil" + "net/http" + + "github.com/docker/docker/api/types" + "golang.org/x/net/context" +) + +// NetworkInspect returns the information for a specific network configured in the docker host. +func (cli *Client) NetworkInspect(ctx context.Context, networkID string) (types.NetworkResource, error) { + networkResource, _, err := cli.NetworkInspectWithRaw(ctx, networkID) + return networkResource, err +} + +// NetworkInspectWithRaw returns the information for a specific network configured in the docker host and its raw representation. +func (cli *Client) NetworkInspectWithRaw(ctx context.Context, networkID string) (types.NetworkResource, []byte, error) { + var networkResource types.NetworkResource + resp, err := cli.get(ctx, "/networks/"+networkID, nil, nil) + if err != nil { + if resp.statusCode == http.StatusNotFound { + return networkResource, nil, networkNotFoundError{networkID} + } + return networkResource, nil, err + } + defer ensureReaderClosed(resp) + + body, err := ioutil.ReadAll(resp.body) + if err != nil { + return networkResource, nil, err + } + rdr := bytes.NewReader(body) + err = json.NewDecoder(rdr).Decode(&networkResource) + return networkResource, body, err +} diff --git a/network_inspect_test.go b/network_inspect_test.go new file mode 100644 index 0000000000..a6eb626c67 --- /dev/null +++ b/network_inspect_test.go @@ -0,0 +1,69 @@ +package client + +import ( + "bytes" + "encoding/json" + "fmt" + "io/ioutil" + "net/http" + "strings" + "testing" + + "github.com/docker/docker/api/types" + "golang.org/x/net/context" +) + +func TestNetworkInspectError(t *testing.T) { + client := &Client{ + transport: newMockClient(nil, errorMock(http.StatusInternalServerError, "Server error")), + } + + _, err := client.NetworkInspect(context.Background(), "nothing") + if err == nil || err.Error() != "Error response from daemon: Server error" { + t.Fatalf("expected a Server Error, got %v", err) + } +} + +func TestNetworkInspectContainerNotFound(t *testing.T) { + client := &Client{ + transport: newMockClient(nil, errorMock(http.StatusNotFound, "Server error")), + } + + _, err := client.NetworkInspect(context.Background(), "unknown") + if err == nil || !IsErrNetworkNotFound(err) { + t.Fatalf("expected a containerNotFound error, got %v", err) + } +} + +func TestNetworkInspect(t *testing.T) { + expectedURL := "/networks/network_id" + client := &Client{ + transport: newMockClient(nil, func(req *http.Request) (*http.Response, error) { + if !strings.HasPrefix(req.URL.Path, expectedURL) { + return nil, fmt.Errorf("Expected URL '%s', got '%s'", expectedURL, req.URL) + } + if req.Method != "GET" { + return nil, fmt.Errorf("expected GET method, got %s", req.Method) + } + + content, err := json.Marshal(types.NetworkResource{ + Name: "mynetwork", + }) + if err != nil { + return nil, err + } + return &http.Response{ + StatusCode: http.StatusOK, + Body: ioutil.NopCloser(bytes.NewReader(content)), + }, nil + }), + } + + r, err := client.NetworkInspect(context.Background(), "network_id") + if err != nil { + t.Fatal(err) + } + if r.Name != "mynetwork" { + t.Fatalf("expected `mynetwork`, got %s", r.Name) + } +} diff --git a/network_list.go b/network_list.go new file mode 100644 index 0000000000..e566a93e23 --- /dev/null +++ b/network_list.go @@ -0,0 +1,31 @@ +package client + +import ( + "encoding/json" + "net/url" + + "github.com/docker/docker/api/types" + "github.com/docker/docker/api/types/filters" + "golang.org/x/net/context" +) + +// NetworkList returns the list of networks configured in the docker host. +func (cli *Client) NetworkList(ctx context.Context, options types.NetworkListOptions) ([]types.NetworkResource, error) { + query := url.Values{} + if options.Filters.Len() > 0 { + filterJSON, err := filters.ToParamWithVersion(cli.version, options.Filters) + if err != nil { + return nil, err + } + + query.Set("filters", filterJSON) + } + var networkResources []types.NetworkResource + resp, err := cli.get(ctx, "/networks", query, nil) + if err != nil { + return networkResources, err + } + err = json.NewDecoder(resp.body).Decode(&networkResources) + ensureReaderClosed(resp) + return networkResources, err +} diff --git a/network_list_test.go b/network_list_test.go new file mode 100644 index 0000000000..cb66139271 --- /dev/null +++ b/network_list_test.go @@ -0,0 +1,108 @@ +package client + +import ( + "bytes" + "encoding/json" + "fmt" + "io/ioutil" + "net/http" + "strings" + "testing" + + "github.com/docker/docker/api/types" + "github.com/docker/docker/api/types/filters" + "golang.org/x/net/context" +) + +func TestNetworkListError(t *testing.T) { + client := &Client{ + transport: newMockClient(nil, errorMock(http.StatusInternalServerError, "Server error")), + } + + _, err := client.NetworkList(context.Background(), types.NetworkListOptions{ + Filters: filters.NewArgs(), + }) + if err == nil || err.Error() != "Error response from daemon: Server error" { + t.Fatalf("expected a Server Error, got %v", err) + } +} + +func TestNetworkList(t *testing.T) { + expectedURL := "/networks" + + noDanglingFilters := filters.NewArgs() + noDanglingFilters.Add("dangling", "false") + + danglingFilters := filters.NewArgs() + danglingFilters.Add("dangling", "true") + + labelFilters := filters.NewArgs() + labelFilters.Add("label", "label1") + labelFilters.Add("label", "label2") + + listCases := []struct { + options types.NetworkListOptions + expectedFilters string + }{ + { + options: types.NetworkListOptions{ + Filters: filters.NewArgs(), + }, + expectedFilters: "", + }, { + options: types.NetworkListOptions{ + Filters: noDanglingFilters, + }, + expectedFilters: `{"dangling":{"false":true}}`, + }, { + options: types.NetworkListOptions{ + Filters: danglingFilters, + }, + expectedFilters: `{"dangling":{"true":true}}`, + }, { + options: types.NetworkListOptions{ + Filters: labelFilters, + }, + expectedFilters: `{"label":{"label1":true,"label2":true}}`, + }, + } + + for _, listCase := range listCases { + client := &Client{ + transport: newMockClient(nil, func(req *http.Request) (*http.Response, error) { + if !strings.HasPrefix(req.URL.Path, expectedURL) { + return nil, fmt.Errorf("Expected URL '%s', got '%s'", expectedURL, req.URL) + } + if req.Method != "GET" { + return nil, fmt.Errorf("expected GET method, got %s", req.Method) + } + query := req.URL.Query() + actualFilters := query.Get("filters") + if actualFilters != listCase.expectedFilters { + return nil, fmt.Errorf("filters not set in URL query properly. Expected '%s', got %s", listCase.expectedFilters, actualFilters) + } + content, err := json.Marshal([]types.NetworkResource{ + { + Name: "network", + Driver: "bridge", + }, + }) + if err != nil { + return nil, err + } + return &http.Response{ + StatusCode: http.StatusOK, + Body: ioutil.NopCloser(bytes.NewReader(content)), + }, nil + }), + } + + networkResources, err := client.NetworkList(context.Background(), listCase.options) + if err != nil { + t.Fatal(err) + } + if len(networkResources) != 1 { + t.Fatalf("expected 1 network resource, got %v", networkResources) + } + } +} diff --git a/network_remove.go b/network_remove.go new file mode 100644 index 0000000000..6bd6748924 --- /dev/null +++ b/network_remove.go @@ -0,0 +1,10 @@ +package client + +import "golang.org/x/net/context" + +// NetworkRemove removes an existent network from the docker host. +func (cli *Client) NetworkRemove(ctx context.Context, networkID string) error { + resp, err := cli.delete(ctx, "/networks/"+networkID, nil, nil) + ensureReaderClosed(resp) + return err +} diff --git a/network_remove_test.go b/network_remove_test.go new file mode 100644 index 0000000000..d8cfa0ed6e --- /dev/null +++ b/network_remove_test.go @@ -0,0 +1,47 @@ +package client + +import ( + "bytes" + "fmt" + "io/ioutil" + "net/http" + "strings" + "testing" + + "golang.org/x/net/context" +) + +func TestNetworkRemoveError(t *testing.T) { + client := &Client{ + transport: newMockClient(nil, errorMock(http.StatusInternalServerError, "Server error")), + } + + err := client.NetworkRemove(context.Background(), "network_id") + if err == nil || err.Error() != "Error response from daemon: Server error" { + t.Fatalf("expected a Server Error, got %v", err) + } +} + +func TestNetworkRemove(t *testing.T) { + expectedURL := "/networks/network_id" + + client := &Client{ + transport: newMockClient(nil, func(req *http.Request) (*http.Response, error) { + if !strings.HasPrefix(req.URL.Path, expectedURL) { + return nil, fmt.Errorf("Expected URL '%s', got '%s'", expectedURL, req.URL) + } + if req.Method != "DELETE" { + return nil, fmt.Errorf("expected DELETE method, got %s", req.Method) + } + return &http.Response{ + StatusCode: http.StatusOK, + Body: ioutil.NopCloser(bytes.NewReader([]byte("body"))), + }, nil + }), + } + + err := client.NetworkRemove(context.Background(), "network_id") + if err != nil { + t.Fatal(err) + } +} diff --git a/node_inspect.go b/node_inspect.go new file mode 100644 index 0000000000..abf505d29c --- /dev/null +++ b/node_inspect.go @@ -0,0 +1,33 @@ +package client + +import ( + "bytes" + "encoding/json" + "io/ioutil" + "net/http" + + "github.com/docker/docker/api/types/swarm" + "golang.org/x/net/context" +) + +// NodeInspectWithRaw returns the node information. +func (cli *Client) NodeInspectWithRaw(ctx context.Context, nodeID string) (swarm.Node, []byte, error) { + serverResp, err := cli.get(ctx, "/nodes/"+nodeID, nil, nil) + if err != nil { + if serverResp.statusCode == http.StatusNotFound { + return swarm.Node{}, nil, nodeNotFoundError{nodeID} + } + return swarm.Node{}, nil, err + } + defer ensureReaderClosed(serverResp) + + body, err := ioutil.ReadAll(serverResp.body) + if err != nil { + return swarm.Node{}, nil, err + } + + var response swarm.Node + rdr := bytes.NewReader(body) + err = json.NewDecoder(rdr).Decode(&response) + return response, body, err +} diff --git a/node_inspect_test.go b/node_inspect_test.go new file mode 100644 index 0000000000..bf67728311 --- /dev/null +++ b/node_inspect_test.go @@ -0,0 +1,65 @@ +package client + +import ( + "bytes" + "encoding/json" + "fmt" + "io/ioutil" + "net/http" + "strings" + "testing" + + "github.com/docker/docker/api/types/swarm" + "golang.org/x/net/context" +) + +func TestNodeInspectError(t *testing.T) { + client := &Client{ + transport: newMockClient(nil, errorMock(http.StatusInternalServerError, "Server error")), + } + + _, _, err := client.NodeInspectWithRaw(context.Background(), "nothing") + if err == nil || err.Error() != "Error response from daemon: Server error" { + t.Fatalf("expected a Server Error, got %v", err) + } +} + +func TestNodeInspectNodeNotFound(t *testing.T) { + client := &Client{ + transport: newMockClient(nil, errorMock(http.StatusNotFound, "Server error")), + } + + _, _, err := client.NodeInspectWithRaw(context.Background(), "unknown") + if err == nil || !IsErrNodeNotFound(err) { + t.Fatalf("expected an nodeNotFoundError error, got %v", err) + } +} + +func TestNodeInspect(t *testing.T) { + expectedURL := "/nodes/node_id" + client := &Client{ + transport: newMockClient(nil, func(req *http.Request) (*http.Response, error) { + if !strings.HasPrefix(req.URL.Path, expectedURL) { + return nil, fmt.Errorf("Expected URL '%s', got '%s'", expectedURL, req.URL) + } + content, err := json.Marshal(swarm.Node{ + ID: "node_id", + }) + if err != nil { + return nil, err + } + return &http.Response{ + StatusCode: http.StatusOK, + Body: ioutil.NopCloser(bytes.NewReader(content)), + }, nil + }), + } + + nodeInspect, _, err := client.NodeInspectWithRaw(context.Background(), "node_id") + if err != nil { + t.Fatal(err) + } + if nodeInspect.ID != "node_id" { + t.Fatalf("expected `node_id`, got %s", nodeInspect.ID) + } +} diff --git a/node_list.go b/node_list.go new file mode 100644 index 0000000000..0716875ccc --- /dev/null +++ b/node_list.go @@ -0,0 +1,36 @@ +package client + +import ( + "encoding/json" + "net/url" + + "github.com/docker/docker/api/types" + "github.com/docker/docker/api/types/filters" + "github.com/docker/docker/api/types/swarm" + "golang.org/x/net/context" +) + +// NodeList returns the list of nodes. +func (cli *Client) NodeList(ctx context.Context, options types.NodeListOptions) ([]swarm.Node, error) { + query := url.Values{} + + if options.Filter.Len() > 0 { + filterJSON, err := filters.ToParam(options.Filter) + + if err != nil { + return nil, err + } + + query.Set("filters", filterJSON) + } + + resp, err := cli.get(ctx, "/nodes", query, nil) + if err != nil { + return nil, err + } + + var nodes []swarm.Node + err = json.NewDecoder(resp.body).Decode(&nodes) + ensureReaderClosed(resp) + return nodes, err +} diff --git a/node_list_test.go b/node_list_test.go new file mode 100644 index 0000000000..899ac7f455 --- /dev/null +++ b/node_list_test.go @@ -0,0 +1,94 @@ +package client + +import ( + "bytes" + "encoding/json" + "fmt" + "io/ioutil" + "net/http" + "strings" + "testing" + + "github.com/docker/docker/api/types" + "github.com/docker/docker/api/types/filters" + "github.com/docker/docker/api/types/swarm" + "golang.org/x/net/context" +) + +func TestNodeListError(t *testing.T) { + client := &Client{ + transport: newMockClient(nil, errorMock(http.StatusInternalServerError, "Server error")), + } + + _, err := client.NodeList(context.Background(), types.NodeListOptions{}) + if err == nil || err.Error() != "Error response from daemon: Server error" { + t.Fatalf("expected a Server Error, got %v", err) + } +} + +func TestNodeList(t *testing.T) { + expectedURL := "/nodes" + + filters := filters.NewArgs() + filters.Add("label", "label1") + filters.Add("label", "label2") + + listCases := []struct { + options types.NodeListOptions + expectedQueryParams map[string]string + }{ + { + options: types.NodeListOptions{}, + expectedQueryParams: map[string]string{ + "filters": "", + }, + }, + { + options: types.NodeListOptions{ + Filter: filters, + }, + expectedQueryParams: map[string]string{ + "filters": `{"label":{"label1":true,"label2":true}}`, + }, + }, + } + for _, listCase := range listCases { + client := &Client{ + transport: newMockClient(nil, func(req *http.Request) (*http.Response, error) { + if !strings.HasPrefix(req.URL.Path, expectedURL) { + return nil, fmt.Errorf("Expected URL '%s', got '%s'", expectedURL, req.URL) + } + query := req.URL.Query() + for key, expected := range listCase.expectedQueryParams { + actual := query.Get(key) + if actual != expected { + return nil, fmt.Errorf("%s not set in URL query properly. Expected '%s', got %s", key, expected, actual) + } + } + content, err := json.Marshal([]swarm.Node{ + { + ID: "node_id1", + }, + { + ID: "node_id2", + }, + }) + if err != nil { + return nil, err + } + return &http.Response{ + StatusCode: http.StatusOK, + Body: ioutil.NopCloser(bytes.NewReader(content)), + }, nil + }), + } + + nodes, err := client.NodeList(context.Background(), listCase.options) + if err != nil { + t.Fatal(err) + } + if len(nodes) != 2 { + t.Fatalf("expected 2 nodes, got %v", nodes) + } + } +} diff --git a/node_remove.go b/node_remove.go new file mode 100644 index 0000000000..0a77f3d578 --- /dev/null +++ b/node_remove.go @@ -0,0 +1,21 @@ +package client + +import ( + "net/url" + + "github.com/docker/docker/api/types" + + "golang.org/x/net/context" +) + +// NodeRemove removes a Node. +func (cli *Client) NodeRemove(ctx context.Context, nodeID string, options types.NodeRemoveOptions) error { + query := url.Values{} + if options.Force { + query.Set("force", "1") + } + + resp, err := cli.delete(ctx, "/nodes/"+nodeID, query, nil) + ensureReaderClosed(resp) + return err +} diff --git a/node_remove_test.go b/node_remove_test.go new file mode 100644 index 0000000000..9fdf2d7eb3 --- /dev/null +++ b/node_remove_test.go @@ -0,0 +1,69 @@ +package client + +import ( + "bytes" + "fmt" + "io/ioutil" + "net/http" + "strings" + "testing" + + "github.com/docker/docker/api/types" + + "golang.org/x/net/context" +) + +func TestNodeRemoveError(t *testing.T) { + client := &Client{ + transport: newMockClient(nil, errorMock(http.StatusInternalServerError, "Server error")), + } + + err := client.NodeRemove(context.Background(), "node_id", types.NodeRemoveOptions{Force: false}) + if err == nil || err.Error() != "Error response from daemon: Server error" { + t.Fatalf("expected a Server Error, got %v", err) + } +} + +func TestNodeRemove(t *testing.T) { + expectedURL := "/nodes/node_id" + + removeCases := []struct { + force bool + expectedForce string + }{ + { + expectedForce: "", + }, + { + force: true, + expectedForce: "1", + }, + } + + for _, removeCase := range removeCases { + client := &Client{ + transport: newMockClient(nil, func(req *http.Request) (*http.Response, error) { + if !strings.HasPrefix(req.URL.Path, expectedURL) { + return nil, fmt.Errorf("Expected URL '%s', got '%s'", expectedURL, req.URL) + } + if req.Method != "DELETE" { + return nil, fmt.Errorf("expected DELETE method, got %s", req.Method) + } + force := req.URL.Query().Get("force") + if force != removeCase.expectedForce { + return nil, fmt.Errorf("force not set in URL query properly. expected '%s', got %s", removeCase.expectedForce, force) + } + + return &http.Response{ + StatusCode: http.StatusOK, + Body: ioutil.NopCloser(bytes.NewReader([]byte("body"))), + }, nil + }), + } + + err := client.NodeRemove(context.Background(), "node_id", types.NodeRemoveOptions{Force: removeCase.force}) + if err != nil { + t.Fatal(err) + } + } +} diff --git a/node_update.go b/node_update.go new file mode 100644 index 0000000000..3ca9760282 --- /dev/null +++ b/node_update.go @@ -0,0 +1,18 @@ +package client + +import ( + "net/url" + "strconv" + + "github.com/docker/docker/api/types/swarm" + "golang.org/x/net/context" +) + +// NodeUpdate updates a Node. +func (cli *Client) NodeUpdate(ctx context.Context, nodeID string, version swarm.Version, node swarm.NodeSpec) error { + query := url.Values{} + query.Set("version", strconv.FormatUint(version.Index, 10)) + resp, err := cli.post(ctx, "/nodes/"+nodeID+"/update", query, node, nil) + ensureReaderClosed(resp) + return err +} diff --git a/node_update_test.go b/node_update_test.go new file mode 100644 index 0000000000..1acf65854a --- /dev/null +++ b/node_update_test.go @@ -0,0 +1,49 @@ +package client + +import ( + "bytes" + "fmt" + "io/ioutil" + "net/http" + "strings" + "testing" + + "golang.org/x/net/context" + + "github.com/docker/docker/api/types/swarm" +) + +func TestNodeUpdateError(t *testing.T) { + client := &Client{ + transport: newMockClient(nil, errorMock(http.StatusInternalServerError, "Server error")), + } + + err := client.NodeUpdate(context.Background(), "node_id", swarm.Version{}, swarm.NodeSpec{}) + if err == nil || err.Error() != "Error response from daemon: Server error" { + t.Fatalf("expected a Server Error, got %v", err) + } +} + +func TestNodeUpdate(t *testing.T) { + expectedURL := "/nodes/node_id/update" + + client := &Client{ + transport: newMockClient(nil, func(req *http.Request) (*http.Response, error) { + if !strings.HasPrefix(req.URL.Path, expectedURL) { + return nil, fmt.Errorf("Expected URL '%s', got '%s'", expectedURL, req.URL) + } + if req.Method != "POST" { + return nil, fmt.Errorf("expected POST method, got %s", req.Method) + } + return &http.Response{ + StatusCode: http.StatusOK, + Body: ioutil.NopCloser(bytes.NewReader([]byte("body"))), + }, nil + }), + } + + err := client.NodeUpdate(context.Background(), "node_id", swarm.Version{}, swarm.NodeSpec{}) + if err != nil { + t.Fatal(err) + } +} diff --git a/plugin_disable.go b/plugin_disable.go new file mode 100644 index 0000000000..893fc6e823 --- /dev/null +++ b/plugin_disable.go @@ -0,0 +1,14 @@ +// +build experimental + +package client + +import ( + "golang.org/x/net/context" +) + +// PluginDisable disables a plugin +func (cli *Client) PluginDisable(ctx context.Context, name string) error { + resp, err := cli.post(ctx, "/plugins/"+name+"/disable", nil, nil, nil) + ensureReaderClosed(resp) + return err +} diff --git a/plugin_disable_test.go b/plugin_disable_test.go new file mode 100644 index 0000000000..f37c157866 --- /dev/null +++ b/plugin_disable_test.go @@ -0,0 +1,49 @@ +// +build experimental + +package client + +import ( + "bytes" + "fmt" + "io/ioutil" + "net/http" + "strings" + "testing" + + "golang.org/x/net/context" +) + +func TestPluginDisableError(t *testing.T) { + client := &Client{ + transport: newMockClient(nil, errorMock(http.StatusInternalServerError, "Server error")), + } + + err := client.PluginDisable(context.Background(), "plugin_name") + if err == nil || err.Error() != "Error response from daemon: Server error" { + t.Fatalf("expected a Server Error, got %v", err) + } +} + +func TestPluginDisable(t *testing.T) { + expectedURL := "/plugins/plugin_name/disable" + + client := &Client{ + transport: newMockClient(nil, func(req *http.Request) (*http.Response, error) { + if !strings.HasPrefix(req.URL.Path, expectedURL) { + return nil, fmt.Errorf("Expected URL '%s', got '%s'", expectedURL, req.URL) + } + if req.Method != "POST" { + return nil, fmt.Errorf("expected POST method, got %s", req.Method) + } + return &http.Response{ + StatusCode: http.StatusOK, + Body: ioutil.NopCloser(bytes.NewReader([]byte(""))), + }, nil + }), + } + + err := client.PluginDisable(context.Background(), "plugin_name") + if err != nil { + t.Fatal(err) + } +} diff --git a/plugin_enable.go b/plugin_enable.go new file mode 100644 index 0000000000..84422abc79 --- /dev/null +++ b/plugin_enable.go @@ -0,0 +1,14 @@ +// +build experimental + +package client + +import ( + "golang.org/x/net/context" +) + +// PluginEnable enables a plugin +func (cli *Client) PluginEnable(ctx context.Context, name string) error { + resp, err := cli.post(ctx, "/plugins/"+name+"/enable", nil, nil, nil) + ensureReaderClosed(resp) + return err +} diff --git a/plugin_enable_test.go b/plugin_enable_test.go new file mode 100644 index 0000000000..fc0fe226a9 --- /dev/null +++ b/plugin_enable_test.go @@ -0,0 +1,49 @@ +// +build experimental + +package client + +import ( + "bytes" + "fmt" + "io/ioutil" + "net/http" + "strings" + "testing" + + "golang.org/x/net/context" +) + +func TestPluginEnableError(t *testing.T) { + client := &Client{ + transport: newMockClient(nil, errorMock(http.StatusInternalServerError, "Server error")), + } + + err := client.PluginEnable(context.Background(), "plugin_name") + if err == nil || err.Error() != "Error response from daemon: Server error" { + t.Fatalf("expected a Server Error, got %v", err) + } +} + +func TestPluginEnable(t *testing.T) { + expectedURL := "/plugins/plugin_name/enable" + + client := &Client{ + transport: newMockClient(nil, func(req *http.Request) (*http.Response, error) { + if !strings.HasPrefix(req.URL.Path, expectedURL) { + return nil, fmt.Errorf("Expected URL '%s', got '%s'", expectedURL, req.URL) + } + if req.Method != "POST" { + return nil, fmt.Errorf("expected POST method, got %s", req.Method) + } + return &http.Response{ + StatusCode: http.StatusOK, + Body: ioutil.NopCloser(bytes.NewReader([]byte(""))), + }, nil + }), + } + + err := client.PluginEnable(context.Background(), "plugin_name") + if err != nil { + t.Fatal(err) + } +} diff --git a/plugin_inspect.go b/plugin_inspect.go new file mode 100644 index 0000000000..7ba8db57a8 --- /dev/null +++ b/plugin_inspect.go @@ -0,0 +1,30 @@ +// +build experimental + +package client + +import ( + "bytes" + "encoding/json" + "io/ioutil" + + "github.com/docker/docker/api/types" + "golang.org/x/net/context" +) + +// PluginInspectWithRaw inspects an existing plugin +func (cli *Client) PluginInspectWithRaw(ctx context.Context, name string) (*types.Plugin, []byte, error) { + resp, err := cli.get(ctx, "/plugins/"+name, nil, nil) + if err != nil { + return nil, nil, err + } + + defer ensureReaderClosed(resp) + body, err := ioutil.ReadAll(resp.body) + if err != nil { + return nil, nil, err + } + var p types.Plugin + rdr := bytes.NewReader(body) + err = json.NewDecoder(rdr).Decode(&p) + return &p, body, err +} diff --git a/plugin_inspect_test.go b/plugin_inspect_test.go new file mode 100644 index 0000000000..19f829b2de --- /dev/null +++ b/plugin_inspect_test.go @@ -0,0 +1,56 @@ +// +build experimental + +package client + +import ( + "bytes" + "encoding/json" + "fmt" + "io/ioutil" + "net/http" + "strings" + "testing" + + "github.com/docker/docker/api/types" + "golang.org/x/net/context" +) + +func TestPluginInspectError(t *testing.T) { + client := &Client{ + transport: newMockClient(nil, errorMock(http.StatusInternalServerError, "Server error")), + } + + _, _, err := client.PluginInspectWithRaw(context.Background(), "nothing") + if err == nil || err.Error() != "Error response from daemon: Server error" { + t.Fatalf("expected a Server Error, got %v", err) + } +} + +func TestPluginInspect(t *testing.T) { + expectedURL := "/plugins/plugin_name" + client := &Client{ + transport: newMockClient(nil, func(req *http.Request) (*http.Response, error) { + if !strings.HasPrefix(req.URL.Path, expectedURL) { + return nil, fmt.Errorf("Expected URL '%s', got '%s'", expectedURL, req.URL) + } + content, err := json.Marshal(types.Plugin{ + ID: "plugin_id", + }) + if err != nil { + return nil, err + } + return &http.Response{ + StatusCode: http.StatusOK, + Body: ioutil.NopCloser(bytes.NewReader(content)), + }, nil + }), + } + + pluginInspect, _, err := client.PluginInspectWithRaw(context.Background(), "plugin_name") + if err != nil { + t.Fatal(err) + } + if pluginInspect.ID != "plugin_id" { + t.Fatalf("expected `plugin_id`, got %s", pluginInspect.ID) + } +} diff --git a/plugin_install.go b/plugin_install.go new file mode 100644 index 0000000000..9ee32eea92 --- /dev/null +++ b/plugin_install.go @@ -0,0 +1,59 @@ +// +build experimental + +package client + +import ( + "encoding/json" + "net/http" + "net/url" + + "github.com/docker/docker/api/types" + "golang.org/x/net/context" +) + +// PluginInstall installs a plugin +func (cli *Client) PluginInstall(ctx context.Context, name string, options types.PluginInstallOptions) error { + // FIXME(vdemeester) name is a ref, we might want to parse/validate it here. + query := url.Values{} + query.Set("name", name) + resp, err := cli.tryPluginPull(ctx, query, options.RegistryAuth) + if resp.statusCode == http.StatusUnauthorized && options.PrivilegeFunc != nil { + newAuthHeader, privilegeErr := options.PrivilegeFunc() + if privilegeErr != nil { + ensureReaderClosed(resp) + return privilegeErr + } + resp, err = cli.tryPluginPull(ctx, query, newAuthHeader) + } + if err != nil { + ensureReaderClosed(resp) + return err + } + var privileges types.PluginPrivileges + if err := json.NewDecoder(resp.body).Decode(&privileges); err != nil { + ensureReaderClosed(resp) + return err + } + ensureReaderClosed(resp) + + if !options.AcceptAllPermissions && options.AcceptPermissionsFunc != nil && len(privileges) > 0 { + accept, err := options.AcceptPermissionsFunc(privileges) + if err != nil { + return err + } + if !accept { + resp, _ := cli.delete(ctx, "/plugins/"+name, nil, nil) + ensureReaderClosed(resp) + return pluginPermissionDenied{name} + } + } + if options.Disabled { + return nil + } + return cli.PluginEnable(ctx, name) +} + +func (cli *Client) tryPluginPull(ctx context.Context, query url.Values, registryAuth string) (serverResponse, error) { + headers := map[string][]string{"X-Registry-Auth": {registryAuth}} + return cli.post(ctx, "/plugins/pull", query, nil, headers) +} diff --git a/plugin_list.go b/plugin_list.go new file mode 100644 index 0000000000..48b470247b --- /dev/null +++ b/plugin_list.go @@ -0,0 +1,23 @@ +// +build experimental + +package client + +import ( + "encoding/json" + + "github.com/docker/docker/api/types" + "golang.org/x/net/context" +) + +// PluginList returns the installed plugins +func (cli *Client) PluginList(ctx context.Context) (types.PluginsListResponse, error) { + var plugins types.PluginsListResponse + resp, err := cli.get(ctx, "/plugins", nil, nil) + if err != nil { + return plugins, err + } + + err = json.NewDecoder(resp.body).Decode(&plugins) + ensureReaderClosed(resp) + return plugins, err +} diff --git a/plugin_list_test.go b/plugin_list_test.go new file mode 100644 index 0000000000..92aee61187 --- /dev/null +++ b/plugin_list_test.go @@ -0,0 +1,61 @@ +// +build experimental + +package client + +import ( + "bytes" + "encoding/json" + "fmt" + "io/ioutil" + "net/http" + "strings" + "testing" + + "github.com/docker/docker/api/types" + "golang.org/x/net/context" +) + +func TestPluginListError(t *testing.T) { + client := &Client{ + transport: newMockClient(nil, errorMock(http.StatusInternalServerError, "Server error")), + } + + _, err := client.PluginList(context.Background()) + if err == nil || err.Error() != "Error response from daemon: Server error" { + t.Fatalf("expected a Server Error, got %v", err) + } +} + +func TestPluginList(t *testing.T) { + expectedURL := "/plugins" + client := &Client{ + transport: newMockClient(nil, func(req *http.Request) (*http.Response, error) { + if !strings.HasPrefix(req.URL.Path, expectedURL) { + return nil, fmt.Errorf("Expected URL '%s', got '%s'", expectedURL, req.URL) + } + content, err := json.Marshal([]*types.Plugin{ + { + ID: "plugin_id1", + }, + { + ID: "plugin_id2", + }, + }) + if err != nil { + return nil, err + } + return &http.Response{ + StatusCode: http.StatusOK, + Body: ioutil.NopCloser(bytes.NewReader(content)), + }, nil + }), + } + + plugins, err := client.PluginList(context.Background()) + if err != nil { + t.Fatal(err) + } + if len(plugins) != 2 { + t.Fatalf("expected 2 plugins, got %v", plugins) + } +} diff --git a/plugin_push.go b/plugin_push.go new file mode 100644 index 0000000000..3afea5ed79 --- /dev/null +++ b/plugin_push.go @@ -0,0 +1,15 @@ +// +build experimental + +package client + +import ( + "golang.org/x/net/context" +) + +// PluginPush pushes a plugin to a registry +func (cli *Client) PluginPush(ctx context.Context, name string, registryAuth string) error { + headers := map[string][]string{"X-Registry-Auth": {registryAuth}} + resp, err := cli.post(ctx, "/plugins/"+name+"/push", nil, nil, headers) + ensureReaderClosed(resp) + return err +} diff --git a/plugin_push_test.go b/plugin_push_test.go new file mode 100644 index 0000000000..b77ea00273 --- /dev/null +++ b/plugin_push_test.go @@ -0,0 +1,53 @@ +// +build experimental + +package client + +import ( + "bytes" + "fmt" + "io/ioutil" + "net/http" + "strings" + "testing" + + "golang.org/x/net/context" +) + +func TestPluginPushError(t *testing.T) { + client := &Client{ + transport: newMockClient(nil, errorMock(http.StatusInternalServerError, "Server error")), + } + + err := client.PluginPush(context.Background(), "plugin_name", "") + if err == nil || err.Error() != "Error response from daemon: Server error" { + t.Fatalf("expected a Server Error, got %v", err) + } +} + +func TestPluginPush(t *testing.T) { + expectedURL := "/plugins/plugin_name" + + client := &Client{ + transport: newMockClient(nil, func(req *http.Request) (*http.Response, error) { + if !strings.HasPrefix(req.URL.Path, expectedURL) { + return nil, fmt.Errorf("Expected URL '%s', got '%s'", expectedURL, req.URL) + } + if req.Method != "POST" { + return nil, fmt.Errorf("expected POST method, got %s", req.Method) + } + auth := req.Header.Get("X-Registry-Auth") + if auth != "authtoken" { + return nil, fmt.Errorf("Invalid auth header : expected %s, got %s", "authtoken", auth) + } + return &http.Response{ + StatusCode: http.StatusOK, + Body: ioutil.NopCloser(bytes.NewReader([]byte(""))), + }, nil + }), + } + + err := client.PluginPush(context.Background(), "plugin_name", "authtoken") + if err != nil { + t.Fatal(err) + } +} diff --git a/plugin_remove.go b/plugin_remove.go new file mode 100644 index 0000000000..1483f2854d --- /dev/null +++ b/plugin_remove.go @@ -0,0 +1,22 @@ +// +build experimental + +package client + +import ( + "net/url" + + "github.com/docker/docker/api/types" + "golang.org/x/net/context" +) + +// PluginRemove removes a plugin +func (cli *Client) PluginRemove(ctx context.Context, name string, options types.PluginRemoveOptions) error { + query := url.Values{} + if options.Force { + query.Set("force", "1") + } + + resp, err := cli.delete(ctx, "/plugins/"+name, query, nil) + ensureReaderClosed(resp) + return err +} diff --git a/plugin_remove_test.go b/plugin_remove_test.go new file mode 100644 index 0000000000..de565f441b --- /dev/null +++ b/plugin_remove_test.go @@ -0,0 +1,51 @@ +// +build experimental + +package client + +import ( + "bytes" + "fmt" + "io/ioutil" + "net/http" + "strings" + "testing" + + "github.com/docker/docker/api/types" + + "golang.org/x/net/context" +) + +func TestPluginRemoveError(t *testing.T) { + client := &Client{ + transport: newMockClient(nil, errorMock(http.StatusInternalServerError, "Server error")), + } + + err := client.PluginRemove(context.Background(), "plugin_name", types.PluginRemoveOptions{}) + if err == nil || err.Error() != "Error response from daemon: Server error" { + t.Fatalf("expected a Server Error, got %v", err) + } +} + +func TestPluginRemove(t *testing.T) { + expectedURL := "/plugins/plugin_name" + + client := &Client{ + transport: newMockClient(nil, func(req *http.Request) (*http.Response, error) { + if !strings.HasPrefix(req.URL.Path, expectedURL) { + return nil, fmt.Errorf("Expected URL '%s', got '%s'", expectedURL, req.URL) + } + if req.Method != "DELETE" { + return nil, fmt.Errorf("expected POST method, got %s", req.Method) + } + return &http.Response{ + StatusCode: http.StatusOK, + Body: ioutil.NopCloser(bytes.NewReader([]byte(""))), + }, nil + }), + } + + err := client.PluginRemove(context.Background(), "plugin_name", types.PluginRemoveOptions{}) + if err != nil { + t.Fatal(err) + } +} diff --git a/plugin_set.go b/plugin_set.go new file mode 100644 index 0000000000..fb40f38b22 --- /dev/null +++ b/plugin_set.go @@ -0,0 +1,14 @@ +// +build experimental + +package client + +import ( + "golang.org/x/net/context" +) + +// PluginSet modifies settings for an existing plugin +func (cli *Client) PluginSet(ctx context.Context, name string, args []string) error { + resp, err := cli.post(ctx, "/plugins/"+name+"/set", nil, args, nil) + ensureReaderClosed(resp) + return err +} diff --git a/plugin_set_test.go b/plugin_set_test.go new file mode 100644 index 0000000000..128dee04be --- /dev/null +++ b/plugin_set_test.go @@ -0,0 +1,49 @@ +// +build experimental + +package client + +import ( + "bytes" + "fmt" + "io/ioutil" + "net/http" + "strings" + "testing" + + "golang.org/x/net/context" +) + +func TestPluginSetError(t *testing.T) { + client := &Client{ + transport: newMockClient(nil, errorMock(http.StatusInternalServerError, "Server error")), + } + + err := client.PluginSet(context.Background(), "plugin_name", []string{}) + if err == nil || err.Error() != "Error response from daemon: Server error" { + t.Fatalf("expected a Server Error, got %v", err) + } +} + +func TestPluginSet(t *testing.T) { + expectedURL := "/plugins/plugin_name/set" + + client := &Client{ + transport: newMockClient(nil, func(req *http.Request) (*http.Response, error) { + if !strings.HasPrefix(req.URL.Path, expectedURL) { + return nil, fmt.Errorf("Expected URL '%s', got '%s'", expectedURL, req.URL) + } + if req.Method != "POST" { + return nil, fmt.Errorf("expected POST method, got %s", req.Method) + } + return &http.Response{ + StatusCode: http.StatusOK, + Body: ioutil.NopCloser(bytes.NewReader([]byte(""))), + }, nil + }), + } + + err := client.PluginSet(context.Background(), "plugin_name", []string{"arg1"}) + if err != nil { + t.Fatal(err) + } +} diff --git a/request.go b/request.go new file mode 100644 index 0000000000..024e973520 --- /dev/null +++ b/request.go @@ -0,0 +1,208 @@ +package client + +import ( + "bytes" + "encoding/json" + "fmt" + "io" + "io/ioutil" + "net" + "net/http" + "net/url" + "strings" + + "github.com/docker/docker/api/types" + "github.com/docker/docker/api/types/versions" + "github.com/docker/docker/client/transport/cancellable" + "golang.org/x/net/context" +) + +// serverResponse is a wrapper for http API responses. +type serverResponse struct { + body io.ReadCloser + header http.Header + statusCode int +} + +// head sends an http request to the docker API using the method HEAD. +func (cli *Client) head(ctx context.Context, path string, query url.Values, headers map[string][]string) (serverResponse, error) { + return cli.sendRequest(ctx, "HEAD", path, query, nil, headers) +} + +// getWithContext sends an http request to the docker API using the method GET with a specific go context. +func (cli *Client) get(ctx context.Context, path string, query url.Values, headers map[string][]string) (serverResponse, error) { + return cli.sendRequest(ctx, "GET", path, query, nil, headers) +} + +// postWithContext sends an http request to the docker API using the method POST with a specific go context. +func (cli *Client) post(ctx context.Context, path string, query url.Values, obj interface{}, headers map[string][]string) (serverResponse, error) { + return cli.sendRequest(ctx, "POST", path, query, obj, headers) +} + +func (cli *Client) postRaw(ctx context.Context, path string, query url.Values, body io.Reader, headers map[string][]string) (serverResponse, error) { + return cli.sendClientRequest(ctx, "POST", path, query, body, headers) +} + +// put sends an http request to the docker API using the method PUT. +func (cli *Client) put(ctx context.Context, path string, query url.Values, obj interface{}, headers map[string][]string) (serverResponse, error) { + return cli.sendRequest(ctx, "PUT", path, query, obj, headers) +} + +// put sends an http request to the docker API using the method PUT. +func (cli *Client) putRaw(ctx context.Context, path string, query url.Values, body io.Reader, headers map[string][]string) (serverResponse, error) { + return cli.sendClientRequest(ctx, "PUT", path, query, body, headers) +} + +// delete sends an http request to the docker API using the method DELETE. +func (cli *Client) delete(ctx context.Context, path string, query url.Values, headers map[string][]string) (serverResponse, error) { + return cli.sendRequest(ctx, "DELETE", path, query, nil, headers) +} + +func (cli *Client) sendRequest(ctx context.Context, method, path string, query url.Values, obj interface{}, headers map[string][]string) (serverResponse, error) { + var body io.Reader + + if obj != nil { + var err error + body, err = encodeData(obj) + if err != nil { + return serverResponse{}, err + } + if headers == nil { + headers = make(map[string][]string) + } + headers["Content-Type"] = []string{"application/json"} + } + + return cli.sendClientRequest(ctx, method, path, query, body, headers) +} + +func (cli *Client) sendClientRequest(ctx context.Context, method, path string, query url.Values, body io.Reader, headers map[string][]string) (serverResponse, error) { + serverResp := serverResponse{ + body: nil, + statusCode: -1, + } + + expectedPayload := (method == "POST" || method == "PUT") + if expectedPayload && body == nil { + body = bytes.NewReader([]byte{}) + } + + req, err := cli.newRequest(method, path, query, body, headers) + if err != nil { + return serverResp, err + } + + if cli.proto == "unix" || cli.proto == "npipe" { + // For local communications, it doesn't matter what the host is. We just + // need a valid and meaningful host name. (See #189) + req.Host = "docker" + } + req.URL.Host = cli.addr + req.URL.Scheme = cli.transport.Scheme() + + if expectedPayload && req.Header.Get("Content-Type") == "" { + req.Header.Set("Content-Type", "text/plain") + } + + resp, err := cancellable.Do(ctx, cli.transport, req) + if err != nil { + if !cli.transport.Secure() && strings.Contains(err.Error(), "malformed HTTP response") { + return serverResp, fmt.Errorf("%v.\n* Are you trying to connect to a TLS-enabled daemon without TLS?", err) + } + + if cli.transport.Secure() && strings.Contains(err.Error(), "bad certificate") { + return serverResp, fmt.Errorf("The server probably has client authentication (--tlsverify) enabled. Please check your TLS client certification settings: %v", err) + } + + // Don't decorate context sentinel errors; users may be comparing to + // them directly. + switch err { + case context.Canceled, context.DeadlineExceeded: + return serverResp, err + } + + if err, ok := err.(net.Error); ok { + if err.Timeout() { + return serverResp, ErrorConnectionFailed(cli.host) + } + if !err.Temporary() { + if strings.Contains(err.Error(), "connection refused") || strings.Contains(err.Error(), "dial unix") { + return serverResp, ErrorConnectionFailed(cli.host) + } + } + } + return serverResp, fmt.Errorf("An error occurred trying to connect: %v", err) + } + + if resp != nil { + serverResp.statusCode = resp.StatusCode + } + + if serverResp.statusCode < 200 || serverResp.statusCode >= 400 { + body, err := ioutil.ReadAll(resp.Body) + if err != nil { + return serverResp, err + } + if len(body) == 0 { + return serverResp, fmt.Errorf("Error: request returned %s for API route and version %s, check if the server supports the requested API version", http.StatusText(serverResp.statusCode), req.URL) + } + + var errorMessage string + if (cli.version == "" || versions.GreaterThan(cli.version, "1.23")) && + resp.Header.Get("Content-Type") == "application/json" { + var errorResponse types.ErrorResponse + if err := json.Unmarshal(body, &errorResponse); err != nil { + return serverResp, fmt.Errorf("Error reading JSON: %v", err) + } + errorMessage = errorResponse.Message + } else { + errorMessage = string(body) + } + + return serverResp, fmt.Errorf("Error response from daemon: %s", strings.TrimSpace(errorMessage)) + } + + serverResp.body = resp.Body + serverResp.header = resp.Header + return serverResp, nil +} + +func (cli *Client) newRequest(method, path string, query url.Values, body io.Reader, headers map[string][]string) (*http.Request, error) { + apiPath := cli.getAPIPath(path, query) + req, err := http.NewRequest(method, apiPath, body) + if err != nil { + return nil, err + } + + // Add CLI Config's HTTP Headers BEFORE we set the Docker headers + // then the user can't change OUR headers + for k, v := range cli.customHTTPHeaders { + req.Header.Set(k, v) + } + + if headers != nil { + for k, v := range headers { + req.Header[k] = v + } + } + + return req, nil +} + +func encodeData(data interface{}) (*bytes.Buffer, error) { + params := bytes.NewBuffer(nil) + if data != nil { + if err := json.NewEncoder(params).Encode(data); err != nil { + return nil, err + } + } + return params, nil +} + +func ensureReaderClosed(response serverResponse) { + if body := response.body; body != nil { + // Drain up to 512 bytes and close the body to let the Transport reuse the connection + io.CopyN(ioutil.Discard, body, 512) + response.body.Close() + } +} diff --git a/request_test.go b/request_test.go new file mode 100644 index 0000000000..446adf9c66 --- /dev/null +++ b/request_test.go @@ -0,0 +1,91 @@ +package client + +import ( + "bytes" + "fmt" + "io/ioutil" + "net/http" + "strings" + "testing" + + "github.com/docker/docker/api/types" + "golang.org/x/net/context" +) + +// TestSetHostHeader should set fake host for local communications, set real host +// for normal communications. +func TestSetHostHeader(t *testing.T) { + testURL := "/test" + testCases := []struct { + host string + expectedHost string + expectedURLHost string + }{ + { + "unix:///var/run/docker.sock", + "docker", + "/var/run/docker.sock", + }, + { + "npipe:////./pipe/docker_engine", + "docker", + "//./pipe/docker_engine", + }, + { + "tcp://0.0.0.0:4243", + "", + "0.0.0.0:4243", + }, + { + "tcp://localhost:4243", + "", + "localhost:4243", + }, + } + + for c, test := range testCases { + proto, addr, basePath, err := ParseHost(test.host) + if err != nil { + t.Fatal(err) + } + + client := &Client{ + transport: newMockClient(nil, func(req *http.Request) (*http.Response, error) { + if !strings.HasPrefix(req.URL.Path, testURL) { + return nil, fmt.Errorf("Test Case #%d: Expected URL %q, got %q", c, testURL, req.URL) + } + if req.Host != test.expectedHost { + return nil, fmt.Errorf("Test Case #%d: Expected host %q, got %q", c, test.expectedHost, req.Host) + } + if req.URL.Host != test.expectedURLHost { + return nil, fmt.Errorf("Test Case #%d: Expected URL host %q, got %q", c, test.expectedURLHost, req.URL.Host) + } + return &http.Response{ + StatusCode: http.StatusOK, + Body: ioutil.NopCloser(bytes.NewReader(([]byte("")))), + }, nil + }), + proto: proto, + addr: addr, + basePath: basePath, + } + + _, err = client.sendRequest(context.Background(), "GET", testURL, nil, nil, nil) + if err != nil { + t.Fatal(err) + } + } +} + +// TestPlainTextError tests the server returning an error in plain text for +// backwards compatibility with API versions <1.24. All other tests use +// errors returned as JSON +func TestPlainTextError(t *testing.T) { + client := &Client{ + transport: newMockClient(nil, plainTextErrorMock(http.StatusInternalServerError, "Server error")), + } + _, err := client.ContainerList(context.Background(), types.ContainerListOptions{}) + if err == nil || err.Error() != "Error response from daemon: Server error" { + t.Fatalf("expected a Server Error, got %v", err) + } +} diff --git a/service_create.go b/service_create.go new file mode 100644 index 0000000000..3d1be225bd --- /dev/null +++ b/service_create.go @@ -0,0 +1,30 @@ +package client + +import ( + "encoding/json" + + "github.com/docker/docker/api/types" + "github.com/docker/docker/api/types/swarm" + "golang.org/x/net/context" +) + +// ServiceCreate creates a new Service. +func (cli *Client) ServiceCreate(ctx context.Context, service swarm.ServiceSpec, options types.ServiceCreateOptions) (types.ServiceCreateResponse, error) { + var headers map[string][]string + + if options.EncodedRegistryAuth != "" { + headers = map[string][]string{ + "X-Registry-Auth": {options.EncodedRegistryAuth}, + } + } + + var response types.ServiceCreateResponse + resp, err := cli.post(ctx, "/services/create", nil, service, headers) + if err != nil { + return response, err + } + + err = json.NewDecoder(resp.body).Decode(&response) + ensureReaderClosed(resp) + return response, err +} diff --git a/service_create_test.go b/service_create_test.go new file mode 100644 index 0000000000..a79f040c0a --- /dev/null +++ b/service_create_test.go @@ -0,0 +1,57 @@ +package client + +import ( + "bytes" + "encoding/json" + "fmt" + "io/ioutil" + "net/http" + "strings" + "testing" + + "github.com/docker/docker/api/types" + "github.com/docker/docker/api/types/swarm" + "golang.org/x/net/context" +) + +func TestServiceCreateError(t *testing.T) { + client := &Client{ + transport: newMockClient(nil, errorMock(http.StatusInternalServerError, "Server error")), + } + _, err := client.ServiceCreate(context.Background(), swarm.ServiceSpec{}, types.ServiceCreateOptions{}) + if err == nil || err.Error() != "Error response from daemon: Server error" { + t.Fatalf("expected a Server Error, got %v", err) + } +} + +func TestServiceCreate(t *testing.T) { + expectedURL := "/services/create" + client := &Client{ + transport: newMockClient(nil, func(req *http.Request) (*http.Response, error) { + if !strings.HasPrefix(req.URL.Path, expectedURL) { + return nil, fmt.Errorf("Expected URL '%s', got '%s'", expectedURL, req.URL) + } + if req.Method != "POST" { + return nil, fmt.Errorf("expected POST method, got %s", req.Method) + } + b, err := json.Marshal(types.ServiceCreateResponse{ + ID: "service_id", + }) + if err != nil { + return nil, err + } + return &http.Response{ + StatusCode: http.StatusOK, + Body: ioutil.NopCloser(bytes.NewReader(b)), + }, nil + }), + } + + r, err := client.ServiceCreate(context.Background(), swarm.ServiceSpec{}, types.ServiceCreateOptions{}) + if err != nil { + t.Fatal(err) + } + if r.ID != "service_id" { + t.Fatalf("expected `service_id`, got %s", r.ID) + } +} diff --git a/service_inspect.go b/service_inspect.go new file mode 100644 index 0000000000..ca71cbde1a --- /dev/null +++ b/service_inspect.go @@ -0,0 +1,33 @@ +package client + +import ( + "bytes" + "encoding/json" + "io/ioutil" + "net/http" + + "github.com/docker/docker/api/types/swarm" + "golang.org/x/net/context" +) + +// ServiceInspectWithRaw returns the service information and the raw data. +func (cli *Client) ServiceInspectWithRaw(ctx context.Context, serviceID string) (swarm.Service, []byte, error) { + serverResp, err := cli.get(ctx, "/services/"+serviceID, nil, nil) + if err != nil { + if serverResp.statusCode == http.StatusNotFound { + return swarm.Service{}, nil, serviceNotFoundError{serviceID} + } + return swarm.Service{}, nil, err + } + defer ensureReaderClosed(serverResp) + + body, err := ioutil.ReadAll(serverResp.body) + if err != nil { + return swarm.Service{}, nil, err + } + + var response swarm.Service + rdr := bytes.NewReader(body) + err = json.NewDecoder(rdr).Decode(&response) + return response, body, err +} diff --git a/service_inspect_test.go b/service_inspect_test.go new file mode 100644 index 0000000000..e4eafff5d7 --- /dev/null +++ b/service_inspect_test.go @@ -0,0 +1,65 @@ +package client + +import ( + "bytes" + "encoding/json" + "fmt" + "io/ioutil" + "net/http" + "strings" + "testing" + + "github.com/docker/docker/api/types/swarm" + "golang.org/x/net/context" +) + +func TestServiceInspectError(t *testing.T) { + client := &Client{ + transport: newMockClient(nil, errorMock(http.StatusInternalServerError, "Server error")), + } + + _, _, err := client.ServiceInspectWithRaw(context.Background(), "nothing") + if err == nil || err.Error() != "Error response from daemon: Server error" { + t.Fatalf("expected a Server Error, got %v", err) + } +} + +func TestServiceInspectServiceNotFound(t *testing.T) { + client := &Client{ + transport: newMockClient(nil, errorMock(http.StatusNotFound, "Server error")), + } + + _, _, err := client.ServiceInspectWithRaw(context.Background(), "unknown") + if err == nil || !IsErrServiceNotFound(err) { + t.Fatalf("expected an serviceNotFoundError error, got %v", err) + } +} + +func TestServiceInspect(t *testing.T) { + expectedURL := "/services/service_id" + client := &Client{ + transport: newMockClient(nil, func(req *http.Request) (*http.Response, error) { + if !strings.HasPrefix(req.URL.Path, expectedURL) { + return nil, fmt.Errorf("Expected URL '%s', got '%s'", expectedURL, req.URL) + } + content, err := json.Marshal(swarm.Service{ + ID: "service_id", + }) + if err != nil { + return nil, err + } + return &http.Response{ + StatusCode: http.StatusOK, + Body: ioutil.NopCloser(bytes.NewReader(content)), + }, nil + }), + } + + serviceInspect, _, err := client.ServiceInspectWithRaw(context.Background(), "service_id") + if err != nil { + t.Fatal(err) + } + if serviceInspect.ID != "service_id" { + t.Fatalf("expected `service_id`, got %s", serviceInspect.ID) + } +} diff --git a/service_list.go b/service_list.go new file mode 100644 index 0000000000..4ebc9f3011 --- /dev/null +++ b/service_list.go @@ -0,0 +1,35 @@ +package client + +import ( + "encoding/json" + "net/url" + + "github.com/docker/docker/api/types" + "github.com/docker/docker/api/types/filters" + "github.com/docker/docker/api/types/swarm" + "golang.org/x/net/context" +) + +// ServiceList returns the list of services. +func (cli *Client) ServiceList(ctx context.Context, options types.ServiceListOptions) ([]swarm.Service, error) { + query := url.Values{} + + if options.Filter.Len() > 0 { + filterJSON, err := filters.ToParam(options.Filter) + if err != nil { + return nil, err + } + + query.Set("filters", filterJSON) + } + + resp, err := cli.get(ctx, "/services", query, nil) + if err != nil { + return nil, err + } + + var services []swarm.Service + err = json.NewDecoder(resp.body).Decode(&services) + ensureReaderClosed(resp) + return services, err +} diff --git a/service_list_test.go b/service_list_test.go new file mode 100644 index 0000000000..6e6851a3a5 --- /dev/null +++ b/service_list_test.go @@ -0,0 +1,94 @@ +package client + +import ( + "bytes" + "encoding/json" + "fmt" + "io/ioutil" + "net/http" + "strings" + "testing" + + "github.com/docker/docker/api/types" + "github.com/docker/docker/api/types/filters" + "github.com/docker/docker/api/types/swarm" + "golang.org/x/net/context" +) + +func TestServiceListError(t *testing.T) { + client := &Client{ + transport: newMockClient(nil, errorMock(http.StatusInternalServerError, "Server error")), + } + + _, err := client.ServiceList(context.Background(), types.ServiceListOptions{}) + if err == nil || err.Error() != "Error response from daemon: Server error" { + t.Fatalf("expected a Server Error, got %v", err) + } +} + +func TestServiceList(t *testing.T) { + expectedURL := "/services" + + filters := filters.NewArgs() + filters.Add("label", "label1") + filters.Add("label", "label2") + + listCases := []struct { + options types.ServiceListOptions + expectedQueryParams map[string]string + }{ + { + options: types.ServiceListOptions{}, + expectedQueryParams: map[string]string{ + "filters": "", + }, + }, + { + options: types.ServiceListOptions{ + Filter: filters, + }, + expectedQueryParams: map[string]string{ + "filters": `{"label":{"label1":true,"label2":true}}`, + }, + }, + } + for _, listCase := range listCases { + client := &Client{ + transport: newMockClient(nil, func(req *http.Request) (*http.Response, error) { + if !strings.HasPrefix(req.URL.Path, expectedURL) { + return nil, fmt.Errorf("Expected URL '%s', got '%s'", expectedURL, req.URL) + } + query := req.URL.Query() + for key, expected := range listCase.expectedQueryParams { + actual := query.Get(key) + if actual != expected { + return nil, fmt.Errorf("%s not set in URL query properly. Expected '%s', got %s", key, expected, actual) + } + } + content, err := json.Marshal([]swarm.Service{ + { + ID: "service_id1", + }, + { + ID: "service_id2", + }, + }) + if err != nil { + return nil, err + } + return &http.Response{ + StatusCode: http.StatusOK, + Body: ioutil.NopCloser(bytes.NewReader(content)), + }, nil + }), + } + + services, err := client.ServiceList(context.Background(), listCase.options) + if err != nil { + t.Fatal(err) + } + if len(services) != 2 { + t.Fatalf("expected 2 services, got %v", services) + } + } +} diff --git a/service_remove.go b/service_remove.go new file mode 100644 index 0000000000..a9331f92c2 --- /dev/null +++ b/service_remove.go @@ -0,0 +1,10 @@ +package client + +import "golang.org/x/net/context" + +// ServiceRemove kills and removes a service. +func (cli *Client) ServiceRemove(ctx context.Context, serviceID string) error { + resp, err := cli.delete(ctx, "/services/"+serviceID, nil, nil) + ensureReaderClosed(resp) + return err +} diff --git a/service_remove_test.go b/service_remove_test.go new file mode 100644 index 0000000000..e1316f959b --- /dev/null +++ b/service_remove_test.go @@ -0,0 +1,47 @@ +package client + +import ( + "bytes" + "fmt" + "io/ioutil" + "net/http" + "strings" + "testing" + + "golang.org/x/net/context" +) + +func TestServiceRemoveError(t *testing.T) { + client := &Client{ + transport: newMockClient(nil, errorMock(http.StatusInternalServerError, "Server error")), + } + + err := client.ServiceRemove(context.Background(), "service_id") + if err == nil || err.Error() != "Error response from daemon: Server error" { + t.Fatalf("expected a Server Error, got %v", err) + } +} + +func TestServiceRemove(t *testing.T) { + expectedURL := "/services/service_id" + + client := &Client{ + transport: newMockClient(nil, func(req *http.Request) (*http.Response, error) { + if !strings.HasPrefix(req.URL.Path, expectedURL) { + return nil, fmt.Errorf("Expected URL '%s', got '%s'", expectedURL, req.URL) + } + if req.Method != "DELETE" { + return nil, fmt.Errorf("expected DELETE method, got %s", req.Method) + } + return &http.Response{ + StatusCode: http.StatusOK, + Body: ioutil.NopCloser(bytes.NewReader([]byte("body"))), + }, nil + }), + } + + err := client.ServiceRemove(context.Background(), "service_id") + if err != nil { + t.Fatal(err) + } +} diff --git a/service_update.go b/service_update.go new file mode 100644 index 0000000000..c5d07e8394 --- /dev/null +++ b/service_update.go @@ -0,0 +1,30 @@ +package client + +import ( + "net/url" + "strconv" + + "github.com/docker/docker/api/types" + "github.com/docker/docker/api/types/swarm" + "golang.org/x/net/context" +) + +// ServiceUpdate updates a Service. +func (cli *Client) ServiceUpdate(ctx context.Context, serviceID string, version swarm.Version, service swarm.ServiceSpec, options types.ServiceUpdateOptions) error { + var ( + headers map[string][]string + query = url.Values{} + ) + + if options.EncodedRegistryAuth != "" { + headers = map[string][]string{ + "X-Registry-Auth": {options.EncodedRegistryAuth}, + } + } + + query.Set("version", strconv.FormatUint(version.Index, 10)) + + resp, err := cli.post(ctx, "/services/"+serviceID+"/update", query, service, headers) + ensureReaderClosed(resp) + return err +} diff --git a/service_update_test.go b/service_update_test.go new file mode 100644 index 0000000000..bd616c09bf --- /dev/null +++ b/service_update_test.go @@ -0,0 +1,77 @@ +package client + +import ( + "bytes" + "fmt" + "io/ioutil" + "net/http" + "strings" + "testing" + + "golang.org/x/net/context" + + "github.com/docker/docker/api/types" + "github.com/docker/docker/api/types/swarm" +) + +func TestServiceUpdateError(t *testing.T) { + client := &Client{ + transport: newMockClient(nil, errorMock(http.StatusInternalServerError, "Server error")), + } + + err := client.ServiceUpdate(context.Background(), "service_id", swarm.Version{}, swarm.ServiceSpec{}, types.ServiceUpdateOptions{}) + if err == nil || err.Error() != "Error response from daemon: Server error" { + t.Fatalf("expected a Server Error, got %v", err) + } +} + +func TestServiceUpdate(t *testing.T) { + expectedURL := "/services/service_id/update" + + updateCases := []struct { + swarmVersion swarm.Version + expectedVersion string + }{ + { + expectedVersion: "0", + }, + { + swarmVersion: swarm.Version{ + Index: 0, + }, + expectedVersion: "0", + }, + { + swarmVersion: swarm.Version{ + Index: 10, + }, + expectedVersion: "10", + }, + } + + for _, updateCase := range updateCases { + client := &Client{ + transport: newMockClient(nil, func(req *http.Request) (*http.Response, error) { + if !strings.HasPrefix(req.URL.Path, expectedURL) { + return nil, fmt.Errorf("Expected URL '%s', got '%s'", expectedURL, req.URL) + } + if req.Method != "POST" { + return nil, fmt.Errorf("expected POST method, got %s", req.Method) + } + version := req.URL.Query().Get("version") + if version != updateCase.expectedVersion { + return nil, fmt.Errorf("version not set in URL query properly, expected '%s', got %s", updateCase.expectedVersion, version) + } + return &http.Response{ + StatusCode: http.StatusOK, + Body: ioutil.NopCloser(bytes.NewReader([]byte("body"))), + }, nil + }), + } + + err := client.ServiceUpdate(context.Background(), "service_id", updateCase.swarmVersion, swarm.ServiceSpec{}, types.ServiceUpdateOptions{}) + if err != nil { + t.Fatal(err) + } + } +} diff --git a/swarm_init.go b/swarm_init.go new file mode 100644 index 0000000000..fd45d066e3 --- /dev/null +++ b/swarm_init.go @@ -0,0 +1,21 @@ +package client + +import ( + "encoding/json" + + "github.com/docker/docker/api/types/swarm" + "golang.org/x/net/context" +) + +// SwarmInit initializes the Swarm. +func (cli *Client) SwarmInit(ctx context.Context, req swarm.InitRequest) (string, error) { + serverResp, err := cli.post(ctx, "/swarm/init", nil, req, nil) + if err != nil { + return "", err + } + + var response string + err = json.NewDecoder(serverResp.body).Decode(&response) + ensureReaderClosed(serverResp) + return response, err +} diff --git a/swarm_init_test.go b/swarm_init_test.go new file mode 100644 index 0000000000..077c8c4efb --- /dev/null +++ b/swarm_init_test.go @@ -0,0 +1,54 @@ +package client + +import ( + "bytes" + "fmt" + "io/ioutil" + "net/http" + "strings" + "testing" + + "golang.org/x/net/context" + + "github.com/docker/docker/api/types/swarm" +) + +func TestSwarmInitError(t *testing.T) { + client := &Client{ + transport: newMockClient(nil, errorMock(http.StatusInternalServerError, "Server error")), + } + + _, err := client.SwarmInit(context.Background(), swarm.InitRequest{}) + if err == nil || err.Error() != "Error response from daemon: Server error" { + t.Fatalf("expected a Server Error, got %v", err) + } +} + +func TestSwarmInit(t *testing.T) { + expectedURL := "/swarm/init" + + client := &Client{ + transport: newMockClient(nil, func(req *http.Request) (*http.Response, error) { + if !strings.HasPrefix(req.URL.Path, expectedURL) { + return nil, fmt.Errorf("Expected URL '%s', got '%s'", expectedURL, req.URL) + } + if req.Method != "POST" { + return nil, fmt.Errorf("expected POST method, got %s", req.Method) + } + return &http.Response{ + StatusCode: http.StatusOK, + Body: ioutil.NopCloser(bytes.NewReader([]byte(`"body"`))), + }, nil + }), + } + + resp, err := client.SwarmInit(context.Background(), swarm.InitRequest{ + ListenAddr: "0.0.0.0:2377", + }) + if err != nil { + t.Fatal(err) + } + if resp != "body" { + t.Fatalf("Expected 'body', got %s", resp) + } +} diff --git a/swarm_inspect.go b/swarm_inspect.go new file mode 100644 index 0000000000..6d95cfc05e --- /dev/null +++ b/swarm_inspect.go @@ -0,0 +1,21 @@ +package client + +import ( + "encoding/json" + + "github.com/docker/docker/api/types/swarm" + "golang.org/x/net/context" +) + +// SwarmInspect inspects the Swarm. +func (cli *Client) SwarmInspect(ctx context.Context) (swarm.Swarm, error) { + serverResp, err := cli.get(ctx, "/swarm", nil, nil) + if err != nil { + return swarm.Swarm{}, err + } + + var response swarm.Swarm + err = json.NewDecoder(serverResp.body).Decode(&response) + ensureReaderClosed(serverResp) + return response, err +} diff --git a/swarm_inspect_test.go b/swarm_inspect_test.go new file mode 100644 index 0000000000..7143e77181 --- /dev/null +++ b/swarm_inspect_test.go @@ -0,0 +1,56 @@ +package client + +import ( + "bytes" + "encoding/json" + "fmt" + "io/ioutil" + "net/http" + "strings" + "testing" + + "github.com/docker/docker/api/types/swarm" + "golang.org/x/net/context" +) + +func TestSwarmInspectError(t *testing.T) { + client := &Client{ + transport: newMockClient(nil, errorMock(http.StatusInternalServerError, "Server error")), + } + + _, err := client.SwarmInspect(context.Background()) + if err == nil || err.Error() != "Error response from daemon: Server error" { + t.Fatalf("expected a Server Error, got %v", err) + } +} + +func TestSwarmInspect(t *testing.T) { + expectedURL := "/swarm" + client := &Client{ + transport: newMockClient(nil, func(req *http.Request) (*http.Response, error) { + if !strings.HasPrefix(req.URL.Path, expectedURL) { + return nil, fmt.Errorf("Expected URL '%s', got '%s'", expectedURL, req.URL) + } + content, err := json.Marshal(swarm.Swarm{ + ClusterInfo: swarm.ClusterInfo{ + ID: "swarm_id", + }, + }) + if err != nil { + return nil, err + } + return &http.Response{ + StatusCode: http.StatusOK, + Body: ioutil.NopCloser(bytes.NewReader(content)), + }, nil + }), + } + + swarmInspect, err := client.SwarmInspect(context.Background()) + if err != nil { + t.Fatal(err) + } + if swarmInspect.ID != "swarm_id" { + t.Fatalf("expected `swarm_id`, got %s", swarmInspect.ID) + } +} diff --git a/swarm_join.go b/swarm_join.go new file mode 100644 index 0000000000..cda99930eb --- /dev/null +++ b/swarm_join.go @@ -0,0 +1,13 @@ +package client + +import ( + "github.com/docker/docker/api/types/swarm" + "golang.org/x/net/context" +) + +// SwarmJoin joins the Swarm. +func (cli *Client) SwarmJoin(ctx context.Context, req swarm.JoinRequest) error { + resp, err := cli.post(ctx, "/swarm/join", nil, req, nil) + ensureReaderClosed(resp) + return err +} diff --git a/swarm_join_test.go b/swarm_join_test.go new file mode 100644 index 0000000000..922716d85f --- /dev/null +++ b/swarm_join_test.go @@ -0,0 +1,51 @@ +package client + +import ( + "bytes" + "fmt" + "io/ioutil" + "net/http" + "strings" + "testing" + + "golang.org/x/net/context" + + "github.com/docker/docker/api/types/swarm" +) + +func TestSwarmJoinError(t *testing.T) { + client := &Client{ + transport: newMockClient(nil, errorMock(http.StatusInternalServerError, "Server error")), + } + + err := client.SwarmJoin(context.Background(), swarm.JoinRequest{}) + if err == nil || err.Error() != "Error response from daemon: Server error" { + t.Fatalf("expected a Server Error, got %v", err) + } +} + +func TestSwarmJoin(t *testing.T) { + expectedURL := "/swarm/join" + + client := &Client{ + transport: newMockClient(nil, func(req *http.Request) (*http.Response, error) { + if !strings.HasPrefix(req.URL.Path, expectedURL) { + return nil, fmt.Errorf("Expected URL '%s', got '%s'", expectedURL, req.URL) + } + if req.Method != "POST" { + return nil, fmt.Errorf("expected POST method, got %s", req.Method) + } + return &http.Response{ + StatusCode: http.StatusOK, + Body: ioutil.NopCloser(bytes.NewReader([]byte(""))), + }, nil + }), + } + + err := client.SwarmJoin(context.Background(), swarm.JoinRequest{ + ListenAddr: "0.0.0.0:2377", + }) + if err != nil { + t.Fatal(err) + } +} diff --git a/swarm_leave.go b/swarm_leave.go new file mode 100644 index 0000000000..a4df732174 --- /dev/null +++ b/swarm_leave.go @@ -0,0 +1,18 @@ +package client + +import ( + "net/url" + + "golang.org/x/net/context" +) + +// SwarmLeave leaves the Swarm. +func (cli *Client) SwarmLeave(ctx context.Context, force bool) error { + query := url.Values{} + if force { + query.Set("force", "1") + } + resp, err := cli.post(ctx, "/swarm/leave", query, nil, nil) + ensureReaderClosed(resp) + return err +} diff --git a/swarm_leave_test.go b/swarm_leave_test.go new file mode 100644 index 0000000000..d0bef2b257 --- /dev/null +++ b/swarm_leave_test.go @@ -0,0 +1,66 @@ +package client + +import ( + "bytes" + "fmt" + "io/ioutil" + "net/http" + "strings" + "testing" + + "golang.org/x/net/context" +) + +func TestSwarmLeaveError(t *testing.T) { + client := &Client{ + transport: newMockClient(nil, errorMock(http.StatusInternalServerError, "Server error")), + } + + err := client.SwarmLeave(context.Background(), false) + if err == nil || err.Error() != "Error response from daemon: Server error" { + t.Fatalf("expected a Server Error, got %v", err) + } +} + +func TestSwarmLeave(t *testing.T) { + expectedURL := "/swarm/leave" + + leaveCases := []struct { + force bool + expectedForce string + }{ + { + expectedForce: "", + }, + { + force: true, + expectedForce: "1", + }, + } + + for _, leaveCase := range leaveCases { + client := &Client{ + transport: newMockClient(nil, func(req *http.Request) (*http.Response, error) { + if !strings.HasPrefix(req.URL.Path, expectedURL) { + return nil, fmt.Errorf("Expected URL '%s', got '%s'", expectedURL, req.URL) + } + if req.Method != "POST" { + return nil, fmt.Errorf("expected POST method, got %s", req.Method) + } + force := req.URL.Query().Get("force") + if force != leaveCase.expectedForce { + return nil, fmt.Errorf("force not set in URL query properly. expected '%s', got %s", leaveCase.expectedForce, force) + } + return &http.Response{ + StatusCode: http.StatusOK, + Body: ioutil.NopCloser(bytes.NewReader([]byte(""))), + }, nil + }), + } + + err := client.SwarmLeave(context.Background(), leaveCase.force) + if err != nil { + t.Fatal(err) + } + } +} diff --git a/swarm_update.go b/swarm_update.go new file mode 100644 index 0000000000..f0be145ba2 --- /dev/null +++ b/swarm_update.go @@ -0,0 +1,21 @@ +package client + +import ( + "fmt" + "net/url" + "strconv" + + "github.com/docker/docker/api/types/swarm" + "golang.org/x/net/context" +) + +// SwarmUpdate updates the Swarm. +func (cli *Client) SwarmUpdate(ctx context.Context, version swarm.Version, swarm swarm.Spec, flags swarm.UpdateFlags) error { + query := url.Values{} + query.Set("version", strconv.FormatUint(version.Index, 10)) + query.Set("rotateWorkerToken", fmt.Sprintf("%v", flags.RotateWorkerToken)) + query.Set("rotateManagerToken", fmt.Sprintf("%v", flags.RotateManagerToken)) + resp, err := cli.post(ctx, "/swarm/update", query, swarm, nil) + ensureReaderClosed(resp) + return err +} diff --git a/swarm_update_test.go b/swarm_update_test.go new file mode 100644 index 0000000000..ecf1731e5b --- /dev/null +++ b/swarm_update_test.go @@ -0,0 +1,49 @@ +package client + +import ( + "bytes" + "fmt" + "io/ioutil" + "net/http" + "strings" + "testing" + + "golang.org/x/net/context" + + "github.com/docker/docker/api/types/swarm" +) + +func TestSwarmUpdateError(t *testing.T) { + client := &Client{ + transport: newMockClient(nil, errorMock(http.StatusInternalServerError, "Server error")), + } + + err := client.SwarmUpdate(context.Background(), swarm.Version{}, swarm.Spec{}, swarm.UpdateFlags{}) + if err == nil || err.Error() != "Error response from daemon: Server error" { + t.Fatalf("expected a Server Error, got %v", err) + } +} + +func TestSwarmUpdate(t *testing.T) { + expectedURL := "/swarm/update" + + client := &Client{ + transport: newMockClient(nil, func(req *http.Request) (*http.Response, error) { + if !strings.HasPrefix(req.URL.Path, expectedURL) { + return nil, fmt.Errorf("Expected URL '%s', got '%s'", expectedURL, req.URL) + } + if req.Method != "POST" { + return nil, fmt.Errorf("expected POST method, got %s", req.Method) + } + return &http.Response{ + StatusCode: http.StatusOK, + Body: ioutil.NopCloser(bytes.NewReader([]byte(""))), + }, nil + }), + } + + err := client.SwarmUpdate(context.Background(), swarm.Version{}, swarm.Spec{}, swarm.UpdateFlags{}) + if err != nil { + t.Fatal(err) + } +} diff --git a/task_inspect.go b/task_inspect.go new file mode 100644 index 0000000000..bc8058fc32 --- /dev/null +++ b/task_inspect.go @@ -0,0 +1,34 @@ +package client + +import ( + "bytes" + "encoding/json" + "io/ioutil" + "net/http" + + "github.com/docker/docker/api/types/swarm" + + "golang.org/x/net/context" +) + +// TaskInspectWithRaw returns the task information and its raw representation.. +func (cli *Client) TaskInspectWithRaw(ctx context.Context, taskID string) (swarm.Task, []byte, error) { + serverResp, err := cli.get(ctx, "/tasks/"+taskID, nil, nil) + if err != nil { + if serverResp.statusCode == http.StatusNotFound { + return swarm.Task{}, nil, taskNotFoundError{taskID} + } + return swarm.Task{}, nil, err + } + defer ensureReaderClosed(serverResp) + + body, err := ioutil.ReadAll(serverResp.body) + if err != nil { + return swarm.Task{}, nil, err + } + + var response swarm.Task + rdr := bytes.NewReader(body) + err = json.NewDecoder(rdr).Decode(&response) + return response, body, err +} diff --git a/task_inspect_test.go b/task_inspect_test.go new file mode 100644 index 0000000000..2c73b37642 --- /dev/null +++ b/task_inspect_test.go @@ -0,0 +1,54 @@ +package client + +import ( + "bytes" + "encoding/json" + "fmt" + "io/ioutil" + "net/http" + "strings" + "testing" + + "github.com/docker/docker/api/types/swarm" + "golang.org/x/net/context" +) + +func TestTaskInspectError(t *testing.T) { + client := &Client{ + transport: newMockClient(nil, errorMock(http.StatusInternalServerError, "Server error")), + } + + _, _, err := client.TaskInspectWithRaw(context.Background(), "nothing") + if err == nil || err.Error() != "Error response from daemon: Server error" { + t.Fatalf("expected a Server Error, got %v", err) + } +} + +func TestTaskInspect(t *testing.T) { + expectedURL := "/tasks/task_id" + client := &Client{ + transport: newMockClient(nil, func(req *http.Request) (*http.Response, error) { + if !strings.HasPrefix(req.URL.Path, expectedURL) { + return nil, fmt.Errorf("Expected URL '%s', got '%s'", expectedURL, req.URL) + } + content, err := json.Marshal(swarm.Task{ + ID: "task_id", + }) + if err != nil { + return nil, err + } + return &http.Response{ + StatusCode: http.StatusOK, + Body: ioutil.NopCloser(bytes.NewReader(content)), + }, nil + }), + } + + taskInspect, _, err := client.TaskInspectWithRaw(context.Background(), "task_id") + if err != nil { + t.Fatal(err) + } + if taskInspect.ID != "task_id" { + t.Fatalf("expected `task_id`, got %s", taskInspect.ID) + } +} diff --git a/task_list.go b/task_list.go new file mode 100644 index 0000000000..07c8324c83 --- /dev/null +++ b/task_list.go @@ -0,0 +1,35 @@ +package client + +import ( + "encoding/json" + "net/url" + + "github.com/docker/docker/api/types" + "github.com/docker/docker/api/types/filters" + "github.com/docker/docker/api/types/swarm" + "golang.org/x/net/context" +) + +// TaskList returns the list of tasks. +func (cli *Client) TaskList(ctx context.Context, options types.TaskListOptions) ([]swarm.Task, error) { + query := url.Values{} + + if options.Filter.Len() > 0 { + filterJSON, err := filters.ToParam(options.Filter) + if err != nil { + return nil, err + } + + query.Set("filters", filterJSON) + } + + resp, err := cli.get(ctx, "/tasks", query, nil) + if err != nil { + return nil, err + } + + var tasks []swarm.Task + err = json.NewDecoder(resp.body).Decode(&tasks) + ensureReaderClosed(resp) + return tasks, err +} diff --git a/task_list_test.go b/task_list_test.go new file mode 100644 index 0000000000..b520ab589f --- /dev/null +++ b/task_list_test.go @@ -0,0 +1,94 @@ +package client + +import ( + "bytes" + "encoding/json" + "fmt" + "io/ioutil" + "net/http" + "strings" + "testing" + + "github.com/docker/docker/api/types" + "github.com/docker/docker/api/types/filters" + "github.com/docker/docker/api/types/swarm" + "golang.org/x/net/context" +) + +func TestTaskListError(t *testing.T) { + client := &Client{ + transport: newMockClient(nil, errorMock(http.StatusInternalServerError, "Server error")), + } + + _, err := client.TaskList(context.Background(), types.TaskListOptions{}) + if err == nil || err.Error() != "Error response from daemon: Server error" { + t.Fatalf("expected a Server Error, got %v", err) + } +} + +func TestTaskList(t *testing.T) { + expectedURL := "/tasks" + + filters := filters.NewArgs() + filters.Add("label", "label1") + filters.Add("label", "label2") + + listCases := []struct { + options types.TaskListOptions + expectedQueryParams map[string]string + }{ + { + options: types.TaskListOptions{}, + expectedQueryParams: map[string]string{ + "filters": "", + }, + }, + { + options: types.TaskListOptions{ + Filter: filters, + }, + expectedQueryParams: map[string]string{ + "filters": `{"label":{"label1":true,"label2":true}}`, + }, + }, + } + for _, listCase := range listCases { + client := &Client{ + transport: newMockClient(nil, func(req *http.Request) (*http.Response, error) { + if !strings.HasPrefix(req.URL.Path, expectedURL) { + return nil, fmt.Errorf("Expected URL '%s', got '%s'", expectedURL, req.URL) + } + query := req.URL.Query() + for key, expected := range listCase.expectedQueryParams { + actual := query.Get(key) + if actual != expected { + return nil, fmt.Errorf("%s not set in URL query properly. Expected '%s', got %s", key, expected, actual) + } + } + content, err := json.Marshal([]swarm.Task{ + { + ID: "task_id1", + }, + { + ID: "task_id2", + }, + }) + if err != nil { + return nil, err + } + return &http.Response{ + StatusCode: http.StatusOK, + Body: ioutil.NopCloser(bytes.NewReader(content)), + }, nil + }), + } + + tasks, err := client.TaskList(context.Background(), listCase.options) + if err != nil { + t.Fatal(err) + } + if len(tasks) != 2 { + t.Fatalf("expected 2 tasks, got %v", tasks) + } + } +} diff --git a/testdata/ca.pem b/testdata/ca.pem new file mode 100644 index 0000000000..ad14d47065 --- /dev/null +++ b/testdata/ca.pem @@ -0,0 +1,18 @@ +-----BEGIN CERTIFICATE----- +MIIC0jCCAbqgAwIBAgIRAILlP5WWLaHkQ/m2ASHP7SowDQYJKoZIhvcNAQELBQAw +EjEQMA4GA1UEChMHdmluY2VudDAeFw0xNjAzMjQxMDE5MDBaFw0xOTAzMDkxMDE5 +MDBaMBIxEDAOBgNVBAoTB3ZpbmNlbnQwggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAw +ggEKAoIBAQD0yZPKAGncoaxaU/QW9tWEHbrvDoGVF/65L8Si/jBrlAgLjhmmV1di +vKG9QPzuU8snxHro3/uCwyA6kTqw0U8bGwHxJq2Bpa6JBYj8N2jMJ+M+sjXgSo2t +E0zIzjTW2Pir3C8qwfrVL6NFp9xClwMD23SFZ0UsEH36NkfyrKBVeM8IOjJd4Wjs +xIcuvF3BTVkji84IJBW2JIKf9ZrzJwUlSCPgptRp4Evdbyp5d+UPxtwxD7qjW4lM +yQQ8vfcC4lKkVx5s/RNJ4fzd5uEgLdEbZ20qt7Zt/bLcxFHpUhH2teA0QjmrOWFh +gbL83s95/+hbSVhsO4hoFW7vTeiCCY4xAgMBAAGjIzAhMA4GA1UdDwEB/wQEAwIC +rDAPBgNVHRMBAf8EBTADAQH/MA0GCSqGSIb3DQEBCwUAA4IBAQBY51RHajuDuhO2 +tcm26jeNROzfffnjhvbOVPjSEdo9vI3JpMU/RuQw+nbNcLwJrdjL6UH7tD/36Y+q +NXH+xSIjWFH0zXGxrIUsVrvt6f8CbOvw7vD+gygOG+849PDQMbL6czP8rvXY7vZV +9pdpQfrENk4b5kePRW/6HaGSTvtgN7XOrYD9fp3pm/G534T2e3IxgYMRNwdB9Ul9 +bLwMqQqf4eiqqMs6x4IVmZUkGVMKiFKcvkNg9a+Ozx5pMizHeAezWMcZ5V+QJZVT +8lElSCKZ2Yy2xkcl7aeQMLwcAeZwfTp+Yu9dVzlqXiiBTLd1+LtAQCuKHzmw4Q8k +EvD5m49l +-----END CERTIFICATE----- diff --git a/testdata/cert.pem b/testdata/cert.pem new file mode 100644 index 0000000000..9000ffb32b --- /dev/null +++ b/testdata/cert.pem @@ -0,0 +1,18 @@ +-----BEGIN CERTIFICATE----- +MIIC8DCCAdigAwIBAgIRAJAS1glgcke4q7eCaretwgUwDQYJKoZIhvcNAQELBQAw +EjEQMA4GA1UEChMHdmluY2VudDAeFw0xNjAzMjQxMDE5MDBaFw0xOTAzMDkxMDE5 +MDBaMB4xHDAaBgNVBAoME3ZpbmNlbnQuPGJvb3RzdHJhcD4wggEiMA0GCSqGSIb3 +DQEBAQUAA4IBDwAwggEKAoIBAQClpvG442dGEvrRgmCrqY4kBml1LVlw2Y7ZDn6B +TKa52+MuGDmfXbO1UhclNqTXjLgAwKjPz/OvnPRxNEUoQEDbBd+Xev7rxTY5TvYI +27YH3fMH2LL2j62jum649abfhZ6ekD5eD8tCn3mnrEOgqRIlK7efPIVixq/ZqU1H +7ez0ggB7dmWHlhnUaxyQOCSnAX/7nKYQXqZgVvGhDeR2jp7GcnhbK/qPrZ/mOm83 +2IjCeYN145opYlzTSp64GYIZz7uqMNcnDKK37ZbS8MYcTjrRaHEiqZVVdIC+ghbx +qYqzbZRVfgztI9jwmifn0mYrN4yt+nhNYwBcRJ4Pv3uLFbo7AgMBAAGjNTAzMA4G +A1UdDwEB/wQEAwIHgDATBgNVHSUEDDAKBggrBgEFBQcDAjAMBgNVHRMBAf8EAjAA +MA0GCSqGSIb3DQEBCwUAA4IBAQDg1r7nksjYgDFYEcBbrRrRHddIoK+RVmSBTTrq +8giC77m0srKdh9XTVWK1PUbGfODV1oD8m9QhPE8zPDyYQ8jeXNRSU5wXdkrTRmmY +w/T3SREqmE7CObMtusokHidjYFuqqCR07sJzqBKRlzr3o0EGe3tuEhUlF5ARY028 +eipaDcVlT5ChGcDa6LeJ4e05u4cVap0dd6Rp1w3Rx1AYAecdgtgBMnw1iWdl/nrC +sp26ZXNaAhFOUovlY9VY257AMd9hQV7WvAK4yNEHcckVu3uXTBmDgNSOPtl0QLsL +Kjlj75ksCx8nCln/hCut/0+kGTsGZqdV5c6ktgcGYRir/5Hs +-----END CERTIFICATE----- diff --git a/testdata/key.pem b/testdata/key.pem new file mode 100644 index 0000000000..c0869dfc1a --- /dev/null +++ b/testdata/key.pem @@ -0,0 +1,27 @@ +-----BEGIN RSA PRIVATE KEY----- +MIIEowIBAAKCAQEApabxuONnRhL60YJgq6mOJAZpdS1ZcNmO2Q5+gUymudvjLhg5 +n12ztVIXJTak14y4AMCoz8/zr5z0cTRFKEBA2wXfl3r+68U2OU72CNu2B93zB9iy +9o+to7puuPWm34WenpA+Xg/LQp95p6xDoKkSJSu3nzyFYsav2alNR+3s9IIAe3Zl +h5YZ1GsckDgkpwF/+5ymEF6mYFbxoQ3kdo6exnJ4Wyv6j62f5jpvN9iIwnmDdeOa +KWJc00qeuBmCGc+7qjDXJwyit+2W0vDGHE460WhxIqmVVXSAvoIW8amKs22UVX4M +7SPY8Jon59JmKzeMrfp4TWMAXESeD797ixW6OwIDAQABAoIBAHfyAAleL8NfrtnR +S+pApbmUIvxD0AWUooispBE/zWG6xC72P5MTqDJctIGvpYCmVf3Fgvamns7EGYN2 +07Sngc6V3Ca1WqyhaffpIuGbJZ1gqr89u6gotRRexBmNVj13ZTlvPJmjWgxtqQsu +AvHsOkVL+HOGwRaaw24Z1umEcBVCepl7PGTqsLeJUtBUZBiqdJTu4JYLAB6BggBI +OxhHoTWvlNWwzezo2C/IXkXcXD/tp3i5vTn5rAXHSMQkdMAUh7/xJ73Fl36gxZhp +W7NoPKaS9qNh8jhs6p54S7tInb6+mrKtvRFKl5XAR3istXrXteT5UaukpuBbQ/5d +qf4BXuECgYEAzoOKxMee5tG/G9iC6ImNq5xGAZm0OnmteNgIEQj49If1Q68av525 +FioqdC9zV+blfHQqXEIUeum4JAou4xqmB8Lw2H0lYwOJ1IkpUy3QJjU1IrI+U5Qy +ryZuA9cxSTLf1AJFbROsoZDpjaBh0uUQkD/4PHpwXMgHu/3CaJ4nTEkCgYEAzVjE +VWgczWJGyRxmHSeR51ft1jrlChZHEd3HwgLfo854JIj+MGUH4KPLSMIkYNuyiwNQ +W7zdXCB47U8afSL/lPTv1M5+ZsWY6sZAT6gtp/IeU0Va943h9cj10fAOBJaz1H6M +jnZS4jjWhVInE7wpCDVCwDRoHHJ84kb6JeflamMCgYBDQDcKie9HP3q6uLE4xMKr +5gIuNz2n5UQGnGNUGNXp2/SVDArr55MEksqsd19aesi01KeOz74XoNDke6R1NJJo +6KTB+08XhWl3GwuoGL02FBGvsNf3I8W1oBAnlAZqzfRx+CNfuA55ttU318jDgvD3 +6L0QBNdef411PNf4dbhacQKBgAd/e0PHFm4lbYJAaDYeUMSKwGN3KQ/SOmwblgSu +iC36BwcGfYmU1tHMCUsx05Q50W4kA9Ylskt/4AqCPexdz8lHnE4/7/uesXO5I3YF +JQ2h2Jufx6+MXbjUyq0Mv+ZI/m3+5PD6vxIFk0ew9T5SO4lSMIrGHxsSzx6QCuhB +bG4TAoGBAJ5PWG7d2CyCjLtfF8J4NxykRvIQ8l/3kDvDdNrXiXbgonojo2lgRYaM +5LoK9ApN8KHdedpTRipBaDA22Sp5SjMcUE7A6q42PJCL9r+BRYF0foFQx/rqpCff +pVWKgwIPoKnfxDqN1RUgyFcx1jbA3XVJZCuT+wbMuDQ9nlvulD1W +-----END RSA PRIVATE KEY----- diff --git a/transport/cancellable/LICENSE b/transport/cancellable/LICENSE new file mode 100644 index 0000000000..6a66aea5ea --- /dev/null +++ b/transport/cancellable/LICENSE @@ -0,0 +1,27 @@ +Copyright (c) 2009 The Go Authors. All rights reserved. + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions are +met: + + * Redistributions of source code must retain the above copyright +notice, this list of conditions and the following disclaimer. + * Redistributions in binary form must reproduce the above +copyright notice, this list of conditions and the following disclaimer +in the documentation and/or other materials provided with the +distribution. + * Neither the name of Google Inc. nor the names of its +contributors may be used to endorse or promote products derived from +this software without specific prior written permission. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT +OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, +DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY +THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. diff --git a/transport/cancellable/canceler.go b/transport/cancellable/canceler.go new file mode 100644 index 0000000000..62770b777b --- /dev/null +++ b/transport/cancellable/canceler.go @@ -0,0 +1,23 @@ +// Copyright 2015 The Go Authors. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +// +build go1.5 + +package cancellable + +import ( + "net/http" + + "github.com/docker/docker/client/transport" +) + +func canceler(client transport.Sender, req *http.Request) func() { + // TODO(djd): Respect any existing value of req.Cancel. + ch := make(chan struct{}) + req.Cancel = ch + + return func() { + close(ch) + } +} diff --git a/transport/cancellable/canceler_go14.go b/transport/cancellable/canceler_go14.go new file mode 100644 index 0000000000..dd2723d94f --- /dev/null +++ b/transport/cancellable/canceler_go14.go @@ -0,0 +1,27 @@ +// Copyright 2015 The Go Authors. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +// +build !go1.5 + +package cancellable + +import ( + "net/http" + + "github.com/docker/docker/client/transport" +) + +type requestCanceler interface { + CancelRequest(*http.Request) +} + +func canceler(client transport.Sender, req *http.Request) func() { + rc, ok := client.(requestCanceler) + if !ok { + return func() {} + } + return func() { + rc.CancelRequest(req) + } +} diff --git a/transport/cancellable/cancellable.go b/transport/cancellable/cancellable.go new file mode 100644 index 0000000000..1f8eac5c1c --- /dev/null +++ b/transport/cancellable/cancellable.go @@ -0,0 +1,115 @@ +// Copyright 2015 The Go Authors. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +// Package cancellable provides helper function to cancel http requests. +package cancellable + +import ( + "io" + "net/http" + "sync" + + "github.com/docker/docker/client/transport" + + "golang.org/x/net/context" +) + +func nop() {} + +var ( + testHookContextDoneBeforeHeaders = nop + testHookDoReturned = nop + testHookDidBodyClose = nop +) + +// Do sends an HTTP request with the provided transport.Sender and returns an HTTP response. +// If the client is nil, http.DefaultClient is used. +// If the context is canceled or times out, ctx.Err() will be returned. +// +// FORK INFORMATION: +// +// This function deviates from the upstream version in golang.org/x/net/context/ctxhttp by +// taking a Sender interface rather than a *http.Client directly. That allow us to use +// this function with mocked clients and hijacked connections. +func Do(ctx context.Context, client transport.Sender, req *http.Request) (*http.Response, error) { + if client == nil { + client = http.DefaultClient + } + + // Request cancelation changed in Go 1.5, see canceler.go and canceler_go14.go. + cancel := canceler(client, req) + + type responseAndError struct { + resp *http.Response + err error + } + result := make(chan responseAndError, 1) + + go func() { + resp, err := client.Do(req) + testHookDoReturned() + result <- responseAndError{resp, err} + }() + + var resp *http.Response + + select { + case <-ctx.Done(): + testHookContextDoneBeforeHeaders() + cancel() + // Clean up after the goroutine calling client.Do: + go func() { + if r := <-result; r.resp != nil && r.resp.Body != nil { + testHookDidBodyClose() + r.resp.Body.Close() + } + }() + return nil, ctx.Err() + case r := <-result: + var err error + resp, err = r.resp, r.err + if err != nil { + return resp, err + } + } + + c := make(chan struct{}) + go func() { + select { + case <-ctx.Done(): + cancel() + case <-c: + // The response's Body is closed. + } + }() + resp.Body = ¬ifyingReader{ReadCloser: resp.Body, notify: c} + + return resp, nil +} + +// notifyingReader is an io.ReadCloser that closes the notify channel after +// Close is called or a Read fails on the underlying ReadCloser. +type notifyingReader struct { + io.ReadCloser + notify chan<- struct{} + notifyOnce sync.Once +} + +func (r *notifyingReader) Read(p []byte) (int, error) { + n, err := r.ReadCloser.Read(p) + if err != nil { + r.notifyOnce.Do(func() { + close(r.notify) + }) + } + return n, err +} + +func (r *notifyingReader) Close() error { + err := r.ReadCloser.Close() + r.notifyOnce.Do(func() { + close(r.notify) + }) + return err +} diff --git a/transport/client.go b/transport/client.go new file mode 100644 index 0000000000..13d4b3ab3d --- /dev/null +++ b/transport/client.go @@ -0,0 +1,47 @@ +package transport + +import ( + "crypto/tls" + "net/http" +) + +// Sender is an interface that clients must implement +// to be able to send requests to a remote connection. +type Sender interface { + // Do sends request to a remote endpoint. + Do(*http.Request) (*http.Response, error) +} + +// Client is an interface that abstracts all remote connections. +type Client interface { + Sender + // Secure tells whether the connection is secure or not. + Secure() bool + // Scheme returns the connection protocol the client uses. + Scheme() string + // TLSConfig returns any TLS configuration the client uses. + TLSConfig() *tls.Config +} + +// tlsInfo returns information about the TLS configuration. +type tlsInfo struct { + tlsConfig *tls.Config +} + +// TLSConfig returns the TLS configuration. +func (t *tlsInfo) TLSConfig() *tls.Config { + return t.tlsConfig +} + +// Scheme returns protocol scheme to use. +func (t *tlsInfo) Scheme() string { + if t.tlsConfig != nil { + return "https" + } + return "http" +} + +// Secure returns true if there is a TLS configuration. +func (t *tlsInfo) Secure() bool { + return t.tlsConfig != nil +} diff --git a/transport/tlsconfig_clone.go b/transport/tlsconfig_clone.go new file mode 100644 index 0000000000..033d5dc0f2 --- /dev/null +++ b/transport/tlsconfig_clone.go @@ -0,0 +1,11 @@ +// +build !go1.7,!windows + +package transport + +import "crypto/tls" + +// TLSConfigClone returns a clone of tls.Config. This function is provided for +// compatibility for go1.7 that doesn't include this method in stdlib. +func TLSConfigClone(c *tls.Config) *tls.Config { + return c.Clone() +} diff --git a/transport/tlsconfig_clone_go17.go b/transport/tlsconfig_clone_go17.go new file mode 100644 index 0000000000..a28c9141b2 --- /dev/null +++ b/transport/tlsconfig_clone_go17.go @@ -0,0 +1,33 @@ +// +build go1.7 + +package transport + +import "crypto/tls" + +// TLSConfigClone returns a clone of tls.Config. This function is provided for +// compatibility for go1.7 that doesn't include this method in stdlib. +func TLSConfigClone(c *tls.Config) *tls.Config { + return &tls.Config{ + Rand: c.Rand, + Time: c.Time, + Certificates: c.Certificates, + NameToCertificate: c.NameToCertificate, + GetCertificate: c.GetCertificate, + RootCAs: c.RootCAs, + NextProtos: c.NextProtos, + ServerName: c.ServerName, + ClientAuth: c.ClientAuth, + ClientCAs: c.ClientCAs, + InsecureSkipVerify: c.InsecureSkipVerify, + CipherSuites: c.CipherSuites, + PreferServerCipherSuites: c.PreferServerCipherSuites, + SessionTicketsDisabled: c.SessionTicketsDisabled, + SessionTicketKey: c.SessionTicketKey, + ClientSessionCache: c.ClientSessionCache, + MinVersion: c.MinVersion, + MaxVersion: c.MaxVersion, + CurvePreferences: c.CurvePreferences, + DynamicRecordSizingDisabled: c.DynamicRecordSizingDisabled, + Renegotiation: c.Renegotiation, + } +} diff --git a/transport/transport.go b/transport/transport.go new file mode 100644 index 0000000000..ff28af1855 --- /dev/null +++ b/transport/transport.go @@ -0,0 +1,57 @@ +// Package transport provides function to send request to remote endpoints. +package transport + +import ( + "fmt" + "net/http" + + "github.com/docker/go-connections/sockets" +) + +// apiTransport holds information about the http transport to connect with the API. +type apiTransport struct { + *http.Client + *tlsInfo + transport *http.Transport +} + +// NewTransportWithHTTP creates a new transport based on the provided proto, address and http client. +// It uses Docker's default http transport configuration if the client is nil. +// It does not modify the client's transport if it's not nil. +func NewTransportWithHTTP(proto, addr string, client *http.Client) (Client, error) { + var transport *http.Transport + + if client != nil { + tr, ok := client.Transport.(*http.Transport) + if !ok { + return nil, fmt.Errorf("unable to verify TLS configuration, invalid transport %v", client.Transport) + } + transport = tr + } else { + transport = defaultTransport(proto, addr) + client = &http.Client{ + Transport: transport, + } + } + + return &apiTransport{ + Client: client, + tlsInfo: &tlsInfo{transport.TLSClientConfig}, + transport: transport, + }, nil +} + +// CancelRequest stops a request execution. +func (a *apiTransport) CancelRequest(req *http.Request) { + a.transport.CancelRequest(req) +} + +// defaultTransport creates a new http.Transport with Docker's +// default transport configuration. +func defaultTransport(proto, addr string) *http.Transport { + tr := new(http.Transport) + sockets.ConfigureTransport(tr, proto, addr) + return tr +} + +var _ Client = &apiTransport{} diff --git a/version.go b/version.go new file mode 100644 index 0000000000..933ceb4a49 --- /dev/null +++ b/version.go @@ -0,0 +1,21 @@ +package client + +import ( + "encoding/json" + + "github.com/docker/docker/api/types" + "golang.org/x/net/context" +) + +// ServerVersion returns information of the docker client and server host. +func (cli *Client) ServerVersion(ctx context.Context) (types.Version, error) { + resp, err := cli.get(ctx, "/version", nil, nil) + if err != nil { + return types.Version{}, err + } + + var server types.Version + err = json.NewDecoder(resp.body).Decode(&server) + ensureReaderClosed(resp) + return server, err +} diff --git a/volume_create.go b/volume_create.go new file mode 100644 index 0000000000..f3a79f1e11 --- /dev/null +++ b/volume_create.go @@ -0,0 +1,20 @@ +package client + +import ( + "encoding/json" + + "github.com/docker/docker/api/types" + "golang.org/x/net/context" +) + +// VolumeCreate creates a volume in the docker host. +func (cli *Client) VolumeCreate(ctx context.Context, options types.VolumeCreateRequest) (types.Volume, error) { + var volume types.Volume + resp, err := cli.post(ctx, "/volumes/create", nil, options, nil) + if err != nil { + return volume, err + } + err = json.NewDecoder(resp.body).Decode(&volume) + ensureReaderClosed(resp) + return volume, err +} diff --git a/volume_create_test.go b/volume_create_test.go new file mode 100644 index 0000000000..d3cfa7132f --- /dev/null +++ b/volume_create_test.go @@ -0,0 +1,74 @@ +package client + +import ( + "bytes" + "encoding/json" + "fmt" + "io/ioutil" + "net/http" + "strings" + "testing" + + "github.com/docker/docker/api/types" + "golang.org/x/net/context" +) + +func TestVolumeCreateError(t *testing.T) { + client := &Client{ + transport: newMockClient(nil, errorMock(http.StatusInternalServerError, "Server error")), + } + + _, err := client.VolumeCreate(context.Background(), types.VolumeCreateRequest{}) + if err == nil || err.Error() != "Error response from daemon: Server error" { + t.Fatalf("expected a Server Error, got %v", err) + } +} + +func TestVolumeCreate(t *testing.T) { + expectedURL := "/volumes/create" + + client := &Client{ + transport: newMockClient(nil, func(req *http.Request) (*http.Response, error) { + if !strings.HasPrefix(req.URL.Path, expectedURL) { + return nil, fmt.Errorf("Expected URL '%s', got '%s'", expectedURL, req.URL) + } + + if req.Method != "POST" { + return nil, fmt.Errorf("expected POST method, got %s", req.Method) + } + + content, err := json.Marshal(types.Volume{ + Name: "volume", + Driver: "local", + Mountpoint: "mountpoint", + }) + if err != nil { + return nil, err + } + return &http.Response{ + StatusCode: http.StatusOK, + Body: ioutil.NopCloser(bytes.NewReader(content)), + }, nil + }), + } + + volume, err := client.VolumeCreate(context.Background(), types.VolumeCreateRequest{ + Name: "myvolume", + Driver: "mydriver", + DriverOpts: map[string]string{ + "opt-key": "opt-value", + }, + }) + if err != nil { + t.Fatal(err) + } + if volume.Name != "volume" { + t.Fatalf("expected volume.Name to be 'volume', got %s", volume.Name) + } + if volume.Driver != "local" { + t.Fatalf("expected volume.Driver to be 'local', got %s", volume.Driver) + } + if volume.Mountpoint != "mountpoint" { + t.Fatalf("expected volume.Mountpoint to be 'mountpoint', got %s", volume.Mountpoint) + } +} diff --git a/volume_inspect.go b/volume_inspect.go new file mode 100644 index 0000000000..3860e9b22c --- /dev/null +++ b/volume_inspect.go @@ -0,0 +1,38 @@ +package client + +import ( + "bytes" + "encoding/json" + "io/ioutil" + "net/http" + + "github.com/docker/docker/api/types" + "golang.org/x/net/context" +) + +// VolumeInspect returns the information about a specific volume in the docker host. +func (cli *Client) VolumeInspect(ctx context.Context, volumeID string) (types.Volume, error) { + volume, _, err := cli.VolumeInspectWithRaw(ctx, volumeID) + return volume, err +} + +// VolumeInspectWithRaw returns the information about a specific volume in the docker host and its raw representation +func (cli *Client) VolumeInspectWithRaw(ctx context.Context, volumeID string) (types.Volume, []byte, error) { + var volume types.Volume + resp, err := cli.get(ctx, "/volumes/"+volumeID, nil, nil) + if err != nil { + if resp.statusCode == http.StatusNotFound { + return volume, nil, volumeNotFoundError{volumeID} + } + return volume, nil, err + } + defer ensureReaderClosed(resp) + + body, err := ioutil.ReadAll(resp.body) + if err != nil { + return volume, nil, err + } + rdr := bytes.NewReader(body) + err = json.NewDecoder(rdr).Decode(&volume) + return volume, body, err +} diff --git a/volume_inspect_test.go b/volume_inspect_test.go new file mode 100644 index 0000000000..4b9f47358d --- /dev/null +++ b/volume_inspect_test.go @@ -0,0 +1,76 @@ +package client + +import ( + "bytes" + "encoding/json" + "fmt" + "io/ioutil" + "net/http" + "strings" + "testing" + + "github.com/docker/docker/api/types" + "golang.org/x/net/context" +) + +func TestVolumeInspectError(t *testing.T) { + client := &Client{ + transport: newMockClient(nil, errorMock(http.StatusInternalServerError, "Server error")), + } + + _, err := client.VolumeInspect(context.Background(), "nothing") + if err == nil || err.Error() != "Error response from daemon: Server error" { + t.Fatalf("expected a Server Error, got %v", err) + } +} + +func TestVolumeInspectNotFound(t *testing.T) { + client := &Client{ + transport: newMockClient(nil, errorMock(http.StatusNotFound, "Server error")), + } + + _, err := client.VolumeInspect(context.Background(), "unknown") + if err == nil || !IsErrVolumeNotFound(err) { + t.Fatalf("expected a volumeNotFound error, got %v", err) + } +} + +func TestVolumeInspect(t *testing.T) { + expectedURL := "/volumes/volume_id" + client := &Client{ + transport: newMockClient(nil, func(req *http.Request) (*http.Response, error) { + if !strings.HasPrefix(req.URL.Path, expectedURL) { + return nil, fmt.Errorf("Expected URL '%s', got '%s'", expectedURL, req.URL) + } + if req.Method != "GET" { + return nil, fmt.Errorf("expected GET method, got %s", req.Method) + } + content, err := json.Marshal(types.Volume{ + Name: "name", + Driver: "driver", + Mountpoint: "mountpoint", + }) + if err != nil { + return nil, err + } + return &http.Response{ + StatusCode: http.StatusOK, + Body: ioutil.NopCloser(bytes.NewReader(content)), + }, nil + }), + } + + v, err := client.VolumeInspect(context.Background(), "volume_id") + if err != nil { + t.Fatal(err) + } + if v.Name != "name" { + t.Fatalf("expected `name`, got %s", v.Name) + } + if v.Driver != "driver" { + t.Fatalf("expected `driver`, got %s", v.Driver) + } + if v.Mountpoint != "mountpoint" { + t.Fatalf("expected `mountpoint`, got %s", v.Mountpoint) + } +} diff --git a/volume_list.go b/volume_list.go new file mode 100644 index 0000000000..44f03cfac7 --- /dev/null +++ b/volume_list.go @@ -0,0 +1,32 @@ +package client + +import ( + "encoding/json" + "net/url" + + "github.com/docker/docker/api/types" + "github.com/docker/docker/api/types/filters" + "golang.org/x/net/context" +) + +// VolumeList returns the volumes configured in the docker host. +func (cli *Client) VolumeList(ctx context.Context, filter filters.Args) (types.VolumesListResponse, error) { + var volumes types.VolumesListResponse + query := url.Values{} + + if filter.Len() > 0 { + filterJSON, err := filters.ToParamWithVersion(cli.version, filter) + if err != nil { + return volumes, err + } + query.Set("filters", filterJSON) + } + resp, err := cli.get(ctx, "/volumes", query, nil) + if err != nil { + return volumes, err + } + + err = json.NewDecoder(resp.body).Decode(&volumes) + ensureReaderClosed(resp) + return volumes, err +} diff --git a/volume_list_test.go b/volume_list_test.go new file mode 100644 index 0000000000..d30d9fcd52 --- /dev/null +++ b/volume_list_test.go @@ -0,0 +1,97 @@ +package client + +import ( + "bytes" + "encoding/json" + "fmt" + "io/ioutil" + "net/http" + "strings" + "testing" + + "github.com/docker/docker/api/types" + "github.com/docker/docker/api/types/filters" + "golang.org/x/net/context" +) + +func TestVolumeListError(t *testing.T) { + client := &Client{ + transport: newMockClient(nil, errorMock(http.StatusInternalServerError, "Server error")), + } + + _, err := client.VolumeList(context.Background(), filters.NewArgs()) + if err == nil || err.Error() != "Error response from daemon: Server error" { + t.Fatalf("expected a Server Error, got %v", err) + } +} + +func TestVolumeList(t *testing.T) { + expectedURL := "/volumes" + + noDanglingFilters := filters.NewArgs() + noDanglingFilters.Add("dangling", "false") + + danglingFilters := filters.NewArgs() + danglingFilters.Add("dangling", "true") + + labelFilters := filters.NewArgs() + labelFilters.Add("label", "label1") + labelFilters.Add("label", "label2") + + listCases := []struct { + filters filters.Args + expectedFilters string + }{ + { + filters: filters.NewArgs(), + expectedFilters: "", + }, { + filters: noDanglingFilters, + expectedFilters: `{"dangling":{"false":true}}`, + }, { + filters: danglingFilters, + expectedFilters: `{"dangling":{"true":true}}`, + }, { + filters: labelFilters, + expectedFilters: `{"label":{"label1":true,"label2":true}}`, + }, + } + + for _, listCase := range listCases { + client := &Client{ + transport: newMockClient(nil, func(req *http.Request) (*http.Response, error) { + if !strings.HasPrefix(req.URL.Path, expectedURL) { + return nil, fmt.Errorf("Expected URL '%s', got '%s'", expectedURL, req.URL) + } + query := req.URL.Query() + actualFilters := query.Get("filters") + if actualFilters != listCase.expectedFilters { + return nil, fmt.Errorf("filters not set in URL query properly. Expected '%s', got %s", listCase.expectedFilters, actualFilters) + } + content, err := json.Marshal(types.VolumesListResponse{ + Volumes: []*types.Volume{ + { + Name: "volume", + Driver: "local", + }, + }, + }) + if err != nil { + return nil, err + } + return &http.Response{ + StatusCode: http.StatusOK, + Body: ioutil.NopCloser(bytes.NewReader(content)), + }, nil + }), + } + + volumeResponse, err := client.VolumeList(context.Background(), listCase.filters) + if err != nil { + t.Fatal(err) + } + if len(volumeResponse.Volumes) != 1 { + t.Fatalf("expected 1 volume, got %v", volumeResponse.Volumes) + } + } +} diff --git a/volume_remove.go b/volume_remove.go new file mode 100644 index 0000000000..3d5aeff252 --- /dev/null +++ b/volume_remove.go @@ -0,0 +1,18 @@ +package client + +import ( + "net/url" + + "golang.org/x/net/context" +) + +// VolumeRemove removes a volume from the docker host. +func (cli *Client) VolumeRemove(ctx context.Context, volumeID string, force bool) error { + query := url.Values{} + if force { + query.Set("force", "1") + } + resp, err := cli.delete(ctx, "/volumes/"+volumeID, query, nil) + ensureReaderClosed(resp) + return err +} diff --git a/volume_remove_test.go b/volume_remove_test.go new file mode 100644 index 0000000000..0675bfd458 --- /dev/null +++ b/volume_remove_test.go @@ -0,0 +1,47 @@ +package client + +import ( + "bytes" + "fmt" + "io/ioutil" + "net/http" + "strings" + "testing" + + "golang.org/x/net/context" +) + +func TestVolumeRemoveError(t *testing.T) { + client := &Client{ + transport: newMockClient(nil, errorMock(http.StatusInternalServerError, "Server error")), + } + + err := client.VolumeRemove(context.Background(), "volume_id", false) + if err == nil || err.Error() != "Error response from daemon: Server error" { + t.Fatalf("expected a Server Error, got %v", err) + } +} + +func TestVolumeRemove(t *testing.T) { + expectedURL := "/volumes/volume_id" + + client := &Client{ + transport: newMockClient(nil, func(req *http.Request) (*http.Response, error) { + if !strings.HasPrefix(req.URL.Path, expectedURL) { + return nil, fmt.Errorf("Expected URL '%s', got '%s'", expectedURL, req.URL) + } + if req.Method != "DELETE" { + return nil, fmt.Errorf("expected DELETE method, got %s", req.Method) + } + return &http.Response{ + StatusCode: http.StatusOK, + Body: ioutil.NopCloser(bytes.NewReader([]byte("body"))), + }, nil + }), + } + + err := client.VolumeRemove(context.Background(), "volume_id", false) + if err != nil { + t.Fatal(err) + } +} From d675c815775ec4814c84c3bb4514721f332b1bca Mon Sep 17 00:00:00 2001 From: Akihiro Suda Date: Thu, 8 Sep 2016 04:38:55 +0000 Subject: [PATCH 020/138] client: transport: fix tlsconfig Clone() on different Golang versions Signed-off-by: Akihiro Suda --- transport/tlsconfig_clone.go | 2 +- transport/tlsconfig_clone_go16.go | 31 +++++++++++++++++++++++++++++++ transport/tlsconfig_clone_go17.go | 2 +- 3 files changed, 33 insertions(+), 2 deletions(-) create mode 100644 transport/tlsconfig_clone_go16.go diff --git a/transport/tlsconfig_clone.go b/transport/tlsconfig_clone.go index 033d5dc0f2..034bc01d33 100644 --- a/transport/tlsconfig_clone.go +++ b/transport/tlsconfig_clone.go @@ -1,4 +1,4 @@ -// +build !go1.7,!windows +// +build go1.8 package transport diff --git a/transport/tlsconfig_clone_go16.go b/transport/tlsconfig_clone_go16.go new file mode 100644 index 0000000000..12f13e4694 --- /dev/null +++ b/transport/tlsconfig_clone_go16.go @@ -0,0 +1,31 @@ +// +build go1.6,!go1.7 + +package transport + +import "crypto/tls" + +// TLSConfigClone returns a clone of tls.Config. This function is provided for +// compatibility for go1.6 that doesn't include this method in stdlib. +func TLSConfigClone(c *tls.Config) *tls.Config { + return &tls.Config{ + Rand: c.Rand, + Time: c.Time, + Certificates: c.Certificates, + NameToCertificate: c.NameToCertificate, + GetCertificate: c.GetCertificate, + RootCAs: c.RootCAs, + NextProtos: c.NextProtos, + ServerName: c.ServerName, + ClientAuth: c.ClientAuth, + ClientCAs: c.ClientCAs, + InsecureSkipVerify: c.InsecureSkipVerify, + CipherSuites: c.CipherSuites, + PreferServerCipherSuites: c.PreferServerCipherSuites, + SessionTicketsDisabled: c.SessionTicketsDisabled, + SessionTicketKey: c.SessionTicketKey, + ClientSessionCache: c.ClientSessionCache, + MinVersion: c.MinVersion, + MaxVersion: c.MaxVersion, + CurvePreferences: c.CurvePreferences, + } +} diff --git a/transport/tlsconfig_clone_go17.go b/transport/tlsconfig_clone_go17.go index a28c9141b2..50bf389e43 100644 --- a/transport/tlsconfig_clone_go17.go +++ b/transport/tlsconfig_clone_go17.go @@ -1,4 +1,4 @@ -// +build go1.7 +// +build go1.7,!go1.8 package transport From 450b3123e30d7920b2e7203a546483f06e9ad4d1 Mon Sep 17 00:00:00 2001 From: Stephen J Day Date: Thu, 8 Sep 2016 15:37:45 -0700 Subject: [PATCH 021/138] client: don't hide context errors Instead of reformatting error from the request action, we wrap it, allowing the cause to be recovered. This is important for consumers that need to be able to detect context errors, such as `Cancelled` and `DeadlineExceeded`. Signed-off-by: Stephen J Day --- request.go | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/request.go b/request.go index 024e973520..7b4f5406b8 100644 --- a/request.go +++ b/request.go @@ -14,6 +14,7 @@ import ( "github.com/docker/docker/api/types" "github.com/docker/docker/api/types/versions" "github.com/docker/docker/client/transport/cancellable" + "github.com/pkg/errors" "golang.org/x/net/context" ) @@ -131,7 +132,8 @@ func (cli *Client) sendClientRequest(ctx context.Context, method, path string, q } } } - return serverResp, fmt.Errorf("An error occurred trying to connect: %v", err) + + return serverResp, errors.Wrap(err, "error during connect") } if resp != nil { From c6f96cb8b40f2f78fef78b23675623df11f8d9d6 Mon Sep 17 00:00:00 2001 From: Stephen J Day Date: Thu, 8 Sep 2016 20:21:27 -0700 Subject: [PATCH 022/138] tlsconfig: move Clone into proper package Signed-off-by: Stephen J Day --- hijack.go | 4 ++-- transport/tlsconfig_clone.go | 11 ----------- transport/tlsconfig_clone_go16.go | 31 ----------------------------- transport/tlsconfig_clone_go17.go | 33 ------------------------------- 4 files changed, 2 insertions(+), 77 deletions(-) delete mode 100644 transport/tlsconfig_clone.go delete mode 100644 transport/tlsconfig_clone_go16.go delete mode 100644 transport/tlsconfig_clone_go17.go diff --git a/hijack.go b/hijack.go index 9376d21b97..e3f63e20c2 100644 --- a/hijack.go +++ b/hijack.go @@ -11,7 +11,7 @@ import ( "time" "github.com/docker/docker/api/types" - "github.com/docker/docker/client/transport" + "github.com/docker/docker/pkg/tlsconfig" "github.com/docker/go-connections/sockets" "golang.org/x/net/context" ) @@ -136,7 +136,7 @@ func tlsDialWithDialer(dialer *net.Dialer, network, addr string, config *tls.Con // from the hostname we're connecting to. if config.ServerName == "" { // Make a copy to avoid polluting argument or default. - config = transport.TLSConfigClone(config) + config = tlsconfig.Clone(config) config.ServerName = hostname } diff --git a/transport/tlsconfig_clone.go b/transport/tlsconfig_clone.go deleted file mode 100644 index 034bc01d33..0000000000 --- a/transport/tlsconfig_clone.go +++ /dev/null @@ -1,11 +0,0 @@ -// +build go1.8 - -package transport - -import "crypto/tls" - -// TLSConfigClone returns a clone of tls.Config. This function is provided for -// compatibility for go1.7 that doesn't include this method in stdlib. -func TLSConfigClone(c *tls.Config) *tls.Config { - return c.Clone() -} diff --git a/transport/tlsconfig_clone_go16.go b/transport/tlsconfig_clone_go16.go deleted file mode 100644 index 12f13e4694..0000000000 --- a/transport/tlsconfig_clone_go16.go +++ /dev/null @@ -1,31 +0,0 @@ -// +build go1.6,!go1.7 - -package transport - -import "crypto/tls" - -// TLSConfigClone returns a clone of tls.Config. This function is provided for -// compatibility for go1.6 that doesn't include this method in stdlib. -func TLSConfigClone(c *tls.Config) *tls.Config { - return &tls.Config{ - Rand: c.Rand, - Time: c.Time, - Certificates: c.Certificates, - NameToCertificate: c.NameToCertificate, - GetCertificate: c.GetCertificate, - RootCAs: c.RootCAs, - NextProtos: c.NextProtos, - ServerName: c.ServerName, - ClientAuth: c.ClientAuth, - ClientCAs: c.ClientCAs, - InsecureSkipVerify: c.InsecureSkipVerify, - CipherSuites: c.CipherSuites, - PreferServerCipherSuites: c.PreferServerCipherSuites, - SessionTicketsDisabled: c.SessionTicketsDisabled, - SessionTicketKey: c.SessionTicketKey, - ClientSessionCache: c.ClientSessionCache, - MinVersion: c.MinVersion, - MaxVersion: c.MaxVersion, - CurvePreferences: c.CurvePreferences, - } -} diff --git a/transport/tlsconfig_clone_go17.go b/transport/tlsconfig_clone_go17.go deleted file mode 100644 index 50bf389e43..0000000000 --- a/transport/tlsconfig_clone_go17.go +++ /dev/null @@ -1,33 +0,0 @@ -// +build go1.7,!go1.8 - -package transport - -import "crypto/tls" - -// TLSConfigClone returns a clone of tls.Config. This function is provided for -// compatibility for go1.7 that doesn't include this method in stdlib. -func TLSConfigClone(c *tls.Config) *tls.Config { - return &tls.Config{ - Rand: c.Rand, - Time: c.Time, - Certificates: c.Certificates, - NameToCertificate: c.NameToCertificate, - GetCertificate: c.GetCertificate, - RootCAs: c.RootCAs, - NextProtos: c.NextProtos, - ServerName: c.ServerName, - ClientAuth: c.ClientAuth, - ClientCAs: c.ClientCAs, - InsecureSkipVerify: c.InsecureSkipVerify, - CipherSuites: c.CipherSuites, - PreferServerCipherSuites: c.PreferServerCipherSuites, - SessionTicketsDisabled: c.SessionTicketsDisabled, - SessionTicketKey: c.SessionTicketKey, - ClientSessionCache: c.ClientSessionCache, - MinVersion: c.MinVersion, - MaxVersion: c.MaxVersion, - CurvePreferences: c.CurvePreferences, - DynamicRecordSizingDisabled: c.DynamicRecordSizingDisabled, - Renegotiation: c.Renegotiation, - } -} From 62e14c713b444f2566b1dffc79f68718608011ff Mon Sep 17 00:00:00 2001 From: Vincent Demeester Date: Mon, 12 Sep 2016 11:41:11 +0200 Subject: [PATCH 023/138] =?UTF-8?q?Add=20a=20README=20to=20the=20client's?= =?UTF-8?q?=20package=E2=80=A6?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit … taken from the old engine-api project. Signed-off-by: Vincent Demeester --- README.md | 37 +++++++++++++++++++++++++++++++++++++ 1 file changed, 37 insertions(+) create mode 100644 README.md diff --git a/README.md b/README.md new file mode 100644 index 0000000000..7872d94a53 --- /dev/null +++ b/README.md @@ -0,0 +1,37 @@ +## Client + +The client package implements a fully featured http client to interact with the Docker engine. It's modeled after the requirements of the Docker engine CLI, but it can also serve other purposes. + +### Usage + +You can use this client package in your applications by creating a new client object. Then use that object to execute operations against the remote server. Follow the example below to see how to list all the containers running in a Docker engine host: + +```go +package main + +import ( + "fmt" + + "github.com/docker/docker/client" + "github.com/docker/docker/api/types" + "golang.org/x/net/context" +) + +func main() { + defaultHeaders := map[string]string{"User-Agent": "engine-api-cli-1.0"} + cli, err := client.NewClient("unix:///var/run/docker.sock", "v1.22", nil, defaultHeaders) + if err != nil { + panic(err) + } + + options := types.ContainerListOptions{All: true} + containers, err := cli.ContainerList(context.Background(), options) + if err != nil { + panic(err) + } + + for _, c := range containers { + fmt.Println(c.ID) + } +} +``` From acb1fc424bd8a0f339be041204a04f7fd9791b52 Mon Sep 17 00:00:00 2001 From: allencloud Date: Sun, 4 Sep 2016 15:17:58 +0800 Subject: [PATCH 024/138] correct some nits in comments Signed-off-by: allencloud --- events_test.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/events_test.go b/events_test.go index f7cb33f611..57689322c3 100644 --- a/events_test.go +++ b/events_test.go @@ -38,7 +38,7 @@ func TestEventsErrorInOptions(t *testing.T) { } _, err := client.Events(context.Background(), e.options) if err == nil || !strings.Contains(err.Error(), e.expectedError) { - t.Fatalf("expected a error %q, got %v", e.expectedError, err) + t.Fatalf("expected an error %q, got %v", e.expectedError, err) } } } From 86c86fc1663049374d6cf86d9a27f812b6691683 Mon Sep 17 00:00:00 2001 From: John Howard Date: Wed, 7 Sep 2016 16:08:51 -0700 Subject: [PATCH 025/138] Windows: stats support Signed-off-by: John Howard --- container_stats.go | 10 ++++++---- container_stats_test.go | 6 +++--- image_build.go | 5 +++-- image_build_test.go | 2 +- interface.go | 2 +- 5 files changed, 14 insertions(+), 11 deletions(-) diff --git a/container_stats.go b/container_stats.go index 2cc67c3af1..3be7a988f4 100644 --- a/container_stats.go +++ b/container_stats.go @@ -1,15 +1,15 @@ package client import ( - "io" "net/url" + "github.com/docker/docker/api/types" "golang.org/x/net/context" ) // ContainerStats returns near realtime stats for a given container. // It's up to the caller to close the io.ReadCloser returned. -func (cli *Client) ContainerStats(ctx context.Context, containerID string, stream bool) (io.ReadCloser, error) { +func (cli *Client) ContainerStats(ctx context.Context, containerID string, stream bool) (types.ContainerStats, error) { query := url.Values{} query.Set("stream", "0") if stream { @@ -18,7 +18,9 @@ func (cli *Client) ContainerStats(ctx context.Context, containerID string, strea resp, err := cli.get(ctx, "/containers/"+containerID+"/stats", query, nil) if err != nil { - return nil, err + return types.ContainerStats{}, err } - return resp.body, err + + osType := GetDockerOS(resp.header.Get("Server")) + return types.ContainerStats{Body: resp.body, OSType: osType}, err } diff --git a/container_stats_test.go b/container_stats_test.go index 22ecd6170f..dc7c56492b 100644 --- a/container_stats_test.go +++ b/container_stats_test.go @@ -54,12 +54,12 @@ func TestContainerStats(t *testing.T) { }, nil }), } - body, err := client.ContainerStats(context.Background(), "container_id", c.stream) + resp, err := client.ContainerStats(context.Background(), "container_id", c.stream) if err != nil { t.Fatal(err) } - defer body.Close() - content, err := ioutil.ReadAll(body) + defer resp.Body.Close() + content, err := ioutil.ReadAll(resp.Body) if err != nil { t.Fatal(err) } diff --git a/image_build.go b/image_build.go index 8dd6744859..a84bf57821 100644 --- a/image_build.go +++ b/image_build.go @@ -39,7 +39,7 @@ func (cli *Client) ImageBuild(ctx context.Context, buildContext io.Reader, optio return types.ImageBuildResponse{}, err } - osType := getDockerOS(serverResp.header.Get("Server")) + osType := GetDockerOS(serverResp.header.Get("Server")) return types.ImageBuildResponse{ Body: serverResp.body, @@ -113,7 +113,8 @@ func imageBuildOptionsToQuery(options types.ImageBuildOptions) (url.Values, erro return query, nil } -func getDockerOS(serverHeader string) string { +// GetDockerOS returns the operating system based on the server header from the daemon. +func GetDockerOS(serverHeader string) string { var osType string matches := headerRegexp.FindStringSubmatch(serverHeader) if len(matches) > 0 { diff --git a/image_build_test.go b/image_build_test.go index 8261c54854..def88c3cb6 100644 --- a/image_build_test.go +++ b/image_build_test.go @@ -222,7 +222,7 @@ func TestGetDockerOS(t *testing.T) { "Foo/v1.22 (bar)": "", } for header, os := range cases { - g := getDockerOS(header) + g := GetDockerOS(header) if g != os { t.Fatalf("Expected %s, got %s", os, g) } diff --git a/interface.go b/interface.go index 1bfeb6aeb6..2d5555ff06 100644 --- a/interface.go +++ b/interface.go @@ -51,7 +51,7 @@ type ContainerAPIClient interface { ContainerResize(ctx context.Context, container string, options types.ResizeOptions) error ContainerRestart(ctx context.Context, container string, timeout *time.Duration) error ContainerStatPath(ctx context.Context, container, path string) (types.ContainerPathStat, error) - ContainerStats(ctx context.Context, container string, stream bool) (io.ReadCloser, error) + ContainerStats(ctx context.Context, container string, stream bool) (types.ContainerStats, error) ContainerStart(ctx context.Context, container string, options types.ContainerStartOptions) error ContainerStop(ctx context.Context, container string, timeout *time.Duration) error ContainerTop(ctx context.Context, container string, arguments []string) (types.ContainerProcessList, error) From 6be7efbe303c9bdc87566807703dbb31d0b0deee Mon Sep 17 00:00:00 2001 From: John Howard Date: Wed, 14 Sep 2016 11:55:07 -0700 Subject: [PATCH 026/138] Windows: OCI process struct convergence Signed-off-by: John Howard --- container_resize.go | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/container_resize.go b/container_resize.go index a7f38b024b..66c3cc1940 100644 --- a/container_resize.go +++ b/container_resize.go @@ -18,10 +18,10 @@ func (cli *Client) ContainerExecResize(ctx context.Context, execID string, optio return cli.resize(ctx, "/exec/"+execID, options.Height, options.Width) } -func (cli *Client) resize(ctx context.Context, basePath string, height, width int) error { +func (cli *Client) resize(ctx context.Context, basePath string, height, width uint) error { query := url.Values{} - query.Set("h", strconv.Itoa(height)) - query.Set("w", strconv.Itoa(width)) + query.Set("h", strconv.Itoa(int(height))) + query.Set("w", strconv.Itoa(int(width))) resp, err := cli.post(ctx, basePath+"/resize", query, nil, nil) ensureReaderClosed(resp) From c648e163ebbe3bc5bf9b10b9390e41c0785a409b Mon Sep 17 00:00:00 2001 From: Stephen J Day Date: Thu, 8 Sep 2016 20:44:25 -0700 Subject: [PATCH 027/138] client: remove transport package This package doesn't really seem to do anything of real interest. Removing it and replacing with a few helper functions. Most of this was maintaining a fork of ctxhttp to support a mock that was unnecessary. We could probably do with a further refactor of the client interface. There is a lot of confusion of between transport, http layer and application layer that makes for some awkward code. This change improves the situation to the point where no breaking changes are introduced. Signed-off-by: Stephen J Day --- checkpoint_create_test.go | 4 +- checkpoint_delete_test.go | 4 +- checkpoint_list_test.go | 4 +- client.go | 20 +++-- client_mock_test.go | 37 +------- client_test.go | 2 +- container_commit_test.go | 4 +- container_copy_test.go | 20 ++--- container_create_test.go | 8 +- container_diff_test.go | 4 +- container_exec_test.go | 12 +-- container_export_test.go | 4 +- container_inspect_test.go | 8 +- container_kill_test.go | 4 +- container_list_test.go | 4 +- container_logs_test.go | 4 +- container_pause_test.go | 4 +- container_remove_test.go | 4 +- container_rename_test.go | 4 +- container_resize_test.go | 8 +- container_restart_test.go | 4 +- container_start_test.go | 4 +- container_stats_test.go | 4 +- container_stop_test.go | 4 +- container_top_test.go | 4 +- container_unpause_test.go | 4 +- container_update_test.go | 4 +- container_wait_test.go | 4 +- events_test.go | 6 +- hijack.go | 7 +- image_build_test.go | 4 +- image_create_test.go | 4 +- image_history_test.go | 4 +- image_import_test.go | 4 +- image_inspect_test.go | 6 +- image_list_test.go | 4 +- image_load_test.go | 4 +- image_pull_test.go | 14 +-- image_push_test.go | 14 +-- image_remove_test.go | 4 +- image_save_test.go | 4 +- image_search_test.go | 12 +-- image_tag_test.go | 6 +- info_test.go | 6 +- network_connect_test.go | 6 +- network_create_test.go | 4 +- network_disconnect_test.go | 4 +- network_inspect_test.go | 6 +- network_list_test.go | 4 +- network_remove_test.go | 4 +- node_inspect_test.go | 6 +- node_list_test.go | 4 +- node_remove_test.go | 4 +- node_update_test.go | 4 +- plugin_disable_test.go | 4 +- plugin_enable_test.go | 4 +- plugin_inspect_test.go | 4 +- plugin_list_test.go | 4 +- plugin_push_test.go | 4 +- plugin_remove_test.go | 4 +- plugin_set_test.go | 4 +- request.go | 17 ++-- request_test.go | 5 +- service_create_test.go | 4 +- service_inspect_test.go | 6 +- service_list_test.go | 4 +- service_remove_test.go | 4 +- service_update_test.go | 4 +- swarm_init_test.go | 4 +- swarm_inspect_test.go | 4 +- swarm_join_test.go | 4 +- swarm_leave_test.go | 4 +- swarm_update_test.go | 4 +- task_inspect_test.go | 4 +- task_list_test.go | 4 +- transport.go | 51 +++++++++++ transport/cancellable/LICENSE | 27 ------ transport/cancellable/canceler.go | 23 ----- transport/cancellable/canceler_go14.go | 27 ------ transport/cancellable/cancellable.go | 115 ------------------------- transport/client.go | 47 ---------- transport/transport.go | 57 ------------ volume_create_test.go | 4 +- volume_inspect_test.go | 6 +- volume_list_test.go | 4 +- volume_remove_test.go | 4 +- 86 files changed, 276 insertions(+), 533 deletions(-) create mode 100644 transport.go delete mode 100644 transport/cancellable/LICENSE delete mode 100644 transport/cancellable/canceler.go delete mode 100644 transport/cancellable/canceler_go14.go delete mode 100644 transport/cancellable/cancellable.go delete mode 100644 transport/client.go delete mode 100644 transport/transport.go diff --git a/checkpoint_create_test.go b/checkpoint_create_test.go index e2ae36e1e0..96e5187618 100644 --- a/checkpoint_create_test.go +++ b/checkpoint_create_test.go @@ -15,7 +15,7 @@ import ( func TestCheckpointCreateError(t *testing.T) { client := &Client{ - transport: newMockClient(nil, errorMock(http.StatusInternalServerError, "Server error")), + client: newMockClient(errorMock(http.StatusInternalServerError, "Server error")), } err := client.CheckpointCreate(context.Background(), "nothing", types.CheckpointCreateOptions{ CheckpointID: "noting", @@ -33,7 +33,7 @@ func TestCheckpointCreate(t *testing.T) { expectedURL := "/containers/container_id/checkpoints" client := &Client{ - transport: newMockClient(nil, func(req *http.Request) (*http.Response, error) { + client: newMockClient(func(req *http.Request) (*http.Response, error) { if !strings.HasPrefix(req.URL.Path, expectedURL) { return nil, fmt.Errorf("Expected URL '%s', got '%s'", expectedURL, req.URL) } diff --git a/checkpoint_delete_test.go b/checkpoint_delete_test.go index 097ab37693..23931c6523 100644 --- a/checkpoint_delete_test.go +++ b/checkpoint_delete_test.go @@ -13,7 +13,7 @@ import ( func TestCheckpointDeleteError(t *testing.T) { client := &Client{ - transport: newMockClient(nil, errorMock(http.StatusInternalServerError, "Server error")), + client: newMockClient(errorMock(http.StatusInternalServerError, "Server error")), } err := client.CheckpointDelete(context.Background(), "container_id", "checkpoint_id") @@ -26,7 +26,7 @@ func TestCheckpointDelete(t *testing.T) { expectedURL := "/containers/container_id/checkpoints/checkpoint_id" client := &Client{ - transport: newMockClient(nil, func(req *http.Request) (*http.Response, error) { + client: newMockClient(func(req *http.Request) (*http.Response, error) { if !strings.HasPrefix(req.URL.Path, expectedURL) { return nil, fmt.Errorf("Expected URL '%s', got '%s'", expectedURL, req.URL) } diff --git a/checkpoint_list_test.go b/checkpoint_list_test.go index 5960436eb1..e636995bc1 100644 --- a/checkpoint_list_test.go +++ b/checkpoint_list_test.go @@ -15,7 +15,7 @@ import ( func TestCheckpointListError(t *testing.T) { client := &Client{ - transport: newMockClient(nil, errorMock(http.StatusInternalServerError, "Server error")), + client: newMockClient(errorMock(http.StatusInternalServerError, "Server error")), } _, err := client.CheckpointList(context.Background(), "container_id") @@ -28,7 +28,7 @@ func TestCheckpointList(t *testing.T) { expectedURL := "/containers/container_id/checkpoints" client := &Client{ - transport: newMockClient(nil, func(req *http.Request) (*http.Response, error) { + client: newMockClient(func(req *http.Request) (*http.Response, error) { if !strings.HasPrefix(req.URL.Path, expectedURL) { return nil, fmt.Errorf("Expected URL '%s', got '%s'", expectedURL, req.URL) } diff --git a/client.go b/client.go index 6a85121c6d..deccb4ab74 100644 --- a/client.go +++ b/client.go @@ -8,7 +8,7 @@ import ( "path/filepath" "strings" - "github.com/docker/docker/client/transport" + "github.com/docker/go-connections/sockets" "github.com/docker/go-connections/tlsconfig" ) @@ -26,8 +26,8 @@ type Client struct { addr string // basePath holds the path to prepend to the requests. basePath string - // transport is the interface to send request with, it implements transport.Client. - transport transport.Client + // client used to send and receive http requests. + client *http.Client // version of the server to talk to. version string // custom http headers configured by users. @@ -86,9 +86,15 @@ func NewClient(host string, version string, client *http.Client, httpHeaders map return nil, err } - transport, err := transport.NewTransportWithHTTP(proto, addr, client) - if err != nil { - return nil, err + if client == nil { + client = &http.Client{} + } + + if client.Transport == nil { + // setup the transport, if not already present + transport := new(http.Transport) + sockets.ConfigureTransport(transport, proto, addr) + client.Transport = transport } return &Client{ @@ -96,7 +102,7 @@ func NewClient(host string, version string, client *http.Client, httpHeaders map proto: proto, addr: addr, basePath: basePath, - transport: transport, + client: client, version: version, customHTTPHeaders: httpHeaders, }, nil diff --git a/client_mock_test.go b/client_mock_test.go index 33c247266c..0ab935d536 100644 --- a/client_mock_test.go +++ b/client_mock_test.go @@ -2,48 +2,17 @@ package client import ( "bytes" - "crypto/tls" "encoding/json" "io/ioutil" "net/http" "github.com/docker/docker/api/types" - "github.com/docker/docker/client/transport" ) -type mockClient struct { - do func(*http.Request) (*http.Response, error) -} - -// TLSConfig returns the TLS configuration. -func (m *mockClient) TLSConfig() *tls.Config { - return &tls.Config{} -} - -// Scheme returns protocol scheme to use. -func (m *mockClient) Scheme() string { - return "http" -} - -// Secure returns true if there is a TLS configuration. -func (m *mockClient) Secure() bool { - return false -} - -// NewMockClient returns a mocked client that runs the function supplied as `client.Do` call -func newMockClient(tlsConfig *tls.Config, doer func(*http.Request) (*http.Response, error)) transport.Client { - if tlsConfig != nil { - panic("this actually gets set!") +func newMockClient(doer func(*http.Request) (*http.Response, error)) *http.Client { + return &http.Client{ + Transport: transportFunc(doer), } - - return &mockClient{ - do: doer, - } -} - -// Do executes the supplied function for the mock. -func (m mockClient) Do(req *http.Request) (*http.Response, error) { - return m.do(req) } func errorMock(statusCode int, message string) func(req *http.Request) (*http.Response, error) { diff --git a/client_test.go b/client_test.go index 60af3db029..60e44dc299 100644 --- a/client_test.go +++ b/client_test.go @@ -173,7 +173,7 @@ func TestParseHost(t *testing.T) { func TestUpdateClientVersion(t *testing.T) { client := &Client{ - transport: newMockClient(nil, func(req *http.Request) (*http.Response, error) { + client: newMockClient(func(req *http.Request) (*http.Response, error) { splitQuery := strings.Split(req.URL.Path, "/") queryVersion := splitQuery[1] b, err := json.Marshal(types.Version{ diff --git a/container_commit_test.go b/container_commit_test.go index 3fc3e5cfd0..8f1b58be81 100644 --- a/container_commit_test.go +++ b/container_commit_test.go @@ -15,7 +15,7 @@ import ( func TestContainerCommitError(t *testing.T) { client := &Client{ - transport: newMockClient(nil, errorMock(http.StatusInternalServerError, "Server error")), + client: newMockClient(errorMock(http.StatusInternalServerError, "Server error")), } _, err := client.ContainerCommit(context.Background(), "nothing", types.ContainerCommitOptions{}) if err == nil || err.Error() != "Error response from daemon: Server error" { @@ -34,7 +34,7 @@ func TestContainerCommit(t *testing.T) { expectedChanges := []string{"change1", "change2"} client := &Client{ - transport: newMockClient(nil, func(req *http.Request) (*http.Response, error) { + client: newMockClient(func(req *http.Request) (*http.Response, error) { if !strings.HasPrefix(req.URL.Path, expectedURL) { return nil, fmt.Errorf("Expected URL '%s', got '%s'", expectedURL, req.URL) } diff --git a/container_copy_test.go b/container_copy_test.go index 39cd05ac2d..7eded611fd 100644 --- a/container_copy_test.go +++ b/container_copy_test.go @@ -17,7 +17,7 @@ import ( func TestContainerStatPathError(t *testing.T) { client := &Client{ - transport: newMockClient(nil, errorMock(http.StatusInternalServerError, "Server error")), + client: newMockClient(errorMock(http.StatusInternalServerError, "Server error")), } _, err := client.ContainerStatPath(context.Background(), "container_id", "path") if err == nil || err.Error() != "Error response from daemon: Server error" { @@ -27,7 +27,7 @@ func TestContainerStatPathError(t *testing.T) { func TestContainerStatPathNoHeaderError(t *testing.T) { client := &Client{ - transport: newMockClient(nil, func(req *http.Request) (*http.Response, error) { + client: newMockClient(func(req *http.Request) (*http.Response, error) { return &http.Response{ StatusCode: http.StatusOK, Body: ioutil.NopCloser(bytes.NewReader([]byte(""))), @@ -44,7 +44,7 @@ func TestContainerStatPath(t *testing.T) { expectedURL := "/containers/container_id/archive" expectedPath := "path/to/file" client := &Client{ - transport: newMockClient(nil, func(req *http.Request) (*http.Response, error) { + client: newMockClient(func(req *http.Request) (*http.Response, error) { if !strings.HasPrefix(req.URL.Path, expectedURL) { return nil, fmt.Errorf("Expected URL '%s', got '%s'", expectedURL, req.URL) } @@ -87,7 +87,7 @@ func TestContainerStatPath(t *testing.T) { func TestCopyToContainerError(t *testing.T) { client := &Client{ - transport: newMockClient(nil, errorMock(http.StatusInternalServerError, "Server error")), + client: newMockClient(errorMock(http.StatusInternalServerError, "Server error")), } err := client.CopyToContainer(context.Background(), "container_id", "path/to/file", bytes.NewReader([]byte("")), types.CopyToContainerOptions{}) if err == nil || err.Error() != "Error response from daemon: Server error" { @@ -97,7 +97,7 @@ func TestCopyToContainerError(t *testing.T) { func TestCopyToContainerNotStatusOKError(t *testing.T) { client := &Client{ - transport: newMockClient(nil, errorMock(http.StatusNoContent, "No content")), + client: newMockClient(errorMock(http.StatusNoContent, "No content")), } err := client.CopyToContainer(context.Background(), "container_id", "path/to/file", bytes.NewReader([]byte("")), types.CopyToContainerOptions{}) if err == nil || err.Error() != "unexpected status code from daemon: 204" { @@ -109,7 +109,7 @@ func TestCopyToContainer(t *testing.T) { expectedURL := "/containers/container_id/archive" expectedPath := "path/to/file" client := &Client{ - transport: newMockClient(nil, func(req *http.Request) (*http.Response, error) { + client: newMockClient(func(req *http.Request) (*http.Response, error) { if !strings.HasPrefix(req.URL.Path, expectedURL) { return nil, fmt.Errorf("Expected URL '%s', got '%s'", expectedURL, req.URL) } @@ -153,7 +153,7 @@ func TestCopyToContainer(t *testing.T) { func TestCopyFromContainerError(t *testing.T) { client := &Client{ - transport: newMockClient(nil, errorMock(http.StatusInternalServerError, "Server error")), + client: newMockClient(errorMock(http.StatusInternalServerError, "Server error")), } _, _, err := client.CopyFromContainer(context.Background(), "container_id", "path/to/file") if err == nil || err.Error() != "Error response from daemon: Server error" { @@ -163,7 +163,7 @@ func TestCopyFromContainerError(t *testing.T) { func TestCopyFromContainerNotStatusOKError(t *testing.T) { client := &Client{ - transport: newMockClient(nil, errorMock(http.StatusNoContent, "No content")), + client: newMockClient(errorMock(http.StatusNoContent, "No content")), } _, _, err := client.CopyFromContainer(context.Background(), "container_id", "path/to/file") if err == nil || err.Error() != "unexpected status code from daemon: 204" { @@ -173,7 +173,7 @@ func TestCopyFromContainerNotStatusOKError(t *testing.T) { func TestCopyFromContainerNoHeaderError(t *testing.T) { client := &Client{ - transport: newMockClient(nil, func(req *http.Request) (*http.Response, error) { + client: newMockClient(func(req *http.Request) (*http.Response, error) { return &http.Response{ StatusCode: http.StatusOK, Body: ioutil.NopCloser(bytes.NewReader([]byte(""))), @@ -190,7 +190,7 @@ func TestCopyFromContainer(t *testing.T) { expectedURL := "/containers/container_id/archive" expectedPath := "path/to/file" client := &Client{ - transport: newMockClient(nil, func(req *http.Request) (*http.Response, error) { + client: newMockClient(func(req *http.Request) (*http.Response, error) { if !strings.HasPrefix(req.URL.Path, expectedURL) { return nil, fmt.Errorf("Expected URL '%s', got '%s'", expectedURL, req.URL) } diff --git a/container_create_test.go b/container_create_test.go index 4c14cdc5d1..5325156beb 100644 --- a/container_create_test.go +++ b/container_create_test.go @@ -16,7 +16,7 @@ import ( func TestContainerCreateError(t *testing.T) { client := &Client{ - transport: newMockClient(nil, errorMock(http.StatusInternalServerError, "Server error")), + client: newMockClient(errorMock(http.StatusInternalServerError, "Server error")), } _, err := client.ContainerCreate(context.Background(), nil, nil, nil, "nothing") if err == nil || err.Error() != "Error response from daemon: Server error" { @@ -25,7 +25,7 @@ func TestContainerCreateError(t *testing.T) { // 404 doesn't automagitally means an unknown image client = &Client{ - transport: newMockClient(nil, errorMock(http.StatusNotFound, "Server error")), + client: newMockClient(errorMock(http.StatusNotFound, "Server error")), } _, err = client.ContainerCreate(context.Background(), nil, nil, nil, "nothing") if err == nil || err.Error() != "Error response from daemon: Server error" { @@ -35,7 +35,7 @@ func TestContainerCreateError(t *testing.T) { func TestContainerCreateImageNotFound(t *testing.T) { client := &Client{ - transport: newMockClient(nil, errorMock(http.StatusNotFound, "No such image")), + client: newMockClient(errorMock(http.StatusNotFound, "No such image")), } _, err := client.ContainerCreate(context.Background(), &container.Config{Image: "unknown_image"}, nil, nil, "unknown") if err == nil || !IsErrImageNotFound(err) { @@ -46,7 +46,7 @@ func TestContainerCreateImageNotFound(t *testing.T) { func TestContainerCreateWithName(t *testing.T) { expectedURL := "/containers/create" client := &Client{ - transport: newMockClient(nil, func(req *http.Request) (*http.Response, error) { + client: newMockClient(func(req *http.Request) (*http.Response, error) { if !strings.HasPrefix(req.URL.Path, expectedURL) { return nil, fmt.Errorf("Expected URL '%s', got '%s'", expectedURL, req.URL) } diff --git a/container_diff_test.go b/container_diff_test.go index 03ea3354d2..1ce1117684 100644 --- a/container_diff_test.go +++ b/container_diff_test.go @@ -15,7 +15,7 @@ import ( func TestContainerDiffError(t *testing.T) { client := &Client{ - transport: newMockClient(nil, errorMock(http.StatusInternalServerError, "Server error")), + client: newMockClient(errorMock(http.StatusInternalServerError, "Server error")), } _, err := client.ContainerDiff(context.Background(), "nothing") if err == nil || err.Error() != "Error response from daemon: Server error" { @@ -27,7 +27,7 @@ func TestContainerDiffError(t *testing.T) { func TestContainerDiff(t *testing.T) { expectedURL := "/containers/container_id/changes" client := &Client{ - transport: newMockClient(nil, func(req *http.Request) (*http.Response, error) { + client: newMockClient(func(req *http.Request) (*http.Response, error) { if !strings.HasPrefix(req.URL.Path, expectedURL) { return nil, fmt.Errorf("Expected URL '%s', got '%s'", expectedURL, req.URL) } diff --git a/container_exec_test.go b/container_exec_test.go index abe824e47b..42146ae8a5 100644 --- a/container_exec_test.go +++ b/container_exec_test.go @@ -16,7 +16,7 @@ import ( func TestContainerExecCreateError(t *testing.T) { client := &Client{ - transport: newMockClient(nil, errorMock(http.StatusInternalServerError, "Server error")), + client: newMockClient(errorMock(http.StatusInternalServerError, "Server error")), } _, err := client.ContainerExecCreate(context.Background(), "container_id", types.ExecConfig{}) if err == nil || err.Error() != "Error response from daemon: Server error" { @@ -27,7 +27,7 @@ func TestContainerExecCreateError(t *testing.T) { func TestContainerExecCreate(t *testing.T) { expectedURL := "/containers/container_id/exec" client := &Client{ - transport: newMockClient(nil, func(req *http.Request) (*http.Response, error) { + client: newMockClient(func(req *http.Request) (*http.Response, error) { if !strings.HasPrefix(req.URL.Path, expectedURL) { return nil, fmt.Errorf("expected URL '%s', got '%s'", expectedURL, req.URL) } @@ -71,7 +71,7 @@ func TestContainerExecCreate(t *testing.T) { func TestContainerExecStartError(t *testing.T) { client := &Client{ - transport: newMockClient(nil, errorMock(http.StatusInternalServerError, "Server error")), + client: newMockClient(errorMock(http.StatusInternalServerError, "Server error")), } err := client.ContainerExecStart(context.Background(), "nothing", types.ExecStartCheck{}) if err == nil || err.Error() != "Error response from daemon: Server error" { @@ -82,7 +82,7 @@ func TestContainerExecStartError(t *testing.T) { func TestContainerExecStart(t *testing.T) { expectedURL := "/exec/exec_id/start" client := &Client{ - transport: newMockClient(nil, func(req *http.Request) (*http.Response, error) { + client: newMockClient(func(req *http.Request) (*http.Response, error) { if !strings.HasPrefix(req.URL.Path, expectedURL) { return nil, fmt.Errorf("Expected URL '%s', got '%s'", expectedURL, req.URL) } @@ -115,7 +115,7 @@ func TestContainerExecStart(t *testing.T) { func TestContainerExecInspectError(t *testing.T) { client := &Client{ - transport: newMockClient(nil, errorMock(http.StatusInternalServerError, "Server error")), + client: newMockClient(errorMock(http.StatusInternalServerError, "Server error")), } _, err := client.ContainerExecInspect(context.Background(), "nothing") if err == nil || err.Error() != "Error response from daemon: Server error" { @@ -126,7 +126,7 @@ func TestContainerExecInspectError(t *testing.T) { func TestContainerExecInspect(t *testing.T) { expectedURL := "/exec/exec_id/json" client := &Client{ - transport: newMockClient(nil, func(req *http.Request) (*http.Response, error) { + client: newMockClient(func(req *http.Request) (*http.Response, error) { if !strings.HasPrefix(req.URL.Path, expectedURL) { return nil, fmt.Errorf("Expected URL '%s', got '%s'", expectedURL, req.URL) } diff --git a/container_export_test.go b/container_export_test.go index 10eba33d2f..5849fe9252 100644 --- a/container_export_test.go +++ b/container_export_test.go @@ -13,7 +13,7 @@ import ( func TestContainerExportError(t *testing.T) { client := &Client{ - transport: newMockClient(nil, errorMock(http.StatusInternalServerError, "Server error")), + client: newMockClient(errorMock(http.StatusInternalServerError, "Server error")), } _, err := client.ContainerExport(context.Background(), "nothing") if err == nil || err.Error() != "Error response from daemon: Server error" { @@ -24,7 +24,7 @@ func TestContainerExportError(t *testing.T) { func TestContainerExport(t *testing.T) { expectedURL := "/containers/container_id/export" client := &Client{ - transport: newMockClient(nil, func(r *http.Request) (*http.Response, error) { + client: newMockClient(func(r *http.Request) (*http.Response, error) { if !strings.HasPrefix(r.URL.Path, expectedURL) { return nil, fmt.Errorf("Expected URL '%s', got '%s'", expectedURL, r.URL) } diff --git a/container_inspect_test.go b/container_inspect_test.go index 0dc8ac3753..f1a6f4ac7d 100644 --- a/container_inspect_test.go +++ b/container_inspect_test.go @@ -15,7 +15,7 @@ import ( func TestContainerInspectError(t *testing.T) { client := &Client{ - transport: newMockClient(nil, errorMock(http.StatusInternalServerError, "Server error")), + client: newMockClient(errorMock(http.StatusInternalServerError, "Server error")), } _, err := client.ContainerInspect(context.Background(), "nothing") @@ -26,7 +26,7 @@ func TestContainerInspectError(t *testing.T) { func TestContainerInspectContainerNotFound(t *testing.T) { client := &Client{ - transport: newMockClient(nil, errorMock(http.StatusNotFound, "Server error")), + client: newMockClient(errorMock(http.StatusNotFound, "Server error")), } _, err := client.ContainerInspect(context.Background(), "unknown") @@ -38,7 +38,7 @@ func TestContainerInspectContainerNotFound(t *testing.T) { func TestContainerInspect(t *testing.T) { expectedURL := "/containers/container_id/json" client := &Client{ - transport: newMockClient(nil, func(req *http.Request) (*http.Response, error) { + client: newMockClient(func(req *http.Request) (*http.Response, error) { if !strings.HasPrefix(req.URL.Path, expectedURL) { return nil, fmt.Errorf("Expected URL '%s', got '%s'", expectedURL, req.URL) } @@ -76,7 +76,7 @@ func TestContainerInspect(t *testing.T) { func TestContainerInspectNode(t *testing.T) { client := &Client{ - transport: newMockClient(nil, func(req *http.Request) (*http.Response, error) { + client: newMockClient(func(req *http.Request) (*http.Response, error) { content, err := json.Marshal(types.ContainerJSON{ ContainerJSONBase: &types.ContainerJSONBase{ ID: "container_id", diff --git a/container_kill_test.go b/container_kill_test.go index a34a7b5b11..9477b0abd2 100644 --- a/container_kill_test.go +++ b/container_kill_test.go @@ -13,7 +13,7 @@ import ( func TestContainerKillError(t *testing.T) { client := &Client{ - transport: newMockClient(nil, errorMock(http.StatusInternalServerError, "Server error")), + client: newMockClient(errorMock(http.StatusInternalServerError, "Server error")), } err := client.ContainerKill(context.Background(), "nothing", "SIGKILL") if err == nil || err.Error() != "Error response from daemon: Server error" { @@ -24,7 +24,7 @@ func TestContainerKillError(t *testing.T) { func TestContainerKill(t *testing.T) { expectedURL := "/containers/container_id/kill" client := &Client{ - transport: newMockClient(nil, func(req *http.Request) (*http.Response, error) { + client: newMockClient(func(req *http.Request) (*http.Response, error) { if !strings.HasPrefix(req.URL.Path, expectedURL) { return nil, fmt.Errorf("Expected URL '%s', got '%s'", expectedURL, req.URL) } diff --git a/container_list_test.go b/container_list_test.go index 3aa2101f27..5068b7573e 100644 --- a/container_list_test.go +++ b/container_list_test.go @@ -16,7 +16,7 @@ import ( func TestContainerListError(t *testing.T) { client := &Client{ - transport: newMockClient(nil, errorMock(http.StatusInternalServerError, "Server error")), + client: newMockClient(errorMock(http.StatusInternalServerError, "Server error")), } _, err := client.ContainerList(context.Background(), types.ContainerListOptions{}) if err == nil || err.Error() != "Error response from daemon: Server error" { @@ -28,7 +28,7 @@ func TestContainerList(t *testing.T) { expectedURL := "/containers/json" expectedFilters := `{"before":{"container":true},"label":{"label1":true,"label2":true}}` client := &Client{ - transport: newMockClient(nil, func(req *http.Request) (*http.Response, error) { + client: newMockClient(func(req *http.Request) (*http.Response, error) { if !strings.HasPrefix(req.URL.Path, expectedURL) { return nil, fmt.Errorf("Expected URL '%s', got '%s'", expectedURL, req.URL) } diff --git a/container_logs_test.go b/container_logs_test.go index d7f0adc9c0..99e31842c9 100644 --- a/container_logs_test.go +++ b/container_logs_test.go @@ -19,7 +19,7 @@ import ( func TestContainerLogsError(t *testing.T) { client := &Client{ - transport: newMockClient(nil, errorMock(http.StatusInternalServerError, "Server error")), + client: newMockClient(errorMock(http.StatusInternalServerError, "Server error")), } _, err := client.ContainerLogs(context.Background(), "container_id", types.ContainerLogsOptions{}) if err == nil || err.Error() != "Error response from daemon: Server error" { @@ -83,7 +83,7 @@ func TestContainerLogs(t *testing.T) { } for _, logCase := range cases { client := &Client{ - transport: newMockClient(nil, func(r *http.Request) (*http.Response, error) { + client: newMockClient(func(r *http.Request) (*http.Response, error) { if !strings.HasPrefix(r.URL.Path, expectedURL) { return nil, fmt.Errorf("Expected URL '%s', got '%s'", expectedURL, r.URL) } diff --git a/container_pause_test.go b/container_pause_test.go index ebd12a6ac7..0ee2f05d7e 100644 --- a/container_pause_test.go +++ b/container_pause_test.go @@ -13,7 +13,7 @@ import ( func TestContainerPauseError(t *testing.T) { client := &Client{ - transport: newMockClient(nil, errorMock(http.StatusInternalServerError, "Server error")), + client: newMockClient(errorMock(http.StatusInternalServerError, "Server error")), } err := client.ContainerPause(context.Background(), "nothing") if err == nil || err.Error() != "Error response from daemon: Server error" { @@ -24,7 +24,7 @@ func TestContainerPauseError(t *testing.T) { func TestContainerPause(t *testing.T) { expectedURL := "/containers/container_id/pause" client := &Client{ - transport: newMockClient(nil, func(req *http.Request) (*http.Response, error) { + client: newMockClient(func(req *http.Request) (*http.Response, error) { if !strings.HasPrefix(req.URL.Path, expectedURL) { return nil, fmt.Errorf("Expected URL '%s', got '%s'", expectedURL, req.URL) } diff --git a/container_remove_test.go b/container_remove_test.go index 6e135d6ef2..798c08b333 100644 --- a/container_remove_test.go +++ b/container_remove_test.go @@ -14,7 +14,7 @@ import ( func TestContainerRemoveError(t *testing.T) { client := &Client{ - transport: newMockClient(nil, errorMock(http.StatusInternalServerError, "Server error")), + client: newMockClient(errorMock(http.StatusInternalServerError, "Server error")), } err := client.ContainerRemove(context.Background(), "container_id", types.ContainerRemoveOptions{}) if err == nil || err.Error() != "Error response from daemon: Server error" { @@ -25,7 +25,7 @@ func TestContainerRemoveError(t *testing.T) { func TestContainerRemove(t *testing.T) { expectedURL := "/containers/container_id" client := &Client{ - transport: newMockClient(nil, func(req *http.Request) (*http.Response, error) { + client: newMockClient(func(req *http.Request) (*http.Response, error) { if !strings.HasPrefix(req.URL.Path, expectedURL) { return nil, fmt.Errorf("Expected URL '%s', got '%s'", expectedURL, req.URL) } diff --git a/container_rename_test.go b/container_rename_test.go index 9344bab7db..732ebff5f7 100644 --- a/container_rename_test.go +++ b/container_rename_test.go @@ -13,7 +13,7 @@ import ( func TestContainerRenameError(t *testing.T) { client := &Client{ - transport: newMockClient(nil, errorMock(http.StatusInternalServerError, "Server error")), + client: newMockClient(errorMock(http.StatusInternalServerError, "Server error")), } err := client.ContainerRename(context.Background(), "nothing", "newNothing") if err == nil || err.Error() != "Error response from daemon: Server error" { @@ -24,7 +24,7 @@ func TestContainerRenameError(t *testing.T) { func TestContainerRename(t *testing.T) { expectedURL := "/containers/container_id/rename" client := &Client{ - transport: newMockClient(nil, func(req *http.Request) (*http.Response, error) { + client: newMockClient(func(req *http.Request) (*http.Response, error) { if !strings.HasPrefix(req.URL.Path, expectedURL) { return nil, fmt.Errorf("Expected URL '%s', got '%s'", expectedURL, req.URL) } diff --git a/container_resize_test.go b/container_resize_test.go index e0056c88d1..5b2efecdce 100644 --- a/container_resize_test.go +++ b/container_resize_test.go @@ -14,7 +14,7 @@ import ( func TestContainerResizeError(t *testing.T) { client := &Client{ - transport: newMockClient(nil, errorMock(http.StatusInternalServerError, "Server error")), + client: newMockClient(errorMock(http.StatusInternalServerError, "Server error")), } err := client.ContainerResize(context.Background(), "container_id", types.ResizeOptions{}) if err == nil || err.Error() != "Error response from daemon: Server error" { @@ -24,7 +24,7 @@ func TestContainerResizeError(t *testing.T) { func TestContainerExecResizeError(t *testing.T) { client := &Client{ - transport: newMockClient(nil, errorMock(http.StatusInternalServerError, "Server error")), + client: newMockClient(errorMock(http.StatusInternalServerError, "Server error")), } err := client.ContainerExecResize(context.Background(), "exec_id", types.ResizeOptions{}) if err == nil || err.Error() != "Error response from daemon: Server error" { @@ -34,7 +34,7 @@ func TestContainerExecResizeError(t *testing.T) { func TestContainerResize(t *testing.T) { client := &Client{ - transport: newMockClient(nil, resizeTransport("/containers/container_id/resize")), + client: newMockClient(resizeTransport("/containers/container_id/resize")), } err := client.ContainerResize(context.Background(), "container_id", types.ResizeOptions{ @@ -48,7 +48,7 @@ func TestContainerResize(t *testing.T) { func TestContainerExecResize(t *testing.T) { client := &Client{ - transport: newMockClient(nil, resizeTransport("/exec/exec_id/resize")), + client: newMockClient(resizeTransport("/exec/exec_id/resize")), } err := client.ContainerExecResize(context.Background(), "exec_id", types.ResizeOptions{ diff --git a/container_restart_test.go b/container_restart_test.go index 080656d368..8c3cfd6a6f 100644 --- a/container_restart_test.go +++ b/container_restart_test.go @@ -14,7 +14,7 @@ import ( func TestContainerRestartError(t *testing.T) { client := &Client{ - transport: newMockClient(nil, errorMock(http.StatusInternalServerError, "Server error")), + client: newMockClient(errorMock(http.StatusInternalServerError, "Server error")), } timeout := 0 * time.Second err := client.ContainerRestart(context.Background(), "nothing", &timeout) @@ -26,7 +26,7 @@ func TestContainerRestartError(t *testing.T) { func TestContainerRestart(t *testing.T) { expectedURL := "/containers/container_id/restart" client := &Client{ - transport: newMockClient(nil, func(req *http.Request) (*http.Response, error) { + client: newMockClient(func(req *http.Request) (*http.Response, error) { if !strings.HasPrefix(req.URL.Path, expectedURL) { return nil, fmt.Errorf("Expected URL '%s', got '%s'", expectedURL, req.URL) } diff --git a/container_start_test.go b/container_start_test.go index 79f85b332a..5826fa8bc7 100644 --- a/container_start_test.go +++ b/container_start_test.go @@ -16,7 +16,7 @@ import ( func TestContainerStartError(t *testing.T) { client := &Client{ - transport: newMockClient(nil, errorMock(http.StatusInternalServerError, "Server error")), + client: newMockClient(errorMock(http.StatusInternalServerError, "Server error")), } err := client.ContainerStart(context.Background(), "nothing", types.ContainerStartOptions{}) if err == nil || err.Error() != "Error response from daemon: Server error" { @@ -27,7 +27,7 @@ func TestContainerStartError(t *testing.T) { func TestContainerStart(t *testing.T) { expectedURL := "/containers/container_id/start" client := &Client{ - transport: newMockClient(nil, func(req *http.Request) (*http.Response, error) { + client: newMockClient(func(req *http.Request) (*http.Response, error) { if !strings.HasPrefix(req.URL.Path, expectedURL) { return nil, fmt.Errorf("Expected URL '%s', got '%s'", expectedURL, req.URL) } diff --git a/container_stats_test.go b/container_stats_test.go index 22ecd6170f..76e4a09ddf 100644 --- a/container_stats_test.go +++ b/container_stats_test.go @@ -13,7 +13,7 @@ import ( func TestContainerStatsError(t *testing.T) { client := &Client{ - transport: newMockClient(nil, errorMock(http.StatusInternalServerError, "Server error")), + client: newMockClient(errorMock(http.StatusInternalServerError, "Server error")), } _, err := client.ContainerStats(context.Background(), "nothing", false) if err == nil || err.Error() != "Error response from daemon: Server error" { @@ -37,7 +37,7 @@ func TestContainerStats(t *testing.T) { } for _, c := range cases { client := &Client{ - transport: newMockClient(nil, func(r *http.Request) (*http.Response, error) { + client: newMockClient(func(r *http.Request) (*http.Response, error) { if !strings.HasPrefix(r.URL.Path, expectedURL) { return nil, fmt.Errorf("Expected URL '%s', got '%s'", expectedURL, r.URL) } diff --git a/container_stop_test.go b/container_stop_test.go index 4b052f9908..c32cd691c4 100644 --- a/container_stop_test.go +++ b/container_stop_test.go @@ -14,7 +14,7 @@ import ( func TestContainerStopError(t *testing.T) { client := &Client{ - transport: newMockClient(nil, errorMock(http.StatusInternalServerError, "Server error")), + client: newMockClient(errorMock(http.StatusInternalServerError, "Server error")), } timeout := 0 * time.Second err := client.ContainerStop(context.Background(), "nothing", &timeout) @@ -26,7 +26,7 @@ func TestContainerStopError(t *testing.T) { func TestContainerStop(t *testing.T) { expectedURL := "/containers/container_id/stop" client := &Client{ - transport: newMockClient(nil, func(req *http.Request) (*http.Response, error) { + client: newMockClient(func(req *http.Request) (*http.Response, error) { if !strings.HasPrefix(req.URL.Path, expectedURL) { return nil, fmt.Errorf("Expected URL '%s', got '%s'", expectedURL, req.URL) } diff --git a/container_top_test.go b/container_top_test.go index 4df7d82d84..7802be063e 100644 --- a/container_top_test.go +++ b/container_top_test.go @@ -16,7 +16,7 @@ import ( func TestContainerTopError(t *testing.T) { client := &Client{ - transport: newMockClient(nil, errorMock(http.StatusInternalServerError, "Server error")), + client: newMockClient(errorMock(http.StatusInternalServerError, "Server error")), } _, err := client.ContainerTop(context.Background(), "nothing", []string{}) if err == nil || err.Error() != "Error response from daemon: Server error" { @@ -33,7 +33,7 @@ func TestContainerTop(t *testing.T) { expectedTitles := []string{"title1", "title2"} client := &Client{ - transport: newMockClient(nil, func(req *http.Request) (*http.Response, error) { + client: newMockClient(func(req *http.Request) (*http.Response, error) { if !strings.HasPrefix(req.URL.Path, expectedURL) { return nil, fmt.Errorf("Expected URL '%s', got '%s'", expectedURL, req.URL) } diff --git a/container_unpause_test.go b/container_unpause_test.go index a5b21bf56c..2c42727191 100644 --- a/container_unpause_test.go +++ b/container_unpause_test.go @@ -13,7 +13,7 @@ import ( func TestContainerUnpauseError(t *testing.T) { client := &Client{ - transport: newMockClient(nil, errorMock(http.StatusInternalServerError, "Server error")), + client: newMockClient(errorMock(http.StatusInternalServerError, "Server error")), } err := client.ContainerUnpause(context.Background(), "nothing") if err == nil || err.Error() != "Error response from daemon: Server error" { @@ -24,7 +24,7 @@ func TestContainerUnpauseError(t *testing.T) { func TestContainerUnpause(t *testing.T) { expectedURL := "/containers/container_id/unpause" client := &Client{ - transport: newMockClient(nil, func(req *http.Request) (*http.Response, error) { + client: newMockClient(func(req *http.Request) (*http.Response, error) { if !strings.HasPrefix(req.URL.Path, expectedURL) { return nil, fmt.Errorf("Expected URL '%s', got '%s'", expectedURL, req.URL) } diff --git a/container_update_test.go b/container_update_test.go index 46e34d6936..e151637a2b 100644 --- a/container_update_test.go +++ b/container_update_test.go @@ -16,7 +16,7 @@ import ( func TestContainerUpdateError(t *testing.T) { client := &Client{ - transport: newMockClient(nil, errorMock(http.StatusInternalServerError, "Server error")), + client: newMockClient(errorMock(http.StatusInternalServerError, "Server error")), } _, err := client.ContainerUpdate(context.Background(), "nothing", container.UpdateConfig{}) if err == nil || err.Error() != "Error response from daemon: Server error" { @@ -28,7 +28,7 @@ func TestContainerUpdate(t *testing.T) { expectedURL := "/containers/container_id/update" client := &Client{ - transport: newMockClient(nil, func(req *http.Request) (*http.Response, error) { + client: newMockClient(func(req *http.Request) (*http.Response, error) { if !strings.HasPrefix(req.URL.Path, expectedURL) { return nil, fmt.Errorf("Expected URL '%s', got '%s'", expectedURL, req.URL) } diff --git a/container_wait_test.go b/container_wait_test.go index bf2ba6b925..dab5acbdd3 100644 --- a/container_wait_test.go +++ b/container_wait_test.go @@ -18,7 +18,7 @@ import ( func TestContainerWaitError(t *testing.T) { client := &Client{ - transport: newMockClient(nil, errorMock(http.StatusInternalServerError, "Server error")), + client: newMockClient(errorMock(http.StatusInternalServerError, "Server error")), } code, err := client.ContainerWait(context.Background(), "nothing") if err == nil || err.Error() != "Error response from daemon: Server error" { @@ -32,7 +32,7 @@ func TestContainerWaitError(t *testing.T) { func TestContainerWait(t *testing.T) { expectedURL := "/containers/container_id/wait" client := &Client{ - transport: newMockClient(nil, func(req *http.Request) (*http.Response, error) { + client: newMockClient(func(req *http.Request) (*http.Response, error) { if !strings.HasPrefix(req.URL.Path, expectedURL) { return nil, fmt.Errorf("Expected URL '%s', got '%s'", expectedURL, req.URL) } diff --git a/events_test.go b/events_test.go index f7cb33f611..48b948fa37 100644 --- a/events_test.go +++ b/events_test.go @@ -34,7 +34,7 @@ func TestEventsErrorInOptions(t *testing.T) { } for _, e := range errorCases { client := &Client{ - transport: newMockClient(nil, errorMock(http.StatusInternalServerError, "Server error")), + client: newMockClient(errorMock(http.StatusInternalServerError, "Server error")), } _, err := client.Events(context.Background(), e.options) if err == nil || !strings.Contains(err.Error(), e.expectedError) { @@ -45,7 +45,7 @@ func TestEventsErrorInOptions(t *testing.T) { func TestEventsErrorFromServer(t *testing.T) { client := &Client{ - transport: newMockClient(nil, errorMock(http.StatusInternalServerError, "Server error")), + client: newMockClient(errorMock(http.StatusInternalServerError, "Server error")), } _, err := client.Events(context.Background(), types.EventsOptions{}) if err == nil || err.Error() != "Error response from daemon: Server error" { @@ -93,7 +93,7 @@ func TestEvents(t *testing.T) { for _, eventsCase := range eventsCases { client := &Client{ - transport: newMockClient(nil, func(req *http.Request) (*http.Response, error) { + client: newMockClient(func(req *http.Request) (*http.Response, error) { if !strings.HasPrefix(req.URL.Path, expectedURL) { return nil, fmt.Errorf("Expected URL '%s', got '%s'", expectedURL, req.URL) } diff --git a/hijack.go b/hijack.go index e3f63e20c2..f3461ecf78 100644 --- a/hijack.go +++ b/hijack.go @@ -47,7 +47,12 @@ func (cli *Client) postHijacked(ctx context.Context, path string, query url.Valu req.Header.Set("Connection", "Upgrade") req.Header.Set("Upgrade", "tcp") - conn, err := dial(cli.proto, cli.addr, cli.transport.TLSConfig()) + tlsConfig, err := resolveTLSConfig(cli.client.Transport) + if err != nil { + return types.HijackedResponse{}, err + } + + conn, err := dial(cli.proto, cli.addr, tlsConfig) if err != nil { if strings.Contains(err.Error(), "connection refused") { return types.HijackedResponse{}, fmt.Errorf("Cannot connect to the Docker daemon. Is 'docker daemon' running on this host?") diff --git a/image_build_test.go b/image_build_test.go index 8261c54854..ec0cbe2ee4 100644 --- a/image_build_test.go +++ b/image_build_test.go @@ -18,7 +18,7 @@ import ( func TestImageBuildError(t *testing.T) { client := &Client{ - transport: newMockClient(nil, errorMock(http.StatusInternalServerError, "Server error")), + client: newMockClient(errorMock(http.StatusInternalServerError, "Server error")), } _, err := client.ImageBuild(context.Background(), nil, types.ImageBuildOptions{}) if err == nil || err.Error() != "Error response from daemon: Server error" { @@ -157,7 +157,7 @@ func TestImageBuild(t *testing.T) { for _, buildCase := range buildCases { expectedURL := "/build" client := &Client{ - transport: newMockClient(nil, func(r *http.Request) (*http.Response, error) { + client: newMockClient(func(r *http.Request) (*http.Response, error) { if !strings.HasPrefix(r.URL.Path, expectedURL) { return nil, fmt.Errorf("Expected URL '%s', got '%s'", expectedURL, r.URL) } diff --git a/image_create_test.go b/image_create_test.go index a2e001be5d..5c2edd2ad5 100644 --- a/image_create_test.go +++ b/image_create_test.go @@ -15,7 +15,7 @@ import ( func TestImageCreateError(t *testing.T) { client := &Client{ - transport: newMockClient(nil, errorMock(http.StatusInternalServerError, "Server error")), + client: newMockClient(errorMock(http.StatusInternalServerError, "Server error")), } _, err := client.ImageCreate(context.Background(), "reference", types.ImageCreateOptions{}) if err == nil || err.Error() != "Error response from daemon: Server error" { @@ -30,7 +30,7 @@ func TestImageCreate(t *testing.T) { expectedReference := fmt.Sprintf("%s@%s", expectedImage, expectedTag) expectedRegistryAuth := "eyJodHRwczovL2luZGV4LmRvY2tlci5pby92MS8iOnsiYXV0aCI6ImRHOTBid289IiwiZW1haWwiOiJqb2huQGRvZS5jb20ifX0=" client := &Client{ - transport: newMockClient(nil, func(r *http.Request) (*http.Response, error) { + client: newMockClient(func(r *http.Request) (*http.Response, error) { if !strings.HasPrefix(r.URL.Path, expectedURL) { return nil, fmt.Errorf("Expected URL '%s', got '%s'", expectedURL, r.URL) } diff --git a/image_history_test.go b/image_history_test.go index c9516151b7..729edb1ad5 100644 --- a/image_history_test.go +++ b/image_history_test.go @@ -15,7 +15,7 @@ import ( func TestImageHistoryError(t *testing.T) { client := &Client{ - transport: newMockClient(nil, errorMock(http.StatusInternalServerError, "Server error")), + client: newMockClient(errorMock(http.StatusInternalServerError, "Server error")), } _, err := client.ImageHistory(context.Background(), "nothing") if err == nil || err.Error() != "Error response from daemon: Server error" { @@ -26,7 +26,7 @@ func TestImageHistoryError(t *testing.T) { func TestImageHistory(t *testing.T) { expectedURL := "/images/image_id/history" client := &Client{ - transport: newMockClient(nil, func(r *http.Request) (*http.Response, error) { + client: newMockClient(func(r *http.Request) (*http.Response, error) { if !strings.HasPrefix(r.URL.Path, expectedURL) { return nil, fmt.Errorf("Expected URL '%s', got '%s'", expectedURL, r.URL) } diff --git a/image_import_test.go b/image_import_test.go index b64ca74d7b..e309be74e6 100644 --- a/image_import_test.go +++ b/image_import_test.go @@ -15,7 +15,7 @@ import ( func TestImageImportError(t *testing.T) { client := &Client{ - transport: newMockClient(nil, errorMock(http.StatusInternalServerError, "Server error")), + client: newMockClient(errorMock(http.StatusInternalServerError, "Server error")), } _, err := client.ImageImport(context.Background(), types.ImageImportSource{}, "image:tag", types.ImageImportOptions{}) if err == nil || err.Error() != "Error response from daemon: Server error" { @@ -26,7 +26,7 @@ func TestImageImportError(t *testing.T) { func TestImageImport(t *testing.T) { expectedURL := "/images/create" client := &Client{ - transport: newMockClient(nil, func(r *http.Request) (*http.Response, error) { + client: newMockClient(func(r *http.Request) (*http.Response, error) { if !strings.HasPrefix(r.URL.Path, expectedURL) { return nil, fmt.Errorf("Expected URL '%s', got '%s'", expectedURL, r.URL) } diff --git a/image_inspect_test.go b/image_inspect_test.go index 5c7ca2721f..74a4e49805 100644 --- a/image_inspect_test.go +++ b/image_inspect_test.go @@ -16,7 +16,7 @@ import ( func TestImageInspectError(t *testing.T) { client := &Client{ - transport: newMockClient(nil, errorMock(http.StatusInternalServerError, "Server error")), + client: newMockClient(errorMock(http.StatusInternalServerError, "Server error")), } _, _, err := client.ImageInspectWithRaw(context.Background(), "nothing") @@ -27,7 +27,7 @@ func TestImageInspectError(t *testing.T) { func TestImageInspectImageNotFound(t *testing.T) { client := &Client{ - transport: newMockClient(nil, errorMock(http.StatusNotFound, "Server error")), + client: newMockClient(errorMock(http.StatusNotFound, "Server error")), } _, _, err := client.ImageInspectWithRaw(context.Background(), "unknown") @@ -40,7 +40,7 @@ func TestImageInspect(t *testing.T) { expectedURL := "/images/image_id/json" expectedTags := []string{"tag1", "tag2"} client := &Client{ - transport: newMockClient(nil, func(req *http.Request) (*http.Response, error) { + client: newMockClient(func(req *http.Request) (*http.Response, error) { if !strings.HasPrefix(req.URL.Path, expectedURL) { return nil, fmt.Errorf("Expected URL '%s', got '%s'", expectedURL, req.URL) } diff --git a/image_list_test.go b/image_list_test.go index 99ed1964a2..2a52279081 100644 --- a/image_list_test.go +++ b/image_list_test.go @@ -16,7 +16,7 @@ import ( func TestImageListError(t *testing.T) { client := &Client{ - transport: newMockClient(nil, errorMock(http.StatusInternalServerError, "Server error")), + client: newMockClient(errorMock(http.StatusInternalServerError, "Server error")), } _, err := client.ImageList(context.Background(), types.ImageListOptions{}) @@ -82,7 +82,7 @@ func TestImageList(t *testing.T) { } for _, listCase := range listCases { client := &Client{ - transport: newMockClient(nil, func(req *http.Request) (*http.Response, error) { + client: newMockClient(func(req *http.Request) (*http.Response, error) { if !strings.HasPrefix(req.URL.Path, expectedURL) { return nil, fmt.Errorf("Expected URL '%s', got '%s'", expectedURL, req.URL) } diff --git a/image_load_test.go b/image_load_test.go index 0ee7cf35a6..68dc14ff22 100644 --- a/image_load_test.go +++ b/image_load_test.go @@ -13,7 +13,7 @@ import ( func TestImageLoadError(t *testing.T) { client := &Client{ - transport: newMockClient(nil, errorMock(http.StatusInternalServerError, "Server error")), + client: newMockClient(errorMock(http.StatusInternalServerError, "Server error")), } _, err := client.ImageLoad(context.Background(), nil, true) @@ -51,7 +51,7 @@ func TestImageLoad(t *testing.T) { } for _, loadCase := range loadCases { client := &Client{ - transport: newMockClient(nil, func(req *http.Request) (*http.Response, error) { + client: newMockClient(func(req *http.Request) (*http.Response, error) { if !strings.HasPrefix(req.URL.Path, expectedURL) { return nil, fmt.Errorf("Expected URL '%s', got '%s'", expectedURL, req.URL) } diff --git a/image_pull_test.go b/image_pull_test.go index c33a6dcc8a..fe6bafed97 100644 --- a/image_pull_test.go +++ b/image_pull_test.go @@ -15,7 +15,7 @@ import ( func TestImagePullReferenceParseError(t *testing.T) { client := &Client{ - transport: newMockClient(nil, func(req *http.Request) (*http.Response, error) { + client: newMockClient(func(req *http.Request) (*http.Response, error) { return nil, nil }), } @@ -28,7 +28,7 @@ func TestImagePullReferenceParseError(t *testing.T) { func TestImagePullAnyError(t *testing.T) { client := &Client{ - transport: newMockClient(nil, errorMock(http.StatusInternalServerError, "Server error")), + client: newMockClient(errorMock(http.StatusInternalServerError, "Server error")), } _, err := client.ImagePull(context.Background(), "myimage", types.ImagePullOptions{}) if err == nil || err.Error() != "Error response from daemon: Server error" { @@ -38,7 +38,7 @@ func TestImagePullAnyError(t *testing.T) { func TestImagePullStatusUnauthorizedError(t *testing.T) { client := &Client{ - transport: newMockClient(nil, errorMock(http.StatusUnauthorized, "Unauthorized error")), + client: newMockClient(errorMock(http.StatusUnauthorized, "Unauthorized error")), } _, err := client.ImagePull(context.Background(), "myimage", types.ImagePullOptions{}) if err == nil || err.Error() != "Error response from daemon: Unauthorized error" { @@ -48,7 +48,7 @@ func TestImagePullStatusUnauthorizedError(t *testing.T) { func TestImagePullWithUnauthorizedErrorAndPrivilegeFuncError(t *testing.T) { client := &Client{ - transport: newMockClient(nil, errorMock(http.StatusUnauthorized, "Unauthorized error")), + client: newMockClient(errorMock(http.StatusUnauthorized, "Unauthorized error")), } privilegeFunc := func() (string, error) { return "", fmt.Errorf("Error requesting privilege") @@ -63,7 +63,7 @@ func TestImagePullWithUnauthorizedErrorAndPrivilegeFuncError(t *testing.T) { func TestImagePullWithUnauthorizedErrorAndAnotherUnauthorizedError(t *testing.T) { client := &Client{ - transport: newMockClient(nil, errorMock(http.StatusUnauthorized, "Unauthorized error")), + client: newMockClient(errorMock(http.StatusUnauthorized, "Unauthorized error")), } privilegeFunc := func() (string, error) { return "a-auth-header", nil @@ -79,7 +79,7 @@ func TestImagePullWithUnauthorizedErrorAndAnotherUnauthorizedError(t *testing.T) func TestImagePullWithPrivilegedFuncNoError(t *testing.T) { expectedURL := "/images/create" client := &Client{ - transport: newMockClient(nil, func(req *http.Request) (*http.Response, error) { + client: newMockClient(func(req *http.Request) (*http.Response, error) { if !strings.HasPrefix(req.URL.Path, expectedURL) { return nil, fmt.Errorf("Expected URL '%s', got '%s'", expectedURL, req.URL) } @@ -163,7 +163,7 @@ func TestImagePullWithoutErrors(t *testing.T) { } for _, pullCase := range pullCases { client := &Client{ - transport: newMockClient(nil, func(req *http.Request) (*http.Response, error) { + client: newMockClient(func(req *http.Request) (*http.Response, error) { if !strings.HasPrefix(req.URL.Path, expectedURL) { return nil, fmt.Errorf("Expected URL '%s', got '%s'", expectedURL, req.URL) } diff --git a/image_push_test.go b/image_push_test.go index d32f3ef3c7..b52da8b8dc 100644 --- a/image_push_test.go +++ b/image_push_test.go @@ -15,7 +15,7 @@ import ( func TestImagePushReferenceError(t *testing.T) { client := &Client{ - transport: newMockClient(nil, func(req *http.Request) (*http.Response, error) { + client: newMockClient(func(req *http.Request) (*http.Response, error) { return nil, nil }), } @@ -33,7 +33,7 @@ func TestImagePushReferenceError(t *testing.T) { func TestImagePushAnyError(t *testing.T) { client := &Client{ - transport: newMockClient(nil, errorMock(http.StatusInternalServerError, "Server error")), + client: newMockClient(errorMock(http.StatusInternalServerError, "Server error")), } _, err := client.ImagePush(context.Background(), "myimage", types.ImagePushOptions{}) if err == nil || err.Error() != "Error response from daemon: Server error" { @@ -43,7 +43,7 @@ func TestImagePushAnyError(t *testing.T) { func TestImagePushStatusUnauthorizedError(t *testing.T) { client := &Client{ - transport: newMockClient(nil, errorMock(http.StatusUnauthorized, "Unauthorized error")), + client: newMockClient(errorMock(http.StatusUnauthorized, "Unauthorized error")), } _, err := client.ImagePush(context.Background(), "myimage", types.ImagePushOptions{}) if err == nil || err.Error() != "Error response from daemon: Unauthorized error" { @@ -53,7 +53,7 @@ func TestImagePushStatusUnauthorizedError(t *testing.T) { func TestImagePushWithUnauthorizedErrorAndPrivilegeFuncError(t *testing.T) { client := &Client{ - transport: newMockClient(nil, errorMock(http.StatusUnauthorized, "Unauthorized error")), + client: newMockClient(errorMock(http.StatusUnauthorized, "Unauthorized error")), } privilegeFunc := func() (string, error) { return "", fmt.Errorf("Error requesting privilege") @@ -68,7 +68,7 @@ func TestImagePushWithUnauthorizedErrorAndPrivilegeFuncError(t *testing.T) { func TestImagePushWithUnauthorizedErrorAndAnotherUnauthorizedError(t *testing.T) { client := &Client{ - transport: newMockClient(nil, errorMock(http.StatusUnauthorized, "Unauthorized error")), + client: newMockClient(errorMock(http.StatusUnauthorized, "Unauthorized error")), } privilegeFunc := func() (string, error) { return "a-auth-header", nil @@ -84,7 +84,7 @@ func TestImagePushWithUnauthorizedErrorAndAnotherUnauthorizedError(t *testing.T) func TestImagePushWithPrivilegedFuncNoError(t *testing.T) { expectedURL := "/images/myimage/push" client := &Client{ - transport: newMockClient(nil, func(req *http.Request) (*http.Response, error) { + client: newMockClient(func(req *http.Request) (*http.Response, error) { if !strings.HasPrefix(req.URL.Path, expectedURL) { return nil, fmt.Errorf("Expected URL '%s', got '%s'", expectedURL, req.URL) } @@ -149,7 +149,7 @@ func TestImagePushWithoutErrors(t *testing.T) { } for _, pullCase := range pullCases { client := &Client{ - transport: newMockClient(nil, func(req *http.Request) (*http.Response, error) { + client: newMockClient(func(req *http.Request) (*http.Response, error) { expectedURL := fmt.Sprintf(expectedURLFormat, pullCase.expectedImage) if !strings.HasPrefix(req.URL.Path, expectedURL) { return nil, fmt.Errorf("Expected URL '%s', got '%s'", expectedURL, req.URL) diff --git a/image_remove_test.go b/image_remove_test.go index 696d06729d..7b004f70e6 100644 --- a/image_remove_test.go +++ b/image_remove_test.go @@ -15,7 +15,7 @@ import ( func TestImageRemoveError(t *testing.T) { client := &Client{ - transport: newMockClient(nil, errorMock(http.StatusInternalServerError, "Server error")), + client: newMockClient(errorMock(http.StatusInternalServerError, "Server error")), } _, err := client.ImageRemove(context.Background(), "image_id", types.ImageRemoveOptions{}) @@ -49,7 +49,7 @@ func TestImageRemove(t *testing.T) { } for _, removeCase := range removeCases { client := &Client{ - transport: newMockClient(nil, func(req *http.Request) (*http.Response, error) { + client: newMockClient(func(req *http.Request) (*http.Response, error) { if !strings.HasPrefix(req.URL.Path, expectedURL) { return nil, fmt.Errorf("expected URL '%s', got '%s'", expectedURL, req.URL) } diff --git a/image_save_test.go b/image_save_test.go index 8ee40c43ae..8f0cf88640 100644 --- a/image_save_test.go +++ b/image_save_test.go @@ -15,7 +15,7 @@ import ( func TestImageSaveError(t *testing.T) { client := &Client{ - transport: newMockClient(nil, errorMock(http.StatusInternalServerError, "Server error")), + client: newMockClient(errorMock(http.StatusInternalServerError, "Server error")), } _, err := client.ImageSave(context.Background(), []string{"nothing"}) if err == nil || err.Error() != "Error response from daemon: Server error" { @@ -26,7 +26,7 @@ func TestImageSaveError(t *testing.T) { func TestImageSave(t *testing.T) { expectedURL := "/images/get" client := &Client{ - transport: newMockClient(nil, func(r *http.Request) (*http.Response, error) { + client: newMockClient(func(r *http.Request) (*http.Response, error) { if !strings.HasPrefix(r.URL.Path, expectedURL) { return nil, fmt.Errorf("Expected URL '%s', got '%s'", expectedURL, r.URL) } diff --git a/image_search_test.go b/image_search_test.go index 2f21b2cc14..e46d86437f 100644 --- a/image_search_test.go +++ b/image_search_test.go @@ -18,7 +18,7 @@ import ( func TestImageSearchAnyError(t *testing.T) { client := &Client{ - transport: newMockClient(nil, errorMock(http.StatusInternalServerError, "Server error")), + client: newMockClient(errorMock(http.StatusInternalServerError, "Server error")), } _, err := client.ImageSearch(context.Background(), "some-image", types.ImageSearchOptions{}) if err == nil || err.Error() != "Error response from daemon: Server error" { @@ -28,7 +28,7 @@ func TestImageSearchAnyError(t *testing.T) { func TestImageSearchStatusUnauthorizedError(t *testing.T) { client := &Client{ - transport: newMockClient(nil, errorMock(http.StatusUnauthorized, "Unauthorized error")), + client: newMockClient(errorMock(http.StatusUnauthorized, "Unauthorized error")), } _, err := client.ImageSearch(context.Background(), "some-image", types.ImageSearchOptions{}) if err == nil || err.Error() != "Error response from daemon: Unauthorized error" { @@ -38,7 +38,7 @@ func TestImageSearchStatusUnauthorizedError(t *testing.T) { func TestImageSearchWithUnauthorizedErrorAndPrivilegeFuncError(t *testing.T) { client := &Client{ - transport: newMockClient(nil, errorMock(http.StatusUnauthorized, "Unauthorized error")), + client: newMockClient(errorMock(http.StatusUnauthorized, "Unauthorized error")), } privilegeFunc := func() (string, error) { return "", fmt.Errorf("Error requesting privilege") @@ -53,7 +53,7 @@ func TestImageSearchWithUnauthorizedErrorAndPrivilegeFuncError(t *testing.T) { func TestImageSearchWithUnauthorizedErrorAndAnotherUnauthorizedError(t *testing.T) { client := &Client{ - transport: newMockClient(nil, errorMock(http.StatusUnauthorized, "Unauthorized error")), + client: newMockClient(errorMock(http.StatusUnauthorized, "Unauthorized error")), } privilegeFunc := func() (string, error) { return "a-auth-header", nil @@ -69,7 +69,7 @@ func TestImageSearchWithUnauthorizedErrorAndAnotherUnauthorizedError(t *testing. func TestImageSearchWithPrivilegedFuncNoError(t *testing.T) { expectedURL := "/images/search" client := &Client{ - transport: newMockClient(nil, func(req *http.Request) (*http.Response, error) { + client: newMockClient(func(req *http.Request) (*http.Response, error) { if !strings.HasPrefix(req.URL.Path, expectedURL) { return nil, fmt.Errorf("Expected URL '%s', got '%s'", expectedURL, req.URL) } @@ -126,7 +126,7 @@ func TestImageSearchWithoutErrors(t *testing.T) { expectedFilters := `{"is-automated":{"true":true},"stars":{"3":true}}` client := &Client{ - transport: newMockClient(nil, func(req *http.Request) (*http.Response, error) { + client: newMockClient(func(req *http.Request) (*http.Response, error) { if !strings.HasPrefix(req.URL.Path, expectedURL) { return nil, fmt.Errorf("Expected URL '%s', got '%s'", expectedURL, req.URL) } diff --git a/image_tag_test.go b/image_tag_test.go index f3571dfdd3..7925db9f1b 100644 --- a/image_tag_test.go +++ b/image_tag_test.go @@ -13,7 +13,7 @@ import ( func TestImageTagError(t *testing.T) { client := &Client{ - transport: newMockClient(nil, errorMock(http.StatusInternalServerError, "Server error")), + client: newMockClient(errorMock(http.StatusInternalServerError, "Server error")), } err := client.ImageTag(context.Background(), "image_id", "repo:tag") @@ -26,7 +26,7 @@ func TestImageTagError(t *testing.T) { // of distribution/reference package. func TestImageTagInvalidReference(t *testing.T) { client := &Client{ - transport: newMockClient(nil, errorMock(http.StatusInternalServerError, "Server error")), + client: newMockClient(errorMock(http.StatusInternalServerError, "Server error")), } err := client.ImageTag(context.Background(), "image_id", "aa/asdf$$^/aa") @@ -93,7 +93,7 @@ func TestImageTag(t *testing.T) { } for _, tagCase := range tagCases { client := &Client{ - transport: newMockClient(nil, func(req *http.Request) (*http.Response, error) { + client: newMockClient(func(req *http.Request) (*http.Response, error) { if !strings.HasPrefix(req.URL.Path, expectedURL) { return nil, fmt.Errorf("expected URL '%s', got '%s'", expectedURL, req.URL) } diff --git a/info_test.go b/info_test.go index 9d51b1a78b..79f23c8af2 100644 --- a/info_test.go +++ b/info_test.go @@ -15,7 +15,7 @@ import ( func TestInfoServerError(t *testing.T) { client := &Client{ - transport: newMockClient(nil, errorMock(http.StatusInternalServerError, "Server error")), + client: newMockClient(errorMock(http.StatusInternalServerError, "Server error")), } _, err := client.Info(context.Background()) if err == nil || err.Error() != "Error response from daemon: Server error" { @@ -25,7 +25,7 @@ func TestInfoServerError(t *testing.T) { func TestInfoInvalidResponseJSONError(t *testing.T) { client := &Client{ - transport: newMockClient(nil, func(req *http.Request) (*http.Response, error) { + client: newMockClient(func(req *http.Request) (*http.Response, error) { return &http.Response{ StatusCode: http.StatusOK, Body: ioutil.NopCloser(bytes.NewReader([]byte("invalid json"))), @@ -41,7 +41,7 @@ func TestInfoInvalidResponseJSONError(t *testing.T) { func TestInfo(t *testing.T) { expectedURL := "/info" client := &Client{ - transport: newMockClient(nil, func(req *http.Request) (*http.Response, error) { + client: newMockClient(func(req *http.Request) (*http.Response, error) { if !strings.HasPrefix(req.URL.Path, expectedURL) { return nil, fmt.Errorf("Expected URL '%s', got '%s'", expectedURL, req.URL) } diff --git a/network_connect_test.go b/network_connect_test.go index 95b149e685..d472f4520c 100644 --- a/network_connect_test.go +++ b/network_connect_test.go @@ -17,7 +17,7 @@ import ( func TestNetworkConnectError(t *testing.T) { client := &Client{ - transport: newMockClient(nil, errorMock(http.StatusInternalServerError, "Server error")), + client: newMockClient(errorMock(http.StatusInternalServerError, "Server error")), } err := client.NetworkConnect(context.Background(), "network_id", "container_id", nil) @@ -30,7 +30,7 @@ func TestNetworkConnectEmptyNilEndpointSettings(t *testing.T) { expectedURL := "/networks/network_id/connect" client := &Client{ - transport: newMockClient(nil, func(req *http.Request) (*http.Response, error) { + client: newMockClient(func(req *http.Request) (*http.Response, error) { if !strings.HasPrefix(req.URL.Path, expectedURL) { return nil, fmt.Errorf("Expected URL '%s', got '%s'", expectedURL, req.URL) } @@ -69,7 +69,7 @@ func TestNetworkConnect(t *testing.T) { expectedURL := "/networks/network_id/connect" client := &Client{ - transport: newMockClient(nil, func(req *http.Request) (*http.Response, error) { + client: newMockClient(func(req *http.Request) (*http.Response, error) { if !strings.HasPrefix(req.URL.Path, expectedURL) { return nil, fmt.Errorf("Expected URL '%s', got '%s'", expectedURL, req.URL) } diff --git a/network_create_test.go b/network_create_test.go index 611ed8173e..0e2457f89c 100644 --- a/network_create_test.go +++ b/network_create_test.go @@ -15,7 +15,7 @@ import ( func TestNetworkCreateError(t *testing.T) { client := &Client{ - transport: newMockClient(nil, errorMock(http.StatusInternalServerError, "Server error")), + client: newMockClient(errorMock(http.StatusInternalServerError, "Server error")), } _, err := client.NetworkCreate(context.Background(), "mynetwork", types.NetworkCreate{}) @@ -28,7 +28,7 @@ func TestNetworkCreate(t *testing.T) { expectedURL := "/networks/create" client := &Client{ - transport: newMockClient(nil, func(req *http.Request) (*http.Response, error) { + client: newMockClient(func(req *http.Request) (*http.Response, error) { if !strings.HasPrefix(req.URL.Path, expectedURL) { return nil, fmt.Errorf("Expected URL '%s', got '%s'", expectedURL, req.URL) } diff --git a/network_disconnect_test.go b/network_disconnect_test.go index d9dbb67159..b54a2b1ccf 100644 --- a/network_disconnect_test.go +++ b/network_disconnect_test.go @@ -15,7 +15,7 @@ import ( func TestNetworkDisconnectError(t *testing.T) { client := &Client{ - transport: newMockClient(nil, errorMock(http.StatusInternalServerError, "Server error")), + client: newMockClient(errorMock(http.StatusInternalServerError, "Server error")), } err := client.NetworkDisconnect(context.Background(), "network_id", "container_id", false) @@ -28,7 +28,7 @@ func TestNetworkDisconnect(t *testing.T) { expectedURL := "/networks/network_id/disconnect" client := &Client{ - transport: newMockClient(nil, func(req *http.Request) (*http.Response, error) { + client: newMockClient(func(req *http.Request) (*http.Response, error) { if !strings.HasPrefix(req.URL.Path, expectedURL) { return nil, fmt.Errorf("Expected URL '%s', got '%s'", expectedURL, req.URL) } diff --git a/network_inspect_test.go b/network_inspect_test.go index a6eb626c67..1f926d66ba 100644 --- a/network_inspect_test.go +++ b/network_inspect_test.go @@ -15,7 +15,7 @@ import ( func TestNetworkInspectError(t *testing.T) { client := &Client{ - transport: newMockClient(nil, errorMock(http.StatusInternalServerError, "Server error")), + client: newMockClient(errorMock(http.StatusInternalServerError, "Server error")), } _, err := client.NetworkInspect(context.Background(), "nothing") @@ -26,7 +26,7 @@ func TestNetworkInspectError(t *testing.T) { func TestNetworkInspectContainerNotFound(t *testing.T) { client := &Client{ - transport: newMockClient(nil, errorMock(http.StatusNotFound, "Server error")), + client: newMockClient(errorMock(http.StatusNotFound, "Server error")), } _, err := client.NetworkInspect(context.Background(), "unknown") @@ -38,7 +38,7 @@ func TestNetworkInspectContainerNotFound(t *testing.T) { func TestNetworkInspect(t *testing.T) { expectedURL := "/networks/network_id" client := &Client{ - transport: newMockClient(nil, func(req *http.Request) (*http.Response, error) { + client: newMockClient(func(req *http.Request) (*http.Response, error) { if !strings.HasPrefix(req.URL.Path, expectedURL) { return nil, fmt.Errorf("Expected URL '%s', got '%s'", expectedURL, req.URL) } diff --git a/network_list_test.go b/network_list_test.go index cb66139271..4d443496ac 100644 --- a/network_list_test.go +++ b/network_list_test.go @@ -16,7 +16,7 @@ import ( func TestNetworkListError(t *testing.T) { client := &Client{ - transport: newMockClient(nil, errorMock(http.StatusInternalServerError, "Server error")), + client: newMockClient(errorMock(http.StatusInternalServerError, "Server error")), } _, err := client.NetworkList(context.Background(), types.NetworkListOptions{ @@ -69,7 +69,7 @@ func TestNetworkList(t *testing.T) { for _, listCase := range listCases { client := &Client{ - transport: newMockClient(nil, func(req *http.Request) (*http.Response, error) { + client: newMockClient(func(req *http.Request) (*http.Response, error) { if !strings.HasPrefix(req.URL.Path, expectedURL) { return nil, fmt.Errorf("Expected URL '%s', got '%s'", expectedURL, req.URL) } diff --git a/network_remove_test.go b/network_remove_test.go index d8cfa0ed6e..2a7b9640c1 100644 --- a/network_remove_test.go +++ b/network_remove_test.go @@ -13,7 +13,7 @@ import ( func TestNetworkRemoveError(t *testing.T) { client := &Client{ - transport: newMockClient(nil, errorMock(http.StatusInternalServerError, "Server error")), + client: newMockClient(errorMock(http.StatusInternalServerError, "Server error")), } err := client.NetworkRemove(context.Background(), "network_id") @@ -26,7 +26,7 @@ func TestNetworkRemove(t *testing.T) { expectedURL := "/networks/network_id" client := &Client{ - transport: newMockClient(nil, func(req *http.Request) (*http.Response, error) { + client: newMockClient(func(req *http.Request) (*http.Response, error) { if !strings.HasPrefix(req.URL.Path, expectedURL) { return nil, fmt.Errorf("Expected URL '%s', got '%s'", expectedURL, req.URL) } diff --git a/node_inspect_test.go b/node_inspect_test.go index bf67728311..fc13283084 100644 --- a/node_inspect_test.go +++ b/node_inspect_test.go @@ -15,7 +15,7 @@ import ( func TestNodeInspectError(t *testing.T) { client := &Client{ - transport: newMockClient(nil, errorMock(http.StatusInternalServerError, "Server error")), + client: newMockClient(errorMock(http.StatusInternalServerError, "Server error")), } _, _, err := client.NodeInspectWithRaw(context.Background(), "nothing") @@ -26,7 +26,7 @@ func TestNodeInspectError(t *testing.T) { func TestNodeInspectNodeNotFound(t *testing.T) { client := &Client{ - transport: newMockClient(nil, errorMock(http.StatusNotFound, "Server error")), + client: newMockClient(errorMock(http.StatusNotFound, "Server error")), } _, _, err := client.NodeInspectWithRaw(context.Background(), "unknown") @@ -38,7 +38,7 @@ func TestNodeInspectNodeNotFound(t *testing.T) { func TestNodeInspect(t *testing.T) { expectedURL := "/nodes/node_id" client := &Client{ - transport: newMockClient(nil, func(req *http.Request) (*http.Response, error) { + client: newMockClient(func(req *http.Request) (*http.Response, error) { if !strings.HasPrefix(req.URL.Path, expectedURL) { return nil, fmt.Errorf("Expected URL '%s', got '%s'", expectedURL, req.URL) } diff --git a/node_list_test.go b/node_list_test.go index 899ac7f455..1b3b35f357 100644 --- a/node_list_test.go +++ b/node_list_test.go @@ -17,7 +17,7 @@ import ( func TestNodeListError(t *testing.T) { client := &Client{ - transport: newMockClient(nil, errorMock(http.StatusInternalServerError, "Server error")), + client: newMockClient(errorMock(http.StatusInternalServerError, "Server error")), } _, err := client.NodeList(context.Background(), types.NodeListOptions{}) @@ -54,7 +54,7 @@ func TestNodeList(t *testing.T) { } for _, listCase := range listCases { client := &Client{ - transport: newMockClient(nil, func(req *http.Request) (*http.Response, error) { + client: newMockClient(func(req *http.Request) (*http.Response, error) { if !strings.HasPrefix(req.URL.Path, expectedURL) { return nil, fmt.Errorf("Expected URL '%s', got '%s'", expectedURL, req.URL) } diff --git a/node_remove_test.go b/node_remove_test.go index 9fdf2d7eb3..f2f8adc4a3 100644 --- a/node_remove_test.go +++ b/node_remove_test.go @@ -15,7 +15,7 @@ import ( func TestNodeRemoveError(t *testing.T) { client := &Client{ - transport: newMockClient(nil, errorMock(http.StatusInternalServerError, "Server error")), + client: newMockClient(errorMock(http.StatusInternalServerError, "Server error")), } err := client.NodeRemove(context.Background(), "node_id", types.NodeRemoveOptions{Force: false}) @@ -42,7 +42,7 @@ func TestNodeRemove(t *testing.T) { for _, removeCase := range removeCases { client := &Client{ - transport: newMockClient(nil, func(req *http.Request) (*http.Response, error) { + client: newMockClient(func(req *http.Request) (*http.Response, error) { if !strings.HasPrefix(req.URL.Path, expectedURL) { return nil, fmt.Errorf("Expected URL '%s', got '%s'", expectedURL, req.URL) } diff --git a/node_update_test.go b/node_update_test.go index 1acf65854a..613ff104eb 100644 --- a/node_update_test.go +++ b/node_update_test.go @@ -15,7 +15,7 @@ import ( func TestNodeUpdateError(t *testing.T) { client := &Client{ - transport: newMockClient(nil, errorMock(http.StatusInternalServerError, "Server error")), + client: newMockClient(errorMock(http.StatusInternalServerError, "Server error")), } err := client.NodeUpdate(context.Background(), "node_id", swarm.Version{}, swarm.NodeSpec{}) @@ -28,7 +28,7 @@ func TestNodeUpdate(t *testing.T) { expectedURL := "/nodes/node_id/update" client := &Client{ - transport: newMockClient(nil, func(req *http.Request) (*http.Response, error) { + client: newMockClient(func(req *http.Request) (*http.Response, error) { if !strings.HasPrefix(req.URL.Path, expectedURL) { return nil, fmt.Errorf("Expected URL '%s', got '%s'", expectedURL, req.URL) } diff --git a/plugin_disable_test.go b/plugin_disable_test.go index f37c157866..7b50b25730 100644 --- a/plugin_disable_test.go +++ b/plugin_disable_test.go @@ -15,7 +15,7 @@ import ( func TestPluginDisableError(t *testing.T) { client := &Client{ - transport: newMockClient(nil, errorMock(http.StatusInternalServerError, "Server error")), + client: newMockClient(errorMock(http.StatusInternalServerError, "Server error")), } err := client.PluginDisable(context.Background(), "plugin_name") @@ -28,7 +28,7 @@ func TestPluginDisable(t *testing.T) { expectedURL := "/plugins/plugin_name/disable" client := &Client{ - transport: newMockClient(nil, func(req *http.Request) (*http.Response, error) { + client: newMockClient(func(req *http.Request) (*http.Response, error) { if !strings.HasPrefix(req.URL.Path, expectedURL) { return nil, fmt.Errorf("Expected URL '%s', got '%s'", expectedURL, req.URL) } diff --git a/plugin_enable_test.go b/plugin_enable_test.go index fc0fe226a9..a2b57be4c2 100644 --- a/plugin_enable_test.go +++ b/plugin_enable_test.go @@ -15,7 +15,7 @@ import ( func TestPluginEnableError(t *testing.T) { client := &Client{ - transport: newMockClient(nil, errorMock(http.StatusInternalServerError, "Server error")), + client: newMockClient(errorMock(http.StatusInternalServerError, "Server error")), } err := client.PluginEnable(context.Background(), "plugin_name") @@ -28,7 +28,7 @@ func TestPluginEnable(t *testing.T) { expectedURL := "/plugins/plugin_name/enable" client := &Client{ - transport: newMockClient(nil, func(req *http.Request) (*http.Response, error) { + client: newMockClient(func(req *http.Request) (*http.Response, error) { if !strings.HasPrefix(req.URL.Path, expectedURL) { return nil, fmt.Errorf("Expected URL '%s', got '%s'", expectedURL, req.URL) } diff --git a/plugin_inspect_test.go b/plugin_inspect_test.go index 19f829b2de..df4ca9c841 100644 --- a/plugin_inspect_test.go +++ b/plugin_inspect_test.go @@ -17,7 +17,7 @@ import ( func TestPluginInspectError(t *testing.T) { client := &Client{ - transport: newMockClient(nil, errorMock(http.StatusInternalServerError, "Server error")), + client: newMockClient(errorMock(http.StatusInternalServerError, "Server error")), } _, _, err := client.PluginInspectWithRaw(context.Background(), "nothing") @@ -29,7 +29,7 @@ func TestPluginInspectError(t *testing.T) { func TestPluginInspect(t *testing.T) { expectedURL := "/plugins/plugin_name" client := &Client{ - transport: newMockClient(nil, func(req *http.Request) (*http.Response, error) { + client: newMockClient(func(req *http.Request) (*http.Response, error) { if !strings.HasPrefix(req.URL.Path, expectedURL) { return nil, fmt.Errorf("Expected URL '%s', got '%s'", expectedURL, req.URL) } diff --git a/plugin_list_test.go b/plugin_list_test.go index 92aee61187..95c51595ca 100644 --- a/plugin_list_test.go +++ b/plugin_list_test.go @@ -17,7 +17,7 @@ import ( func TestPluginListError(t *testing.T) { client := &Client{ - transport: newMockClient(nil, errorMock(http.StatusInternalServerError, "Server error")), + client: newMockClient(errorMock(http.StatusInternalServerError, "Server error")), } _, err := client.PluginList(context.Background()) @@ -29,7 +29,7 @@ func TestPluginListError(t *testing.T) { func TestPluginList(t *testing.T) { expectedURL := "/plugins" client := &Client{ - transport: newMockClient(nil, func(req *http.Request) (*http.Response, error) { + client: newMockClient(func(req *http.Request) (*http.Response, error) { if !strings.HasPrefix(req.URL.Path, expectedURL) { return nil, fmt.Errorf("Expected URL '%s', got '%s'", expectedURL, req.URL) } diff --git a/plugin_push_test.go b/plugin_push_test.go index b77ea00273..ed685694ec 100644 --- a/plugin_push_test.go +++ b/plugin_push_test.go @@ -15,7 +15,7 @@ import ( func TestPluginPushError(t *testing.T) { client := &Client{ - transport: newMockClient(nil, errorMock(http.StatusInternalServerError, "Server error")), + client: newMockClient(errorMock(http.StatusInternalServerError, "Server error")), } err := client.PluginPush(context.Background(), "plugin_name", "") @@ -28,7 +28,7 @@ func TestPluginPush(t *testing.T) { expectedURL := "/plugins/plugin_name" client := &Client{ - transport: newMockClient(nil, func(req *http.Request) (*http.Response, error) { + client: newMockClient(func(req *http.Request) (*http.Response, error) { if !strings.HasPrefix(req.URL.Path, expectedURL) { return nil, fmt.Errorf("Expected URL '%s', got '%s'", expectedURL, req.URL) } diff --git a/plugin_remove_test.go b/plugin_remove_test.go index de565f441b..fc789fd04d 100644 --- a/plugin_remove_test.go +++ b/plugin_remove_test.go @@ -17,7 +17,7 @@ import ( func TestPluginRemoveError(t *testing.T) { client := &Client{ - transport: newMockClient(nil, errorMock(http.StatusInternalServerError, "Server error")), + client: newMockClient(errorMock(http.StatusInternalServerError, "Server error")), } err := client.PluginRemove(context.Background(), "plugin_name", types.PluginRemoveOptions{}) @@ -30,7 +30,7 @@ func TestPluginRemove(t *testing.T) { expectedURL := "/plugins/plugin_name" client := &Client{ - transport: newMockClient(nil, func(req *http.Request) (*http.Response, error) { + client: newMockClient(func(req *http.Request) (*http.Response, error) { if !strings.HasPrefix(req.URL.Path, expectedURL) { return nil, fmt.Errorf("Expected URL '%s', got '%s'", expectedURL, req.URL) } diff --git a/plugin_set_test.go b/plugin_set_test.go index 128dee04be..fa1cde044e 100644 --- a/plugin_set_test.go +++ b/plugin_set_test.go @@ -15,7 +15,7 @@ import ( func TestPluginSetError(t *testing.T) { client := &Client{ - transport: newMockClient(nil, errorMock(http.StatusInternalServerError, "Server error")), + client: newMockClient(errorMock(http.StatusInternalServerError, "Server error")), } err := client.PluginSet(context.Background(), "plugin_name", []string{}) @@ -28,7 +28,7 @@ func TestPluginSet(t *testing.T) { expectedURL := "/plugins/plugin_name/set" client := &Client{ - transport: newMockClient(nil, func(req *http.Request) (*http.Response, error) { + client: newMockClient(func(req *http.Request) (*http.Response, error) { if !strings.HasPrefix(req.URL.Path, expectedURL) { return nil, fmt.Errorf("Expected URL '%s', got '%s'", expectedURL, req.URL) } diff --git a/request.go b/request.go index 7b4f5406b8..f5c239bf25 100644 --- a/request.go +++ b/request.go @@ -13,9 +13,9 @@ import ( "github.com/docker/docker/api/types" "github.com/docker/docker/api/types/versions" - "github.com/docker/docker/client/transport/cancellable" "github.com/pkg/errors" "golang.org/x/net/context" + "golang.org/x/net/context/ctxhttp" ) // serverResponse is a wrapper for http API responses. @@ -98,20 +98,27 @@ func (cli *Client) sendClientRequest(ctx context.Context, method, path string, q // need a valid and meaningful host name. (See #189) req.Host = "docker" } + + scheme, err := resolveScheme(cli.client.Transport) + if err != nil { + return serverResp, err + } + req.URL.Host = cli.addr - req.URL.Scheme = cli.transport.Scheme() + req.URL.Scheme = scheme if expectedPayload && req.Header.Get("Content-Type") == "" { req.Header.Set("Content-Type", "text/plain") } - resp, err := cancellable.Do(ctx, cli.transport, req) + resp, err := ctxhttp.Do(ctx, cli.client, req) if err != nil { - if !cli.transport.Secure() && strings.Contains(err.Error(), "malformed HTTP response") { + + if scheme == "https" && strings.Contains(err.Error(), "malformed HTTP response") { return serverResp, fmt.Errorf("%v.\n* Are you trying to connect to a TLS-enabled daemon without TLS?", err) } - if cli.transport.Secure() && strings.Contains(err.Error(), "bad certificate") { + if scheme == "https" && strings.Contains(err.Error(), "bad certificate") { return serverResp, fmt.Errorf("The server probably has client authentication (--tlsverify) enabled. Please check your TLS client certification settings: %v", err) } diff --git a/request_test.go b/request_test.go index 446adf9c66..63908aec4b 100644 --- a/request_test.go +++ b/request_test.go @@ -50,7 +50,7 @@ func TestSetHostHeader(t *testing.T) { } client := &Client{ - transport: newMockClient(nil, func(req *http.Request) (*http.Response, error) { + client: newMockClient(func(req *http.Request) (*http.Response, error) { if !strings.HasPrefix(req.URL.Path, testURL) { return nil, fmt.Errorf("Test Case #%d: Expected URL %q, got %q", c, testURL, req.URL) } @@ -65,6 +65,7 @@ func TestSetHostHeader(t *testing.T) { Body: ioutil.NopCloser(bytes.NewReader(([]byte("")))), }, nil }), + proto: proto, addr: addr, basePath: basePath, @@ -82,7 +83,7 @@ func TestSetHostHeader(t *testing.T) { // errors returned as JSON func TestPlainTextError(t *testing.T) { client := &Client{ - transport: newMockClient(nil, plainTextErrorMock(http.StatusInternalServerError, "Server error")), + client: newMockClient(plainTextErrorMock(http.StatusInternalServerError, "Server error")), } _, err := client.ContainerList(context.Background(), types.ContainerListOptions{}) if err == nil || err.Error() != "Error response from daemon: Server error" { diff --git a/service_create_test.go b/service_create_test.go index a79f040c0a..1e07382870 100644 --- a/service_create_test.go +++ b/service_create_test.go @@ -16,7 +16,7 @@ import ( func TestServiceCreateError(t *testing.T) { client := &Client{ - transport: newMockClient(nil, errorMock(http.StatusInternalServerError, "Server error")), + client: newMockClient(errorMock(http.StatusInternalServerError, "Server error")), } _, err := client.ServiceCreate(context.Background(), swarm.ServiceSpec{}, types.ServiceCreateOptions{}) if err == nil || err.Error() != "Error response from daemon: Server error" { @@ -27,7 +27,7 @@ func TestServiceCreateError(t *testing.T) { func TestServiceCreate(t *testing.T) { expectedURL := "/services/create" client := &Client{ - transport: newMockClient(nil, func(req *http.Request) (*http.Response, error) { + client: newMockClient(func(req *http.Request) (*http.Response, error) { if !strings.HasPrefix(req.URL.Path, expectedURL) { return nil, fmt.Errorf("Expected URL '%s', got '%s'", expectedURL, req.URL) } diff --git a/service_inspect_test.go b/service_inspect_test.go index e4eafff5d7..e235cf0fef 100644 --- a/service_inspect_test.go +++ b/service_inspect_test.go @@ -15,7 +15,7 @@ import ( func TestServiceInspectError(t *testing.T) { client := &Client{ - transport: newMockClient(nil, errorMock(http.StatusInternalServerError, "Server error")), + client: newMockClient(errorMock(http.StatusInternalServerError, "Server error")), } _, _, err := client.ServiceInspectWithRaw(context.Background(), "nothing") @@ -26,7 +26,7 @@ func TestServiceInspectError(t *testing.T) { func TestServiceInspectServiceNotFound(t *testing.T) { client := &Client{ - transport: newMockClient(nil, errorMock(http.StatusNotFound, "Server error")), + client: newMockClient(errorMock(http.StatusNotFound, "Server error")), } _, _, err := client.ServiceInspectWithRaw(context.Background(), "unknown") @@ -38,7 +38,7 @@ func TestServiceInspectServiceNotFound(t *testing.T) { func TestServiceInspect(t *testing.T) { expectedURL := "/services/service_id" client := &Client{ - transport: newMockClient(nil, func(req *http.Request) (*http.Response, error) { + client: newMockClient(func(req *http.Request) (*http.Response, error) { if !strings.HasPrefix(req.URL.Path, expectedURL) { return nil, fmt.Errorf("Expected URL '%s', got '%s'", expectedURL, req.URL) } diff --git a/service_list_test.go b/service_list_test.go index 6e6851a3a5..728187919f 100644 --- a/service_list_test.go +++ b/service_list_test.go @@ -17,7 +17,7 @@ import ( func TestServiceListError(t *testing.T) { client := &Client{ - transport: newMockClient(nil, errorMock(http.StatusInternalServerError, "Server error")), + client: newMockClient(errorMock(http.StatusInternalServerError, "Server error")), } _, err := client.ServiceList(context.Background(), types.ServiceListOptions{}) @@ -54,7 +54,7 @@ func TestServiceList(t *testing.T) { } for _, listCase := range listCases { client := &Client{ - transport: newMockClient(nil, func(req *http.Request) (*http.Response, error) { + client: newMockClient(func(req *http.Request) (*http.Response, error) { if !strings.HasPrefix(req.URL.Path, expectedURL) { return nil, fmt.Errorf("Expected URL '%s', got '%s'", expectedURL, req.URL) } diff --git a/service_remove_test.go b/service_remove_test.go index e1316f959b..8e2ac259c1 100644 --- a/service_remove_test.go +++ b/service_remove_test.go @@ -13,7 +13,7 @@ import ( func TestServiceRemoveError(t *testing.T) { client := &Client{ - transport: newMockClient(nil, errorMock(http.StatusInternalServerError, "Server error")), + client: newMockClient(errorMock(http.StatusInternalServerError, "Server error")), } err := client.ServiceRemove(context.Background(), "service_id") @@ -26,7 +26,7 @@ func TestServiceRemove(t *testing.T) { expectedURL := "/services/service_id" client := &Client{ - transport: newMockClient(nil, func(req *http.Request) (*http.Response, error) { + client: newMockClient(func(req *http.Request) (*http.Response, error) { if !strings.HasPrefix(req.URL.Path, expectedURL) { return nil, fmt.Errorf("Expected URL '%s', got '%s'", expectedURL, req.URL) } diff --git a/service_update_test.go b/service_update_test.go index bd616c09bf..081649f492 100644 --- a/service_update_test.go +++ b/service_update_test.go @@ -16,7 +16,7 @@ import ( func TestServiceUpdateError(t *testing.T) { client := &Client{ - transport: newMockClient(nil, errorMock(http.StatusInternalServerError, "Server error")), + client: newMockClient(errorMock(http.StatusInternalServerError, "Server error")), } err := client.ServiceUpdate(context.Background(), "service_id", swarm.Version{}, swarm.ServiceSpec{}, types.ServiceUpdateOptions{}) @@ -51,7 +51,7 @@ func TestServiceUpdate(t *testing.T) { for _, updateCase := range updateCases { client := &Client{ - transport: newMockClient(nil, func(req *http.Request) (*http.Response, error) { + client: newMockClient(func(req *http.Request) (*http.Response, error) { if !strings.HasPrefix(req.URL.Path, expectedURL) { return nil, fmt.Errorf("Expected URL '%s', got '%s'", expectedURL, req.URL) } diff --git a/swarm_init_test.go b/swarm_init_test.go index 077c8c4efb..811155aff4 100644 --- a/swarm_init_test.go +++ b/swarm_init_test.go @@ -15,7 +15,7 @@ import ( func TestSwarmInitError(t *testing.T) { client := &Client{ - transport: newMockClient(nil, errorMock(http.StatusInternalServerError, "Server error")), + client: newMockClient(errorMock(http.StatusInternalServerError, "Server error")), } _, err := client.SwarmInit(context.Background(), swarm.InitRequest{}) @@ -28,7 +28,7 @@ func TestSwarmInit(t *testing.T) { expectedURL := "/swarm/init" client := &Client{ - transport: newMockClient(nil, func(req *http.Request) (*http.Response, error) { + client: newMockClient(func(req *http.Request) (*http.Response, error) { if !strings.HasPrefix(req.URL.Path, expectedURL) { return nil, fmt.Errorf("Expected URL '%s', got '%s'", expectedURL, req.URL) } diff --git a/swarm_inspect_test.go b/swarm_inspect_test.go index 7143e77181..6432d172b4 100644 --- a/swarm_inspect_test.go +++ b/swarm_inspect_test.go @@ -15,7 +15,7 @@ import ( func TestSwarmInspectError(t *testing.T) { client := &Client{ - transport: newMockClient(nil, errorMock(http.StatusInternalServerError, "Server error")), + client: newMockClient(errorMock(http.StatusInternalServerError, "Server error")), } _, err := client.SwarmInspect(context.Background()) @@ -27,7 +27,7 @@ func TestSwarmInspectError(t *testing.T) { func TestSwarmInspect(t *testing.T) { expectedURL := "/swarm" client := &Client{ - transport: newMockClient(nil, func(req *http.Request) (*http.Response, error) { + client: newMockClient(func(req *http.Request) (*http.Response, error) { if !strings.HasPrefix(req.URL.Path, expectedURL) { return nil, fmt.Errorf("Expected URL '%s', got '%s'", expectedURL, req.URL) } diff --git a/swarm_join_test.go b/swarm_join_test.go index 922716d85f..31ef2a76ee 100644 --- a/swarm_join_test.go +++ b/swarm_join_test.go @@ -15,7 +15,7 @@ import ( func TestSwarmJoinError(t *testing.T) { client := &Client{ - transport: newMockClient(nil, errorMock(http.StatusInternalServerError, "Server error")), + client: newMockClient(errorMock(http.StatusInternalServerError, "Server error")), } err := client.SwarmJoin(context.Background(), swarm.JoinRequest{}) @@ -28,7 +28,7 @@ func TestSwarmJoin(t *testing.T) { expectedURL := "/swarm/join" client := &Client{ - transport: newMockClient(nil, func(req *http.Request) (*http.Response, error) { + client: newMockClient(func(req *http.Request) (*http.Response, error) { if !strings.HasPrefix(req.URL.Path, expectedURL) { return nil, fmt.Errorf("Expected URL '%s', got '%s'", expectedURL, req.URL) } diff --git a/swarm_leave_test.go b/swarm_leave_test.go index d0bef2b257..c96dac8120 100644 --- a/swarm_leave_test.go +++ b/swarm_leave_test.go @@ -13,7 +13,7 @@ import ( func TestSwarmLeaveError(t *testing.T) { client := &Client{ - transport: newMockClient(nil, errorMock(http.StatusInternalServerError, "Server error")), + client: newMockClient(errorMock(http.StatusInternalServerError, "Server error")), } err := client.SwarmLeave(context.Background(), false) @@ -40,7 +40,7 @@ func TestSwarmLeave(t *testing.T) { for _, leaveCase := range leaveCases { client := &Client{ - transport: newMockClient(nil, func(req *http.Request) (*http.Response, error) { + client: newMockClient(func(req *http.Request) (*http.Response, error) { if !strings.HasPrefix(req.URL.Path, expectedURL) { return nil, fmt.Errorf("Expected URL '%s', got '%s'", expectedURL, req.URL) } diff --git a/swarm_update_test.go b/swarm_update_test.go index ecf1731e5b..3b23db078f 100644 --- a/swarm_update_test.go +++ b/swarm_update_test.go @@ -15,7 +15,7 @@ import ( func TestSwarmUpdateError(t *testing.T) { client := &Client{ - transport: newMockClient(nil, errorMock(http.StatusInternalServerError, "Server error")), + client: newMockClient(errorMock(http.StatusInternalServerError, "Server error")), } err := client.SwarmUpdate(context.Background(), swarm.Version{}, swarm.Spec{}, swarm.UpdateFlags{}) @@ -28,7 +28,7 @@ func TestSwarmUpdate(t *testing.T) { expectedURL := "/swarm/update" client := &Client{ - transport: newMockClient(nil, func(req *http.Request) (*http.Response, error) { + client: newMockClient(func(req *http.Request) (*http.Response, error) { if !strings.HasPrefix(req.URL.Path, expectedURL) { return nil, fmt.Errorf("Expected URL '%s', got '%s'", expectedURL, req.URL) } diff --git a/task_inspect_test.go b/task_inspect_test.go index 2c73b37642..148cdad3a7 100644 --- a/task_inspect_test.go +++ b/task_inspect_test.go @@ -15,7 +15,7 @@ import ( func TestTaskInspectError(t *testing.T) { client := &Client{ - transport: newMockClient(nil, errorMock(http.StatusInternalServerError, "Server error")), + client: newMockClient(errorMock(http.StatusInternalServerError, "Server error")), } _, _, err := client.TaskInspectWithRaw(context.Background(), "nothing") @@ -27,7 +27,7 @@ func TestTaskInspectError(t *testing.T) { func TestTaskInspect(t *testing.T) { expectedURL := "/tasks/task_id" client := &Client{ - transport: newMockClient(nil, func(req *http.Request) (*http.Response, error) { + client: newMockClient(func(req *http.Request) (*http.Response, error) { if !strings.HasPrefix(req.URL.Path, expectedURL) { return nil, fmt.Errorf("Expected URL '%s', got '%s'", expectedURL, req.URL) } diff --git a/task_list_test.go b/task_list_test.go index b520ab589f..2d9b812bc2 100644 --- a/task_list_test.go +++ b/task_list_test.go @@ -17,7 +17,7 @@ import ( func TestTaskListError(t *testing.T) { client := &Client{ - transport: newMockClient(nil, errorMock(http.StatusInternalServerError, "Server error")), + client: newMockClient(errorMock(http.StatusInternalServerError, "Server error")), } _, err := client.TaskList(context.Background(), types.TaskListOptions{}) @@ -54,7 +54,7 @@ func TestTaskList(t *testing.T) { } for _, listCase := range listCases { client := &Client{ - transport: newMockClient(nil, func(req *http.Request) (*http.Response, error) { + client: newMockClient(func(req *http.Request) (*http.Response, error) { if !strings.HasPrefix(req.URL.Path, expectedURL) { return nil, fmt.Errorf("Expected URL '%s', got '%s'", expectedURL, req.URL) } diff --git a/transport.go b/transport.go new file mode 100644 index 0000000000..43a667272d --- /dev/null +++ b/transport.go @@ -0,0 +1,51 @@ +package client + +import ( + "crypto/tls" + "errors" + "net/http" +) + +var errTLSConfigUnavailable = errors.New("TLSConfig unavailable") + +// transportFunc allows us to inject a mock transport for testing. We define it +// here so we can detect the tlsconfig and return nil for only this type. +type transportFunc func(*http.Request) (*http.Response, error) + +func (tf transportFunc) RoundTrip(req *http.Request) (*http.Response, error) { + return tf(req) +} + +// resolveTLSConfig attempts to resolve the tls configuration from the +// RoundTripper. +func resolveTLSConfig(transport http.RoundTripper) (*tls.Config, error) { + switch tr := transport.(type) { + case *http.Transport: + return tr.TLSClientConfig, nil + case transportFunc: + return nil, nil // detect this type for testing. + default: + return nil, errTLSConfigUnavailable + } +} + +// resolveScheme detects a tls config on the transport and returns the +// appropriate http scheme. +// +// TODO(stevvooe): This isn't really the right way to write clients in Go. +// `NewClient` should probably only take an `*http.Client` and work from there. +// Unfortunately, the model of having a host-ish/url-thingy as the connection +// string has us confusing protocol and transport layers. We continue doing +// this to avoid breaking existing clients but this should be addressed. +func resolveScheme(transport http.RoundTripper) (string, error) { + c, err := resolveTLSConfig(transport) + if err != nil { + return "", err + } + + if c != nil { + return "https", nil + } + + return "http", nil +} diff --git a/transport/cancellable/LICENSE b/transport/cancellable/LICENSE deleted file mode 100644 index 6a66aea5ea..0000000000 --- a/transport/cancellable/LICENSE +++ /dev/null @@ -1,27 +0,0 @@ -Copyright (c) 2009 The Go Authors. All rights reserved. - -Redistribution and use in source and binary forms, with or without -modification, are permitted provided that the following conditions are -met: - - * Redistributions of source code must retain the above copyright -notice, this list of conditions and the following disclaimer. - * Redistributions in binary form must reproduce the above -copyright notice, this list of conditions and the following disclaimer -in the documentation and/or other materials provided with the -distribution. - * Neither the name of Google Inc. nor the names of its -contributors may be used to endorse or promote products derived from -this software without specific prior written permission. - -THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS -"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT -LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR -A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT -OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, -SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT -LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, -DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY -THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT -(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE -OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. diff --git a/transport/cancellable/canceler.go b/transport/cancellable/canceler.go deleted file mode 100644 index 62770b777b..0000000000 --- a/transport/cancellable/canceler.go +++ /dev/null @@ -1,23 +0,0 @@ -// Copyright 2015 The Go Authors. All rights reserved. -// Use of this source code is governed by a BSD-style -// license that can be found in the LICENSE file. - -// +build go1.5 - -package cancellable - -import ( - "net/http" - - "github.com/docker/docker/client/transport" -) - -func canceler(client transport.Sender, req *http.Request) func() { - // TODO(djd): Respect any existing value of req.Cancel. - ch := make(chan struct{}) - req.Cancel = ch - - return func() { - close(ch) - } -} diff --git a/transport/cancellable/canceler_go14.go b/transport/cancellable/canceler_go14.go deleted file mode 100644 index dd2723d94f..0000000000 --- a/transport/cancellable/canceler_go14.go +++ /dev/null @@ -1,27 +0,0 @@ -// Copyright 2015 The Go Authors. All rights reserved. -// Use of this source code is governed by a BSD-style -// license that can be found in the LICENSE file. - -// +build !go1.5 - -package cancellable - -import ( - "net/http" - - "github.com/docker/docker/client/transport" -) - -type requestCanceler interface { - CancelRequest(*http.Request) -} - -func canceler(client transport.Sender, req *http.Request) func() { - rc, ok := client.(requestCanceler) - if !ok { - return func() {} - } - return func() { - rc.CancelRequest(req) - } -} diff --git a/transport/cancellable/cancellable.go b/transport/cancellable/cancellable.go deleted file mode 100644 index 1f8eac5c1c..0000000000 --- a/transport/cancellable/cancellable.go +++ /dev/null @@ -1,115 +0,0 @@ -// Copyright 2015 The Go Authors. All rights reserved. -// Use of this source code is governed by a BSD-style -// license that can be found in the LICENSE file. - -// Package cancellable provides helper function to cancel http requests. -package cancellable - -import ( - "io" - "net/http" - "sync" - - "github.com/docker/docker/client/transport" - - "golang.org/x/net/context" -) - -func nop() {} - -var ( - testHookContextDoneBeforeHeaders = nop - testHookDoReturned = nop - testHookDidBodyClose = nop -) - -// Do sends an HTTP request with the provided transport.Sender and returns an HTTP response. -// If the client is nil, http.DefaultClient is used. -// If the context is canceled or times out, ctx.Err() will be returned. -// -// FORK INFORMATION: -// -// This function deviates from the upstream version in golang.org/x/net/context/ctxhttp by -// taking a Sender interface rather than a *http.Client directly. That allow us to use -// this function with mocked clients and hijacked connections. -func Do(ctx context.Context, client transport.Sender, req *http.Request) (*http.Response, error) { - if client == nil { - client = http.DefaultClient - } - - // Request cancelation changed in Go 1.5, see canceler.go and canceler_go14.go. - cancel := canceler(client, req) - - type responseAndError struct { - resp *http.Response - err error - } - result := make(chan responseAndError, 1) - - go func() { - resp, err := client.Do(req) - testHookDoReturned() - result <- responseAndError{resp, err} - }() - - var resp *http.Response - - select { - case <-ctx.Done(): - testHookContextDoneBeforeHeaders() - cancel() - // Clean up after the goroutine calling client.Do: - go func() { - if r := <-result; r.resp != nil && r.resp.Body != nil { - testHookDidBodyClose() - r.resp.Body.Close() - } - }() - return nil, ctx.Err() - case r := <-result: - var err error - resp, err = r.resp, r.err - if err != nil { - return resp, err - } - } - - c := make(chan struct{}) - go func() { - select { - case <-ctx.Done(): - cancel() - case <-c: - // The response's Body is closed. - } - }() - resp.Body = ¬ifyingReader{ReadCloser: resp.Body, notify: c} - - return resp, nil -} - -// notifyingReader is an io.ReadCloser that closes the notify channel after -// Close is called or a Read fails on the underlying ReadCloser. -type notifyingReader struct { - io.ReadCloser - notify chan<- struct{} - notifyOnce sync.Once -} - -func (r *notifyingReader) Read(p []byte) (int, error) { - n, err := r.ReadCloser.Read(p) - if err != nil { - r.notifyOnce.Do(func() { - close(r.notify) - }) - } - return n, err -} - -func (r *notifyingReader) Close() error { - err := r.ReadCloser.Close() - r.notifyOnce.Do(func() { - close(r.notify) - }) - return err -} diff --git a/transport/client.go b/transport/client.go deleted file mode 100644 index 13d4b3ab3d..0000000000 --- a/transport/client.go +++ /dev/null @@ -1,47 +0,0 @@ -package transport - -import ( - "crypto/tls" - "net/http" -) - -// Sender is an interface that clients must implement -// to be able to send requests to a remote connection. -type Sender interface { - // Do sends request to a remote endpoint. - Do(*http.Request) (*http.Response, error) -} - -// Client is an interface that abstracts all remote connections. -type Client interface { - Sender - // Secure tells whether the connection is secure or not. - Secure() bool - // Scheme returns the connection protocol the client uses. - Scheme() string - // TLSConfig returns any TLS configuration the client uses. - TLSConfig() *tls.Config -} - -// tlsInfo returns information about the TLS configuration. -type tlsInfo struct { - tlsConfig *tls.Config -} - -// TLSConfig returns the TLS configuration. -func (t *tlsInfo) TLSConfig() *tls.Config { - return t.tlsConfig -} - -// Scheme returns protocol scheme to use. -func (t *tlsInfo) Scheme() string { - if t.tlsConfig != nil { - return "https" - } - return "http" -} - -// Secure returns true if there is a TLS configuration. -func (t *tlsInfo) Secure() bool { - return t.tlsConfig != nil -} diff --git a/transport/transport.go b/transport/transport.go deleted file mode 100644 index ff28af1855..0000000000 --- a/transport/transport.go +++ /dev/null @@ -1,57 +0,0 @@ -// Package transport provides function to send request to remote endpoints. -package transport - -import ( - "fmt" - "net/http" - - "github.com/docker/go-connections/sockets" -) - -// apiTransport holds information about the http transport to connect with the API. -type apiTransport struct { - *http.Client - *tlsInfo - transport *http.Transport -} - -// NewTransportWithHTTP creates a new transport based on the provided proto, address and http client. -// It uses Docker's default http transport configuration if the client is nil. -// It does not modify the client's transport if it's not nil. -func NewTransportWithHTTP(proto, addr string, client *http.Client) (Client, error) { - var transport *http.Transport - - if client != nil { - tr, ok := client.Transport.(*http.Transport) - if !ok { - return nil, fmt.Errorf("unable to verify TLS configuration, invalid transport %v", client.Transport) - } - transport = tr - } else { - transport = defaultTransport(proto, addr) - client = &http.Client{ - Transport: transport, - } - } - - return &apiTransport{ - Client: client, - tlsInfo: &tlsInfo{transport.TLSClientConfig}, - transport: transport, - }, nil -} - -// CancelRequest stops a request execution. -func (a *apiTransport) CancelRequest(req *http.Request) { - a.transport.CancelRequest(req) -} - -// defaultTransport creates a new http.Transport with Docker's -// default transport configuration. -func defaultTransport(proto, addr string) *http.Transport { - tr := new(http.Transport) - sockets.ConfigureTransport(tr, proto, addr) - return tr -} - -var _ Client = &apiTransport{} diff --git a/volume_create_test.go b/volume_create_test.go index d3cfa7132f..75085296cc 100644 --- a/volume_create_test.go +++ b/volume_create_test.go @@ -15,7 +15,7 @@ import ( func TestVolumeCreateError(t *testing.T) { client := &Client{ - transport: newMockClient(nil, errorMock(http.StatusInternalServerError, "Server error")), + client: newMockClient(errorMock(http.StatusInternalServerError, "Server error")), } _, err := client.VolumeCreate(context.Background(), types.VolumeCreateRequest{}) @@ -28,7 +28,7 @@ func TestVolumeCreate(t *testing.T) { expectedURL := "/volumes/create" client := &Client{ - transport: newMockClient(nil, func(req *http.Request) (*http.Response, error) { + client: newMockClient(func(req *http.Request) (*http.Response, error) { if !strings.HasPrefix(req.URL.Path, expectedURL) { return nil, fmt.Errorf("Expected URL '%s', got '%s'", expectedURL, req.URL) } diff --git a/volume_inspect_test.go b/volume_inspect_test.go index 4b9f47358d..0d1d118828 100644 --- a/volume_inspect_test.go +++ b/volume_inspect_test.go @@ -15,7 +15,7 @@ import ( func TestVolumeInspectError(t *testing.T) { client := &Client{ - transport: newMockClient(nil, errorMock(http.StatusInternalServerError, "Server error")), + client: newMockClient(errorMock(http.StatusInternalServerError, "Server error")), } _, err := client.VolumeInspect(context.Background(), "nothing") @@ -26,7 +26,7 @@ func TestVolumeInspectError(t *testing.T) { func TestVolumeInspectNotFound(t *testing.T) { client := &Client{ - transport: newMockClient(nil, errorMock(http.StatusNotFound, "Server error")), + client: newMockClient(errorMock(http.StatusNotFound, "Server error")), } _, err := client.VolumeInspect(context.Background(), "unknown") @@ -38,7 +38,7 @@ func TestVolumeInspectNotFound(t *testing.T) { func TestVolumeInspect(t *testing.T) { expectedURL := "/volumes/volume_id" client := &Client{ - transport: newMockClient(nil, func(req *http.Request) (*http.Response, error) { + client: newMockClient(func(req *http.Request) (*http.Response, error) { if !strings.HasPrefix(req.URL.Path, expectedURL) { return nil, fmt.Errorf("Expected URL '%s', got '%s'", expectedURL, req.URL) } diff --git a/volume_list_test.go b/volume_list_test.go index d30d9fcd52..0af420eaff 100644 --- a/volume_list_test.go +++ b/volume_list_test.go @@ -16,7 +16,7 @@ import ( func TestVolumeListError(t *testing.T) { client := &Client{ - transport: newMockClient(nil, errorMock(http.StatusInternalServerError, "Server error")), + client: newMockClient(errorMock(http.StatusInternalServerError, "Server error")), } _, err := client.VolumeList(context.Background(), filters.NewArgs()) @@ -59,7 +59,7 @@ func TestVolumeList(t *testing.T) { for _, listCase := range listCases { client := &Client{ - transport: newMockClient(nil, func(req *http.Request) (*http.Response, error) { + client: newMockClient(func(req *http.Request) (*http.Response, error) { if !strings.HasPrefix(req.URL.Path, expectedURL) { return nil, fmt.Errorf("Expected URL '%s', got '%s'", expectedURL, req.URL) } diff --git a/volume_remove_test.go b/volume_remove_test.go index 0675bfd458..1fe657349a 100644 --- a/volume_remove_test.go +++ b/volume_remove_test.go @@ -13,7 +13,7 @@ import ( func TestVolumeRemoveError(t *testing.T) { client := &Client{ - transport: newMockClient(nil, errorMock(http.StatusInternalServerError, "Server error")), + client: newMockClient(errorMock(http.StatusInternalServerError, "Server error")), } err := client.VolumeRemove(context.Background(), "volume_id", false) @@ -26,7 +26,7 @@ func TestVolumeRemove(t *testing.T) { expectedURL := "/volumes/volume_id" client := &Client{ - transport: newMockClient(nil, func(req *http.Request) (*http.Response, error) { + client: newMockClient(func(req *http.Request) (*http.Response, error) { if !strings.HasPrefix(req.URL.Path, expectedURL) { return nil, fmt.Errorf("Expected URL '%s', got '%s'", expectedURL, req.URL) } From 59e38197ffa4ada6c4db550fda9e55308e971abd Mon Sep 17 00:00:00 2001 From: Josh Chorlton Date: Thu, 22 Sep 2016 15:00:30 +0800 Subject: [PATCH 028/138] Move /x/net/context to context in docker client README Signed-off-by: Josh Chorlton --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 7872d94a53..34cf7372db 100644 --- a/README.md +++ b/README.md @@ -11,10 +11,10 @@ package main import ( "fmt" + "context" "github.com/docker/docker/client" "github.com/docker/docker/api/types" - "golang.org/x/net/context" ) func main() { From 9acc93282ec40ffef007c936254261f69d662cc0 Mon Sep 17 00:00:00 2001 From: Josh Horwitz Date: Tue, 9 Aug 2016 10:34:07 -1000 Subject: [PATCH 029/138] Refactor to new events api Signed-off-by: Josh Horwitz --- events.go | 69 +++++++++++++++++++++++++++++++------ events_test.go | 93 +++++++++++++++++++++++++++++++++++--------------- interface.go | 3 +- 3 files changed, 127 insertions(+), 38 deletions(-) diff --git a/events.go b/events.go index 0ba7114f94..c154f7dcf9 100644 --- a/events.go +++ b/events.go @@ -1,20 +1,71 @@ package client import ( - "io" + "encoding/json" "net/url" "time" "golang.org/x/net/context" "github.com/docker/docker/api/types" + "github.com/docker/docker/api/types/events" "github.com/docker/docker/api/types/filters" timetypes "github.com/docker/docker/api/types/time" ) -// Events returns a stream of events in the daemon in a ReadCloser. -// It's up to the caller to close the stream. -func (cli *Client) Events(ctx context.Context, options types.EventsOptions) (io.ReadCloser, error) { +// Events returns a stream of events in the daemon. It's up to the caller to close the stream +// by cancelling the context. Once the stream has been completely read an io.EOF error will +// be sent over the error channel. If an error is sent all processing will be stopped. It's up +// to the caller to reopen the stream in the event of an error by reinvoking this method. +func (cli *Client) Events(ctx context.Context, options types.EventsOptions) (<-chan events.Message, <-chan error) { + + messages := make(chan events.Message) + errs := make(chan error, 1) + + go func() { + defer close(errs) + + query, err := buildEventsQueryParams(cli.version, options) + if err != nil { + errs <- err + return + } + + resp, err := cli.get(ctx, "/events", query, nil) + if err != nil { + errs <- err + return + } + defer resp.body.Close() + + decoder := json.NewDecoder(resp.body) + + for { + select { + case <-ctx.Done(): + errs <- ctx.Err() + return + default: + var event events.Message + if err := decoder.Decode(&event); err != nil { + errs <- err + return + } + + select { + case messages <- event: + case <-ctx.Done(): + errs <- ctx.Err() + return + } + } + } + }() + + return messages, errs +} + +func buildEventsQueryParams(cliVersion string, options types.EventsOptions) (url.Values, error) { query := url.Values{} ref := time.Now() @@ -25,6 +76,7 @@ func (cli *Client) Events(ctx context.Context, options types.EventsOptions) (io. } query.Set("since", ts) } + if options.Until != "" { ts, err := timetypes.GetTimestamp(options.Until, ref) if err != nil { @@ -32,17 +84,14 @@ func (cli *Client) Events(ctx context.Context, options types.EventsOptions) (io. } query.Set("until", ts) } + if options.Filters.Len() > 0 { - filterJSON, err := filters.ToParamWithVersion(cli.version, options.Filters) + filterJSON, err := filters.ToParamWithVersion(cliVersion, options.Filters) if err != nil { return nil, err } query.Set("filters", filterJSON) } - serverResponse, err := cli.get(ctx, "/events", query, nil) - if err != nil { - return nil, err - } - return serverResponse.body, nil + return query, nil } diff --git a/events_test.go b/events_test.go index 6328983609..ba82d2f542 100644 --- a/events_test.go +++ b/events_test.go @@ -2,7 +2,9 @@ package client import ( "bytes" + "encoding/json" "fmt" + "io" "io/ioutil" "net/http" "strings" @@ -11,6 +13,7 @@ import ( "golang.org/x/net/context" "github.com/docker/docker/api/types" + "github.com/docker/docker/api/types/events" "github.com/docker/docker/api/types/filters" ) @@ -36,7 +39,8 @@ func TestEventsErrorInOptions(t *testing.T) { client := &Client{ client: newMockClient(errorMock(http.StatusInternalServerError, "Server error")), } - _, err := client.Events(context.Background(), e.options) + _, errs := client.Events(context.Background(), e.options) + err := <-errs if err == nil || !strings.Contains(err.Error(), e.expectedError) { t.Fatalf("expected an error %q, got %v", e.expectedError, err) } @@ -47,39 +51,36 @@ func TestEventsErrorFromServer(t *testing.T) { client := &Client{ client: newMockClient(errorMock(http.StatusInternalServerError, "Server error")), } - _, err := client.Events(context.Background(), types.EventsOptions{}) + _, errs := client.Events(context.Background(), types.EventsOptions{}) + err := <-errs if err == nil || err.Error() != "Error response from daemon: Server error" { t.Fatalf("expected a Server Error, got %v", err) } } func TestEvents(t *testing.T) { + expectedURL := "/events" filters := filters.NewArgs() - filters.Add("label", "label1") - filters.Add("label", "label2") - expectedFiltersJSON := `{"label":{"label1":true,"label2":true}}` + filters.Add("type", events.ContainerEventType) + expectedFiltersJSON := fmt.Sprintf(`{"type":{"%s":true}}`, events.ContainerEventType) eventsCases := []struct { options types.EventsOptions + events []events.Message + expectedEvents map[string]bool expectedQueryParams map[string]string }{ { options: types.EventsOptions{ - Since: "invalid but valid", + Filters: filters, }, expectedQueryParams: map[string]string{ - "since": "invalid but valid", - }, - }, - { - options: types.EventsOptions{ - Until: "invalid but valid", - }, - expectedQueryParams: map[string]string{ - "until": "invalid but valid", + "filters": expectedFiltersJSON, }, + events: []events.Message{}, + expectedEvents: make(map[string]bool), }, { options: types.EventsOptions{ @@ -88,6 +89,28 @@ func TestEvents(t *testing.T) { expectedQueryParams: map[string]string{ "filters": expectedFiltersJSON, }, + events: []events.Message{ + { + Type: "container", + ID: "1", + Action: "create", + }, + { + Type: "container", + ID: "2", + Action: "die", + }, + { + Type: "container", + ID: "3", + Action: "create", + }, + }, + expectedEvents: map[string]bool{ + "1": true, + "2": true, + "3": true, + }, }, } @@ -98,29 +121,45 @@ func TestEvents(t *testing.T) { return nil, fmt.Errorf("Expected URL '%s', got '%s'", expectedURL, req.URL) } query := req.URL.Query() + for key, expected := range eventsCase.expectedQueryParams { actual := query.Get(key) if actual != expected { return nil, fmt.Errorf("%s not set in URL query properly. Expected '%s', got %s", key, expected, actual) } } + + buffer := new(bytes.Buffer) + + for _, e := range eventsCase.events { + b, _ := json.Marshal(e) + buffer.Write(b) + } + return &http.Response{ StatusCode: http.StatusOK, - Body: ioutil.NopCloser(bytes.NewReader([]byte("response"))), + Body: ioutil.NopCloser(buffer), }, nil }), } - body, err := client.Events(context.Background(), eventsCase.options) - if err != nil { - t.Fatal(err) - } - defer body.Close() - content, err := ioutil.ReadAll(body) - if err != nil { - t.Fatal(err) - } - if string(content) != "response" { - t.Fatalf("expected response to contain 'response', got %s", string(content)) + + messages, errs := client.Events(context.Background(), eventsCase.options) + + loop: + for { + select { + case err := <-errs: + if err != nil && err != io.EOF { + t.Fatal(err) + } + + break loop + case e := <-messages: + _, ok := eventsCase.expectedEvents[e.ID] + if !ok { + t.Fatalf("event received not expected with action %s & id %s", e.Action, e.ID) + } + } } } } diff --git a/interface.go b/interface.go index 2d5555ff06..81320918b3 100644 --- a/interface.go +++ b/interface.go @@ -6,6 +6,7 @@ import ( "github.com/docker/docker/api/types" "github.com/docker/docker/api/types/container" + "github.com/docker/docker/api/types/events" "github.com/docker/docker/api/types/filters" "github.com/docker/docker/api/types/network" "github.com/docker/docker/api/types/registry" @@ -120,7 +121,7 @@ type SwarmAPIClient interface { // SystemAPIClient defines API client methods for the system type SystemAPIClient interface { - Events(ctx context.Context, options types.EventsOptions) (io.ReadCloser, error) + Events(ctx context.Context, options types.EventsOptions) (<-chan events.Message, <-chan error) Info(ctx context.Context) (types.Info, error) RegistryLogin(ctx context.Context, auth types.AuthConfig) (types.AuthResponse, error) } From 9f20fabc69b43be38f717065667c50e1ff616efa Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?To=CC=83nis=20Tiigi?= Date: Thu, 22 Sep 2016 14:38:00 -0700 Subject: [PATCH 030/138] Implement build cache based on history array Based on work by KJ Tsanaktsidis Signed-off-by: Tonis Tiigi Signed-off-by: KJ Tsanaktsidis --- image_build.go | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/image_build.go b/image_build.go index a84bf57821..0094602a6e 100644 --- a/image_build.go +++ b/image_build.go @@ -110,6 +110,13 @@ func imageBuildOptionsToQuery(options types.ImageBuildOptions) (url.Values, erro return query, err } query.Set("labels", string(labelsJSON)) + + cacheFromJSON, err := json.Marshal(options.CacheFrom) + if err != nil { + return query, err + } + query.Set("cachefrom", string(cacheFromJSON)) + return query, nil } From 9403a5b63e1ba5e86ffeacfb19f35e67a192cbdd Mon Sep 17 00:00:00 2001 From: qudongfang Date: Thu, 8 Sep 2016 09:57:54 +0800 Subject: [PATCH 031/138] ensures that transport.Client is closed while using cli.NewClient with *http.Client = nil. Signed-off-by: qudongfang --- client.go | 13 +++++++++++++ client_test.go | 5 +++++ 2 files changed, 18 insertions(+) diff --git a/client.go b/client.go index deccb4ab74..bee429b8ca 100644 --- a/client.go +++ b/client.go @@ -108,6 +108,19 @@ func NewClient(host string, version string, client *http.Client, httpHeaders map }, nil } +// Close ensures that transport.Client is closed +// especially needed while using NewClient with *http.Client = nil +// for example +// client.NewClient("unix:///var/run/docker.sock", nil, "v1.18", map[string]string{"User-Agent": "engine-api-cli-1.0"}) +func (cli *Client) Close() error { + + if t, ok := cli.client.Transport.(*http.Transport); ok { + t.CloseIdleConnections() + } + + return nil +} + // getAPIPath returns the versioned request path to call the api. // It appends the query parameters to the path if they are not empty. func (cli *Client) getAPIPath(p string, query url.Values) string { diff --git a/client_test.go b/client_test.go index 60e44dc299..222f23d45e 100644 --- a/client_test.go +++ b/client_test.go @@ -133,6 +133,11 @@ func TestGetAPIPath(t *testing.T) { if g != cs.e { t.Fatalf("Expected %s, got %s", cs.e, g) } + + err = c.Close() + if nil != err { + t.Fatalf("close client failed, error message: %s", err) + } } } From e7678f3a37e49533366e4a669f102df70ea1116c Mon Sep 17 00:00:00 2001 From: Stephen J Day Date: Wed, 21 Sep 2016 19:16:44 -0700 Subject: [PATCH 032/138] client: pedantic checking of tlsconfig Under the convoluted code path for the transport configuration, TLSConfig was being set even though the socket type is unix. This caused other code detecting the TLSConfig to assume https, rather than using the http scheme. This led to a situation where if `DOCKER_CERT_PATH` is set, unix sockets start reverting to https. There is other odd behavior from go-connections that is also reproduced here. For the most part, we try to reproduce the side-effecting behavior from go-connections to retain the current docker behavior. This whole mess needs to ripped out and fixed, as this pile spaghetti is unnacceptable. This code is way to convoluted for an http client. We'll need to fix this but the Go API will break to do it. Signed-off-by: Stephen J Day --- client.go | 15 ++++++++------- client_test.go | 31 ++++++++++++++++++++++++++++++- hijack.go | 7 +------ request.go | 8 ++------ transport.go | 20 +++++++------------- 5 files changed, 48 insertions(+), 33 deletions(-) diff --git a/client.go b/client.go index deccb4ab74..ff9efa5700 100644 --- a/client.go +++ b/client.go @@ -86,15 +86,16 @@ func NewClient(host string, version string, client *http.Client, httpHeaders map return nil, err } - if client == nil { - client = &http.Client{} - } - - if client.Transport == nil { - // setup the transport, if not already present + if client != nil { + if _, ok := client.Transport.(*http.Transport); !ok { + return nil, fmt.Errorf("unable to verify TLS configuration, invalid transport %v", client.Transport) + } + } else { transport := new(http.Transport) sockets.ConfigureTransport(transport, proto, addr) - client.Transport = transport + client = &http.Client{ + Transport: transport, + } } return &Client{ diff --git a/client_test.go b/client_test.go index 60e44dc299..eaac339658 100644 --- a/client_test.go +++ b/client_test.go @@ -40,6 +40,20 @@ func TestNewEnvClient(t *testing.T) { }, expectedVersion: DefaultVersion, }, + { + envs: map[string]string{ + "DOCKER_CERT_PATH": "testdata/", + "DOCKER_TLS_VERIFY": "1", + }, + expectedVersion: DefaultVersion, + }, + { + envs: map[string]string{ + "DOCKER_CERT_PATH": "testdata/", + "DOCKER_HOST": "https://notaunixsocket", + }, + expectedVersion: DefaultVersion, + }, { envs: map[string]string{ "DOCKER_HOST": "host", @@ -69,7 +83,9 @@ func TestNewEnvClient(t *testing.T) { recoverEnvs := setupEnvs(t, c.envs) apiclient, err := NewEnvClient() if c.expectedError != "" { - if err == nil || err.Error() != c.expectedError { + if err == nil { + t.Errorf("expected an error for %v", c) + } else if err.Error() != c.expectedError { t.Errorf("expected an error %s, got %s, for %v", c.expectedError, err.Error(), c) } } else { @@ -81,6 +97,19 @@ func TestNewEnvClient(t *testing.T) { t.Errorf("expected %s, got %s, for %v", c.expectedVersion, version, c) } } + + if c.envs["DOCKER_TLS_VERIFY"] != "" { + // pedantic checking that this is handled correctly + tr := apiclient.client.Transport.(*http.Transport) + if tr.TLSClientConfig == nil { + t.Errorf("no tls config found when DOCKER_TLS_VERIFY enabled") + } + + if tr.TLSClientConfig.InsecureSkipVerify { + t.Errorf("tls verification should be enabled") + } + } + recoverEnvs(t) } } diff --git a/hijack.go b/hijack.go index f3461ecf78..dededb7af2 100644 --- a/hijack.go +++ b/hijack.go @@ -47,12 +47,7 @@ func (cli *Client) postHijacked(ctx context.Context, path string, query url.Valu req.Header.Set("Connection", "Upgrade") req.Header.Set("Upgrade", "tcp") - tlsConfig, err := resolveTLSConfig(cli.client.Transport) - if err != nil { - return types.HijackedResponse{}, err - } - - conn, err := dial(cli.proto, cli.addr, tlsConfig) + conn, err := dial(cli.proto, cli.addr, resolveTLSConfig(cli.client.Transport)) if err != nil { if strings.Contains(err.Error(), "connection refused") { return types.HijackedResponse{}, fmt.Errorf("Cannot connect to the Docker daemon. Is 'docker daemon' running on this host?") diff --git a/request.go b/request.go index f5c239bf25..07a12657a4 100644 --- a/request.go +++ b/request.go @@ -99,10 +99,7 @@ func (cli *Client) sendClientRequest(ctx context.Context, method, path string, q req.Host = "docker" } - scheme, err := resolveScheme(cli.client.Transport) - if err != nil { - return serverResp, err - } + scheme := resolveScheme(cli.client.Transport) req.URL.Host = cli.addr req.URL.Scheme = scheme @@ -113,8 +110,7 @@ func (cli *Client) sendClientRequest(ctx context.Context, method, path string, q resp, err := ctxhttp.Do(ctx, cli.client, req) if err != nil { - - if scheme == "https" && strings.Contains(err.Error(), "malformed HTTP response") { + if scheme != "https" && strings.Contains(err.Error(), "malformed HTTP response") { return serverResp, fmt.Errorf("%v.\n* Are you trying to connect to a TLS-enabled daemon without TLS?", err) } diff --git a/transport.go b/transport.go index 43a667272d..771d76f06b 100644 --- a/transport.go +++ b/transport.go @@ -18,14 +18,12 @@ func (tf transportFunc) RoundTrip(req *http.Request) (*http.Response, error) { // resolveTLSConfig attempts to resolve the tls configuration from the // RoundTripper. -func resolveTLSConfig(transport http.RoundTripper) (*tls.Config, error) { +func resolveTLSConfig(transport http.RoundTripper) *tls.Config { switch tr := transport.(type) { case *http.Transport: - return tr.TLSClientConfig, nil - case transportFunc: - return nil, nil // detect this type for testing. + return tr.TLSClientConfig default: - return nil, errTLSConfigUnavailable + return nil } } @@ -37,15 +35,11 @@ func resolveTLSConfig(transport http.RoundTripper) (*tls.Config, error) { // Unfortunately, the model of having a host-ish/url-thingy as the connection // string has us confusing protocol and transport layers. We continue doing // this to avoid breaking existing clients but this should be addressed. -func resolveScheme(transport http.RoundTripper) (string, error) { - c, err := resolveTLSConfig(transport) - if err != nil { - return "", err - } - +func resolveScheme(transport http.RoundTripper) string { + c := resolveTLSConfig(transport) if c != nil { - return "https", nil + return "https" } - return "http", nil + return "http" } From da8eef56ce3eb5380d379046bacc7304171b2fe7 Mon Sep 17 00:00:00 2001 From: Kenfe-Mickael Laventure Date: Thu, 22 Sep 2016 14:04:34 -0700 Subject: [PATCH 033/138] Add subcommand prune to the container, volume, image and system commands Signed-off-by: Kenfe-Mickael Laventure --- container_prune.go | 26 ++++++++++++++++++++++++++ image_prune.go | 26 ++++++++++++++++++++++++++ interface.go | 4 ++++ volume_prune.go | 26 ++++++++++++++++++++++++++ 4 files changed, 82 insertions(+) create mode 100644 container_prune.go create mode 100644 image_prune.go create mode 100644 volume_prune.go diff --git a/container_prune.go b/container_prune.go new file mode 100644 index 0000000000..0d8bd3292c --- /dev/null +++ b/container_prune.go @@ -0,0 +1,26 @@ +package client + +import ( + "encoding/json" + "fmt" + + "github.com/docker/docker/api/types" + "golang.org/x/net/context" +) + +// ContainersPrune requests the daemon to delete unused data +func (cli *Client) ContainersPrune(ctx context.Context, cfg types.ContainersPruneConfig) (types.ContainersPruneReport, error) { + var report types.ContainersPruneReport + + serverResp, err := cli.post(ctx, "/containers/prune", nil, cfg, nil) + if err != nil { + return report, err + } + defer ensureReaderClosed(serverResp) + + if err := json.NewDecoder(serverResp.body).Decode(&report); err != nil { + return report, fmt.Errorf("Error retrieving disk usage: %v", err) + } + + return report, nil +} diff --git a/image_prune.go b/image_prune.go new file mode 100644 index 0000000000..f6752e5043 --- /dev/null +++ b/image_prune.go @@ -0,0 +1,26 @@ +package client + +import ( + "encoding/json" + "fmt" + + "github.com/docker/docker/api/types" + "golang.org/x/net/context" +) + +// ImagesPrune requests the daemon to delete unused data +func (cli *Client) ImagesPrune(ctx context.Context, cfg types.ImagesPruneConfig) (types.ImagesPruneReport, error) { + var report types.ImagesPruneReport + + serverResp, err := cli.post(ctx, "/images/prune", nil, cfg, nil) + if err != nil { + return report, err + } + defer ensureReaderClosed(serverResp) + + if err := json.NewDecoder(serverResp.body).Decode(&report); err != nil { + return report, fmt.Errorf("Error retrieving disk usage: %v", err) + } + + return report, nil +} diff --git a/interface.go b/interface.go index 81320918b3..de06b848ae 100644 --- a/interface.go +++ b/interface.go @@ -61,6 +61,7 @@ type ContainerAPIClient interface { ContainerWait(ctx context.Context, container string) (int, error) CopyFromContainer(ctx context.Context, container, srcPath string) (io.ReadCloser, types.ContainerPathStat, error) CopyToContainer(ctx context.Context, container, path string, content io.Reader, options types.CopyToContainerOptions) error + ContainersPrune(ctx context.Context, cfg types.ContainersPruneConfig) (types.ContainersPruneReport, error) } // ImageAPIClient defines API client methods for the images @@ -78,6 +79,7 @@ type ImageAPIClient interface { ImageSearch(ctx context.Context, term string, options types.ImageSearchOptions) ([]registry.SearchResult, error) ImageSave(ctx context.Context, images []string) (io.ReadCloser, error) ImageTag(ctx context.Context, image, ref string) error + ImagesPrune(ctx context.Context, cfg types.ImagesPruneConfig) (types.ImagesPruneReport, error) } // NetworkAPIClient defines API client methods for the networks @@ -124,6 +126,7 @@ type SystemAPIClient interface { Events(ctx context.Context, options types.EventsOptions) (<-chan events.Message, <-chan error) Info(ctx context.Context) (types.Info, error) RegistryLogin(ctx context.Context, auth types.AuthConfig) (types.AuthResponse, error) + DiskUsage(ctx context.Context) (types.DiskUsage, error) } // VolumeAPIClient defines API client methods for the volumes @@ -133,4 +136,5 @@ type VolumeAPIClient interface { VolumeInspectWithRaw(ctx context.Context, volumeID string) (types.Volume, []byte, error) VolumeList(ctx context.Context, filter filters.Args) (types.VolumesListResponse, error) VolumeRemove(ctx context.Context, volumeID string, force bool) error + VolumesPrune(ctx context.Context, cfg types.VolumesPruneConfig) (types.VolumesPruneReport, error) } diff --git a/volume_prune.go b/volume_prune.go new file mode 100644 index 0000000000..e7ea7b591d --- /dev/null +++ b/volume_prune.go @@ -0,0 +1,26 @@ +package client + +import ( + "encoding/json" + "fmt" + + "github.com/docker/docker/api/types" + "golang.org/x/net/context" +) + +// VolumesPrune requests the daemon to delete unused data +func (cli *Client) VolumesPrune(ctx context.Context, cfg types.VolumesPruneConfig) (types.VolumesPruneReport, error) { + var report types.VolumesPruneReport + + serverResp, err := cli.post(ctx, "/volumes/prune", nil, cfg, nil) + if err != nil { + return report, err + } + defer ensureReaderClosed(serverResp) + + if err := json.NewDecoder(serverResp.body).Decode(&report); err != nil { + return report, fmt.Errorf("Error retrieving disk usage: %v", err) + } + + return report, nil +} From d7efdb095ed8c738f2c734cbd36102f97ec68d6f Mon Sep 17 00:00:00 2001 From: Kenfe-Mickael Laventure Date: Thu, 8 Sep 2016 13:45:05 -0700 Subject: [PATCH 034/138] Add DiskUsage method to SystemApiclient Signed-off-by: Kenfe-Mickael Laventure --- disk_usage.go | 26 ++++++++++++++++++++++++++ 1 file changed, 26 insertions(+) create mode 100644 disk_usage.go diff --git a/disk_usage.go b/disk_usage.go new file mode 100644 index 0000000000..03c80b39af --- /dev/null +++ b/disk_usage.go @@ -0,0 +1,26 @@ +package client + +import ( + "encoding/json" + "fmt" + + "github.com/docker/docker/api/types" + "golang.org/x/net/context" +) + +// DiskUsage requests the current data usage from the daemon +func (cli *Client) DiskUsage(ctx context.Context) (types.DiskUsage, error) { + var du types.DiskUsage + + serverResp, err := cli.get(ctx, "/system/df", nil, nil) + if err != nil { + return du, err + } + defer ensureReaderClosed(serverResp) + + if err := json.NewDecoder(serverResp.body).Decode(&du); err != nil { + return du, fmt.Errorf("Error retrieving disk usage: %v", err) + } + + return du, nil +} From a318ab842a3dfa7988ada3e81893c92340308bd6 Mon Sep 17 00:00:00 2001 From: Sean Rodman Date: Wed, 21 Sep 2016 16:04:44 -0500 Subject: [PATCH 035/138] Updated the client/request.go sendClientRequest method to return a PermissionDenied error if the connection failed due to permissions. Signed-off-by: Sean Rodman Updated the check for the permission error to use os.IsPermission instead of checking the error string. Also, changed the PermissionDenied method to just a new error. Fixed a typo in client/request.go Fixed Error name as specified by Pull request builder output. Worked on making changes to the permissiondenied error. Fixed typo Signed-off-by: Sean Rodman Updated error message as requested. Fixed the error as requested Signed-off-by: Sean Rodman --- request.go | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/request.go b/request.go index f5c239bf25..0749ae3254 100644 --- a/request.go +++ b/request.go @@ -9,6 +9,7 @@ import ( "net" "net/http" "net/url" + "os" "strings" "github.com/docker/docker/api/types" @@ -129,6 +130,14 @@ func (cli *Client) sendClientRequest(ctx context.Context, method, path string, q return serverResp, err } + if nErr, ok := err.(*url.Error); ok { + if nErr, ok := nErr.Err.(*net.OpError); ok { + if os.IsPermission(nErr.Err) { + return serverResp, errors.Wrapf(err, "Got permission denied while trying to connect to the Docker daemon socket at %v", cli.host) + } + } + } + if err, ok := err.(net.Error); ok { if err.Timeout() { return serverResp, ErrorConnectionFailed(cli.host) From 6bc667128a8565ed33e19170a2802e218b2d13c0 Mon Sep 17 00:00:00 2001 From: John Howard Date: Tue, 7 Jun 2016 12:15:50 -0700 Subject: [PATCH 036/138] Windows: Support credential specs Signed-off-by: John Howard --- image_build.go | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/image_build.go b/image_build.go index 0094602a6e..3abd87025e 100644 --- a/image_build.go +++ b/image_build.go @@ -49,7 +49,8 @@ func (cli *Client) ImageBuild(ctx context.Context, buildContext io.Reader, optio func imageBuildOptionsToQuery(options types.ImageBuildOptions) (url.Values, error) { query := url.Values{ - "t": options.Tags, + "t": options.Tags, + "securityopt": options.SecurityOpt, } if options.SuppressOutput { query.Set("q", "1") From afb60b86d7f2b32ac22f0fc14ca08e3a97fea74e Mon Sep 17 00:00:00 2001 From: Deng Guangxing Date: Sat, 8 Oct 2016 15:29:32 +0800 Subject: [PATCH 037/138] fix typo in client/errors.go comments Signed-off-by: Deng Guangxing --- errors.go | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/errors.go b/errors.go index 71e25a7ae1..ad1dadabb6 100644 --- a/errors.go +++ b/errors.go @@ -30,7 +30,7 @@ type imageNotFoundError struct { imageID string } -// NoFound indicates that this error type is of NotFound +// NotFound indicates that this error type is of NotFound func (e imageNotFoundError) NotFound() bool { return true } @@ -51,7 +51,7 @@ type containerNotFoundError struct { containerID string } -// NoFound indicates that this error type is of NotFound +// NotFound indicates that this error type is of NotFound func (e containerNotFoundError) NotFound() bool { return true } @@ -72,7 +72,7 @@ type networkNotFoundError struct { networkID string } -// NoFound indicates that this error type is of NotFound +// NotFound indicates that this error type is of NotFound func (e networkNotFoundError) NotFound() bool { return true } @@ -93,12 +93,12 @@ type volumeNotFoundError struct { volumeID string } -// NoFound indicates that this error type is of NotFound +// NotFound indicates that this error type is of NotFound func (e volumeNotFoundError) NotFound() bool { return true } -// Error returns a string representation of a networkNotFoundError +// Error returns a string representation of a volumeNotFoundError func (e volumeNotFoundError) Error() string { return fmt.Sprintf("Error: No such volume: %s", e.volumeID) } @@ -136,7 +136,7 @@ func (e nodeNotFoundError) Error() string { return fmt.Sprintf("Error: No such node: %s", e.nodeID) } -// NoFound indicates that this error type is of NotFound +// NotFound indicates that this error type is of NotFound func (e nodeNotFoundError) NotFound() bool { return true } @@ -158,7 +158,7 @@ func (e serviceNotFoundError) Error() string { return fmt.Sprintf("Error: No such service: %s", e.serviceID) } -// NoFound indicates that this error type is of NotFound +// NotFound indicates that this error type is of NotFound func (e serviceNotFoundError) NotFound() bool { return true } @@ -180,7 +180,7 @@ func (e taskNotFoundError) Error() string { return fmt.Sprintf("Error: No such task: %s", e.taskID) } -// NoFound indicates that this error type is of NotFound +// NotFound indicates that this error type is of NotFound func (e taskNotFoundError) NotFound() bool { return true } From a41ec7d802267adedcdd1cdca49c6911d3739c4f Mon Sep 17 00:00:00 2001 From: Ben Firshman Date: Sat, 8 Oct 2016 14:34:37 +0100 Subject: [PATCH 038/138] Add GoDoc for client package - Tightened up copy in README - Make example in README a bit simpler - Update README to point at GoDoc Signed-off-by: Ben Firshman --- README.md | 24 +++++++++++------------- client.go | 45 +++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 56 insertions(+), 13 deletions(-) diff --git a/README.md b/README.md index 34cf7372db..2b7d81fada 100644 --- a/README.md +++ b/README.md @@ -1,37 +1,35 @@ -## Client +## Go client for the Docker Remote API -The client package implements a fully featured http client to interact with the Docker engine. It's modeled after the requirements of the Docker engine CLI, but it can also serve other purposes. +The `docker` command uses this package to communicate with the daemon. It can also be used by your own Go applications to do anything the command-line interface does – running containers, pulling images, managing swarms, etc. -### Usage - -You can use this client package in your applications by creating a new client object. Then use that object to execute operations against the remote server. Follow the example below to see how to list all the containers running in a Docker engine host: +For example, to list running containers (the equivalent of `docker ps`): ```go package main import ( - "fmt" "context" + "fmt" - "github.com/docker/docker/client" "github.com/docker/docker/api/types" + "github.com/docker/docker/client" ) func main() { - defaultHeaders := map[string]string{"User-Agent": "engine-api-cli-1.0"} - cli, err := client.NewClient("unix:///var/run/docker.sock", "v1.22", nil, defaultHeaders) + cli, err := client.NewEnvClient() if err != nil { panic(err) } - options := types.ContainerListOptions{All: true} - containers, err := cli.ContainerList(context.Background(), options) + containers, err := cli.ContainerList(context.Background(), types.ContainerListOptions{}) if err != nil { panic(err) } - for _, c := range containers { - fmt.Println(c.ID) + for _, container := range containers { + fmt.Printf("%s %s\n", container.ID[:10], container.Image) } } ``` + +[Full documentation is available on GoDoc.](https://godoc.org/github.com/docker/docker/client) diff --git a/client.go b/client.go index deccb4ab74..58e8430cf2 100644 --- a/client.go +++ b/client.go @@ -1,3 +1,48 @@ +/* +Package client is a Go client for the Docker Remote API. + +The "docker" command uses this package to communicate with the daemon. It can also +be used by your own Go applications to do anything the command-line interface does +– running containers, pulling images, managing swarms, etc. + +For more information about the Remote API, see the documentation: +https://docs.docker.com/engine/reference/api/docker_remote_api/ + +Usage + +You use the library by creating a client object and calling methods on it. The +client can be created either from environment variables with NewEnvClient, or +configured manually with NewClient. + +For example, to list running containers (the equivalent of "docker ps"): + + package main + + import ( + "context" + "fmt" + + "github.com/docker/docker/api/types" + "github.com/docker/docker/client" + ) + + func main() { + cli, err := client.NewEnvClient() + if err != nil { + panic(err) + } + + containers, err := cli.ContainerList(context.Background(), types.ContainerListOptions{}) + if err != nil { + panic(err) + } + + for _, container := range containers { + fmt.Printf("%s %s\n", container.ID[:10], container.Image) + } + } + +*/ package client import ( From 4d1a6a43cd5a67ef2f8ec79f827d8619a7cc79ad Mon Sep 17 00:00:00 2001 From: Stephen J Day Date: Tue, 11 Oct 2016 15:53:14 -0700 Subject: [PATCH 039/138] client: deterministically resolve http scheme The docker client has historically used Transport.TLSClientConfig to set the scheme for the API client. A recent moved the resolution to use the http.Transport directly, rather than save the TLSClientConfig state on a client struct. This caused issues when mutliple calls made with a single client would have this field set in the http package on pre-1.7 installations. This fix detects the presence of the TLSClientConfig once and sets the scheme accordingly. We still don't know why this issue doesn't happen with Go 1.7 but it must be more deterministic in the newer version. Signed-off-by: Stephen J Day --- client.go | 14 ++++++++++++++ request.go | 8 +++----- transport.go | 17 ----------------- 3 files changed, 17 insertions(+), 22 deletions(-) diff --git a/client.go b/client.go index 025eaaf9a9..75073881c7 100644 --- a/client.go +++ b/client.go @@ -63,6 +63,8 @@ const DefaultVersion string = "1.23" // Client is the API client that performs all operations // against a docker server. type Client struct { + // scheme sets the scheme for the client + scheme string // host holds the server address to connect to host string // proto holds the client protocol i.e. unix. @@ -143,7 +145,19 @@ func NewClient(host string, version string, client *http.Client, httpHeaders map } } + scheme := "http" + tlsConfig := resolveTLSConfig(client.Transport) + if tlsConfig != nil { + // TODO(stevvooe): This isn't really the right way to write clients in Go. + // `NewClient` should probably only take an `*http.Client` and work from there. + // Unfortunately, the model of having a host-ish/url-thingy as the connection + // string has us confusing protocol and transport layers. We continue doing + // this to avoid breaking existing clients but this should be addressed. + scheme = "https" + } + return &Client{ + scheme: scheme, host: host, proto: proto, addr: addr, diff --git a/request.go b/request.go index d585b46ab1..bfd62bad1e 100644 --- a/request.go +++ b/request.go @@ -100,10 +100,8 @@ func (cli *Client) sendClientRequest(ctx context.Context, method, path string, q req.Host = "docker" } - scheme := resolveScheme(cli.client.Transport) - req.URL.Host = cli.addr - req.URL.Scheme = scheme + req.URL.Scheme = cli.scheme if expectedPayload && req.Header.Get("Content-Type") == "" { req.Header.Set("Content-Type", "text/plain") @@ -111,11 +109,11 @@ func (cli *Client) sendClientRequest(ctx context.Context, method, path string, q resp, err := ctxhttp.Do(ctx, cli.client, req) if err != nil { - if scheme != "https" && strings.Contains(err.Error(), "malformed HTTP response") { + if cli.scheme != "https" && strings.Contains(err.Error(), "malformed HTTP response") { return serverResp, fmt.Errorf("%v.\n* Are you trying to connect to a TLS-enabled daemon without TLS?", err) } - if scheme == "https" && strings.Contains(err.Error(), "bad certificate") { + if cli.scheme == "https" && strings.Contains(err.Error(), "bad certificate") { return serverResp, fmt.Errorf("The server probably has client authentication (--tlsverify) enabled. Please check your TLS client certification settings: %v", err) } diff --git a/transport.go b/transport.go index 771d76f06b..f04e601649 100644 --- a/transport.go +++ b/transport.go @@ -26,20 +26,3 @@ func resolveTLSConfig(transport http.RoundTripper) *tls.Config { return nil } } - -// resolveScheme detects a tls config on the transport and returns the -// appropriate http scheme. -// -// TODO(stevvooe): This isn't really the right way to write clients in Go. -// `NewClient` should probably only take an `*http.Client` and work from there. -// Unfortunately, the model of having a host-ish/url-thingy as the connection -// string has us confusing protocol and transport layers. We continue doing -// this to avoid breaking existing clients but this should be addressed. -func resolveScheme(transport http.RoundTripper) string { - c := resolveTLSConfig(transport) - if c != nil { - return "https" - } - - return "http" -} From 86322803158ed5db5a5148af1d228be668ca96f9 Mon Sep 17 00:00:00 2001 From: John Howard Date: Fri, 14 Oct 2016 10:14:43 -0700 Subject: [PATCH 040/138] Windows: Hint to run client elevated Signed-off-by: John Howard --- request.go | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/request.go b/request.go index bfd62bad1e..91a05824e1 100644 --- a/request.go +++ b/request.go @@ -143,6 +143,20 @@ func (cli *Client) sendClientRequest(ctx context.Context, method, path string, q } } + // Although there's not a strongly typed error for this in go-winio, + // lots of people are using the default configuration for the docker + // daemon on Windows where the daemon is listening on a named pipe + // `//./pipe/docker_engine, and the client must be running elevated. + // Give users a clue rather than the not-overly useful message + // such as `error during connect: Get http://%2F%2F.%2Fpipe%2Fdocker_engine/v1.25/info: + // open //./pipe/docker_engine: The system cannot find the file specified.`. + // Note we can't string compare "The system cannot find the file specified" as + // this is localised - for example in French the error would be + // `open //./pipe/docker_engine: Le fichier spécifié est introuvable.` + if strings.Contains(err.Error(), `open //./pipe/docker_engine`) { + err = errors.New(err.Error() + " In the default daemon configuration on Windows, the docker client must be run elevated to connect. This error may also indicate that the docker daemon is not running.") + } + return serverResp, errors.Wrap(err, "error during connect") } From 671fe5c051ebfdb7217dedc049c4acc7e8e6497e Mon Sep 17 00:00:00 2001 From: Aaron Lehmann Date: Wed, 7 Sep 2016 16:32:44 -0700 Subject: [PATCH 041/138] API changes for service rollback and failure threshold Signed-off-by: Aaron Lehmann --- service_update.go | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/service_update.go b/service_update.go index c5d07e8394..8e03f7f483 100644 --- a/service_update.go +++ b/service_update.go @@ -22,6 +22,10 @@ func (cli *Client) ServiceUpdate(ctx context.Context, serviceID string, version } } + if options.RegistryAuthFrom != "" { + query.Set("registryAuthFrom", options.RegistryAuthFrom) + } + query.Set("version", strconv.FormatUint(version.Index, 10)) resp, err := cli.post(ctx, "/services/"+serviceID+"/update", query, service, headers) From 27bab36800713d248c1f7cb7f3666926426eabf2 Mon Sep 17 00:00:00 2001 From: Andy Goldstein Date: Mon, 19 Sep 2016 14:55:52 -0400 Subject: [PATCH 042/138] Add Logs to ContainerAttachOptions Signed-off-by: Andy Goldstein --- container_attach.go | 3 +++ 1 file changed, 3 insertions(+) diff --git a/container_attach.go b/container_attach.go index 7cfc860fcc..eea4682158 100644 --- a/container_attach.go +++ b/container_attach.go @@ -28,6 +28,9 @@ func (cli *Client) ContainerAttach(ctx context.Context, container string, option if options.DetachKeys != "" { query.Set("detachKeys", options.DetachKeys) } + if options.Logs { + query.Set("logs", "1") + } headers := map[string][]string{"Content-Type": {"text/plain"}} return cli.postHijacked(ctx, "/containers/"+container+"/attach", query, nil, headers) From a6a247fdf9b650c663f9ca58da28b6fd4f5dfc4d Mon Sep 17 00:00:00 2001 From: Daniel Nephin Date: Mon, 3 Oct 2016 15:17:39 -0400 Subject: [PATCH 043/138] Generate api/types:Image from the swagger spec and rename it to a more appropriate name ImageSummary. Signed-off-by: Daniel Nephin --- image_list.go | 4 ++-- image_list_test.go | 2 +- interface.go | 2 +- 3 files changed, 4 insertions(+), 4 deletions(-) diff --git a/image_list.go b/image_list.go index 00f27dc0c9..6ebb460541 100644 --- a/image_list.go +++ b/image_list.go @@ -10,8 +10,8 @@ import ( ) // ImageList returns a list of images in the docker host. -func (cli *Client) ImageList(ctx context.Context, options types.ImageListOptions) ([]types.Image, error) { - var images []types.Image +func (cli *Client) ImageList(ctx context.Context, options types.ImageListOptions) ([]types.ImageSummary, error) { + var images []types.ImageSummary query := url.Values{} if options.Filters.Len() > 0 { diff --git a/image_list_test.go b/image_list_test.go index 2a52279081..1ea6f1f05a 100644 --- a/image_list_test.go +++ b/image_list_test.go @@ -93,7 +93,7 @@ func TestImageList(t *testing.T) { return nil, fmt.Errorf("%s not set in URL query properly. Expected '%s', got %s", key, expected, actual) } } - content, err := json.Marshal([]types.Image{ + content, err := json.Marshal([]types.ImageSummary{ { ID: "image_id2", }, diff --git a/interface.go b/interface.go index de06b848ae..4d450d8316 100644 --- a/interface.go +++ b/interface.go @@ -71,7 +71,7 @@ type ImageAPIClient interface { ImageHistory(ctx context.Context, image string) ([]types.ImageHistory, error) ImageImport(ctx context.Context, source types.ImageImportSource, ref string, options types.ImageImportOptions) (io.ReadCloser, error) ImageInspectWithRaw(ctx context.Context, image string) (types.ImageInspect, []byte, error) - ImageList(ctx context.Context, options types.ImageListOptions) ([]types.Image, error) + ImageList(ctx context.Context, options types.ImageListOptions) ([]types.ImageSummary, error) ImageLoad(ctx context.Context, input io.Reader, quiet bool) (types.ImageLoadResponse, error) ImagePull(ctx context.Context, ref string, options types.ImagePullOptions) (io.ReadCloser, error) ImagePush(ctx context.Context, ref string, options types.ImagePushOptions) (io.ReadCloser, error) From 31f5d9b5437ee8dcede311cdc1509cae94c5820b Mon Sep 17 00:00:00 2001 From: Kenfe-Mickael Laventure Date: Thu, 6 Oct 2016 07:09:54 -0700 Subject: [PATCH 044/138] Make experimental a runtime flag Signed-off-by: Kenfe-Mickael Laventure --- interface.go | 1 + interface_experimental.go | 9 +-------- interface_stable.go | 3 +-- ping.go | 19 +++++++++++++++++++ plugin_disable.go | 2 -- plugin_disable_test.go | 2 -- plugin_enable.go | 2 -- plugin_enable_test.go | 2 -- plugin_inspect.go | 2 -- plugin_inspect_test.go | 2 -- plugin_install.go | 2 -- plugin_list.go | 2 -- plugin_list_test.go | 2 -- plugin_push.go | 2 -- plugin_push_test.go | 2 -- plugin_remove.go | 2 -- plugin_remove_test.go | 2 -- plugin_set.go | 2 -- plugin_set_test.go | 2 -- 19 files changed, 22 insertions(+), 40 deletions(-) create mode 100644 ping.go diff --git a/interface.go b/interface.go index 4d450d8316..f919612163 100644 --- a/interface.go +++ b/interface.go @@ -127,6 +127,7 @@ type SystemAPIClient interface { Info(ctx context.Context) (types.Info, error) RegistryLogin(ctx context.Context, auth types.AuthConfig) (types.AuthResponse, error) DiskUsage(ctx context.Context) (types.DiskUsage, error) + Ping(ctx context.Context) (bool, error) } // VolumeAPIClient defines API client methods for the volumes diff --git a/interface_experimental.go b/interface_experimental.go index 1ddc517c9a..ddb9f79b5a 100644 --- a/interface_experimental.go +++ b/interface_experimental.go @@ -1,5 +1,3 @@ -// +build experimental - package client import ( @@ -7,9 +5,7 @@ import ( "golang.org/x/net/context" ) -// APIClient is an interface that clients that talk with a docker server must implement. -type APIClient interface { - CommonAPIClient +type apiClientExperimental interface { CheckpointAPIClient PluginAPIClient } @@ -32,6 +28,3 @@ type PluginAPIClient interface { PluginSet(ctx context.Context, name string, args []string) error PluginInspectWithRaw(ctx context.Context, name string) (*types.Plugin, []byte, error) } - -// Ensure that Client always implements APIClient. -var _ APIClient = &Client{} diff --git a/interface_stable.go b/interface_stable.go index 496f522d51..cc90a3cbb9 100644 --- a/interface_stable.go +++ b/interface_stable.go @@ -1,10 +1,9 @@ -// +build !experimental - package client // APIClient is an interface that clients that talk with a docker server must implement. type APIClient interface { CommonAPIClient + apiClientExperimental } // Ensure that Client always implements APIClient. diff --git a/ping.go b/ping.go new file mode 100644 index 0000000000..5e99e1bba1 --- /dev/null +++ b/ping.go @@ -0,0 +1,19 @@ +package client + +import "golang.org/x/net/context" + +// Ping pings the server and return the value of the "Docker-Experimental" header +func (cli *Client) Ping(ctx context.Context) (bool, error) { + serverResp, err := cli.get(ctx, "/_ping", nil, nil) + if err != nil { + return false, err + } + defer ensureReaderClosed(serverResp) + + exp := serverResp.header.Get("Docker-Experimental") + if exp != "true" { + return false, nil + } + + return true, nil +} diff --git a/plugin_disable.go b/plugin_disable.go index 893fc6e823..51e4565125 100644 --- a/plugin_disable.go +++ b/plugin_disable.go @@ -1,5 +1,3 @@ -// +build experimental - package client import ( diff --git a/plugin_disable_test.go b/plugin_disable_test.go index 7b50b25730..2818008ab9 100644 --- a/plugin_disable_test.go +++ b/plugin_disable_test.go @@ -1,5 +1,3 @@ -// +build experimental - package client import ( diff --git a/plugin_enable.go b/plugin_enable.go index 84422abc79..8109814ddb 100644 --- a/plugin_enable.go +++ b/plugin_enable.go @@ -1,5 +1,3 @@ -// +build experimental - package client import ( diff --git a/plugin_enable_test.go b/plugin_enable_test.go index a2b57be4c2..d919914e75 100644 --- a/plugin_enable_test.go +++ b/plugin_enable_test.go @@ -1,5 +1,3 @@ -// +build experimental - package client import ( diff --git a/plugin_inspect.go b/plugin_inspect.go index 7ba8db57a8..e9474b5a98 100644 --- a/plugin_inspect.go +++ b/plugin_inspect.go @@ -1,5 +1,3 @@ -// +build experimental - package client import ( diff --git a/plugin_inspect_test.go b/plugin_inspect_test.go index df4ca9c841..fae407eb9b 100644 --- a/plugin_inspect_test.go +++ b/plugin_inspect_test.go @@ -1,5 +1,3 @@ -// +build experimental - package client import ( diff --git a/plugin_install.go b/plugin_install.go index 9ee32eea92..636c95364d 100644 --- a/plugin_install.go +++ b/plugin_install.go @@ -1,5 +1,3 @@ -// +build experimental - package client import ( diff --git a/plugin_list.go b/plugin_list.go index 48b470247b..88c480a3e1 100644 --- a/plugin_list.go +++ b/plugin_list.go @@ -1,5 +1,3 @@ -// +build experimental - package client import ( diff --git a/plugin_list_test.go b/plugin_list_test.go index 95c51595ca..173e4b87f5 100644 --- a/plugin_list_test.go +++ b/plugin_list_test.go @@ -1,5 +1,3 @@ -// +build experimental - package client import ( diff --git a/plugin_push.go b/plugin_push.go index 3afea5ed79..d83bbdc358 100644 --- a/plugin_push.go +++ b/plugin_push.go @@ -1,5 +1,3 @@ -// +build experimental - package client import ( diff --git a/plugin_push_test.go b/plugin_push_test.go index ed685694ec..efdbdc6db1 100644 --- a/plugin_push_test.go +++ b/plugin_push_test.go @@ -1,5 +1,3 @@ -// +build experimental - package client import ( diff --git a/plugin_remove.go b/plugin_remove.go index 1483f2854d..b017e4d348 100644 --- a/plugin_remove.go +++ b/plugin_remove.go @@ -1,5 +1,3 @@ -// +build experimental - package client import ( diff --git a/plugin_remove_test.go b/plugin_remove_test.go index fc789fd04d..a15f1661f6 100644 --- a/plugin_remove_test.go +++ b/plugin_remove_test.go @@ -1,5 +1,3 @@ -// +build experimental - package client import ( diff --git a/plugin_set.go b/plugin_set.go index fb40f38b22..3260d2a90d 100644 --- a/plugin_set.go +++ b/plugin_set.go @@ -1,5 +1,3 @@ -// +build experimental - package client import ( diff --git a/plugin_set_test.go b/plugin_set_test.go index fa1cde044e..2450254463 100644 --- a/plugin_set_test.go +++ b/plugin_set_test.go @@ -1,5 +1,3 @@ -// +build experimental - package client import ( From 171e533ba278de015984e88fa05effd11a5070b1 Mon Sep 17 00:00:00 2001 From: Akihiro Suda Date: Tue, 18 Oct 2016 04:36:52 +0000 Subject: [PATCH 045/138] add `docker network prune` `docker network prune` prunes unused networks, including overlay ones. `docker system prune` also prunes unused networks. Signed-off-by: Akihiro Suda --- interface.go | 1 + network_prune.go | 26 ++++++++++++++++++++++++++ 2 files changed, 27 insertions(+) create mode 100644 network_prune.go diff --git a/interface.go b/interface.go index f919612163..8abdb0f6fc 100644 --- a/interface.go +++ b/interface.go @@ -91,6 +91,7 @@ type NetworkAPIClient interface { NetworkInspectWithRaw(ctx context.Context, networkID string) (types.NetworkResource, []byte, error) NetworkList(ctx context.Context, options types.NetworkListOptions) ([]types.NetworkResource, error) NetworkRemove(ctx context.Context, networkID string) error + NetworksPrune(ctx context.Context, cfg types.NetworksPruneConfig) (types.NetworksPruneReport, error) } // NodeAPIClient defines API client methods for the nodes diff --git a/network_prune.go b/network_prune.go new file mode 100644 index 0000000000..01185f2e02 --- /dev/null +++ b/network_prune.go @@ -0,0 +1,26 @@ +package client + +import ( + "encoding/json" + "fmt" + + "github.com/docker/docker/api/types" + "golang.org/x/net/context" +) + +// NetworksPrune requests the daemon to delete unused networks +func (cli *Client) NetworksPrune(ctx context.Context, cfg types.NetworksPruneConfig) (types.NetworksPruneReport, error) { + var report types.NetworksPruneReport + + serverResp, err := cli.post(ctx, "/networks/prune", nil, cfg, nil) + if err != nil { + return report, err + } + defer ensureReaderClosed(serverResp) + + if err := json.NewDecoder(serverResp.body).Decode(&report); err != nil { + return report, fmt.Errorf("Error retrieving network prune report: %v", err) + } + + return report, nil +} From 01832cc0ab5741ba4caeeeb5666c302f235fb101 Mon Sep 17 00:00:00 2001 From: sandyskies Date: Sun, 6 Mar 2016 20:29:23 +0800 Subject: [PATCH 046/138] add --network option for docker build Signed-off-by: sandyskies Signed-off-by: Tonis Tiigi --- image_build.go | 1 + 1 file changed, 1 insertion(+) diff --git a/image_build.go b/image_build.go index 3abd87025e..4d611d5430 100644 --- a/image_build.go +++ b/image_build.go @@ -84,6 +84,7 @@ func imageBuildOptionsToQuery(options types.ImageBuildOptions) (url.Values, erro } query.Set("cpusetcpus", options.CPUSetCPUs) + query.Set("networkmode", options.NetworkMode) query.Set("cpusetmems", options.CPUSetMems) query.Set("cpushares", strconv.FormatInt(options.CPUShares, 10)) query.Set("cpuquota", strconv.FormatInt(options.CPUQuota, 10)) From a38761aba448f948231bbb20452332d9b7524bf5 Mon Sep 17 00:00:00 2001 From: boucher Date: Mon, 19 Sep 2016 12:01:16 -0400 Subject: [PATCH 047/138] Allow providing a custom storage directory for docker checkpoints Signed-off-by: boucher --- checkpoint_delete.go | 12 ++++++++++-- checkpoint_delete_test.go | 11 +++++++++-- checkpoint_list.go | 10 ++++++++-- checkpoint_list_test.go | 4 ++-- container_start.go | 3 +++ interface_experimental.go | 4 ++-- 6 files changed, 34 insertions(+), 10 deletions(-) diff --git a/checkpoint_delete.go b/checkpoint_delete.go index a4e9ed0c06..e6e75588b1 100644 --- a/checkpoint_delete.go +++ b/checkpoint_delete.go @@ -1,12 +1,20 @@ package client import ( + "net/url" + + "github.com/docker/docker/api/types" "golang.org/x/net/context" ) // CheckpointDelete deletes the checkpoint with the given name from the given container -func (cli *Client) CheckpointDelete(ctx context.Context, containerID string, checkpointID string) error { - resp, err := cli.delete(ctx, "/containers/"+containerID+"/checkpoints/"+checkpointID, nil, nil) +func (cli *Client) CheckpointDelete(ctx context.Context, containerID string, options types.CheckpointDeleteOptions) error { + query := url.Values{} + if options.CheckpointDir != "" { + query.Set("dir", options.CheckpointDir) + } + + resp, err := cli.delete(ctx, "/containers/"+containerID+"/checkpoints/"+options.CheckpointID, query, nil) ensureReaderClosed(resp) return err } diff --git a/checkpoint_delete_test.go b/checkpoint_delete_test.go index 23931c6523..a78b050487 100644 --- a/checkpoint_delete_test.go +++ b/checkpoint_delete_test.go @@ -8,6 +8,7 @@ import ( "strings" "testing" + "github.com/docker/docker/api/types" "golang.org/x/net/context" ) @@ -16,7 +17,10 @@ func TestCheckpointDeleteError(t *testing.T) { client: newMockClient(errorMock(http.StatusInternalServerError, "Server error")), } - err := client.CheckpointDelete(context.Background(), "container_id", "checkpoint_id") + err := client.CheckpointDelete(context.Background(), "container_id", types.CheckpointDeleteOptions{ + CheckpointID: "checkpoint_id", + }) + if err == nil || err.Error() != "Error response from daemon: Server error" { t.Fatalf("expected a Server Error, got %v", err) } @@ -40,7 +44,10 @@ func TestCheckpointDelete(t *testing.T) { }), } - err := client.CheckpointDelete(context.Background(), "container_id", "checkpoint_id") + err := client.CheckpointDelete(context.Background(), "container_id", types.CheckpointDeleteOptions{ + CheckpointID: "checkpoint_id", + }) + if err != nil { t.Fatal(err) } diff --git a/checkpoint_list.go b/checkpoint_list.go index bb471e0056..8eb720a6b2 100644 --- a/checkpoint_list.go +++ b/checkpoint_list.go @@ -2,16 +2,22 @@ package client import ( "encoding/json" + "net/url" "github.com/docker/docker/api/types" "golang.org/x/net/context" ) // CheckpointList returns the volumes configured in the docker host. -func (cli *Client) CheckpointList(ctx context.Context, container string) ([]types.Checkpoint, error) { +func (cli *Client) CheckpointList(ctx context.Context, container string, options types.CheckpointListOptions) ([]types.Checkpoint, error) { var checkpoints []types.Checkpoint - resp, err := cli.get(ctx, "/containers/"+container+"/checkpoints", nil, nil) + query := url.Values{} + if options.CheckpointDir != "" { + query.Set("dir", options.CheckpointDir) + } + + resp, err := cli.get(ctx, "/containers/"+container+"/checkpoints", query, nil) if err != nil { return checkpoints, err } diff --git a/checkpoint_list_test.go b/checkpoint_list_test.go index e636995bc1..6c90f61e8c 100644 --- a/checkpoint_list_test.go +++ b/checkpoint_list_test.go @@ -18,7 +18,7 @@ func TestCheckpointListError(t *testing.T) { client: newMockClient(errorMock(http.StatusInternalServerError, "Server error")), } - _, err := client.CheckpointList(context.Background(), "container_id") + _, err := client.CheckpointList(context.Background(), "container_id", types.CheckpointListOptions{}) if err == nil || err.Error() != "Error response from daemon: Server error" { t.Fatalf("expected a Server Error, got %v", err) } @@ -47,7 +47,7 @@ func TestCheckpointList(t *testing.T) { }), } - checkpoints, err := client.CheckpointList(context.Background(), "container_id") + checkpoints, err := client.CheckpointList(context.Background(), "container_id", types.CheckpointListOptions{}) if err != nil { t.Fatal(err) } diff --git a/container_start.go b/container_start.go index 44bb0080c0..b1f08de416 100644 --- a/container_start.go +++ b/container_start.go @@ -14,6 +14,9 @@ func (cli *Client) ContainerStart(ctx context.Context, containerID string, optio if len(options.CheckpointID) != 0 { query.Set("checkpoint", options.CheckpointID) } + if len(options.CheckpointDir) != 0 { + query.Set("checkpoint-dir", options.CheckpointDir) + } resp, err := cli.post(ctx, "/containers/"+containerID+"/start", query, nil, nil) ensureReaderClosed(resp) diff --git a/interface_experimental.go b/interface_experimental.go index ddb9f79b5a..4f5cf853b8 100644 --- a/interface_experimental.go +++ b/interface_experimental.go @@ -13,8 +13,8 @@ type apiClientExperimental interface { // CheckpointAPIClient defines API client methods for the checkpoints type CheckpointAPIClient interface { CheckpointCreate(ctx context.Context, container string, options types.CheckpointCreateOptions) error - CheckpointDelete(ctx context.Context, container string, checkpointID string) error - CheckpointList(ctx context.Context, container string) ([]types.Checkpoint, error) + CheckpointDelete(ctx context.Context, container string, options types.CheckpointDeleteOptions) error + CheckpointList(ctx context.Context, container string, options types.CheckpointListOptions) ([]types.Checkpoint, error) } // PluginAPIClient defines API client methods for the plugins From 3e13296c4eca92652c7e29e95a25178a94fe692c Mon Sep 17 00:00:00 2001 From: Daniel Nephin Date: Tue, 4 Oct 2016 11:40:17 -0400 Subject: [PATCH 048/138] Generate VolumeList response from the swagger spec Signed-off-by: Daniel Nephin --- interface.go | 3 ++- volume_list.go | 6 +++--- volume_list_test.go | 3 ++- 3 files changed, 7 insertions(+), 5 deletions(-) diff --git a/interface.go b/interface.go index 8abdb0f6fc..613015f865 100644 --- a/interface.go +++ b/interface.go @@ -4,6 +4,7 @@ import ( "io" "time" + volumetypes "github.com/docker/docker/api/server/types/volume" "github.com/docker/docker/api/types" "github.com/docker/docker/api/types/container" "github.com/docker/docker/api/types/events" @@ -136,7 +137,7 @@ type VolumeAPIClient interface { VolumeCreate(ctx context.Context, options types.VolumeCreateRequest) (types.Volume, error) VolumeInspect(ctx context.Context, volumeID string) (types.Volume, error) VolumeInspectWithRaw(ctx context.Context, volumeID string) (types.Volume, []byte, error) - VolumeList(ctx context.Context, filter filters.Args) (types.VolumesListResponse, error) + VolumeList(ctx context.Context, filter filters.Args) (volumetypes.VolumesListOKBody, error) VolumeRemove(ctx context.Context, volumeID string, force bool) error VolumesPrune(ctx context.Context, cfg types.VolumesPruneConfig) (types.VolumesPruneReport, error) } diff --git a/volume_list.go b/volume_list.go index 44f03cfac7..9923ecb82c 100644 --- a/volume_list.go +++ b/volume_list.go @@ -4,14 +4,14 @@ import ( "encoding/json" "net/url" - "github.com/docker/docker/api/types" + volumetypes "github.com/docker/docker/api/server/types/volume" "github.com/docker/docker/api/types/filters" "golang.org/x/net/context" ) // VolumeList returns the volumes configured in the docker host. -func (cli *Client) VolumeList(ctx context.Context, filter filters.Args) (types.VolumesListResponse, error) { - var volumes types.VolumesListResponse +func (cli *Client) VolumeList(ctx context.Context, filter filters.Args) (volumetypes.VolumesListOKBody, error) { + var volumes volumetypes.VolumesListOKBody query := url.Values{} if filter.Len() > 0 { diff --git a/volume_list_test.go b/volume_list_test.go index 0af420eaff..ffdd904b58 100644 --- a/volume_list_test.go +++ b/volume_list_test.go @@ -9,6 +9,7 @@ import ( "strings" "testing" + volumetypes "github.com/docker/docker/api/server/types/volume" "github.com/docker/docker/api/types" "github.com/docker/docker/api/types/filters" "golang.org/x/net/context" @@ -68,7 +69,7 @@ func TestVolumeList(t *testing.T) { if actualFilters != listCase.expectedFilters { return nil, fmt.Errorf("filters not set in URL query properly. Expected '%s', got %s", listCase.expectedFilters, actualFilters) } - content, err := json.Marshal(types.VolumesListResponse{ + content, err := json.Marshal(volumetypes.VolumesListOKBody{ Volumes: []*types.Volume{ { Name: "volume", From 0325c474b881c6f29bd688c2558bf3c0b9495daa Mon Sep 17 00:00:00 2001 From: Daniel Nephin Date: Thu, 6 Oct 2016 12:57:17 -0400 Subject: [PATCH 049/138] Generate VolumesCreateRequest from the swagger spec. Signed-off-by: Daniel Nephin --- interface.go | 2 +- volume_create.go | 3 ++- volume_create_test.go | 5 +++-- 3 files changed, 6 insertions(+), 4 deletions(-) diff --git a/interface.go b/interface.go index 613015f865..5ec750abe1 100644 --- a/interface.go +++ b/interface.go @@ -134,7 +134,7 @@ type SystemAPIClient interface { // VolumeAPIClient defines API client methods for the volumes type VolumeAPIClient interface { - VolumeCreate(ctx context.Context, options types.VolumeCreateRequest) (types.Volume, error) + VolumeCreate(ctx context.Context, options volumetypes.VolumesCreateBody) (types.Volume, error) VolumeInspect(ctx context.Context, volumeID string) (types.Volume, error) VolumeInspectWithRaw(ctx context.Context, volumeID string) (types.Volume, []byte, error) VolumeList(ctx context.Context, filter filters.Args) (volumetypes.VolumesListOKBody, error) diff --git a/volume_create.go b/volume_create.go index f3a79f1e11..b18e5fe600 100644 --- a/volume_create.go +++ b/volume_create.go @@ -3,12 +3,13 @@ package client import ( "encoding/json" + volumetypes "github.com/docker/docker/api/server/types/volume" "github.com/docker/docker/api/types" "golang.org/x/net/context" ) // VolumeCreate creates a volume in the docker host. -func (cli *Client) VolumeCreate(ctx context.Context, options types.VolumeCreateRequest) (types.Volume, error) { +func (cli *Client) VolumeCreate(ctx context.Context, options volumetypes.VolumesCreateBody) (types.Volume, error) { var volume types.Volume resp, err := cli.post(ctx, "/volumes/create", nil, options, nil) if err != nil { diff --git a/volume_create_test.go b/volume_create_test.go index 75085296cc..d5d3791685 100644 --- a/volume_create_test.go +++ b/volume_create_test.go @@ -9,6 +9,7 @@ import ( "strings" "testing" + volumetypes "github.com/docker/docker/api/server/types/volume" "github.com/docker/docker/api/types" "golang.org/x/net/context" ) @@ -18,7 +19,7 @@ func TestVolumeCreateError(t *testing.T) { client: newMockClient(errorMock(http.StatusInternalServerError, "Server error")), } - _, err := client.VolumeCreate(context.Background(), types.VolumeCreateRequest{}) + _, err := client.VolumeCreate(context.Background(), volumetypes.VolumesCreateBody{}) if err == nil || err.Error() != "Error response from daemon: Server error" { t.Fatalf("expected a Server Error, got %v", err) } @@ -52,7 +53,7 @@ func TestVolumeCreate(t *testing.T) { }), } - volume, err := client.VolumeCreate(context.Background(), types.VolumeCreateRequest{ + volume, err := client.VolumeCreate(context.Background(), volumetypes.VolumesCreateBody{ Name: "myvolume", Driver: "mydriver", DriverOpts: map[string]string{ From 6dc945ab369fbd67dff60deda19aab21b886a254 Mon Sep 17 00:00:00 2001 From: Daniel Nephin Date: Fri, 14 Oct 2016 16:20:13 -0400 Subject: [PATCH 050/138] Use a config to generate swagger api types Moves the resposne types to a package under api/types Signed-off-by: Daniel Nephin --- interface.go | 2 +- volume_create.go | 2 +- volume_create_test.go | 2 +- volume_list.go | 2 +- volume_list_test.go | 2 +- 5 files changed, 5 insertions(+), 5 deletions(-) diff --git a/interface.go b/interface.go index 5ec750abe1..1f20a8be73 100644 --- a/interface.go +++ b/interface.go @@ -4,7 +4,6 @@ import ( "io" "time" - volumetypes "github.com/docker/docker/api/server/types/volume" "github.com/docker/docker/api/types" "github.com/docker/docker/api/types/container" "github.com/docker/docker/api/types/events" @@ -12,6 +11,7 @@ import ( "github.com/docker/docker/api/types/network" "github.com/docker/docker/api/types/registry" "github.com/docker/docker/api/types/swarm" + volumetypes "github.com/docker/docker/api/types/volume" "golang.org/x/net/context" ) diff --git a/volume_create.go b/volume_create.go index b18e5fe600..9620c87cbf 100644 --- a/volume_create.go +++ b/volume_create.go @@ -3,8 +3,8 @@ package client import ( "encoding/json" - volumetypes "github.com/docker/docker/api/server/types/volume" "github.com/docker/docker/api/types" + volumetypes "github.com/docker/docker/api/types/volume" "golang.org/x/net/context" ) diff --git a/volume_create_test.go b/volume_create_test.go index d5d3791685..9f1b2540b5 100644 --- a/volume_create_test.go +++ b/volume_create_test.go @@ -9,8 +9,8 @@ import ( "strings" "testing" - volumetypes "github.com/docker/docker/api/server/types/volume" "github.com/docker/docker/api/types" + volumetypes "github.com/docker/docker/api/types/volume" "golang.org/x/net/context" ) diff --git a/volume_list.go b/volume_list.go index 9923ecb82c..32247ce115 100644 --- a/volume_list.go +++ b/volume_list.go @@ -4,8 +4,8 @@ import ( "encoding/json" "net/url" - volumetypes "github.com/docker/docker/api/server/types/volume" "github.com/docker/docker/api/types/filters" + volumetypes "github.com/docker/docker/api/types/volume" "golang.org/x/net/context" ) diff --git a/volume_list_test.go b/volume_list_test.go index ffdd904b58..f29639be23 100644 --- a/volume_list_test.go +++ b/volume_list_test.go @@ -9,9 +9,9 @@ import ( "strings" "testing" - volumetypes "github.com/docker/docker/api/server/types/volume" "github.com/docker/docker/api/types" "github.com/docker/docker/api/types/filters" + volumetypes "github.com/docker/docker/api/types/volume" "golang.org/x/net/context" ) From e0f7f8d0dd71fe646faa81bf343a9082918ebd38 Mon Sep 17 00:00:00 2001 From: Daniel Nephin Date: Fri, 14 Oct 2016 16:28:47 -0400 Subject: [PATCH 051/138] Generate container create response from swagger spec. Signed-off-by: Daniel Nephin --- container_create.go | 5 ++--- container_create_test.go | 3 +-- interface.go | 2 +- 3 files changed, 4 insertions(+), 6 deletions(-) diff --git a/container_create.go b/container_create.go index a862172956..c042b17468 100644 --- a/container_create.go +++ b/container_create.go @@ -5,7 +5,6 @@ import ( "net/url" "strings" - "github.com/docker/docker/api/types" "github.com/docker/docker/api/types/container" "github.com/docker/docker/api/types/network" "golang.org/x/net/context" @@ -19,8 +18,8 @@ type configWrapper struct { // ContainerCreate creates a new container based in the given configuration. // It can be associated with a name, but it's not mandatory. -func (cli *Client) ContainerCreate(ctx context.Context, config *container.Config, hostConfig *container.HostConfig, networkingConfig *network.NetworkingConfig, containerName string) (types.ContainerCreateResponse, error) { - var response types.ContainerCreateResponse +func (cli *Client) ContainerCreate(ctx context.Context, config *container.Config, hostConfig *container.HostConfig, networkingConfig *network.NetworkingConfig, containerName string) (container.ContainerCreateCreatedBody, error) { + var response container.ContainerCreateCreatedBody query := url.Values{} if containerName != "" { query.Set("name", containerName) diff --git a/container_create_test.go b/container_create_test.go index 5325156beb..89641038f7 100644 --- a/container_create_test.go +++ b/container_create_test.go @@ -9,7 +9,6 @@ import ( "strings" "testing" - "github.com/docker/docker/api/types" "github.com/docker/docker/api/types/container" "golang.org/x/net/context" ) @@ -54,7 +53,7 @@ func TestContainerCreateWithName(t *testing.T) { if name != "container_name" { return nil, fmt.Errorf("container name not set in URL query properly. Expected `container_name`, got %s", name) } - b, err := json.Marshal(types.ContainerCreateResponse{ + b, err := json.Marshal(container.ContainerCreateCreatedBody{ ID: "container_id", }) if err != nil { diff --git a/interface.go b/interface.go index 1f20a8be73..8f8bbaf55f 100644 --- a/interface.go +++ b/interface.go @@ -34,7 +34,7 @@ type CommonAPIClient interface { type ContainerAPIClient interface { ContainerAttach(ctx context.Context, container string, options types.ContainerAttachOptions) (types.HijackedResponse, error) ContainerCommit(ctx context.Context, container string, options types.ContainerCommitOptions) (types.ContainerCommitResponse, error) - ContainerCreate(ctx context.Context, config *container.Config, hostConfig *container.HostConfig, networkingConfig *network.NetworkingConfig, containerName string) (types.ContainerCreateResponse, error) + ContainerCreate(ctx context.Context, config *container.Config, hostConfig *container.HostConfig, networkingConfig *network.NetworkingConfig, containerName string) (container.ContainerCreateCreatedBody, error) ContainerDiff(ctx context.Context, container string) ([]types.ContainerChange, error) ContainerExecAttach(ctx context.Context, execID string, config types.ExecConfig) (types.HijackedResponse, error) ContainerExecCreate(ctx context.Context, container string, config types.ExecConfig) (types.ContainerExecCreateResponse, error) From d4d914bd5226b2c9933da7c3881716e1a9e9003a Mon Sep 17 00:00:00 2001 From: Daniel Nephin Date: Tue, 18 Oct 2016 15:56:45 -0700 Subject: [PATCH 052/138] Add an IDResponse type Generated from a swagger spec and use it for container exec response Signed-off-by: Daniel Nephin --- container_exec.go | 4 ++-- container_exec_test.go | 2 +- interface.go | 2 +- 3 files changed, 4 insertions(+), 4 deletions(-) diff --git a/container_exec.go b/container_exec.go index 34173d3194..f6df722918 100644 --- a/container_exec.go +++ b/container_exec.go @@ -8,8 +8,8 @@ import ( ) // ContainerExecCreate creates a new exec configuration to run an exec process. -func (cli *Client) ContainerExecCreate(ctx context.Context, container string, config types.ExecConfig) (types.ContainerExecCreateResponse, error) { - var response types.ContainerExecCreateResponse +func (cli *Client) ContainerExecCreate(ctx context.Context, container string, config types.ExecConfig) (types.IDResponse, error) { + var response types.IDResponse resp, err := cli.post(ctx, "/containers/"+container+"/exec", nil, config, nil) if err != nil { return response, err diff --git a/container_exec_test.go b/container_exec_test.go index 42146ae8a5..0e296a50ad 100644 --- a/container_exec_test.go +++ b/container_exec_test.go @@ -45,7 +45,7 @@ func TestContainerExecCreate(t *testing.T) { if execConfig.User != "user" { return nil, fmt.Errorf("expected an execConfig with User == 'user', got %v", execConfig) } - b, err := json.Marshal(types.ContainerExecCreateResponse{ + b, err := json.Marshal(types.IDResponse{ ID: "exec_id", }) if err != nil { diff --git a/interface.go b/interface.go index 8f8bbaf55f..0575ce5c3b 100644 --- a/interface.go +++ b/interface.go @@ -37,7 +37,7 @@ type ContainerAPIClient interface { ContainerCreate(ctx context.Context, config *container.Config, hostConfig *container.HostConfig, networkingConfig *network.NetworkingConfig, containerName string) (container.ContainerCreateCreatedBody, error) ContainerDiff(ctx context.Context, container string) ([]types.ContainerChange, error) ContainerExecAttach(ctx context.Context, execID string, config types.ExecConfig) (types.HijackedResponse, error) - ContainerExecCreate(ctx context.Context, container string, config types.ExecConfig) (types.ContainerExecCreateResponse, error) + ContainerExecCreate(ctx context.Context, container string, config types.ExecConfig) (types.IDResponse, error) ContainerExecInspect(ctx context.Context, execID string) (types.ContainerExecInspect, error) ContainerExecResize(ctx context.Context, execID string, options types.ResizeOptions) error ContainerExecStart(ctx context.Context, execID string, config types.ExecStartCheck) error From f8cdc5ae711142dfc805a8a3483fb3976f3edaf8 Mon Sep 17 00:00:00 2001 From: Daniel Nephin Date: Tue, 18 Oct 2016 17:27:55 -0700 Subject: [PATCH 053/138] Use IDResponse for container create response. Signed-off-by: Daniel Nephin --- container_commit.go | 8 ++++---- container_commit_test.go | 2 +- interface.go | 2 +- 3 files changed, 6 insertions(+), 6 deletions(-) diff --git a/container_commit.go b/container_commit.go index 363950cc24..c766d62e40 100644 --- a/container_commit.go +++ b/container_commit.go @@ -12,16 +12,16 @@ import ( ) // ContainerCommit applies changes into a container and creates a new tagged image. -func (cli *Client) ContainerCommit(ctx context.Context, container string, options types.ContainerCommitOptions) (types.ContainerCommitResponse, error) { +func (cli *Client) ContainerCommit(ctx context.Context, container string, options types.ContainerCommitOptions) (types.IDResponse, error) { var repository, tag string if options.Reference != "" { distributionRef, err := distreference.ParseNamed(options.Reference) if err != nil { - return types.ContainerCommitResponse{}, err + return types.IDResponse{}, err } if _, isCanonical := distributionRef.(distreference.Canonical); isCanonical { - return types.ContainerCommitResponse{}, errors.New("refusing to create a tag with a digest reference") + return types.IDResponse{}, errors.New("refusing to create a tag with a digest reference") } tag = reference.GetTagFromNamedRef(distributionRef) @@ -41,7 +41,7 @@ func (cli *Client) ContainerCommit(ctx context.Context, container string, option query.Set("pause", "0") } - var response types.ContainerCommitResponse + var response types.IDResponse resp, err := cli.post(ctx, "/commit", query, options.Config, nil) if err != nil { return response, err diff --git a/container_commit_test.go b/container_commit_test.go index 8f1b58be81..a844675368 100644 --- a/container_commit_test.go +++ b/container_commit_test.go @@ -67,7 +67,7 @@ func TestContainerCommit(t *testing.T) { if len(changes) != len(expectedChanges) { return nil, fmt.Errorf("expected container changes size to be '%d', got %d", len(expectedChanges), len(changes)) } - b, err := json.Marshal(types.ContainerCommitResponse{ + b, err := json.Marshal(types.IDResponse{ ID: "new_container_id", }) if err != nil { diff --git a/interface.go b/interface.go index 0575ce5c3b..2a355fa8ad 100644 --- a/interface.go +++ b/interface.go @@ -33,7 +33,7 @@ type CommonAPIClient interface { // ContainerAPIClient defines API client methods for the containers type ContainerAPIClient interface { ContainerAttach(ctx context.Context, container string, options types.ContainerAttachOptions) (types.HijackedResponse, error) - ContainerCommit(ctx context.Context, container string, options types.ContainerCommitOptions) (types.ContainerCommitResponse, error) + ContainerCommit(ctx context.Context, container string, options types.ContainerCommitOptions) (types.IDResponse, error) ContainerCreate(ctx context.Context, config *container.Config, hostConfig *container.HostConfig, networkingConfig *network.NetworkingConfig, containerName string) (container.ContainerCreateCreatedBody, error) ContainerDiff(ctx context.Context, container string) ([]types.ContainerChange, error) ContainerExecAttach(ctx context.Context, execID string, config types.ExecConfig) (types.HijackedResponse, error) From 598e3a4874e28ecdc53b0993011125dde1052ace Mon Sep 17 00:00:00 2001 From: Daniel Nephin Date: Tue, 18 Oct 2016 17:35:45 -0700 Subject: [PATCH 054/138] Generate container update response from swagger spec. Signed-off-by: Daniel Nephin --- container_update.go | 5 ++--- container_update_test.go | 3 +-- interface.go | 2 +- 3 files changed, 4 insertions(+), 6 deletions(-) diff --git a/container_update.go b/container_update.go index 48b75bee30..5082f22dfa 100644 --- a/container_update.go +++ b/container_update.go @@ -3,14 +3,13 @@ package client import ( "encoding/json" - "github.com/docker/docker/api/types" "github.com/docker/docker/api/types/container" "golang.org/x/net/context" ) // ContainerUpdate updates resources of a container -func (cli *Client) ContainerUpdate(ctx context.Context, containerID string, updateConfig container.UpdateConfig) (types.ContainerUpdateResponse, error) { - var response types.ContainerUpdateResponse +func (cli *Client) ContainerUpdate(ctx context.Context, containerID string, updateConfig container.UpdateConfig) (container.ContainerUpdateOKBody, error) { + var response container.ContainerUpdateOKBody serverResp, err := cli.post(ctx, "/containers/"+containerID+"/update", nil, updateConfig, nil) if err != nil { return response, err diff --git a/container_update_test.go b/container_update_test.go index e151637a2b..715bb7ca23 100644 --- a/container_update_test.go +++ b/container_update_test.go @@ -9,7 +9,6 @@ import ( "strings" "testing" - "github.com/docker/docker/api/types" "github.com/docker/docker/api/types/container" "golang.org/x/net/context" ) @@ -33,7 +32,7 @@ func TestContainerUpdate(t *testing.T) { return nil, fmt.Errorf("Expected URL '%s', got '%s'", expectedURL, req.URL) } - b, err := json.Marshal(types.ContainerUpdateResponse{}) + b, err := json.Marshal(container.ContainerUpdateOKBody{}) if err != nil { return nil, err } diff --git a/interface.go b/interface.go index 2a355fa8ad..b303d2fde8 100644 --- a/interface.go +++ b/interface.go @@ -58,7 +58,7 @@ type ContainerAPIClient interface { ContainerStop(ctx context.Context, container string, timeout *time.Duration) error ContainerTop(ctx context.Context, container string, arguments []string) (types.ContainerProcessList, error) ContainerUnpause(ctx context.Context, container string) error - ContainerUpdate(ctx context.Context, container string, updateConfig container.UpdateConfig) (types.ContainerUpdateResponse, error) + ContainerUpdate(ctx context.Context, container string, updateConfig container.UpdateConfig) (container.ContainerUpdateOKBody, error) ContainerWait(ctx context.Context, container string) (int, error) CopyFromContainer(ctx context.Context, container, srcPath string) (io.ReadCloser, types.ContainerPathStat, error) CopyToContainer(ctx context.Context, container, path string, content io.Reader, options types.CopyToContainerOptions) error From ca7404a80acd68cc8aeebdaed24914c5aa481cd0 Mon Sep 17 00:00:00 2001 From: Daniel Nephin Date: Tue, 18 Oct 2016 17:52:46 -0700 Subject: [PATCH 055/138] generate AuthResponse type from swagger spec. Signed-off-by: Daniel Nephin --- interface.go | 2 +- login.go | 9 +++++---- 2 files changed, 6 insertions(+), 5 deletions(-) diff --git a/interface.go b/interface.go index b303d2fde8..f044c32352 100644 --- a/interface.go +++ b/interface.go @@ -127,7 +127,7 @@ type SwarmAPIClient interface { type SystemAPIClient interface { Events(ctx context.Context, options types.EventsOptions) (<-chan events.Message, <-chan error) Info(ctx context.Context) (types.Info, error) - RegistryLogin(ctx context.Context, auth types.AuthConfig) (types.AuthResponse, error) + RegistryLogin(ctx context.Context, auth types.AuthConfig) (registry.AuthenticateOKBody, error) DiskUsage(ctx context.Context) (types.DiskUsage, error) Ping(ctx context.Context) (bool, error) } diff --git a/login.go b/login.go index d8d277ccba..600dc7196f 100644 --- a/login.go +++ b/login.go @@ -6,22 +6,23 @@ import ( "net/url" "github.com/docker/docker/api/types" + "github.com/docker/docker/api/types/registry" "golang.org/x/net/context" ) // RegistryLogin authenticates the docker server with a given docker registry. // It returns UnauthorizerError when the authentication fails. -func (cli *Client) RegistryLogin(ctx context.Context, auth types.AuthConfig) (types.AuthResponse, error) { +func (cli *Client) RegistryLogin(ctx context.Context, auth types.AuthConfig) (registry.AuthenticateOKBody, error) { resp, err := cli.post(ctx, "/auth", url.Values{}, auth, nil) if resp.statusCode == http.StatusUnauthorized { - return types.AuthResponse{}, unauthorizedError{err} + return registry.AuthenticateOKBody{}, unauthorizedError{err} } if err != nil { - return types.AuthResponse{}, err + return registry.AuthenticateOKBody{}, err } - var response types.AuthResponse + var response registry.AuthenticateOKBody err = json.NewDecoder(resp.body).Decode(&response) ensureReaderClosed(resp) return response, err From 85a0bd062de0d3dc0bcd3b7082feca678e6dd946 Mon Sep 17 00:00:00 2001 From: Daniel Nephin Date: Thu, 20 Oct 2016 15:56:27 -0700 Subject: [PATCH 056/138] Generate ContainerWait response from the swagger spec. Signed-off-by: Daniel Nephin --- container_wait.go | 6 +++--- container_wait_test.go | 4 ++-- interface.go | 2 +- 3 files changed, 6 insertions(+), 6 deletions(-) diff --git a/container_wait.go b/container_wait.go index 8a858f0ea3..93212c70ee 100644 --- a/container_wait.go +++ b/container_wait.go @@ -5,19 +5,19 @@ import ( "golang.org/x/net/context" - "github.com/docker/docker/api/types" + "github.com/docker/docker/api/types/container" ) // ContainerWait pauses execution until a container exits. // It returns the API status code as response of its readiness. -func (cli *Client) ContainerWait(ctx context.Context, containerID string) (int, error) { +func (cli *Client) ContainerWait(ctx context.Context, containerID string) (int64, error) { resp, err := cli.post(ctx, "/containers/"+containerID+"/wait", nil, nil, nil) if err != nil { return -1, err } defer ensureReaderClosed(resp) - var res types.ContainerWaitResponse + var res container.ContainerWaitOKBody if err := json.NewDecoder(resp.body).Decode(&res); err != nil { return -1, err } diff --git a/container_wait_test.go b/container_wait_test.go index dab5acbdd3..9300bc0a54 100644 --- a/container_wait_test.go +++ b/container_wait_test.go @@ -11,7 +11,7 @@ import ( "testing" "time" - "github.com/docker/docker/api/types" + "github.com/docker/docker/api/types/container" "golang.org/x/net/context" ) @@ -36,7 +36,7 @@ func TestContainerWait(t *testing.T) { if !strings.HasPrefix(req.URL.Path, expectedURL) { return nil, fmt.Errorf("Expected URL '%s', got '%s'", expectedURL, req.URL) } - b, err := json.Marshal(types.ContainerWaitResponse{ + b, err := json.Marshal(container.ContainerWaitOKBody{ StatusCode: 15, }) if err != nil { diff --git a/interface.go b/interface.go index f044c32352..a78cb759cd 100644 --- a/interface.go +++ b/interface.go @@ -59,7 +59,7 @@ type ContainerAPIClient interface { ContainerTop(ctx context.Context, container string, arguments []string) (types.ContainerProcessList, error) ContainerUnpause(ctx context.Context, container string) error ContainerUpdate(ctx context.Context, container string, updateConfig container.UpdateConfig) (container.ContainerUpdateOKBody, error) - ContainerWait(ctx context.Context, container string) (int, error) + ContainerWait(ctx context.Context, container string) (int64, error) CopyFromContainer(ctx context.Context, container, srcPath string) (io.ReadCloser, types.ContainerPathStat, error) CopyToContainer(ctx context.Context, container, path string, content io.Reader, options types.CopyToContainerOptions) error ContainersPrune(ctx context.Context, cfg types.ContainersPruneConfig) (types.ContainersPruneReport, error) From 5f066ed250180f2a66454397671d1ddb8b6f73a6 Mon Sep 17 00:00:00 2001 From: Daniel Nephin Date: Mon, 31 Oct 2016 12:39:38 -0400 Subject: [PATCH 057/138] Refactor client/request Signed-off-by: Daniel Nephin --- hijack.go | 7 +++-- request.go | 78 ++++++++++++++++++++++++++++++------------------------ 2 files changed, 49 insertions(+), 36 deletions(-) diff --git a/hijack.go b/hijack.go index dededb7af2..74c53f52b3 100644 --- a/hijack.go +++ b/hijack.go @@ -5,6 +5,7 @@ import ( "errors" "fmt" "net" + "net/http" "net/http/httputil" "net/url" "strings" @@ -38,12 +39,14 @@ func (cli *Client) postHijacked(ctx context.Context, path string, query url.Valu return types.HijackedResponse{}, err } - req, err := cli.newRequest("POST", path, query, bodyEncoded, headers) + apiPath := cli.getAPIPath(path, query) + req, err := http.NewRequest("POST", apiPath, bodyEncoded) if err != nil { return types.HijackedResponse{}, err } - req.Host = cli.addr + req = cli.addHeaders(req, headers) + req.Host = cli.addr req.Header.Set("Connection", "Upgrade") req.Header.Set("Upgrade", "tcp") diff --git a/request.go b/request.go index 91a05824e1..c73464b54d 100644 --- a/request.go +++ b/request.go @@ -38,21 +38,29 @@ func (cli *Client) get(ctx context.Context, path string, query url.Values, heade // postWithContext sends an http request to the docker API using the method POST with a specific go context. func (cli *Client) post(ctx context.Context, path string, query url.Values, obj interface{}, headers map[string][]string) (serverResponse, error) { - return cli.sendRequest(ctx, "POST", path, query, obj, headers) + body, headers, err := encodeBody(obj, headers) + if err != nil { + return serverResponse{}, err + } + return cli.sendRequest(ctx, "POST", path, query, body, headers) } func (cli *Client) postRaw(ctx context.Context, path string, query url.Values, body io.Reader, headers map[string][]string) (serverResponse, error) { - return cli.sendClientRequest(ctx, "POST", path, query, body, headers) + return cli.sendRequest(ctx, "POST", path, query, body, headers) } // put sends an http request to the docker API using the method PUT. func (cli *Client) put(ctx context.Context, path string, query url.Values, obj interface{}, headers map[string][]string) (serverResponse, error) { - return cli.sendRequest(ctx, "PUT", path, query, obj, headers) + body, headers, err := encodeBody(obj, headers) + if err != nil { + return serverResponse{}, err + } + return cli.sendRequest(ctx, "PUT", path, query, body, headers) } // put sends an http request to the docker API using the method PUT. func (cli *Client) putRaw(ctx context.Context, path string, query url.Values, body io.Reader, headers map[string][]string) (serverResponse, error) { - return cli.sendClientRequest(ctx, "PUT", path, query, body, headers) + return cli.sendRequest(ctx, "PUT", path, query, body, headers) } // delete sends an http request to the docker API using the method DELETE. @@ -60,39 +68,35 @@ func (cli *Client) delete(ctx context.Context, path string, query url.Values, he return cli.sendRequest(ctx, "DELETE", path, query, nil, headers) } -func (cli *Client) sendRequest(ctx context.Context, method, path string, query url.Values, obj interface{}, headers map[string][]string) (serverResponse, error) { - var body io.Reader +type headers map[string][]string - if obj != nil { - var err error - body, err = encodeData(obj) - if err != nil { - return serverResponse{}, err - } - if headers == nil { - headers = make(map[string][]string) - } - headers["Content-Type"] = []string{"application/json"} +func encodeBody(obj interface{}, headers headers) (io.Reader, headers, error) { + if obj == nil { + return nil, headers, nil } - return cli.sendClientRequest(ctx, method, path, query, body, headers) + body, err := encodeData(obj) + if err != nil { + return nil, headers, err + } + if headers == nil { + headers = make(map[string][]string) + } + headers["Content-Type"] = []string{"application/json"} + return body, headers, nil } -func (cli *Client) sendClientRequest(ctx context.Context, method, path string, query url.Values, body io.Reader, headers map[string][]string) (serverResponse, error) { - serverResp := serverResponse{ - body: nil, - statusCode: -1, - } - +func (cli *Client) buildRequest(method, path string, body io.Reader, headers headers) (*http.Request, error) { expectedPayload := (method == "POST" || method == "PUT") if expectedPayload && body == nil { body = bytes.NewReader([]byte{}) } - req, err := cli.newRequest(method, path, query, body, headers) + req, err := http.NewRequest(method, path, body) if err != nil { - return serverResp, err + return nil, err } + req = cli.addHeaders(req, headers) if cli.proto == "unix" || cli.proto == "npipe" { // For local communications, it doesn't matter what the host is. We just @@ -106,6 +110,19 @@ func (cli *Client) sendClientRequest(ctx context.Context, method, path string, q if expectedPayload && req.Header.Get("Content-Type") == "" { req.Header.Set("Content-Type", "text/plain") } + return req, nil +} + +func (cli *Client) sendRequest(ctx context.Context, method, path string, query url.Values, body io.Reader, headers headers) (serverResponse, error) { + req, err := cli.buildRequest(method, cli.getAPIPath(path, query), body, headers) + if err != nil { + return serverResponse{}, err + } + return cli.doRequest(ctx, req) +} + +func (cli *Client) doRequest(ctx context.Context, req *http.Request) (serverResponse, error) { + serverResp := serverResponse{statusCode: -1} resp, err := ctxhttp.Do(ctx, cli.client, req) if err != nil { @@ -193,13 +210,7 @@ func (cli *Client) sendClientRequest(ctx context.Context, method, path string, q return serverResp, nil } -func (cli *Client) newRequest(method, path string, query url.Values, body io.Reader, headers map[string][]string) (*http.Request, error) { - apiPath := cli.getAPIPath(path, query) - req, err := http.NewRequest(method, apiPath, body) - if err != nil { - return nil, err - } - +func (cli *Client) addHeaders(req *http.Request, headers headers) *http.Request { // Add CLI Config's HTTP Headers BEFORE we set the Docker headers // then the user can't change OUR headers for k, v := range cli.customHTTPHeaders { @@ -211,8 +222,7 @@ func (cli *Client) newRequest(method, path string, query url.Values, body io.Rea req.Header[k] = v } } - - return req, nil + return req } func encodeData(data interface{}) (*bytes.Buffer, error) { From d121e14ccded94cf01c21a7a22e7e43f91fd9838 Mon Sep 17 00:00:00 2001 From: Zhang Wei Date: Tue, 1 Nov 2016 22:01:16 +0800 Subject: [PATCH 058/138] Replace all "Filter" field with "Filters" for consistency In file `api/types/client.go`, some of the "*Options{}" structs own a `Filters` field while some else have the name of `Filter`, this commit will rename all `Filter` to `Filters` for consistency. Also `Filters` is consistent with API with format `/xxx?filters=xxx`, that's why `Filters` is the right name. Signed-off-by: Zhang Wei --- container_list.go | 4 ++-- container_list_test.go | 8 ++++---- node_list.go | 4 ++-- node_list_test.go | 2 +- service_list.go | 4 ++-- service_list_test.go | 2 +- task_list.go | 4 ++-- task_list_test.go | 2 +- 8 files changed, 15 insertions(+), 15 deletions(-) diff --git a/container_list.go b/container_list.go index a8945d84f1..4398912197 100644 --- a/container_list.go +++ b/container_list.go @@ -34,8 +34,8 @@ func (cli *Client) ContainerList(ctx context.Context, options types.ContainerLis query.Set("size", "1") } - if options.Filter.Len() > 0 { - filterJSON, err := filters.ToParamWithVersion(cli.version, options.Filter) + if options.Filters.Len() > 0 { + filterJSON, err := filters.ToParamWithVersion(cli.version, options.Filters) if err != nil { return nil, err diff --git a/container_list_test.go b/container_list_test.go index 5068b7573e..e41c6874b5 100644 --- a/container_list_test.go +++ b/container_list_test.go @@ -82,10 +82,10 @@ func TestContainerList(t *testing.T) { filters.Add("label", "label2") filters.Add("before", "container") containers, err := client.ContainerList(context.Background(), types.ContainerListOptions{ - Size: true, - All: true, - Since: "container", - Filter: filters, + Size: true, + All: true, + Since: "container", + Filters: filters, }) if err != nil { t.Fatal(err) diff --git a/node_list.go b/node_list.go index 0716875ccc..3e8440f08e 100644 --- a/node_list.go +++ b/node_list.go @@ -14,8 +14,8 @@ import ( func (cli *Client) NodeList(ctx context.Context, options types.NodeListOptions) ([]swarm.Node, error) { query := url.Values{} - if options.Filter.Len() > 0 { - filterJSON, err := filters.ToParam(options.Filter) + if options.Filters.Len() > 0 { + filterJSON, err := filters.ToParam(options.Filters) if err != nil { return nil, err diff --git a/node_list_test.go b/node_list_test.go index 1b3b35f357..0251b5cce4 100644 --- a/node_list_test.go +++ b/node_list_test.go @@ -45,7 +45,7 @@ func TestNodeList(t *testing.T) { }, { options: types.NodeListOptions{ - Filter: filters, + Filters: filters, }, expectedQueryParams: map[string]string{ "filters": `{"label":{"label1":true,"label2":true}}`, diff --git a/service_list.go b/service_list.go index 4ebc9f3011..c29e6d407d 100644 --- a/service_list.go +++ b/service_list.go @@ -14,8 +14,8 @@ import ( func (cli *Client) ServiceList(ctx context.Context, options types.ServiceListOptions) ([]swarm.Service, error) { query := url.Values{} - if options.Filter.Len() > 0 { - filterJSON, err := filters.ToParam(options.Filter) + if options.Filters.Len() > 0 { + filterJSON, err := filters.ToParam(options.Filters) if err != nil { return nil, err } diff --git a/service_list_test.go b/service_list_test.go index 728187919f..213981ef70 100644 --- a/service_list_test.go +++ b/service_list_test.go @@ -45,7 +45,7 @@ func TestServiceList(t *testing.T) { }, { options: types.ServiceListOptions{ - Filter: filters, + Filters: filters, }, expectedQueryParams: map[string]string{ "filters": `{"label":{"label1":true,"label2":true}}`, diff --git a/task_list.go b/task_list.go index 07c8324c83..66324da959 100644 --- a/task_list.go +++ b/task_list.go @@ -14,8 +14,8 @@ import ( func (cli *Client) TaskList(ctx context.Context, options types.TaskListOptions) ([]swarm.Task, error) { query := url.Values{} - if options.Filter.Len() > 0 { - filterJSON, err := filters.ToParam(options.Filter) + if options.Filters.Len() > 0 { + filterJSON, err := filters.ToParam(options.Filters) if err != nil { return nil, err } diff --git a/task_list_test.go b/task_list_test.go index 2d9b812bc2..2a9a4c4346 100644 --- a/task_list_test.go +++ b/task_list_test.go @@ -45,7 +45,7 @@ func TestTaskList(t *testing.T) { }, { options: types.TaskListOptions{ - Filter: filters, + Filters: filters, }, expectedQueryParams: map[string]string{ "filters": `{"label":{"label1":true,"label2":true}}`, From 485bb69238647a23ecf8066e32cfe404440818e3 Mon Sep 17 00:00:00 2001 From: Antonio Murdaca Date: Fri, 2 Sep 2016 15:20:54 +0200 Subject: [PATCH 059/138] daemon: add a flag to override the default seccomp profile Signed-off-by: Antonio Murdaca --- info_test.go | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/info_test.go b/info_test.go index 79f23c8af2..7af82a8a31 100644 --- a/info_test.go +++ b/info_test.go @@ -46,8 +46,10 @@ func TestInfo(t *testing.T) { return nil, fmt.Errorf("Expected URL '%s', got '%s'", expectedURL, req.URL) } info := &types.Info{ - ID: "daemonID", - Containers: 3, + InfoBase: &types.InfoBase{ + ID: "daemonID", + Containers: 3, + }, } b, err := json.Marshal(info) if err != nil { From a98c89b310e709f78d64e1321273f5aed17a79f1 Mon Sep 17 00:00:00 2001 From: lixiaobing10051267 Date: Fri, 4 Nov 2016 17:16:11 +0800 Subject: [PATCH 060/138] add error information to distinguish different test scene Signed-off-by: lixiaobing10051267 --- container_create_test.go | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/container_create_test.go b/container_create_test.go index 89641038f7..15dbd5ea01 100644 --- a/container_create_test.go +++ b/container_create_test.go @@ -19,7 +19,7 @@ func TestContainerCreateError(t *testing.T) { } _, err := client.ContainerCreate(context.Background(), nil, nil, nil, "nothing") if err == nil || err.Error() != "Error response from daemon: Server error" { - t.Fatalf("expected a Server Error, got %v", err) + t.Fatalf("expected a Server Error while testing StatusInternalServerError, got %v", err) } // 404 doesn't automagitally means an unknown image @@ -28,7 +28,7 @@ func TestContainerCreateError(t *testing.T) { } _, err = client.ContainerCreate(context.Background(), nil, nil, nil, "nothing") if err == nil || err.Error() != "Error response from daemon: Server error" { - t.Fatalf("expected a Server Error, got %v", err) + t.Fatalf("expected a Server Error while testing StatusNotFound, got %v", err) } } From 58c2d938dd653ed6ab2135aee21f2105981deaaa Mon Sep 17 00:00:00 2001 From: Antonio Murdaca Date: Mon, 7 Nov 2016 10:01:28 +0100 Subject: [PATCH 061/138] client: bump default version to v1.25 Signed-off-by: Antonio Murdaca --- client.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/client.go b/client.go index 9dcb3986cf..3b97720e00 100644 --- a/client.go +++ b/client.go @@ -58,7 +58,7 @@ import ( ) // DefaultVersion is the version of the current stable API -const DefaultVersion string = "1.23" +const DefaultVersion string = "1.25" // Client is the API client that performs all operations // against a docker server. From 3f7264473d9afc3cb5fdb430c4807e1a0bc71434 Mon Sep 17 00:00:00 2001 From: Victor Vieux Date: Mon, 7 Nov 2016 17:43:11 -0800 Subject: [PATCH 062/138] support settings in docker plugins install Signed-off-by: Victor Vieux --- plugin_install.go | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/plugin_install.go b/plugin_install.go index 636c95364d..d0a3d517fc 100644 --- a/plugin_install.go +++ b/plugin_install.go @@ -45,9 +45,17 @@ func (cli *Client) PluginInstall(ctx context.Context, name string, options types return pluginPermissionDenied{name} } } + + if len(options.Args) > 0 { + if err := cli.PluginSet(ctx, name, options.Args); err != nil { + return err + } + } + if options.Disabled { return nil } + return cli.PluginEnable(ctx, name) } From 4f63bfb619a88fa3df8b57bf1b11e89f90250061 Mon Sep 17 00:00:00 2001 From: Victor Vieux Date: Wed, 2 Nov 2016 17:43:32 -0700 Subject: [PATCH 063/138] always add but hide experimental cmds and flags Signed-off-by: Victor Vieux update cobra and use Tags Signed-off-by: Victor Vieux allow client to talk to an older server Signed-off-by: Victor Vieux --- client.go | 17 ++++++++++++++--- container_create.go | 5 +++++ container_exec.go | 5 +++++ container_prune.go | 4 ++++ errors.go | 11 +++++++++++ image_build.go | 7 +++++-- image_prune.go | 4 ++++ interface.go | 2 +- ping.go | 29 ++++++++++++++++++++--------- request.go | 3 +++ volume_prune.go | 4 ++++ volume_remove.go | 7 +++++-- 12 files changed, 81 insertions(+), 17 deletions(-) diff --git a/client.go b/client.go index 3b97720e00..76a1ac07c0 100644 --- a/client.go +++ b/client.go @@ -79,6 +79,8 @@ type Client struct { version string // custom http headers configured by users. customHTTPHeaders map[string]string + // manualOverride is set to true when the version was set by users. + manualOverride bool } // NewEnvClient initializes a new API client based on environment variables. @@ -111,13 +113,19 @@ func NewEnvClient() (*Client, error) { if host == "" { host = DefaultDockerHost } - version := os.Getenv("DOCKER_API_VERSION") if version == "" { version = DefaultVersion } - return NewClient(host, version, client, nil) + cli, err := NewClient(host, version, client, nil) + if err != nil { + return cli, err + } + if version != "" { + cli.manualOverride = true + } + return cli, nil } // NewClient initializes a new API client for the given host and API version. @@ -211,7 +219,10 @@ func (cli *Client) ClientVersion() string { // UpdateClientVersion updates the version string associated with this // instance of the Client. func (cli *Client) UpdateClientVersion(v string) { - cli.version = v + if !cli.manualOverride { + cli.version = v + } + } // ParseHost verifies that the given host strings is valid. diff --git a/container_create.go b/container_create.go index c042b17468..9f627aafa6 100644 --- a/container_create.go +++ b/container_create.go @@ -20,6 +20,11 @@ type configWrapper struct { // It can be associated with a name, but it's not mandatory. func (cli *Client) ContainerCreate(ctx context.Context, config *container.Config, hostConfig *container.HostConfig, networkingConfig *network.NetworkingConfig, containerName string) (container.ContainerCreateCreatedBody, error) { var response container.ContainerCreateCreatedBody + + if err := cli.NewVersionError("1.25", "stop timeout"); config != nil && config.StopTimeout != nil && err != nil { + return response, err + } + query := url.Values{} if containerName != "" { query.Set("name", containerName) diff --git a/container_exec.go b/container_exec.go index f6df722918..0665c54fbd 100644 --- a/container_exec.go +++ b/container_exec.go @@ -10,6 +10,11 @@ import ( // ContainerExecCreate creates a new exec configuration to run an exec process. func (cli *Client) ContainerExecCreate(ctx context.Context, container string, config types.ExecConfig) (types.IDResponse, error) { var response types.IDResponse + + if err := cli.NewVersionError("1.25", "env"); len(config.Env) != 0 && err != nil { + return response, err + } + resp, err := cli.post(ctx, "/containers/"+container+"/exec", nil, config, nil) if err != nil { return response, err diff --git a/container_prune.go b/container_prune.go index 0d8bd3292c..3eabe71a7f 100644 --- a/container_prune.go +++ b/container_prune.go @@ -12,6 +12,10 @@ import ( func (cli *Client) ContainersPrune(ctx context.Context, cfg types.ContainersPruneConfig) (types.ContainersPruneReport, error) { var report types.ContainersPruneReport + if err := cli.NewVersionError("1.25", "container prune"); err != nil { + return report, err + } + serverResp, err := cli.post(ctx, "/containers/prune", nil, cfg, nil) if err != nil { return report, err diff --git a/errors.go b/errors.go index ad1dadabb6..53e2065332 100644 --- a/errors.go +++ b/errors.go @@ -3,6 +3,8 @@ package client import ( "errors" "fmt" + + "github.com/docker/docker/api/types/versions" ) // ErrConnectionFailed is an error raised when the connection between the client and the server failed. @@ -206,3 +208,12 @@ func IsErrPluginPermissionDenied(err error) bool { _, ok := err.(pluginPermissionDenied) return ok } + +// NewVersionError returns an error if the APIVersion required +// if less than the current supported version +func (cli *Client) NewVersionError(APIrequired, feature string) error { + if versions.LessThan(cli.version, APIrequired) { + return fmt.Errorf("%q requires API version %s, but the Docker server is version %s", feature, APIrequired, cli.version) + } + return nil +} diff --git a/image_build.go b/image_build.go index 4d611d5430..0049e4e290 100644 --- a/image_build.go +++ b/image_build.go @@ -21,7 +21,7 @@ var headerRegexp = regexp.MustCompile(`\ADocker/.+\s\((.+)\)\z`) // The Body in the response implement an io.ReadCloser and it's up to the caller to // close it. func (cli *Client) ImageBuild(ctx context.Context, buildContext io.Reader, options types.ImageBuildOptions) (types.ImageBuildResponse, error) { - query, err := imageBuildOptionsToQuery(options) + query, err := cli.imageBuildOptionsToQuery(options) if err != nil { return types.ImageBuildResponse{}, err } @@ -47,7 +47,7 @@ func (cli *Client) ImageBuild(ctx context.Context, buildContext io.Reader, optio }, nil } -func imageBuildOptionsToQuery(options types.ImageBuildOptions) (url.Values, error) { +func (cli *Client) imageBuildOptionsToQuery(options types.ImageBuildOptions) (url.Values, error) { query := url.Values{ "t": options.Tags, "securityopt": options.SecurityOpt, @@ -76,6 +76,9 @@ func imageBuildOptionsToQuery(options types.ImageBuildOptions) (url.Values, erro } if options.Squash { + if err := cli.NewVersionError("1.25", "squash"); err != nil { + return query, err + } query.Set("squash", "1") } diff --git a/image_prune.go b/image_prune.go index f6752e5043..d5e69d5b19 100644 --- a/image_prune.go +++ b/image_prune.go @@ -12,6 +12,10 @@ import ( func (cli *Client) ImagesPrune(ctx context.Context, cfg types.ImagesPruneConfig) (types.ImagesPruneReport, error) { var report types.ImagesPruneReport + if err := cli.NewVersionError("1.25", "image prune"); err != nil { + return report, err + } + serverResp, err := cli.post(ctx, "/images/prune", nil, cfg, nil) if err != nil { return report, err diff --git a/interface.go b/interface.go index a78cb759cd..99b06709b5 100644 --- a/interface.go +++ b/interface.go @@ -129,7 +129,7 @@ type SystemAPIClient interface { Info(ctx context.Context) (types.Info, error) RegistryLogin(ctx context.Context, auth types.AuthConfig) (registry.AuthenticateOKBody, error) DiskUsage(ctx context.Context) (types.DiskUsage, error) - Ping(ctx context.Context) (bool, error) + Ping(ctx context.Context) (types.Ping, error) } // VolumeAPIClient defines API client methods for the volumes diff --git a/ping.go b/ping.go index 5e99e1bba1..22dcda24fd 100644 --- a/ping.go +++ b/ping.go @@ -1,19 +1,30 @@ package client -import "golang.org/x/net/context" +import ( + "fmt" -// Ping pings the server and return the value of the "Docker-Experimental" header -func (cli *Client) Ping(ctx context.Context) (bool, error) { - serverResp, err := cli.get(ctx, "/_ping", nil, nil) + "github.com/docker/docker/api/types" + "golang.org/x/net/context" +) + +// Ping pings the server and return the value of the "Docker-Experimental" & "API-Version" headers +func (cli *Client) Ping(ctx context.Context) (types.Ping, error) { + var ping types.Ping + req, err := cli.buildRequest("GET", fmt.Sprintf("%s/_ping", cli.basePath), nil, nil) if err != nil { - return false, err + return ping, err + } + serverResp, err := cli.doRequest(ctx, req) + if err != nil { + return ping, err } defer ensureReaderClosed(serverResp) - exp := serverResp.header.Get("Docker-Experimental") - if exp != "true" { - return false, nil + ping.APIVersion = serverResp.header.Get("API-Version") + + if serverResp.header.Get("Docker-Experimental") == "true" { + ping.Experimental = true } - return true, nil + return ping, nil } diff --git a/request.go b/request.go index c73464b54d..ac05363655 100644 --- a/request.go +++ b/request.go @@ -214,6 +214,9 @@ func (cli *Client) addHeaders(req *http.Request, headers headers) *http.Request // Add CLI Config's HTTP Headers BEFORE we set the Docker headers // then the user can't change OUR headers for k, v := range cli.customHTTPHeaders { + if versions.LessThan(cli.version, "1.25") && k == "User-Agent" { + continue + } req.Header.Set(k, v) } diff --git a/volume_prune.go b/volume_prune.go index e7ea7b591d..ea4e234a30 100644 --- a/volume_prune.go +++ b/volume_prune.go @@ -12,6 +12,10 @@ import ( func (cli *Client) VolumesPrune(ctx context.Context, cfg types.VolumesPruneConfig) (types.VolumesPruneReport, error) { var report types.VolumesPruneReport + if err := cli.NewVersionError("1.25", "volume prune"); err != nil { + return report, err + } + serverResp, err := cli.post(ctx, "/volumes/prune", nil, cfg, nil) if err != nil { return report, err diff --git a/volume_remove.go b/volume_remove.go index 3d5aeff252..6c26575b49 100644 --- a/volume_remove.go +++ b/volume_remove.go @@ -3,14 +3,17 @@ package client import ( "net/url" + "github.com/docker/docker/api/types/versions" "golang.org/x/net/context" ) // VolumeRemove removes a volume from the docker host. func (cli *Client) VolumeRemove(ctx context.Context, volumeID string, force bool) error { query := url.Values{} - if force { - query.Set("force", "1") + if versions.GreaterThanOrEqualTo(cli.version, "1.25") { + if force { + query.Set("force", "1") + } } resp, err := cli.delete(ctx, "/volumes/"+volumeID, query, nil) ensureReaderClosed(resp) From 72ff77999cbd4c943ad9e86f30c55a05992f41c4 Mon Sep 17 00:00:00 2001 From: Evan Hazlett Date: Wed, 19 Oct 2016 12:22:02 -0400 Subject: [PATCH 064/138] secrets: secret management for swarm Signed-off-by: Evan Hazlett wip: use tmpfs for swarm secrets Signed-off-by: Evan Hazlett wip: inject secrets from swarm secret store Signed-off-by: Evan Hazlett secrets: use secret names in cli for service create Signed-off-by: Evan Hazlett switch to use mounts instead of volumes Signed-off-by: Evan Hazlett vendor: use ehazlett swarmkit Signed-off-by: Evan Hazlett secrets: finish secret update Signed-off-by: Evan Hazlett --- errors.go | 22 ++++++++++ interface.go | 9 ++++ secret_create.go | 24 +++++++++++ secret_create_test.go | 57 +++++++++++++++++++++++++ secret_inspect.go | 34 +++++++++++++++ secret_inspect_test.go | 65 +++++++++++++++++++++++++++++ secret_list.go | 35 ++++++++++++++++ secret_list_test.go | 94 ++++++++++++++++++++++++++++++++++++++++++ secret_remove.go | 10 +++++ secret_remove_test.go | 47 +++++++++++++++++++++ 10 files changed, 397 insertions(+) create mode 100644 secret_create.go create mode 100644 secret_create_test.go create mode 100644 secret_inspect.go create mode 100644 secret_inspect_test.go create mode 100644 secret_list.go create mode 100644 secret_list_test.go create mode 100644 secret_remove.go create mode 100644 secret_remove_test.go diff --git a/errors.go b/errors.go index 53e2065332..db7294daa8 100644 --- a/errors.go +++ b/errors.go @@ -217,3 +217,25 @@ func (cli *Client) NewVersionError(APIrequired, feature string) error { } return nil } + +// secretNotFoundError implements an error returned when a secret is not found. +type secretNotFoundError struct { + name string +} + +// Error returns a string representation of a secretNotFoundError +func (e secretNotFoundError) Error() string { + return fmt.Sprintf("Error: No such secret: %s", e.name) +} + +// NoFound indicates that this error type is of NotFound +func (e secretNotFoundError) NotFound() bool { + return true +} + +// IsErrSecretNotFound returns true if the error is caused +// when a secret is not found. +func IsErrSecretNotFound(err error) bool { + _, ok := err.(secretNotFoundError) + return ok +} diff --git a/interface.go b/interface.go index 99b06709b5..49b66b1d17 100644 --- a/interface.go +++ b/interface.go @@ -23,6 +23,7 @@ type CommonAPIClient interface { NetworkAPIClient ServiceAPIClient SwarmAPIClient + SecretAPIClient SystemAPIClient VolumeAPIClient ClientVersion() string @@ -141,3 +142,11 @@ type VolumeAPIClient interface { VolumeRemove(ctx context.Context, volumeID string, force bool) error VolumesPrune(ctx context.Context, cfg types.VolumesPruneConfig) (types.VolumesPruneReport, error) } + +// SecretAPIClient defines API client methods for secrets +type SecretAPIClient interface { + SecretList(ctx context.Context, options types.SecretListOptions) ([]swarm.Secret, error) + SecretCreate(ctx context.Context, secret swarm.SecretSpec) (types.SecretCreateResponse, error) + SecretRemove(ctx context.Context, id string) error + SecretInspectWithRaw(ctx context.Context, name string) (swarm.Secret, []byte, error) +} diff --git a/secret_create.go b/secret_create.go new file mode 100644 index 0000000000..de8b041567 --- /dev/null +++ b/secret_create.go @@ -0,0 +1,24 @@ +package client + +import ( + "encoding/json" + + "github.com/docker/docker/api/types" + "github.com/docker/docker/api/types/swarm" + "golang.org/x/net/context" +) + +// SecretCreate creates a new Secret. +func (cli *Client) SecretCreate(ctx context.Context, secret swarm.SecretSpec) (types.SecretCreateResponse, error) { + var headers map[string][]string + + var response types.SecretCreateResponse + resp, err := cli.post(ctx, "/secrets/create", nil, secret, headers) + if err != nil { + return response, err + } + + err = json.NewDecoder(resp.body).Decode(&response) + ensureReaderClosed(resp) + return response, err +} diff --git a/secret_create_test.go b/secret_create_test.go new file mode 100644 index 0000000000..d264eb6692 --- /dev/null +++ b/secret_create_test.go @@ -0,0 +1,57 @@ +package client + +import ( + "bytes" + "encoding/json" + "fmt" + "io/ioutil" + "net/http" + "strings" + "testing" + + "github.com/docker/docker/api/types" + "github.com/docker/docker/api/types/swarm" + "golang.org/x/net/context" +) + +func TestSecretCreateError(t *testing.T) { + client := &Client{ + client: newMockClient(errorMock(http.StatusInternalServerError, "Server error")), + } + _, err := client.SecretCreate(context.Background(), swarm.SecretSpec{}) + if err == nil || err.Error() != "Error response from daemon: Server error" { + t.Fatalf("expected a Server Error, got %v", err) + } +} + +func TestSecretCreate(t *testing.T) { + expectedURL := "/secrets/create" + client := &Client{ + client: newMockClient(func(req *http.Request) (*http.Response, error) { + if !strings.HasPrefix(req.URL.Path, expectedURL) { + return nil, fmt.Errorf("Expected URL '%s', got '%s'", expectedURL, req.URL) + } + if req.Method != "POST" { + return nil, fmt.Errorf("expected POST method, got %s", req.Method) + } + b, err := json.Marshal(types.SecretCreateResponse{ + ID: "test_secret", + }) + if err != nil { + return nil, err + } + return &http.Response{ + StatusCode: http.StatusOK, + Body: ioutil.NopCloser(bytes.NewReader(b)), + }, nil + }), + } + + r, err := client.SecretCreate(context.Background(), swarm.SecretSpec{}) + if err != nil { + t.Fatal(err) + } + if r.ID != "test_secret" { + t.Fatalf("expected `test_secret`, got %s", r.ID) + } +} diff --git a/secret_inspect.go b/secret_inspect.go new file mode 100644 index 0000000000..f774576118 --- /dev/null +++ b/secret_inspect.go @@ -0,0 +1,34 @@ +package client + +import ( + "bytes" + "encoding/json" + "io/ioutil" + "net/http" + + "github.com/docker/docker/api/types/swarm" + "golang.org/x/net/context" +) + +// SecretInspectWithRaw returns the secret information with raw data +func (cli *Client) SecretInspectWithRaw(ctx context.Context, id string) (swarm.Secret, []byte, error) { + resp, err := cli.get(ctx, "/secrets/"+id, nil, nil) + if err != nil { + if resp.statusCode == http.StatusNotFound { + return swarm.Secret{}, nil, secretNotFoundError{id} + } + return swarm.Secret{}, nil, err + } + defer ensureReaderClosed(resp) + + body, err := ioutil.ReadAll(resp.body) + if err != nil { + return swarm.Secret{}, nil, err + } + + var secret swarm.Secret + rdr := bytes.NewReader(body) + err = json.NewDecoder(rdr).Decode(&secret) + + return secret, body, err +} diff --git a/secret_inspect_test.go b/secret_inspect_test.go new file mode 100644 index 0000000000..423d986968 --- /dev/null +++ b/secret_inspect_test.go @@ -0,0 +1,65 @@ +package client + +import ( + "bytes" + "encoding/json" + "fmt" + "io/ioutil" + "net/http" + "strings" + "testing" + + "github.com/docker/docker/api/types/swarm" + "golang.org/x/net/context" +) + +func TestSecretInspectError(t *testing.T) { + client := &Client{ + client: newMockClient(errorMock(http.StatusInternalServerError, "Server error")), + } + + _, _, err := client.SecretInspectWithRaw(context.Background(), "nothing") + if err == nil || err.Error() != "Error response from daemon: Server error" { + t.Fatalf("expected a Server Error, got %v", err) + } +} + +func TestSecretInspectSecretNotFound(t *testing.T) { + client := &Client{ + client: newMockClient(errorMock(http.StatusNotFound, "Server error")), + } + + _, _, err := client.SecretInspectWithRaw(context.Background(), "unknown") + if err == nil || !IsErrSecretNotFound(err) { + t.Fatalf("expected an secretNotFoundError error, got %v", err) + } +} + +func TestSecretInspect(t *testing.T) { + expectedURL := "/secrets/secret_id" + client := &Client{ + client: newMockClient(func(req *http.Request) (*http.Response, error) { + if !strings.HasPrefix(req.URL.Path, expectedURL) { + return nil, fmt.Errorf("Expected URL '%s', got '%s'", expectedURL, req.URL) + } + content, err := json.Marshal(swarm.Secret{ + ID: "secret_id", + }) + if err != nil { + return nil, err + } + return &http.Response{ + StatusCode: http.StatusOK, + Body: ioutil.NopCloser(bytes.NewReader(content)), + }, nil + }), + } + + secretInspect, _, err := client.SecretInspectWithRaw(context.Background(), "secret_id") + if err != nil { + t.Fatal(err) + } + if secretInspect.ID != "secret_id" { + t.Fatalf("expected `secret_id`, got %s", secretInspect.ID) + } +} diff --git a/secret_list.go b/secret_list.go new file mode 100644 index 0000000000..5e9d2b5098 --- /dev/null +++ b/secret_list.go @@ -0,0 +1,35 @@ +package client + +import ( + "encoding/json" + "net/url" + + "github.com/docker/docker/api/types" + "github.com/docker/docker/api/types/filters" + "github.com/docker/docker/api/types/swarm" + "golang.org/x/net/context" +) + +// SecretList returns the list of secrets. +func (cli *Client) SecretList(ctx context.Context, options types.SecretListOptions) ([]swarm.Secret, error) { + query := url.Values{} + + if options.Filter.Len() > 0 { + filterJSON, err := filters.ToParam(options.Filter) + if err != nil { + return nil, err + } + + query.Set("filters", filterJSON) + } + + resp, err := cli.get(ctx, "/secrets", query, nil) + if err != nil { + return nil, err + } + + var secrets []swarm.Secret + err = json.NewDecoder(resp.body).Decode(&secrets) + ensureReaderClosed(resp) + return secrets, err +} diff --git a/secret_list_test.go b/secret_list_test.go new file mode 100644 index 0000000000..174963c7ee --- /dev/null +++ b/secret_list_test.go @@ -0,0 +1,94 @@ +package client + +import ( + "bytes" + "encoding/json" + "fmt" + "io/ioutil" + "net/http" + "strings" + "testing" + + "github.com/docker/docker/api/types" + "github.com/docker/docker/api/types/filters" + "github.com/docker/docker/api/types/swarm" + "golang.org/x/net/context" +) + +func TestSecretListError(t *testing.T) { + client := &Client{ + client: newMockClient(errorMock(http.StatusInternalServerError, "Server error")), + } + + _, err := client.SecretList(context.Background(), types.SecretListOptions{}) + if err == nil || err.Error() != "Error response from daemon: Server error" { + t.Fatalf("expected a Server Error, got %v", err) + } +} + +func TestSecretList(t *testing.T) { + expectedURL := "/secrets" + + filters := filters.NewArgs() + filters.Add("label", "label1") + filters.Add("label", "label2") + + listCases := []struct { + options types.SecretListOptions + expectedQueryParams map[string]string + }{ + { + options: types.SecretListOptions{}, + expectedQueryParams: map[string]string{ + "filters": "", + }, + }, + { + options: types.SecretListOptions{ + Filter: filters, + }, + expectedQueryParams: map[string]string{ + "filters": `{"label":{"label1":true,"label2":true}}`, + }, + }, + } + for _, listCase := range listCases { + client := &Client{ + client: newMockClient(func(req *http.Request) (*http.Response, error) { + if !strings.HasPrefix(req.URL.Path, expectedURL) { + return nil, fmt.Errorf("Expected URL '%s', got '%s'", expectedURL, req.URL) + } + query := req.URL.Query() + for key, expected := range listCase.expectedQueryParams { + actual := query.Get(key) + if actual != expected { + return nil, fmt.Errorf("%s not set in URL query properly. Expected '%s', got %s", key, expected, actual) + } + } + content, err := json.Marshal([]swarm.Secret{ + { + ID: "secret_id1", + }, + { + ID: "secret_id2", + }, + }) + if err != nil { + return nil, err + } + return &http.Response{ + StatusCode: http.StatusOK, + Body: ioutil.NopCloser(bytes.NewReader(content)), + }, nil + }), + } + + secrets, err := client.SecretList(context.Background(), listCase.options) + if err != nil { + t.Fatal(err) + } + if len(secrets) != 2 { + t.Fatalf("expected 2 secrets, got %v", secrets) + } + } +} diff --git a/secret_remove.go b/secret_remove.go new file mode 100644 index 0000000000..1955b988a9 --- /dev/null +++ b/secret_remove.go @@ -0,0 +1,10 @@ +package client + +import "golang.org/x/net/context" + +// SecretRemove removes a Secret. +func (cli *Client) SecretRemove(ctx context.Context, id string) error { + resp, err := cli.delete(ctx, "/secrets/"+id, nil, nil) + ensureReaderClosed(resp) + return err +} diff --git a/secret_remove_test.go b/secret_remove_test.go new file mode 100644 index 0000000000..f269f787d2 --- /dev/null +++ b/secret_remove_test.go @@ -0,0 +1,47 @@ +package client + +import ( + "bytes" + "fmt" + "io/ioutil" + "net/http" + "strings" + "testing" + + "golang.org/x/net/context" +) + +func TestSecretRemoveError(t *testing.T) { + client := &Client{ + client: newMockClient(errorMock(http.StatusInternalServerError, "Server error")), + } + + err := client.SecretRemove(context.Background(), "secret_id") + if err == nil || err.Error() != "Error response from daemon: Server error" { + t.Fatalf("expected a Server Error, got %v", err) + } +} + +func TestSecretRemove(t *testing.T) { + expectedURL := "/secrets/secret_id" + + client := &Client{ + client: newMockClient(func(req *http.Request) (*http.Response, error) { + if !strings.HasPrefix(req.URL.Path, expectedURL) { + return nil, fmt.Errorf("Expected URL '%s', got '%s'", expectedURL, req.URL) + } + if req.Method != "DELETE" { + return nil, fmt.Errorf("expected DELETE method, got %s", req.Method) + } + return &http.Response{ + StatusCode: http.StatusOK, + Body: ioutil.NopCloser(bytes.NewReader([]byte("body"))), + }, nil + }), + } + + err := client.SecretRemove(context.Background(), "secret_id") + if err != nil { + t.Fatal(err) + } +} From 548728bb843ec0ca4f5f8a36edf7e92556eb2f77 Mon Sep 17 00:00:00 2001 From: Evan Hazlett Date: Thu, 27 Oct 2016 00:41:32 -0700 Subject: [PATCH 065/138] more review updates - use /secrets for swarm secret create route - do not specify omitempty for secret and secret reference - simplify lookup for secret ids - do not use pointer for secret grpc conversion Signed-off-by: Evan Hazlett --- errors.go | 2 +- secret_create.go | 2 +- secret_create_test.go | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/errors.go b/errors.go index db7294daa8..94c22a728a 100644 --- a/errors.go +++ b/errors.go @@ -225,7 +225,7 @@ type secretNotFoundError struct { // Error returns a string representation of a secretNotFoundError func (e secretNotFoundError) Error() string { - return fmt.Sprintf("Error: No such secret: %s", e.name) + return fmt.Sprintf("Error: no such secret: %s", e.name) } // NoFound indicates that this error type is of NotFound diff --git a/secret_create.go b/secret_create.go index de8b041567..f92a3d1510 100644 --- a/secret_create.go +++ b/secret_create.go @@ -13,7 +13,7 @@ func (cli *Client) SecretCreate(ctx context.Context, secret swarm.SecretSpec) (t var headers map[string][]string var response types.SecretCreateResponse - resp, err := cli.post(ctx, "/secrets/create", nil, secret, headers) + resp, err := cli.post(ctx, "/secrets", nil, secret, headers) if err != nil { return response, err } diff --git a/secret_create_test.go b/secret_create_test.go index d264eb6692..b7def89d0e 100644 --- a/secret_create_test.go +++ b/secret_create_test.go @@ -25,7 +25,7 @@ func TestSecretCreateError(t *testing.T) { } func TestSecretCreate(t *testing.T) { - expectedURL := "/secrets/create" + expectedURL := "/secrets" client := &Client{ client: newMockClient(func(req *http.Request) (*http.Response, error) { if !strings.HasPrefix(req.URL.Path, expectedURL) { From ab6c38e01414042eb3edc70c39453e7b3f45a563 Mon Sep 17 00:00:00 2001 From: Evan Hazlett Date: Thu, 3 Nov 2016 14:09:13 -0400 Subject: [PATCH 066/138] review updates - use Filters instead of Filter for secret list - UID, GID -> string - getSecrets -> getSecretsByName - updated test case for secrets with better source - use golang.org/x/context instead of context - for grpc conversion allocate with make - check for nil with task.Spec.GetContainer() Signed-off-by: Evan Hazlett --- secret_list.go | 4 ++-- secret_list_test.go | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/secret_list.go b/secret_list.go index 5e9d2b5098..7e9d5ec167 100644 --- a/secret_list.go +++ b/secret_list.go @@ -14,8 +14,8 @@ import ( func (cli *Client) SecretList(ctx context.Context, options types.SecretListOptions) ([]swarm.Secret, error) { query := url.Values{} - if options.Filter.Len() > 0 { - filterJSON, err := filters.ToParam(options.Filter) + if options.Filters.Len() > 0 { + filterJSON, err := filters.ToParam(options.Filters) if err != nil { return nil, err } diff --git a/secret_list_test.go b/secret_list_test.go index 174963c7ee..1ac11cddb3 100644 --- a/secret_list_test.go +++ b/secret_list_test.go @@ -45,7 +45,7 @@ func TestSecretList(t *testing.T) { }, { options: types.SecretListOptions{ - Filter: filters, + Filters: filters, }, expectedQueryParams: map[string]string{ "filters": `{"label":{"label1":true,"label2":true}}`, From c941751fb2b629f7e48699b3983a74d0745aa491 Mon Sep 17 00:00:00 2001 From: John Howard Date: Wed, 9 Nov 2016 14:46:53 -0800 Subject: [PATCH 067/138] Tidy GetDockerOS() function Signed-off-by: John Howard --- container_stats.go | 2 +- image_build.go | 15 +-------------- image_build_test.go | 2 +- utils.go | 15 +++++++++++++++ 4 files changed, 18 insertions(+), 16 deletions(-) create mode 100644 utils.go diff --git a/container_stats.go b/container_stats.go index 3be7a988f4..4758c66e32 100644 --- a/container_stats.go +++ b/container_stats.go @@ -21,6 +21,6 @@ func (cli *Client) ContainerStats(ctx context.Context, containerID string, strea return types.ContainerStats{}, err } - osType := GetDockerOS(resp.header.Get("Server")) + osType := getDockerOS(resp.header.Get("Server")) return types.ContainerStats{Body: resp.body, OSType: osType}, err } diff --git a/image_build.go b/image_build.go index 0049e4e290..6fde75dcfd 100644 --- a/image_build.go +++ b/image_build.go @@ -6,7 +6,6 @@ import ( "io" "net/http" "net/url" - "regexp" "strconv" "golang.org/x/net/context" @@ -15,8 +14,6 @@ import ( "github.com/docker/docker/api/types/container" ) -var headerRegexp = regexp.MustCompile(`\ADocker/.+\s\((.+)\)\z`) - // ImageBuild sends request to the daemon to build images. // The Body in the response implement an io.ReadCloser and it's up to the caller to // close it. @@ -39,7 +36,7 @@ func (cli *Client) ImageBuild(ctx context.Context, buildContext io.Reader, optio return types.ImageBuildResponse{}, err } - osType := GetDockerOS(serverResp.header.Get("Server")) + osType := getDockerOS(serverResp.header.Get("Server")) return types.ImageBuildResponse{ Body: serverResp.body, @@ -124,13 +121,3 @@ func (cli *Client) imageBuildOptionsToQuery(options types.ImageBuildOptions) (ur return query, nil } - -// GetDockerOS returns the operating system based on the server header from the daemon. -func GetDockerOS(serverHeader string) string { - var osType string - matches := headerRegexp.FindStringSubmatch(serverHeader) - if len(matches) > 0 { - osType = matches[1] - } - return osType -} diff --git a/image_build_test.go b/image_build_test.go index 53dd93376a..ec0cbe2ee4 100644 --- a/image_build_test.go +++ b/image_build_test.go @@ -222,7 +222,7 @@ func TestGetDockerOS(t *testing.T) { "Foo/v1.22 (bar)": "", } for header, os := range cases { - g := GetDockerOS(header) + g := getDockerOS(header) if g != os { t.Fatalf("Expected %s, got %s", os, g) } diff --git a/utils.go b/utils.go new file mode 100644 index 0000000000..03bf4c82fa --- /dev/null +++ b/utils.go @@ -0,0 +1,15 @@ +package client + +import "regexp" + +var headerRegexp = regexp.MustCompile(`\ADocker/.+\s\((.+)\)\z`) + +// getDockerOS returns the operating system based on the server header from the daemon. +func getDockerOS(serverHeader string) string { + var osType string + matches := headerRegexp.FindStringSubmatch(serverHeader) + if len(matches) > 0 { + osType = matches[1] + } + return osType +} From 3d7a95829efba4f088ea8632d6fd1cdfbb5db366 Mon Sep 17 00:00:00 2001 From: Anusha Ragunathan Date: Tue, 4 Oct 2016 12:01:19 -0700 Subject: [PATCH 068/138] Add plugin create functionality. Signed-off-by: Anusha Ragunathan --- interface_experimental.go | 3 +++ plugin_create.go | 26 ++++++++++++++++++++++++++ 2 files changed, 29 insertions(+) create mode 100644 plugin_create.go diff --git a/interface_experimental.go b/interface_experimental.go index 4f5cf853b8..709b5d8ffb 100644 --- a/interface_experimental.go +++ b/interface_experimental.go @@ -1,6 +1,8 @@ package client import ( + "io" + "github.com/docker/docker/api/types" "golang.org/x/net/context" ) @@ -27,4 +29,5 @@ type PluginAPIClient interface { PluginPush(ctx context.Context, name string, registryAuth string) error PluginSet(ctx context.Context, name string, args []string) error PluginInspectWithRaw(ctx context.Context, name string) (*types.Plugin, []byte, error) + PluginCreate(ctx context.Context, createContext io.Reader, options types.PluginCreateOptions) error } diff --git a/plugin_create.go b/plugin_create.go new file mode 100644 index 0000000000..a660ba5733 --- /dev/null +++ b/plugin_create.go @@ -0,0 +1,26 @@ +package client + +import ( + "io" + "net/http" + "net/url" + + "github.com/docker/docker/api/types" + "golang.org/x/net/context" +) + +// PluginCreate creates a plugin +func (cli *Client) PluginCreate(ctx context.Context, createContext io.Reader, createOptions types.PluginCreateOptions) error { + headers := http.Header(make(map[string][]string)) + headers.Set("Content-Type", "application/tar") + + query := url.Values{} + query.Set("name", createOptions.RepoName) + + resp, err := cli.postRaw(ctx, "/plugins/create", query, createContext, headers) + if err != nil { + return err + } + ensureReaderClosed(resp) + return err +} From dd81022c2368e120c0ab4aca0d32e91b78048a11 Mon Sep 17 00:00:00 2001 From: Tonis Tiigi Date: Fri, 21 Oct 2016 18:07:55 -0700 Subject: [PATCH 069/138] Add support for swarm init lock and swarm unlock Signed-off-by: Tonis Tiigi --- interface.go | 1 + swarm_unlock.go | 17 +++++++++++++++++ 2 files changed, 18 insertions(+) create mode 100644 swarm_unlock.go diff --git a/interface.go b/interface.go index 49b66b1d17..d0834afa94 100644 --- a/interface.go +++ b/interface.go @@ -119,6 +119,7 @@ type ServiceAPIClient interface { type SwarmAPIClient interface { SwarmInit(ctx context.Context, req swarm.InitRequest) (string, error) SwarmJoin(ctx context.Context, req swarm.JoinRequest) error + SwarmUnlock(ctx context.Context, req swarm.UnlockRequest) error SwarmLeave(ctx context.Context, force bool) error SwarmInspect(ctx context.Context) (swarm.Swarm, error) SwarmUpdate(ctx context.Context, version swarm.Version, swarm swarm.Spec, flags swarm.UpdateFlags) error diff --git a/swarm_unlock.go b/swarm_unlock.go new file mode 100644 index 0000000000..addfb59f0a --- /dev/null +++ b/swarm_unlock.go @@ -0,0 +1,17 @@ +package client + +import ( + "github.com/docker/docker/api/types/swarm" + "golang.org/x/net/context" +) + +// SwarmUnlock unlockes locked swarm. +func (cli *Client) SwarmUnlock(ctx context.Context, req swarm.UnlockRequest) error { + serverResp, err := cli.post(ctx, "/swarm/unlock", nil, req, nil) + if err != nil { + return err + } + + ensureReaderClosed(serverResp) + return err +} From a8dc2ff916ff27a699b31353a4912a04a04af31e Mon Sep 17 00:00:00 2001 From: Aaron Lehmann Date: Thu, 27 Oct 2016 18:50:49 -0700 Subject: [PATCH 070/138] Revise swarm init/update flags, add unlocking capability - Neither swarm init or swarm update should take an unlock key - Add an autolock flag to turn on autolock - Make the necessary docker api changes - Add SwarmGetUnlockKey API call and use it when turning on autolock - Add swarm unlock-key subcommand Signed-off-by: Aaron Lehmann --- interface.go | 1 + swarm_get_unlock_key.go | 21 +++++++++++++++++++++ 2 files changed, 22 insertions(+) create mode 100644 swarm_get_unlock_key.go diff --git a/interface.go b/interface.go index d0834afa94..f24c9a51f6 100644 --- a/interface.go +++ b/interface.go @@ -119,6 +119,7 @@ type ServiceAPIClient interface { type SwarmAPIClient interface { SwarmInit(ctx context.Context, req swarm.InitRequest) (string, error) SwarmJoin(ctx context.Context, req swarm.JoinRequest) error + SwarmGetUnlockKey(ctx context.Context) (types.SwarmUnlockKeyResponse, error) SwarmUnlock(ctx context.Context, req swarm.UnlockRequest) error SwarmLeave(ctx context.Context, force bool) error SwarmInspect(ctx context.Context) (swarm.Swarm, error) diff --git a/swarm_get_unlock_key.go b/swarm_get_unlock_key.go new file mode 100644 index 0000000000..be28d32628 --- /dev/null +++ b/swarm_get_unlock_key.go @@ -0,0 +1,21 @@ +package client + +import ( + "encoding/json" + + "github.com/docker/docker/api/types" + "golang.org/x/net/context" +) + +// SwarmGetUnlockKey retrieves the swarm's unlock key. +func (cli *Client) SwarmGetUnlockKey(ctx context.Context) (types.SwarmUnlockKeyResponse, error) { + serverResp, err := cli.get(ctx, "/swarm/unlockkey", nil, nil) + if err != nil { + return types.SwarmUnlockKeyResponse{}, err + } + + var response types.SwarmUnlockKeyResponse + err = json.NewDecoder(serverResp.body).Decode(&response) + ensureReaderClosed(serverResp) + return response, err +} From de1b8f94399039109d34068eff875ebf3de27638 Mon Sep 17 00:00:00 2001 From: Aaron Lehmann Date: Fri, 28 Oct 2016 16:35:49 -0700 Subject: [PATCH 071/138] Add unlock key rotation Signed-off-by: Aaron Lehmann --- swarm_update.go | 1 + 1 file changed, 1 insertion(+) diff --git a/swarm_update.go b/swarm_update.go index f0be145ba2..cc8eeb6554 100644 --- a/swarm_update.go +++ b/swarm_update.go @@ -15,6 +15,7 @@ func (cli *Client) SwarmUpdate(ctx context.Context, version swarm.Version, swarm query.Set("version", strconv.FormatUint(version.Index, 10)) query.Set("rotateWorkerToken", fmt.Sprintf("%v", flags.RotateWorkerToken)) query.Set("rotateManagerToken", fmt.Sprintf("%v", flags.RotateManagerToken)) + query.Set("rotateManagerUnlockKey", fmt.Sprintf("%v", flags.RotateManagerUnlockKey)) resp, err := cli.post(ctx, "/swarm/update", query, swarm, nil) ensureReaderClosed(resp) return err From f88c041647f89d815e3289431dd50f5fafa2e9aa Mon Sep 17 00:00:00 2001 From: Andrea Luzzardi Date: Wed, 26 Oct 2016 01:17:31 -0700 Subject: [PATCH 072/138] api: Service Logs support Signed-off-by: Andrea Luzzardi --- interface.go | 1 + service_logs.go | 52 +++++++++++++++++ service_logs_test.go | 133 +++++++++++++++++++++++++++++++++++++++++++ 3 files changed, 186 insertions(+) create mode 100644 service_logs.go create mode 100644 service_logs_test.go diff --git a/interface.go b/interface.go index f24c9a51f6..883e8801fa 100644 --- a/interface.go +++ b/interface.go @@ -111,6 +111,7 @@ type ServiceAPIClient interface { ServiceList(ctx context.Context, options types.ServiceListOptions) ([]swarm.Service, error) ServiceRemove(ctx context.Context, serviceID string) error ServiceUpdate(ctx context.Context, serviceID string, version swarm.Version, service swarm.ServiceSpec, options types.ServiceUpdateOptions) error + ServiceLogs(ctx context.Context, serviceID string, options types.ContainerLogsOptions) (io.ReadCloser, error) TaskInspectWithRaw(ctx context.Context, taskID string) (swarm.Task, []byte, error) TaskList(ctx context.Context, options types.TaskListOptions) ([]swarm.Task, error) } diff --git a/service_logs.go b/service_logs.go new file mode 100644 index 0000000000..24384e3ec0 --- /dev/null +++ b/service_logs.go @@ -0,0 +1,52 @@ +package client + +import ( + "io" + "net/url" + "time" + + "golang.org/x/net/context" + + "github.com/docker/docker/api/types" + timetypes "github.com/docker/docker/api/types/time" +) + +// ServiceLogs returns the logs generated by a service in an io.ReadCloser. +// It's up to the caller to close the stream. +func (cli *Client) ServiceLogs(ctx context.Context, serviceID string, options types.ContainerLogsOptions) (io.ReadCloser, error) { + query := url.Values{} + if options.ShowStdout { + query.Set("stdout", "1") + } + + if options.ShowStderr { + query.Set("stderr", "1") + } + + if options.Since != "" { + ts, err := timetypes.GetTimestamp(options.Since, time.Now()) + if err != nil { + return nil, err + } + query.Set("since", ts) + } + + if options.Timestamps { + query.Set("timestamps", "1") + } + + if options.Details { + query.Set("details", "1") + } + + if options.Follow { + query.Set("follow", "1") + } + query.Set("tail", options.Tail) + + resp, err := cli.get(ctx, "/services/"+serviceID+"/logs", query, nil) + if err != nil { + return nil, err + } + return resp.body, nil +} diff --git a/service_logs_test.go b/service_logs_test.go new file mode 100644 index 0000000000..a6d002ba75 --- /dev/null +++ b/service_logs_test.go @@ -0,0 +1,133 @@ +package client + +import ( + "bytes" + "fmt" + "io" + "io/ioutil" + "log" + "net/http" + "os" + "strings" + "testing" + "time" + + "github.com/docker/docker/api/types" + + "golang.org/x/net/context" +) + +func TestServiceLogsError(t *testing.T) { + client := &Client{ + client: newMockClient(errorMock(http.StatusInternalServerError, "Server error")), + } + _, err := client.ServiceLogs(context.Background(), "service_id", types.ContainerLogsOptions{}) + if err == nil || err.Error() != "Error response from daemon: Server error" { + t.Fatalf("expected a Server Error, got %v", err) + } + _, err = client.ServiceLogs(context.Background(), "service_id", types.ContainerLogsOptions{ + Since: "2006-01-02TZ", + }) + if err == nil || !strings.Contains(err.Error(), `parsing time "2006-01-02TZ"`) { + t.Fatalf("expected a 'parsing time' error, got %v", err) + } +} + +func TestServiceLogs(t *testing.T) { + expectedURL := "/services/service_id/logs" + cases := []struct { + options types.ContainerLogsOptions + expectedQueryParams map[string]string + }{ + { + expectedQueryParams: map[string]string{ + "tail": "", + }, + }, + { + options: types.ContainerLogsOptions{ + Tail: "any", + }, + expectedQueryParams: map[string]string{ + "tail": "any", + }, + }, + { + options: types.ContainerLogsOptions{ + ShowStdout: true, + ShowStderr: true, + Timestamps: true, + Details: true, + Follow: true, + }, + expectedQueryParams: map[string]string{ + "tail": "", + "stdout": "1", + "stderr": "1", + "timestamps": "1", + "details": "1", + "follow": "1", + }, + }, + { + options: types.ContainerLogsOptions{ + // An complete invalid date, timestamp or go duration will be + // passed as is + Since: "invalid but valid", + }, + expectedQueryParams: map[string]string{ + "tail": "", + "since": "invalid but valid", + }, + }, + } + for _, logCase := range cases { + client := &Client{ + client: newMockClient(func(r *http.Request) (*http.Response, error) { + if !strings.HasPrefix(r.URL.Path, expectedURL) { + return nil, fmt.Errorf("Expected URL '%s', got '%s'", expectedURL, r.URL) + } + // Check query parameters + query := r.URL.Query() + for key, expected := range logCase.expectedQueryParams { + actual := query.Get(key) + if actual != expected { + return nil, fmt.Errorf("%s not set in URL query properly. Expected '%s', got %s", key, expected, actual) + } + } + return &http.Response{ + StatusCode: http.StatusOK, + Body: ioutil.NopCloser(bytes.NewReader([]byte("response"))), + }, nil + }), + } + body, err := client.ServiceLogs(context.Background(), "service_id", logCase.options) + if err != nil { + t.Fatal(err) + } + defer body.Close() + content, err := ioutil.ReadAll(body) + if err != nil { + t.Fatal(err) + } + if string(content) != "response" { + t.Fatalf("expected response to contain 'response', got %s", string(content)) + } + } +} + +func ExampleClient_ServiceLogs_withTimeout() { + ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second) + defer cancel() + + client, _ := NewEnvClient() + reader, err := client.ServiceLogs(ctx, "service_id", types.ContainerLogsOptions{}) + if err != nil { + log.Fatal(err) + } + + _, err = io.Copy(os.Stdout, reader) + if err != nil && err != io.EOF { + log.Fatal(err) + } +} From 1f6f5bec49e95981169c9713abe10b5ea6e4aaa1 Mon Sep 17 00:00:00 2001 From: Victor Vieux Date: Wed, 9 Nov 2016 17:49:09 -0800 Subject: [PATCH 073/138] move plugins out of experimental Signed-off-by: Victor Vieux --- interface.go | 14 ++++++++++++++ interface_experimental.go | 16 ---------------- 2 files changed, 14 insertions(+), 16 deletions(-) diff --git a/interface.go b/interface.go index 883e8801fa..7a3ebe8b4c 100644 --- a/interface.go +++ b/interface.go @@ -21,6 +21,7 @@ type CommonAPIClient interface { ImageAPIClient NodeAPIClient NetworkAPIClient + PluginAPIClient ServiceAPIClient SwarmAPIClient SecretAPIClient @@ -104,6 +105,19 @@ type NodeAPIClient interface { NodeUpdate(ctx context.Context, nodeID string, version swarm.Version, node swarm.NodeSpec) error } +// PluginAPIClient defines API client methods for the plugins +type PluginAPIClient interface { + PluginList(ctx context.Context) (types.PluginsListResponse, error) + PluginRemove(ctx context.Context, name string, options types.PluginRemoveOptions) error + PluginEnable(ctx context.Context, name string) error + PluginDisable(ctx context.Context, name string) error + PluginInstall(ctx context.Context, name string, options types.PluginInstallOptions) error + PluginPush(ctx context.Context, name string, registryAuth string) error + PluginSet(ctx context.Context, name string, args []string) error + PluginInspectWithRaw(ctx context.Context, name string) (*types.Plugin, []byte, error) + PluginCreate(ctx context.Context, createContext io.Reader, options types.PluginCreateOptions) error +} + // ServiceAPIClient defines API client methods for the services type ServiceAPIClient interface { ServiceCreate(ctx context.Context, service swarm.ServiceSpec, options types.ServiceCreateOptions) (types.ServiceCreateResponse, error) diff --git a/interface_experimental.go b/interface_experimental.go index 709b5d8ffb..51da98ecdd 100644 --- a/interface_experimental.go +++ b/interface_experimental.go @@ -1,15 +1,12 @@ package client import ( - "io" - "github.com/docker/docker/api/types" "golang.org/x/net/context" ) type apiClientExperimental interface { CheckpointAPIClient - PluginAPIClient } // CheckpointAPIClient defines API client methods for the checkpoints @@ -18,16 +15,3 @@ type CheckpointAPIClient interface { CheckpointDelete(ctx context.Context, container string, options types.CheckpointDeleteOptions) error CheckpointList(ctx context.Context, container string, options types.CheckpointListOptions) ([]types.Checkpoint, error) } - -// PluginAPIClient defines API client methods for the plugins -type PluginAPIClient interface { - PluginList(ctx context.Context) (types.PluginsListResponse, error) - PluginRemove(ctx context.Context, name string, options types.PluginRemoveOptions) error - PluginEnable(ctx context.Context, name string) error - PluginDisable(ctx context.Context, name string) error - PluginInstall(ctx context.Context, name string, options types.PluginInstallOptions) error - PluginPush(ctx context.Context, name string, registryAuth string) error - PluginSet(ctx context.Context, name string, args []string) error - PluginInspectWithRaw(ctx context.Context, name string) (*types.Plugin, []byte, error) - PluginCreate(ctx context.Context, createContext io.Reader, options types.PluginCreateOptions) error -} From 85e72de60c165ef682a8b5cee88188a85ca34261 Mon Sep 17 00:00:00 2001 From: Vincent Demeester Date: Fri, 11 Nov 2016 15:34:01 +0100 Subject: [PATCH 074/138] =?UTF-8?q?Add=20reference=20filter=20and=20deprec?= =?UTF-8?q?ated=20filter=20param=E2=80=A6?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit … for `docker images`. This deprecates the `filter` param for the `/images` endpoint and make a new filter called `reference` to replace it. It does change the CLI side (still possible to do `docker images busybox:musl`) but changes the cli code to use the filter instead (so that `docker images --filter busybox:musl` and `docker images busybox:musl` act the same). Signed-off-by: Vincent Demeester --- image_list.go | 4 ---- image_list_test.go | 11 ----------- 2 files changed, 15 deletions(-) diff --git a/image_list.go b/image_list.go index 6ebb460541..63c71b1dd1 100644 --- a/image_list.go +++ b/image_list.go @@ -21,10 +21,6 @@ func (cli *Client) ImageList(ctx context.Context, options types.ImageListOptions } query.Set("filters", filterJSON) } - if options.MatchName != "" { - // FIXME rename this parameter, to not be confused with the filters flag - query.Set("filter", options.MatchName) - } if options.All { query.Set("all", "1") } diff --git a/image_list_test.go b/image_list_test.go index 1ea6f1f05a..1c9406ddda 100644 --- a/image_list_test.go +++ b/image_list_test.go @@ -48,17 +48,6 @@ func TestImageList(t *testing.T) { "filters": "", }, }, - { - options: types.ImageListOptions{ - All: true, - MatchName: "image_name", - }, - expectedQueryParams: map[string]string{ - "all": "1", - "filter": "image_name", - "filters": "", - }, - }, { options: types.ImageListOptions{ Filters: filters, From 2eb3e2ce0ffb45fbb3698b8bb618ec90f73f6890 Mon Sep 17 00:00:00 2001 From: wefine Date: Mon, 14 Nov 2016 17:01:17 +0800 Subject: [PATCH 075/138] fix t.Errorf to t.Error in serveral _test.go Signed-off-by: wefine --- client_test.go | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/client_test.go b/client_test.go index ee199c2bec..3a6575c9cc 100644 --- a/client_test.go +++ b/client_test.go @@ -102,11 +102,11 @@ func TestNewEnvClient(t *testing.T) { // pedantic checking that this is handled correctly tr := apiclient.client.Transport.(*http.Transport) if tr.TLSClientConfig == nil { - t.Errorf("no tls config found when DOCKER_TLS_VERIFY enabled") + t.Error("no tls config found when DOCKER_TLS_VERIFY enabled") } if tr.TLSClientConfig.InsecureSkipVerify { - t.Errorf("tls verification should be enabled") + t.Error("tls verification should be enabled") } } From b5b095c01a74c0de4808a24d3a460fe45bcfb77a Mon Sep 17 00:00:00 2001 From: John Howard Date: Fri, 11 Nov 2016 11:51:26 -0800 Subject: [PATCH 076/138] Bump API to v1.26 Signed-off-by: John Howard --- client.go | 4 ++-- request.go | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/client.go b/client.go index 76a1ac07c0..814c537c65 100644 --- a/client.go +++ b/client.go @@ -3,7 +3,7 @@ Package client is a Go client for the Docker Remote API. The "docker" command uses this package to communicate with the daemon. It can also be used by your own Go applications to do anything the command-line interface does -– running containers, pulling images, managing swarms, etc. +- running containers, pulling images, managing swarms, etc. For more information about the Remote API, see the documentation: https://docs.docker.com/engine/reference/api/docker_remote_api/ @@ -58,7 +58,7 @@ import ( ) // DefaultVersion is the version of the current stable API -const DefaultVersion string = "1.25" +const DefaultVersion string = "1.26" // Client is the API client that performs all operations // against a docker server. diff --git a/request.go b/request.go index ac05363655..f15e380339 100644 --- a/request.go +++ b/request.go @@ -165,7 +165,7 @@ func (cli *Client) doRequest(ctx context.Context, req *http.Request) (serverResp // daemon on Windows where the daemon is listening on a named pipe // `//./pipe/docker_engine, and the client must be running elevated. // Give users a clue rather than the not-overly useful message - // such as `error during connect: Get http://%2F%2F.%2Fpipe%2Fdocker_engine/v1.25/info: + // such as `error during connect: Get http://%2F%2F.%2Fpipe%2Fdocker_engine/v1.26/info: // open //./pipe/docker_engine: The system cannot find the file specified.`. // Note we can't string compare "The system cannot find the file specified" as // this is localised - for example in French the error would be From 4749582510fdda9189ecb582b131945fb1b1297b Mon Sep 17 00:00:00 2001 From: Anusha Ragunathan Date: Wed, 16 Nov 2016 14:42:46 -0800 Subject: [PATCH 077/138] Cleanup after plugin install. During error cases, we dont cleanup correctly. This commit takes care of removing the plugin, if there are errors after the pull passed. It also shuts down the plugin, if there are errors after the plugin in the enable path. Signed-off-by: Anusha Ragunathan --- plugin_install.go | 12 +++++++++--- 1 file changed, 9 insertions(+), 3 deletions(-) diff --git a/plugin_install.go b/plugin_install.go index d0a3d517fc..407f1cddf2 100644 --- a/plugin_install.go +++ b/plugin_install.go @@ -10,7 +10,7 @@ import ( ) // PluginInstall installs a plugin -func (cli *Client) PluginInstall(ctx context.Context, name string, options types.PluginInstallOptions) error { +func (cli *Client) PluginInstall(ctx context.Context, name string, options types.PluginInstallOptions) (err error) { // FIXME(vdemeester) name is a ref, we might want to parse/validate it here. query := url.Values{} query.Set("name", name) @@ -27,6 +27,14 @@ func (cli *Client) PluginInstall(ctx context.Context, name string, options types ensureReaderClosed(resp) return err } + + defer func() { + if err != nil { + delResp, _ := cli.delete(ctx, "/plugins/"+name, nil, nil) + ensureReaderClosed(delResp) + } + }() + var privileges types.PluginPrivileges if err := json.NewDecoder(resp.body).Decode(&privileges); err != nil { ensureReaderClosed(resp) @@ -40,8 +48,6 @@ func (cli *Client) PluginInstall(ctx context.Context, name string, options types return err } if !accept { - resp, _ := cli.delete(ctx, "/plugins/"+name, nil, nil) - ensureReaderClosed(resp) return pluginPermissionDenied{name} } } From 42788cad9c1a56fe3eb613437d8a561b57032074 Mon Sep 17 00:00:00 2001 From: allencloud Date: Thu, 17 Nov 2016 10:51:16 +0800 Subject: [PATCH 078/138] update secret create url for consistency Signed-off-by: allencloud --- secret_create.go | 2 +- secret_create_test.go | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/secret_create.go b/secret_create.go index f92a3d1510..de8b041567 100644 --- a/secret_create.go +++ b/secret_create.go @@ -13,7 +13,7 @@ func (cli *Client) SecretCreate(ctx context.Context, secret swarm.SecretSpec) (t var headers map[string][]string var response types.SecretCreateResponse - resp, err := cli.post(ctx, "/secrets", nil, secret, headers) + resp, err := cli.post(ctx, "/secrets/create", nil, secret, headers) if err != nil { return response, err } diff --git a/secret_create_test.go b/secret_create_test.go index b7def89d0e..cb378c77ff 100644 --- a/secret_create_test.go +++ b/secret_create_test.go @@ -25,7 +25,7 @@ func TestSecretCreateError(t *testing.T) { } func TestSecretCreate(t *testing.T) { - expectedURL := "/secrets" + expectedURL := "/secrets/create" client := &Client{ client: newMockClient(func(req *http.Request) (*http.Response, error) { if !strings.HasPrefix(req.URL.Path, expectedURL) { @@ -41,7 +41,7 @@ func TestSecretCreate(t *testing.T) { return nil, err } return &http.Response{ - StatusCode: http.StatusOK, + StatusCode: http.StatusCreated, Body: ioutil.NopCloser(bytes.NewReader(b)), }, nil }), From e98be4c62f80ba0d240616ce1cdcd15ac3d30561 Mon Sep 17 00:00:00 2001 From: lixiaobing10051267 Date: Thu, 17 Nov 2016 15:50:38 +0800 Subject: [PATCH 079/138] expected new_container_id while testing ContainerCommit Signed-off-by: lixiaobing10051267 --- container_commit_test.go | 2 +- container_copy_test.go | 2 +- container_inspect_test.go | 8 ++++---- image_import_test.go | 2 +- network_inspect_test.go | 2 +- plugin_remove_test.go | 2 +- 6 files changed, 9 insertions(+), 9 deletions(-) diff --git a/container_commit_test.go b/container_commit_test.go index a844675368..6947ed3861 100644 --- a/container_commit_test.go +++ b/container_commit_test.go @@ -91,6 +91,6 @@ func TestContainerCommit(t *testing.T) { t.Fatal(err) } if r.ID != "new_container_id" { - t.Fatalf("expected `container_id`, got %s", r.ID) + t.Fatalf("expected `new_container_id`, got %s", r.ID) } } diff --git a/container_copy_test.go b/container_copy_test.go index 7eded611fd..6863cfba20 100644 --- a/container_copy_test.go +++ b/container_copy_test.go @@ -195,7 +195,7 @@ func TestCopyFromContainer(t *testing.T) { return nil, fmt.Errorf("Expected URL '%s', got '%s'", expectedURL, req.URL) } if req.Method != "GET" { - return nil, fmt.Errorf("expected PUT method, got %s", req.Method) + return nil, fmt.Errorf("expected GET method, got %s", req.Method) } query := req.URL.Query() path := query.Get("path") diff --git a/container_inspect_test.go b/container_inspect_test.go index f1a6f4ac7d..98f83bd8db 100644 --- a/container_inspect_test.go +++ b/container_inspect_test.go @@ -67,10 +67,10 @@ func TestContainerInspect(t *testing.T) { t.Fatalf("expected `container_id`, got %s", r.ID) } if r.Image != "image" { - t.Fatalf("expected `image`, got %s", r.ID) + t.Fatalf("expected `image`, got %s", r.Image) } if r.Name != "name" { - t.Fatalf("expected `name`, got %s", r.ID) + t.Fatalf("expected `name`, got %s", r.Name) } } @@ -107,10 +107,10 @@ func TestContainerInspectNode(t *testing.T) { t.Fatalf("expected `container_id`, got %s", r.ID) } if r.Image != "image" { - t.Fatalf("expected `image`, got %s", r.ID) + t.Fatalf("expected `image`, got %s", r.Image) } if r.Name != "name" { - t.Fatalf("expected `name`, got %s", r.ID) + t.Fatalf("expected `name`, got %s", r.Name) } if r.Node.ID != "container_node_id" { t.Fatalf("expected `container_node_id`, got %s", r.Node.ID) diff --git a/image_import_test.go b/image_import_test.go index e309be74e6..370ad5fbed 100644 --- a/image_import_test.go +++ b/image_import_test.go @@ -37,7 +37,7 @@ func TestImageImport(t *testing.T) { } repo := query.Get("repo") if repo != "repository_name:imported" { - return nil, fmt.Errorf("repo not set in URL query properly. Expected 'repository_name', got %s", repo) + return nil, fmt.Errorf("repo not set in URL query properly. Expected 'repository_name:imported', got %s", repo) } tag := query.Get("tag") if tag != "imported" { diff --git a/network_inspect_test.go b/network_inspect_test.go index 1f926d66ba..55f04eca2c 100644 --- a/network_inspect_test.go +++ b/network_inspect_test.go @@ -31,7 +31,7 @@ func TestNetworkInspectContainerNotFound(t *testing.T) { _, err := client.NetworkInspect(context.Background(), "unknown") if err == nil || !IsErrNetworkNotFound(err) { - t.Fatalf("expected a containerNotFound error, got %v", err) + t.Fatalf("expected a networkNotFound error, got %v", err) } } diff --git a/plugin_remove_test.go b/plugin_remove_test.go index a15f1661f6..b2d515793a 100644 --- a/plugin_remove_test.go +++ b/plugin_remove_test.go @@ -33,7 +33,7 @@ func TestPluginRemove(t *testing.T) { return nil, fmt.Errorf("Expected URL '%s', got '%s'", expectedURL, req.URL) } if req.Method != "DELETE" { - return nil, fmt.Errorf("expected POST method, got %s", req.Method) + return nil, fmt.Errorf("expected DELETE method, got %s", req.Method) } return &http.Response{ StatusCode: http.StatusOK, From b58a973b1820673b328e89514b26df2bb358f016 Mon Sep 17 00:00:00 2001 From: Aaron Lehmann Date: Mon, 14 Nov 2016 18:08:24 -0800 Subject: [PATCH 080/138] Return warnings from service create and service update when digest pinning fails Modify the service update and create APIs to return optional warning messages as part of the response. Populate these messages with an informative reason when digest resolution fails. This is a small API change, but significantly improves the UX. The user can now get immediate feedback when they've specified a nonexistent image or unreachable registry. Signed-off-by: Aaron Lehmann --- interface.go | 2 +- service_update.go | 11 +++++++++-- service_update_test.go | 6 +++--- 3 files changed, 13 insertions(+), 6 deletions(-) diff --git a/interface.go b/interface.go index 7a3ebe8b4c..d46720e6c7 100644 --- a/interface.go +++ b/interface.go @@ -124,7 +124,7 @@ type ServiceAPIClient interface { ServiceInspectWithRaw(ctx context.Context, serviceID string) (swarm.Service, []byte, error) ServiceList(ctx context.Context, options types.ServiceListOptions) ([]swarm.Service, error) ServiceRemove(ctx context.Context, serviceID string) error - ServiceUpdate(ctx context.Context, serviceID string, version swarm.Version, service swarm.ServiceSpec, options types.ServiceUpdateOptions) error + ServiceUpdate(ctx context.Context, serviceID string, version swarm.Version, service swarm.ServiceSpec, options types.ServiceUpdateOptions) (types.ServiceUpdateResponse, error) ServiceLogs(ctx context.Context, serviceID string, options types.ContainerLogsOptions) (io.ReadCloser, error) TaskInspectWithRaw(ctx context.Context, taskID string) (swarm.Task, []byte, error) TaskList(ctx context.Context, options types.TaskListOptions) ([]swarm.Task, error) diff --git a/service_update.go b/service_update.go index 8e03f7f483..afa94d47e2 100644 --- a/service_update.go +++ b/service_update.go @@ -1,6 +1,7 @@ package client import ( + "encoding/json" "net/url" "strconv" @@ -10,7 +11,7 @@ import ( ) // ServiceUpdate updates a Service. -func (cli *Client) ServiceUpdate(ctx context.Context, serviceID string, version swarm.Version, service swarm.ServiceSpec, options types.ServiceUpdateOptions) error { +func (cli *Client) ServiceUpdate(ctx context.Context, serviceID string, version swarm.Version, service swarm.ServiceSpec, options types.ServiceUpdateOptions) (types.ServiceUpdateResponse, error) { var ( headers map[string][]string query = url.Values{} @@ -28,7 +29,13 @@ func (cli *Client) ServiceUpdate(ctx context.Context, serviceID string, version query.Set("version", strconv.FormatUint(version.Index, 10)) + var response types.ServiceUpdateResponse resp, err := cli.post(ctx, "/services/"+serviceID+"/update", query, service, headers) + if err != nil { + return response, err + } + + err = json.NewDecoder(resp.body).Decode(&response) ensureReaderClosed(resp) - return err + return response, err } diff --git a/service_update_test.go b/service_update_test.go index 081649f492..76bea176bf 100644 --- a/service_update_test.go +++ b/service_update_test.go @@ -19,7 +19,7 @@ func TestServiceUpdateError(t *testing.T) { client: newMockClient(errorMock(http.StatusInternalServerError, "Server error")), } - err := client.ServiceUpdate(context.Background(), "service_id", swarm.Version{}, swarm.ServiceSpec{}, types.ServiceUpdateOptions{}) + _, err := client.ServiceUpdate(context.Background(), "service_id", swarm.Version{}, swarm.ServiceSpec{}, types.ServiceUpdateOptions{}) if err == nil || err.Error() != "Error response from daemon: Server error" { t.Fatalf("expected a Server Error, got %v", err) } @@ -64,12 +64,12 @@ func TestServiceUpdate(t *testing.T) { } return &http.Response{ StatusCode: http.StatusOK, - Body: ioutil.NopCloser(bytes.NewReader([]byte("body"))), + Body: ioutil.NopCloser(bytes.NewReader([]byte("{}"))), }, nil }), } - err := client.ServiceUpdate(context.Background(), "service_id", updateCase.swarmVersion, swarm.ServiceSpec{}, types.ServiceUpdateOptions{}) + _, err := client.ServiceUpdate(context.Background(), "service_id", updateCase.swarmVersion, swarm.ServiceSpec{}, types.ServiceUpdateOptions{}) if err != nil { t.Fatal(err) } From b4fe4fb42b56ba0d985fbbf074f8a9127385f09e Mon Sep 17 00:00:00 2001 From: Antonio Murdaca Date: Wed, 16 Nov 2016 22:30:29 +0100 Subject: [PATCH 081/138] api: types: keep info.SecurityOptions a string slice Signed-off-by: Antonio Murdaca --- info_test.go | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/info_test.go b/info_test.go index 7af82a8a31..79f23c8af2 100644 --- a/info_test.go +++ b/info_test.go @@ -46,10 +46,8 @@ func TestInfo(t *testing.T) { return nil, fmt.Errorf("Expected URL '%s', got '%s'", expectedURL, req.URL) } info := &types.Info{ - InfoBase: &types.InfoBase{ - ID: "daemonID", - Containers: 3, - }, + ID: "daemonID", + Containers: 3, } b, err := json.Marshal(info) if err != nil { From 20ded0afd962de543f5ee8fd42c76fe3e49d4281 Mon Sep 17 00:00:00 2001 From: Zhang Wei Date: Mon, 21 Nov 2016 17:31:46 +0800 Subject: [PATCH 082/138] Bugfix: set cli.manualOverride when env var not empty If env var "DOCKER_API_VERSION" is specified by user, we'll set `cli.manualOverride`, before this, this field is always true due to wrong logic. Signed-off-by: Zhang Wei --- client.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/client.go b/client.go index 814c537c65..a85b392674 100644 --- a/client.go +++ b/client.go @@ -122,7 +122,7 @@ func NewEnvClient() (*Client, error) { if err != nil { return cli, err } - if version != "" { + if os.Getenv("DOCKER_API_VERSION") != "" { cli.manualOverride = true } return cli, nil From 43e89b53879f179427fa492193cb7a2a8f12867e Mon Sep 17 00:00:00 2001 From: Anusha Ragunathan Date: Mon, 21 Nov 2016 09:24:01 -0800 Subject: [PATCH 083/138] Add HTTP client timeout. Signed-off-by: Anusha Ragunathan --- interface.go | 2 +- plugin_enable.go | 11 +++++++++-- plugin_enable_test.go | 5 +++-- plugin_install.go | 2 +- 4 files changed, 14 insertions(+), 6 deletions(-) diff --git a/interface.go b/interface.go index d46720e6c7..0d722d9075 100644 --- a/interface.go +++ b/interface.go @@ -109,7 +109,7 @@ type NodeAPIClient interface { type PluginAPIClient interface { PluginList(ctx context.Context) (types.PluginsListResponse, error) PluginRemove(ctx context.Context, name string, options types.PluginRemoveOptions) error - PluginEnable(ctx context.Context, name string) error + PluginEnable(ctx context.Context, name string, options types.PluginEnableOptions) error PluginDisable(ctx context.Context, name string) error PluginInstall(ctx context.Context, name string, options types.PluginInstallOptions) error PluginPush(ctx context.Context, name string, registryAuth string) error diff --git a/plugin_enable.go b/plugin_enable.go index 8109814ddb..95517c4b80 100644 --- a/plugin_enable.go +++ b/plugin_enable.go @@ -1,12 +1,19 @@ package client import ( + "net/url" + "strconv" + + "github.com/docker/docker/api/types" "golang.org/x/net/context" ) // PluginEnable enables a plugin -func (cli *Client) PluginEnable(ctx context.Context, name string) error { - resp, err := cli.post(ctx, "/plugins/"+name+"/enable", nil, nil, nil) +func (cli *Client) PluginEnable(ctx context.Context, name string, options types.PluginEnableOptions) error { + query := url.Values{} + query.Set("timeout", strconv.Itoa(options.Timeout)) + + resp, err := cli.post(ctx, "/plugins/"+name+"/enable", query, nil, nil) ensureReaderClosed(resp) return err } diff --git a/plugin_enable_test.go b/plugin_enable_test.go index d919914e75..b27681348f 100644 --- a/plugin_enable_test.go +++ b/plugin_enable_test.go @@ -8,6 +8,7 @@ import ( "strings" "testing" + "github.com/docker/docker/api/types" "golang.org/x/net/context" ) @@ -16,7 +17,7 @@ func TestPluginEnableError(t *testing.T) { client: newMockClient(errorMock(http.StatusInternalServerError, "Server error")), } - err := client.PluginEnable(context.Background(), "plugin_name") + err := client.PluginEnable(context.Background(), "plugin_name", types.PluginEnableOptions{}) if err == nil || err.Error() != "Error response from daemon: Server error" { t.Fatalf("expected a Server Error, got %v", err) } @@ -40,7 +41,7 @@ func TestPluginEnable(t *testing.T) { }), } - err := client.PluginEnable(context.Background(), "plugin_name") + err := client.PluginEnable(context.Background(), "plugin_name", types.PluginEnableOptions{}) if err != nil { t.Fatal(err) } diff --git a/plugin_install.go b/plugin_install.go index 407f1cddf2..f73362ccd3 100644 --- a/plugin_install.go +++ b/plugin_install.go @@ -62,7 +62,7 @@ func (cli *Client) PluginInstall(ctx context.Context, name string, options types return nil } - return cli.PluginEnable(ctx, name) + return cli.PluginEnable(ctx, name, types.PluginEnableOptions{Timeout: 0}) } func (cli *Client) tryPluginPull(ctx context.Context, query url.Values, registryAuth string) (serverResponse, error) { From 4e4541540ff600ae666c6633e3262033c156ad00 Mon Sep 17 00:00:00 2001 From: yupeng Date: Mon, 21 Nov 2016 17:08:28 +0800 Subject: [PATCH 084/138] First header should be a top level header Signed-off-by: yupeng --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 2b7d81fada..161686c0a6 100644 --- a/README.md +++ b/README.md @@ -1,4 +1,4 @@ -## Go client for the Docker Remote API +# Go client for the Docker Remote API The `docker` command uses this package to communicate with the daemon. It can also be used by your own Go applications to do anything the command-line interface does – running containers, pulling images, managing swarms, etc. From b35205ed1284dcc2237e48e4f2a4d915e779b848 Mon Sep 17 00:00:00 2001 From: Reficul Date: Tue, 22 Nov 2016 10:42:55 +0800 Subject: [PATCH 085/138] fix incorrect ErrConnectFailed comparison Signed-off-by: Reficul --- errors.go | 24 ++++++++++++++++++++---- 1 file changed, 20 insertions(+), 4 deletions(-) diff --git a/errors.go b/errors.go index 94c22a728a..854516669c 100644 --- a/errors.go +++ b/errors.go @@ -1,18 +1,34 @@ package client import ( - "errors" "fmt" "github.com/docker/docker/api/types/versions" + "github.com/pkg/errors" ) -// ErrConnectionFailed is an error raised when the connection between the client and the server failed. -var ErrConnectionFailed = errors.New("Cannot connect to the Docker daemon. Is the docker daemon running on this host?") +// errConnectionFailed implements an error returned when connection failed. +type errConnectionFailed struct { + host string +} + +// Error returns a string representation of an errConnectionFailed +func (err errConnectionFailed) Error() string { + if err.host == "" { + return "Cannot connect to the Docker daemon. Is the docker daemon running on this host?" + } + return fmt.Sprintf("Cannot connect to the Docker daemon at %s. Is the docker daemon running?", err.host) +} + +// IsErrConnectionFailed returns true if the error is caused by connection failed. +func IsErrConnectionFailed(err error) bool { + _, ok := errors.Cause(err).(errConnectionFailed) + return ok +} // ErrorConnectionFailed returns an error with host in the error message when connection to docker daemon failed. func ErrorConnectionFailed(host string) error { - return fmt.Errorf("Cannot connect to the Docker daemon at %s. Is the docker daemon running?", host) + return errConnectionFailed{host: host} } type notFound interface { From 9c9ae79e64a7b6c83c16cc89cc021cf19b41f7f7 Mon Sep 17 00:00:00 2001 From: Ben Firshman Date: Tue, 15 Nov 2016 19:45:20 +0000 Subject: [PATCH 086/138] Rename Remote API to Engine API Implementation of https://github.com/docker/docker/issues/28319 Signed-off-by: Ben Firshman --- README.md | 2 +- client.go | 6 +++--- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/README.md b/README.md index 161686c0a6..059dfb3ce7 100644 --- a/README.md +++ b/README.md @@ -1,4 +1,4 @@ -# Go client for the Docker Remote API +# Go client for the Docker Engine API The `docker` command uses this package to communicate with the daemon. It can also be used by your own Go applications to do anything the command-line interface does – running containers, pulling images, managing swarms, etc. diff --git a/client.go b/client.go index a85b392674..31a311e7d5 100644 --- a/client.go +++ b/client.go @@ -1,12 +1,12 @@ /* -Package client is a Go client for the Docker Remote API. +Package client is a Go client for the Docker Engine API. The "docker" command uses this package to communicate with the daemon. It can also be used by your own Go applications to do anything the command-line interface does - running containers, pulling images, managing swarms, etc. -For more information about the Remote API, see the documentation: -https://docs.docker.com/engine/reference/api/docker_remote_api/ +For more information about the Engine API, see the documentation: +https://docs.docker.com/engine/reference/api/ Usage From 32f410cd353d50e1c0546e9f6255df8d0d52a078 Mon Sep 17 00:00:00 2001 From: Vincent Demeester Date: Mon, 28 Nov 2016 22:15:50 +0100 Subject: [PATCH 087/138] Fixes ImageList to be retro-compatible with older API Make sure current client code can talk for ImageList can still talk to older daemon. Signed-off-by: Vincent Demeester --- image_list.go | 13 +++++++++++-- image_list_test.go | 48 ++++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 59 insertions(+), 2 deletions(-) diff --git a/image_list.go b/image_list.go index 63c71b1dd1..f26464f67c 100644 --- a/image_list.go +++ b/image_list.go @@ -6,6 +6,7 @@ import ( "github.com/docker/docker/api/types" "github.com/docker/docker/api/types/filters" + "github.com/docker/docker/api/types/versions" "golang.org/x/net/context" ) @@ -14,8 +15,16 @@ func (cli *Client) ImageList(ctx context.Context, options types.ImageListOptions var images []types.ImageSummary query := url.Values{} - if options.Filters.Len() > 0 { - filterJSON, err := filters.ToParamWithVersion(cli.version, options.Filters) + optionFilters := options.Filters + referenceFilters := optionFilters.Get("reference") + if versions.LessThan(cli.version, "1.25") && len(referenceFilters) > 0 { + query.Set("filter", referenceFilters[0]) + for _, filterValue := range referenceFilters { + optionFilters.Del("reference", filterValue) + } + } + if optionFilters.Len() > 0 { + filterJSON, err := filters.ToParamWithVersion(cli.version, optionFilters) if err != nil { return images, err } diff --git a/image_list_test.go b/image_list_test.go index 1c9406ddda..7c4a46414d 100644 --- a/image_list_test.go +++ b/image_list_test.go @@ -109,3 +109,51 @@ func TestImageList(t *testing.T) { } } } + +func TestImageListApiBefore125(t *testing.T) { + expectedFilter := "image:tag" + client := &Client{ + client: newMockClient(func(req *http.Request) (*http.Response, error) { + query := req.URL.Query() + actualFilter := query.Get("filter") + if actualFilter != expectedFilter { + return nil, fmt.Errorf("filter not set in URL query properly. Expected '%s', got %s", expectedFilter, actualFilter) + } + actualFilters := query.Get("filters") + if actualFilters != "" { + return nil, fmt.Errorf("filters should have not been present, were with value: %s", actualFilters) + } + content, err := json.Marshal([]types.ImageSummary{ + { + ID: "image_id2", + }, + { + ID: "image_id2", + }, + }) + if err != nil { + return nil, err + } + return &http.Response{ + StatusCode: http.StatusOK, + Body: ioutil.NopCloser(bytes.NewReader(content)), + }, nil + }), + version: "1.24", + } + + filters := filters.NewArgs() + filters.Add("reference", "image:tag") + + options := types.ImageListOptions{ + Filters: filters, + } + + images, err := client.ImageList(context.Background(), options) + if err != nil { + t.Fatal(err) + } + if len(images) != 2 { + t.Fatalf("expected 2 images, got %v", images) + } +} From 232944cc1531c4a0377a960dccdf8a4b263589d3 Mon Sep 17 00:00:00 2001 From: Akihiro Suda Date: Fri, 21 Oct 2016 05:41:54 +0000 Subject: [PATCH 088/138] client: add accessor methods for client.customHTTPHeaders Added two methods: - *Client.CustomHTTPHeaders() map[string]string - *Client.SetCustomHTTPHeaders(headers map[string]string) Signed-off-by: Akihiro Suda --- client.go | 19 ++++++++++++++++++- 1 file changed, 18 insertions(+), 1 deletion(-) diff --git a/client.go b/client.go index 31a311e7d5..4c0f097e53 100644 --- a/client.go +++ b/client.go @@ -212,12 +212,13 @@ func (cli *Client) getAPIPath(p string, query url.Values) string { // ClientVersion returns the version string associated with this // instance of the Client. Note that this value can be changed // via the DOCKER_API_VERSION env var. +// This operation doesn't acquire a mutex. func (cli *Client) ClientVersion() string { return cli.version } // UpdateClientVersion updates the version string associated with this -// instance of the Client. +// instance of the Client. This operation doesn't acquire a mutex. func (cli *Client) UpdateClientVersion(v string) { if !cli.manualOverride { cli.version = v @@ -244,3 +245,19 @@ func ParseHost(host string) (string, string, string, error) { } return proto, addr, basePath, nil } + +// CustomHTTPHeaders returns the custom http headers associated with this +// instance of the Client. This operation doesn't acquire a mutex. +func (cli *Client) CustomHTTPHeaders() map[string]string { + m := make(map[string]string) + for k, v := range cli.customHTTPHeaders { + m[k] = v + } + return m +} + +// SetCustomHTTPHeaders updates the custom http headers associated with this +// instance of the Client. This operation doesn't acquire a mutex. +func (cli *Client) SetCustomHTTPHeaders(headers map[string]string) { + cli.customHTTPHeaders = headers +} From 9a9c077e6340b65fc23406e1a71d916fe4876dbb Mon Sep 17 00:00:00 2001 From: yuexiao-wang Date: Fri, 2 Dec 2016 03:32:04 +0800 Subject: [PATCH 089/138] Optimize the log info for client test Signed-off-by: yuexiao-wang --- container_copy_test.go | 8 ++++---- image_search_test.go | 8 ++++---- plugin_push_test.go | 2 +- 3 files changed, 9 insertions(+), 9 deletions(-) diff --git a/container_copy_test.go b/container_copy_test.go index 6863cfba20..c84f82e9fb 100644 --- a/container_copy_test.go +++ b/container_copy_test.go @@ -78,10 +78,10 @@ func TestContainerStatPath(t *testing.T) { t.Fatal(err) } if stat.Name != "name" { - t.Fatalf("expected container path stat name to be 'name', was '%s'", stat.Name) + t.Fatalf("expected container path stat name to be 'name', got '%s'", stat.Name) } if stat.Mode != 0700 { - t.Fatalf("expected container path stat mode to be 0700, was '%v'", stat.Mode) + t.Fatalf("expected container path stat mode to be 0700, got '%v'", stat.Mode) } } @@ -226,10 +226,10 @@ func TestCopyFromContainer(t *testing.T) { t.Fatal(err) } if stat.Name != "name" { - t.Fatalf("expected container path stat name to be 'name', was '%s'", stat.Name) + t.Fatalf("expected container path stat name to be 'name', got '%s'", stat.Name) } if stat.Mode != 0700 { - t.Fatalf("expected container path stat mode to be 0700, was '%v'", stat.Mode) + t.Fatalf("expected container path stat mode to be 0700, got '%v'", stat.Mode) } content, err := ioutil.ReadAll(r) if err != nil { diff --git a/image_search_test.go b/image_search_test.go index e46d86437f..108bd96744 100644 --- a/image_search_test.go +++ b/image_search_test.go @@ -81,12 +81,12 @@ func TestImageSearchWithPrivilegedFuncNoError(t *testing.T) { }, nil } if auth != "IAmValid" { - return nil, fmt.Errorf("Invalid auth header : expected %s, got %s", "IAmValid", auth) + return nil, fmt.Errorf("Invalid auth header : expected 'IAmValid', got %s", auth) } query := req.URL.Query() term := query.Get("term") if term != "some-image" { - return nil, fmt.Errorf("tag not set in URL query properly. Expected '%s', got %s", "some-image", term) + return nil, fmt.Errorf("term not set in URL query properly. Expected 'some-image', got %s", term) } content, err := json.Marshal([]registry.SearchResult{ { @@ -113,7 +113,7 @@ func TestImageSearchWithPrivilegedFuncNoError(t *testing.T) { t.Fatal(err) } if len(results) != 1 { - t.Fatalf("expected a result, got %v", results) + t.Fatalf("expected 1 result, got %v", results) } } @@ -133,7 +133,7 @@ func TestImageSearchWithoutErrors(t *testing.T) { query := req.URL.Query() term := query.Get("term") if term != "some-image" { - return nil, fmt.Errorf("tag not set in URL query properly. Expected '%s', got %s", "some-image", term) + return nil, fmt.Errorf("term not set in URL query properly. Expected 'some-image', got %s", term) } filters := query.Get("filters") if filters != expectedFilters { diff --git a/plugin_push_test.go b/plugin_push_test.go index efdbdc6db1..7b8eb865d6 100644 --- a/plugin_push_test.go +++ b/plugin_push_test.go @@ -35,7 +35,7 @@ func TestPluginPush(t *testing.T) { } auth := req.Header.Get("X-Registry-Auth") if auth != "authtoken" { - return nil, fmt.Errorf("Invalid auth header : expected %s, got %s", "authtoken", auth) + return nil, fmt.Errorf("Invalid auth header : expected 'authtoken', got %s", auth) } return &http.Response{ StatusCode: http.StatusOK, From 7673aad2234349cfc884390ed72881e644a9e0b3 Mon Sep 17 00:00:00 2001 From: yuexiao-wang Date: Fri, 2 Dec 2016 04:18:02 +0800 Subject: [PATCH 090/138] Fix the inconsistent function name for client Signed-off-by: yuexiao-wang --- login.go | 2 +- request.go | 6 +++--- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/login.go b/login.go index 600dc7196f..79219ff59c 100644 --- a/login.go +++ b/login.go @@ -11,7 +11,7 @@ import ( ) // RegistryLogin authenticates the docker server with a given docker registry. -// It returns UnauthorizerError when the authentication fails. +// It returns unauthorizedError when the authentication fails. func (cli *Client) RegistryLogin(ctx context.Context, auth types.AuthConfig) (registry.AuthenticateOKBody, error) { resp, err := cli.post(ctx, "/auth", url.Values{}, auth, nil) diff --git a/request.go b/request.go index f15e380339..6457b316a3 100644 --- a/request.go +++ b/request.go @@ -31,12 +31,12 @@ func (cli *Client) head(ctx context.Context, path string, query url.Values, head return cli.sendRequest(ctx, "HEAD", path, query, nil, headers) } -// getWithContext sends an http request to the docker API using the method GET with a specific go context. +// get sends an http request to the docker API using the method GET with a specific Go context. func (cli *Client) get(ctx context.Context, path string, query url.Values, headers map[string][]string) (serverResponse, error) { return cli.sendRequest(ctx, "GET", path, query, nil, headers) } -// postWithContext sends an http request to the docker API using the method POST with a specific go context. +// post sends an http request to the docker API using the method POST with a specific Go context. func (cli *Client) post(ctx context.Context, path string, query url.Values, obj interface{}, headers map[string][]string) (serverResponse, error) { body, headers, err := encodeBody(obj, headers) if err != nil { @@ -58,7 +58,7 @@ func (cli *Client) put(ctx context.Context, path string, query url.Values, obj i return cli.sendRequest(ctx, "PUT", path, query, body, headers) } -// put sends an http request to the docker API using the method PUT. +// putRaw sends an http request to the docker API using the method PUT. func (cli *Client) putRaw(ctx context.Context, path string, query url.Values, body io.Reader, headers map[string][]string) (serverResponse, error) { return cli.sendRequest(ctx, "PUT", path, query, body, headers) } From 47f0fde2cf0c09e571d476c680229086a9e994ef Mon Sep 17 00:00:00 2001 From: Yong Tang Date: Tue, 29 Nov 2016 17:31:29 -0800 Subject: [PATCH 091/138] Support plugins in `docker inspect` This fix tries to address the proposal raised in 28946 to support plugins in `docker inspect`. The command `docker inspect` already supports "container", "image", "node", "network", "service", "volume", "task". However, `--type plugin` is not supported yet at the moment. This fix address this issue by adding the support of `--type plugin` for `docker inspect`. An additional integration test has been added to cover the changes. This fix fixes 28946. Signed-off-by: Yong Tang --- errors.go | 21 +++++++++++++++++++++ plugin_inspect.go | 4 ++++ 2 files changed, 25 insertions(+) diff --git a/errors.go b/errors.go index 854516669c..bf6923f134 100644 --- a/errors.go +++ b/errors.go @@ -255,3 +255,24 @@ func IsErrSecretNotFound(err error) bool { _, ok := err.(secretNotFoundError) return ok } + +// pluginNotFoundError implements an error returned when a plugin is not in the docker host. +type pluginNotFoundError struct { + name string +} + +// NotFound indicates that this error type is of NotFound +func (e pluginNotFoundError) NotFound() bool { + return true +} + +// Error returns a string representation of a pluginNotFoundError +func (e pluginNotFoundError) Error() string { + return fmt.Sprintf("Error: No such plugin: %s", e.name) +} + +// IsErrPluginNotFound returns true if the error is caused +// when a plugin is not found in the docker host. +func IsErrPluginNotFound(err error) bool { + return IsErrNotFound(err) +} diff --git a/plugin_inspect.go b/plugin_inspect.go index e9474b5a98..72900a1310 100644 --- a/plugin_inspect.go +++ b/plugin_inspect.go @@ -4,6 +4,7 @@ import ( "bytes" "encoding/json" "io/ioutil" + "net/http" "github.com/docker/docker/api/types" "golang.org/x/net/context" @@ -13,6 +14,9 @@ import ( func (cli *Client) PluginInspectWithRaw(ctx context.Context, name string) (*types.Plugin, []byte, error) { resp, err := cli.get(ctx, "/plugins/"+name, nil, nil) if err != nil { + if resp.statusCode == http.StatusNotFound { + return nil, nil, pluginNotFoundError{name} + } return nil, nil, err } From 7520858943638c53fb39ce7487651733654865e2 Mon Sep 17 00:00:00 2001 From: Victor Vieux Date: Wed, 23 Nov 2016 17:29:21 -0800 Subject: [PATCH 092/138] refactor plugin install Signed-off-by: Victor Vieux --- plugin_inspect.go | 2 +- plugin_install.go | 33 ++++++++++++++++++++++----------- 2 files changed, 23 insertions(+), 12 deletions(-) diff --git a/plugin_inspect.go b/plugin_inspect.go index e9474b5a98..1fb40624ca 100644 --- a/plugin_inspect.go +++ b/plugin_inspect.go @@ -11,7 +11,7 @@ import ( // PluginInspectWithRaw inspects an existing plugin func (cli *Client) PluginInspectWithRaw(ctx context.Context, name string) (*types.Plugin, []byte, error) { - resp, err := cli.get(ctx, "/plugins/"+name, nil, nil) + resp, err := cli.get(ctx, "/plugins/"+name+"/json", nil, nil) if err != nil { return nil, nil, err } diff --git a/plugin_install.go b/plugin_install.go index f73362ccd3..e7b67f2051 100644 --- a/plugin_install.go +++ b/plugin_install.go @@ -14,27 +14,21 @@ func (cli *Client) PluginInstall(ctx context.Context, name string, options types // FIXME(vdemeester) name is a ref, we might want to parse/validate it here. query := url.Values{} query.Set("name", name) - resp, err := cli.tryPluginPull(ctx, query, options.RegistryAuth) + resp, err := cli.tryPluginPrivileges(ctx, query, options.RegistryAuth) if resp.statusCode == http.StatusUnauthorized && options.PrivilegeFunc != nil { newAuthHeader, privilegeErr := options.PrivilegeFunc() if privilegeErr != nil { ensureReaderClosed(resp) return privilegeErr } - resp, err = cli.tryPluginPull(ctx, query, newAuthHeader) + options.RegistryAuth = newAuthHeader + resp, err = cli.tryPluginPrivileges(ctx, query, options.RegistryAuth) } if err != nil { ensureReaderClosed(resp) return err } - defer func() { - if err != nil { - delResp, _ := cli.delete(ctx, "/plugins/"+name, nil, nil) - ensureReaderClosed(delResp) - } - }() - var privileges types.PluginPrivileges if err := json.NewDecoder(resp.body).Decode(&privileges); err != nil { ensureReaderClosed(resp) @@ -52,6 +46,18 @@ func (cli *Client) PluginInstall(ctx context.Context, name string, options types } } + _, err = cli.tryPluginPull(ctx, query, privileges, options.RegistryAuth) + if err != nil { + return err + } + + defer func() { + if err != nil { + delResp, _ := cli.delete(ctx, "/plugins/"+name, nil, nil) + ensureReaderClosed(delResp) + } + }() + if len(options.Args) > 0 { if err := cli.PluginSet(ctx, name, options.Args); err != nil { return err @@ -65,7 +71,12 @@ func (cli *Client) PluginInstall(ctx context.Context, name string, options types return cli.PluginEnable(ctx, name, types.PluginEnableOptions{Timeout: 0}) } -func (cli *Client) tryPluginPull(ctx context.Context, query url.Values, registryAuth string) (serverResponse, error) { +func (cli *Client) tryPluginPrivileges(ctx context.Context, query url.Values, registryAuth string) (serverResponse, error) { headers := map[string][]string{"X-Registry-Auth": {registryAuth}} - return cli.post(ctx, "/plugins/pull", query, nil, headers) + return cli.get(ctx, "/plugins/privileges", query, headers) +} + +func (cli *Client) tryPluginPull(ctx context.Context, query url.Values, privileges types.PluginPrivileges, registryAuth string) (serverResponse, error) { + headers := map[string][]string{"X-Registry-Auth": {registryAuth}} + return cli.post(ctx, "/plugins/pull", query, privileges, headers) } From b3c4bacff29ac669fc38822562b740030bdb3b60 Mon Sep 17 00:00:00 2001 From: Yong Tang Date: Wed, 16 Nov 2016 21:46:37 -0800 Subject: [PATCH 093/138] Convert DanglingOnly to Filters for `docker image prune` This fix convert DanglingOnly in ImagesPruneConfig to Filters, so that it is possible to maintain API compatibility in the future. Several integration tests have been added to cover changes. This fix is related to 28497. A follow up to this PR will be done once this PR is merged. Signed-off-by: Yong Tang --- container_prune.go | 10 ++++++++-- image_prune.go | 10 ++++++++-- interface.go | 8 ++++---- network_prune.go | 14 ++++++++++++-- utils.go | 20 +++++++++++++++++++- volume_prune.go | 10 ++++++++-- 6 files changed, 59 insertions(+), 13 deletions(-) diff --git a/container_prune.go b/container_prune.go index 3eabe71a7f..b582170867 100644 --- a/container_prune.go +++ b/container_prune.go @@ -5,18 +5,24 @@ import ( "fmt" "github.com/docker/docker/api/types" + "github.com/docker/docker/api/types/filters" "golang.org/x/net/context" ) // ContainersPrune requests the daemon to delete unused data -func (cli *Client) ContainersPrune(ctx context.Context, cfg types.ContainersPruneConfig) (types.ContainersPruneReport, error) { +func (cli *Client) ContainersPrune(ctx context.Context, pruneFilters filters.Args) (types.ContainersPruneReport, error) { var report types.ContainersPruneReport if err := cli.NewVersionError("1.25", "container prune"); err != nil { return report, err } - serverResp, err := cli.post(ctx, "/containers/prune", nil, cfg, nil) + query, err := getFiltersQuery(pruneFilters) + if err != nil { + return report, err + } + + serverResp, err := cli.post(ctx, "/containers/prune", query, nil, nil) if err != nil { return report, err } diff --git a/image_prune.go b/image_prune.go index d5e69d5b19..5ef98b7f02 100644 --- a/image_prune.go +++ b/image_prune.go @@ -5,18 +5,24 @@ import ( "fmt" "github.com/docker/docker/api/types" + "github.com/docker/docker/api/types/filters" "golang.org/x/net/context" ) // ImagesPrune requests the daemon to delete unused data -func (cli *Client) ImagesPrune(ctx context.Context, cfg types.ImagesPruneConfig) (types.ImagesPruneReport, error) { +func (cli *Client) ImagesPrune(ctx context.Context, pruneFilters filters.Args) (types.ImagesPruneReport, error) { var report types.ImagesPruneReport if err := cli.NewVersionError("1.25", "image prune"); err != nil { return report, err } - serverResp, err := cli.post(ctx, "/images/prune", nil, cfg, nil) + query, err := getFiltersQuery(pruneFilters) + if err != nil { + return report, err + } + + serverResp, err := cli.post(ctx, "/images/prune", query, nil, nil) if err != nil { return report, err } diff --git a/interface.go b/interface.go index 0d722d9075..6319f34f1e 100644 --- a/interface.go +++ b/interface.go @@ -64,7 +64,7 @@ type ContainerAPIClient interface { ContainerWait(ctx context.Context, container string) (int64, error) CopyFromContainer(ctx context.Context, container, srcPath string) (io.ReadCloser, types.ContainerPathStat, error) CopyToContainer(ctx context.Context, container, path string, content io.Reader, options types.CopyToContainerOptions) error - ContainersPrune(ctx context.Context, cfg types.ContainersPruneConfig) (types.ContainersPruneReport, error) + ContainersPrune(ctx context.Context, pruneFilters filters.Args) (types.ContainersPruneReport, error) } // ImageAPIClient defines API client methods for the images @@ -82,7 +82,7 @@ type ImageAPIClient interface { ImageSearch(ctx context.Context, term string, options types.ImageSearchOptions) ([]registry.SearchResult, error) ImageSave(ctx context.Context, images []string) (io.ReadCloser, error) ImageTag(ctx context.Context, image, ref string) error - ImagesPrune(ctx context.Context, cfg types.ImagesPruneConfig) (types.ImagesPruneReport, error) + ImagesPrune(ctx context.Context, pruneFilter filters.Args) (types.ImagesPruneReport, error) } // NetworkAPIClient defines API client methods for the networks @@ -94,7 +94,7 @@ type NetworkAPIClient interface { NetworkInspectWithRaw(ctx context.Context, networkID string) (types.NetworkResource, []byte, error) NetworkList(ctx context.Context, options types.NetworkListOptions) ([]types.NetworkResource, error) NetworkRemove(ctx context.Context, networkID string) error - NetworksPrune(ctx context.Context, cfg types.NetworksPruneConfig) (types.NetworksPruneReport, error) + NetworksPrune(ctx context.Context, pruneFilter filters.Args) (types.NetworksPruneReport, error) } // NodeAPIClient defines API client methods for the nodes @@ -157,7 +157,7 @@ type VolumeAPIClient interface { VolumeInspectWithRaw(ctx context.Context, volumeID string) (types.Volume, []byte, error) VolumeList(ctx context.Context, filter filters.Args) (volumetypes.VolumesListOKBody, error) VolumeRemove(ctx context.Context, volumeID string, force bool) error - VolumesPrune(ctx context.Context, cfg types.VolumesPruneConfig) (types.VolumesPruneReport, error) + VolumesPrune(ctx context.Context, pruneFilter filters.Args) (types.VolumesPruneReport, error) } // SecretAPIClient defines API client methods for secrets diff --git a/network_prune.go b/network_prune.go index 01185f2e02..7352a7f0c5 100644 --- a/network_prune.go +++ b/network_prune.go @@ -5,14 +5,24 @@ import ( "fmt" "github.com/docker/docker/api/types" + "github.com/docker/docker/api/types/filters" "golang.org/x/net/context" ) // NetworksPrune requests the daemon to delete unused networks -func (cli *Client) NetworksPrune(ctx context.Context, cfg types.NetworksPruneConfig) (types.NetworksPruneReport, error) { +func (cli *Client) NetworksPrune(ctx context.Context, pruneFilters filters.Args) (types.NetworksPruneReport, error) { var report types.NetworksPruneReport - serverResp, err := cli.post(ctx, "/networks/prune", nil, cfg, nil) + if err := cli.NewVersionError("1.25", "network prune"); err != nil { + return report, err + } + + query, err := getFiltersQuery(pruneFilters) + if err != nil { + return report, err + } + + serverResp, err := cli.post(ctx, "/networks/prune", query, nil, nil) if err != nil { return report, err } diff --git a/utils.go b/utils.go index 03bf4c82fa..23d520ecb8 100644 --- a/utils.go +++ b/utils.go @@ -1,6 +1,10 @@ package client -import "regexp" +import ( + "github.com/docker/docker/api/types/filters" + "net/url" + "regexp" +) var headerRegexp = regexp.MustCompile(`\ADocker/.+\s\((.+)\)\z`) @@ -13,3 +17,17 @@ func getDockerOS(serverHeader string) string { } return osType } + +// getFiltersQuery returns a url query with "filters" query term, based on the +// filters provided. +func getFiltersQuery(f filters.Args) (url.Values, error) { + query := url.Values{} + if f.Len() > 0 { + filterJSON, err := filters.ToParam(f) + if err != nil { + return query, err + } + query.Set("filters", filterJSON) + } + return query, nil +} diff --git a/volume_prune.go b/volume_prune.go index ea4e234a30..a07e4ce637 100644 --- a/volume_prune.go +++ b/volume_prune.go @@ -5,18 +5,24 @@ import ( "fmt" "github.com/docker/docker/api/types" + "github.com/docker/docker/api/types/filters" "golang.org/x/net/context" ) // VolumesPrune requests the daemon to delete unused data -func (cli *Client) VolumesPrune(ctx context.Context, cfg types.VolumesPruneConfig) (types.VolumesPruneReport, error) { +func (cli *Client) VolumesPrune(ctx context.Context, pruneFilters filters.Args) (types.VolumesPruneReport, error) { var report types.VolumesPruneReport if err := cli.NewVersionError("1.25", "volume prune"); err != nil { return report, err } - serverResp, err := cli.post(ctx, "/volumes/prune", nil, cfg, nil) + query, err := getFiltersQuery(pruneFilters) + if err != nil { + return report, err + } + + serverResp, err := cli.post(ctx, "/volumes/prune", query, nil, nil) if err != nil { return report, err } From ee4988f4b2ec8d5e630a08f2cdd6e1425e66c915 Mon Sep 17 00:00:00 2001 From: unclejack Date: Mon, 5 Dec 2016 17:00:36 +0200 Subject: [PATCH 094/138] api/types/container,client: gofmt Signed-off-by: Cristian Staretu --- image_search_test.go | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/image_search_test.go b/image_search_test.go index 108bd96744..b17bbd8343 100644 --- a/image_search_test.go +++ b/image_search_test.go @@ -86,7 +86,7 @@ func TestImageSearchWithPrivilegedFuncNoError(t *testing.T) { query := req.URL.Query() term := query.Get("term") if term != "some-image" { - return nil, fmt.Errorf("term not set in URL query properly. Expected 'some-image', got %s", term) + return nil, fmt.Errorf("term not set in URL query properly. Expected 'some-image', got %s", term) } content, err := json.Marshal([]registry.SearchResult{ { @@ -133,7 +133,7 @@ func TestImageSearchWithoutErrors(t *testing.T) { query := req.URL.Query() term := query.Get("term") if term != "some-image" { - return nil, fmt.Errorf("term not set in URL query properly. Expected 'some-image', got %s", term) + return nil, fmt.Errorf("term not set in URL query properly. Expected 'some-image', got %s", term) } filters := query.Get("filters") if filters != expectedFilters { From 259859289ba534e917993b1682d11703bf3403ad Mon Sep 17 00:00:00 2001 From: Doug Davis Date: Sat, 3 Dec 2016 05:46:04 -0800 Subject: [PATCH 095/138] Fix processing of unset build-args during build This reverts 26103. 26103 was trying to make it so that if someone did: docker build --build-arg FOO . and FOO wasn't set as an env var then it would pick-up FOO from the Dockerfile's ARG cmd. However, it went too far and removed the ability to specify a build arg w/o any value. Meaning it required the --build-arg param to always be in the form "name=value", and not just "name". This PR does the right fix - it allows just "name" and it'll grab the value from the env vars if set. If "name" isn't set in the env then it still needs to send "name" to the server so that a warning can be printed about an unused --build-arg. And this is why buildArgs in the options is now a *string instead of just a string - 'nil' == mentioned but no value. Closes #29084 Signed-off-by: Doug Davis --- image_build_test.go | 11 +++++++---- 1 file changed, 7 insertions(+), 4 deletions(-) diff --git a/image_build_test.go b/image_build_test.go index ec0cbe2ee4..b9d04f817a 100644 --- a/image_build_test.go +++ b/image_build_test.go @@ -27,6 +27,8 @@ func TestImageBuildError(t *testing.T) { } func TestImageBuild(t *testing.T) { + v1 := "value1" + v2 := "value2" emptyRegistryConfig := "bnVsbA==" buildCases := []struct { buildOptions types.ImageBuildOptions @@ -105,13 +107,14 @@ func TestImageBuild(t *testing.T) { }, { buildOptions: types.ImageBuildOptions{ - BuildArgs: map[string]string{ - "ARG1": "value1", - "ARG2": "value2", + BuildArgs: map[string]*string{ + "ARG1": &v1, + "ARG2": &v2, + "ARG3": nil, }, }, expectedQueryParams: map[string]string{ - "buildargs": `{"ARG1":"value1","ARG2":"value2"}`, + "buildargs": `{"ARG1":"value1","ARG2":"value2","ARG3":null}`, "rm": "0", }, expectedTags: []string{}, From bcb7147ae5cff5f3a8d8186a46a6cec33dd49cd3 Mon Sep 17 00:00:00 2001 From: Brian Goff Date: Thu, 15 Dec 2016 13:07:27 -0500 Subject: [PATCH 096/138] Fixes a race condition in client events monitoring In cases where there is high latency (ie, not-local network) `waitExitOrRemoved` was not receiving events for short-lived containers. This caused the client to hang while waiting for a notification that the container has stopped. This happens because `client.Events()` returns immediately and spins a goroutine up to process events. The problem here is it returns before the request to the events endpoint is even made. Even without high-latency issues, there is no guarantee that the goroutine is even scheduled by the time the function returns. Signed-off-by: Brian Goff --- events.go | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/events.go b/events.go index c154f7dcf9..af47aefa74 100644 --- a/events.go +++ b/events.go @@ -22,17 +22,20 @@ func (cli *Client) Events(ctx context.Context, options types.EventsOptions) (<-c messages := make(chan events.Message) errs := make(chan error, 1) + started := make(chan struct{}) go func() { defer close(errs) query, err := buildEventsQueryParams(cli.version, options) if err != nil { + close(started) errs <- err return } resp, err := cli.get(ctx, "/events", query, nil) if err != nil { + close(started) errs <- err return } @@ -40,6 +43,7 @@ func (cli *Client) Events(ctx context.Context, options types.EventsOptions) (<-c decoder := json.NewDecoder(resp.body) + close(started) for { select { case <-ctx.Done(): @@ -61,6 +65,7 @@ func (cli *Client) Events(ctx context.Context, options types.EventsOptions) (<-c } } }() + <-started return messages, errs } From 693328f346e094b972963736b2890afae5dc3a47 Mon Sep 17 00:00:00 2001 From: allencloud Date: Mon, 19 Dec 2016 14:45:48 +0800 Subject: [PATCH 097/138] change minor mistake of spelling Signed-off-by: allencloud --- errors.go | 2 +- node_inspect_test.go | 2 +- ping.go | 2 +- secret_inspect_test.go | 2 +- service_inspect_test.go | 2 +- 5 files changed, 5 insertions(+), 5 deletions(-) diff --git a/errors.go b/errors.go index bf6923f134..2912692ec1 100644 --- a/errors.go +++ b/errors.go @@ -244,7 +244,7 @@ func (e secretNotFoundError) Error() string { return fmt.Sprintf("Error: no such secret: %s", e.name) } -// NoFound indicates that this error type is of NotFound +// NotFound indicates that this error type is of NotFound func (e secretNotFoundError) NotFound() bool { return true } diff --git a/node_inspect_test.go b/node_inspect_test.go index fc13283084..dca16a8cdc 100644 --- a/node_inspect_test.go +++ b/node_inspect_test.go @@ -31,7 +31,7 @@ func TestNodeInspectNodeNotFound(t *testing.T) { _, _, err := client.NodeInspectWithRaw(context.Background(), "unknown") if err == nil || !IsErrNodeNotFound(err) { - t.Fatalf("expected an nodeNotFoundError error, got %v", err) + t.Fatalf("expected a nodeNotFoundError error, got %v", err) } } diff --git a/ping.go b/ping.go index 22dcda24fd..150b1dc8d8 100644 --- a/ping.go +++ b/ping.go @@ -7,7 +7,7 @@ import ( "golang.org/x/net/context" ) -// Ping pings the server and return the value of the "Docker-Experimental" & "API-Version" headers +// Ping pings the server and returns the value of the "Docker-Experimental" & "API-Version" headers func (cli *Client) Ping(ctx context.Context) (types.Ping, error) { var ping types.Ping req, err := cli.buildRequest("GET", fmt.Sprintf("%s/_ping", cli.basePath), nil, nil) diff --git a/secret_inspect_test.go b/secret_inspect_test.go index 423d986968..0142a3ca9f 100644 --- a/secret_inspect_test.go +++ b/secret_inspect_test.go @@ -31,7 +31,7 @@ func TestSecretInspectSecretNotFound(t *testing.T) { _, _, err := client.SecretInspectWithRaw(context.Background(), "unknown") if err == nil || !IsErrSecretNotFound(err) { - t.Fatalf("expected an secretNotFoundError error, got %v", err) + t.Fatalf("expected a secretNotFoundError error, got %v", err) } } diff --git a/service_inspect_test.go b/service_inspect_test.go index e235cf0fef..0346847317 100644 --- a/service_inspect_test.go +++ b/service_inspect_test.go @@ -31,7 +31,7 @@ func TestServiceInspectServiceNotFound(t *testing.T) { _, _, err := client.ServiceInspectWithRaw(context.Background(), "unknown") if err == nil || !IsErrServiceNotFound(err) { - t.Fatalf("expected an serviceNotFoundError error, got %v", err) + t.Fatalf("expected a serviceNotFoundError error, got %v", err) } } From d044b55ee0551f20a12507713a1102385beb387d Mon Sep 17 00:00:00 2001 From: yuexiao-wang Date: Tue, 20 Dec 2016 19:14:41 +0800 Subject: [PATCH 098/138] Change tls to TLS Signed-off-by: yuexiao-wang --- client.go | 2 +- client_test.go | 4 ++-- swarm_init.go | 2 +- swarm_inspect.go | 2 +- swarm_join.go | 2 +- swarm_leave.go | 2 +- swarm_update.go | 2 +- transport.go | 2 +- 8 files changed, 9 insertions(+), 9 deletions(-) diff --git a/client.go b/client.go index 4c0f097e53..75cfc8698b 100644 --- a/client.go +++ b/client.go @@ -86,7 +86,7 @@ type Client struct { // NewEnvClient initializes a new API client based on environment variables. // Use DOCKER_HOST to set the url to the docker server. // Use DOCKER_API_VERSION to set the version of the API to reach, leave empty for latest. -// Use DOCKER_CERT_PATH to load the tls certificates from. +// Use DOCKER_CERT_PATH to load the TLS certificates from. // Use DOCKER_TLS_VERIFY to enable or disable TLS verification, off by default. func NewEnvClient() (*Client, error) { var client *http.Client diff --git a/client_test.go b/client_test.go index 3a6575c9cc..7c26403ebe 100644 --- a/client_test.go +++ b/client_test.go @@ -102,11 +102,11 @@ func TestNewEnvClient(t *testing.T) { // pedantic checking that this is handled correctly tr := apiclient.client.Transport.(*http.Transport) if tr.TLSClientConfig == nil { - t.Error("no tls config found when DOCKER_TLS_VERIFY enabled") + t.Error("no TLS config found when DOCKER_TLS_VERIFY enabled") } if tr.TLSClientConfig.InsecureSkipVerify { - t.Error("tls verification should be enabled") + t.Error("TLS verification should be enabled") } } diff --git a/swarm_init.go b/swarm_init.go index fd45d066e3..9e65e1cca4 100644 --- a/swarm_init.go +++ b/swarm_init.go @@ -7,7 +7,7 @@ import ( "golang.org/x/net/context" ) -// SwarmInit initializes the Swarm. +// SwarmInit initializes the swarm. func (cli *Client) SwarmInit(ctx context.Context, req swarm.InitRequest) (string, error) { serverResp, err := cli.post(ctx, "/swarm/init", nil, req, nil) if err != nil { diff --git a/swarm_inspect.go b/swarm_inspect.go index 6d95cfc05e..77e72f8466 100644 --- a/swarm_inspect.go +++ b/swarm_inspect.go @@ -7,7 +7,7 @@ import ( "golang.org/x/net/context" ) -// SwarmInspect inspects the Swarm. +// SwarmInspect inspects the swarm. func (cli *Client) SwarmInspect(ctx context.Context) (swarm.Swarm, error) { serverResp, err := cli.get(ctx, "/swarm", nil, nil) if err != nil { diff --git a/swarm_join.go b/swarm_join.go index cda99930eb..19e5192b9e 100644 --- a/swarm_join.go +++ b/swarm_join.go @@ -5,7 +5,7 @@ import ( "golang.org/x/net/context" ) -// SwarmJoin joins the Swarm. +// SwarmJoin joins the swarm. func (cli *Client) SwarmJoin(ctx context.Context, req swarm.JoinRequest) error { resp, err := cli.post(ctx, "/swarm/join", nil, req, nil) ensureReaderClosed(resp) diff --git a/swarm_leave.go b/swarm_leave.go index a4df732174..3a205cf3b5 100644 --- a/swarm_leave.go +++ b/swarm_leave.go @@ -6,7 +6,7 @@ import ( "golang.org/x/net/context" ) -// SwarmLeave leaves the Swarm. +// SwarmLeave leaves the swarm. func (cli *Client) SwarmLeave(ctx context.Context, force bool) error { query := url.Values{} if force { diff --git a/swarm_update.go b/swarm_update.go index cc8eeb6554..7245fd4e38 100644 --- a/swarm_update.go +++ b/swarm_update.go @@ -9,7 +9,7 @@ import ( "golang.org/x/net/context" ) -// SwarmUpdate updates the Swarm. +// SwarmUpdate updates the swarm. func (cli *Client) SwarmUpdate(ctx context.Context, version swarm.Version, swarm swarm.Spec, flags swarm.UpdateFlags) error { query := url.Values{} query.Set("version", strconv.FormatUint(version.Index, 10)) diff --git a/transport.go b/transport.go index f04e601649..02ebadeac6 100644 --- a/transport.go +++ b/transport.go @@ -16,7 +16,7 @@ func (tf transportFunc) RoundTrip(req *http.Request) (*http.Response, error) { return tf(req) } -// resolveTLSConfig attempts to resolve the tls configuration from the +// resolveTLSConfig attempts to resolve the TLS configuration from the // RoundTripper. func resolveTLSConfig(transport http.RoundTripper) *tls.Config { switch tr := transport.(type) { From fe5937d0a7b083986d07228dbc4f23d9050ec81b Mon Sep 17 00:00:00 2001 From: WANG Yuexiao Date: Wed, 21 Dec 2016 19:41:14 +0800 Subject: [PATCH 099/138] Remove unused var 'errTLSConfigUnavailable' (#29626) Signed-off-by: yuexiao-wang --- transport.go | 3 --- 1 file changed, 3 deletions(-) diff --git a/transport.go b/transport.go index f04e601649..6cd47f2efd 100644 --- a/transport.go +++ b/transport.go @@ -2,12 +2,9 @@ package client import ( "crypto/tls" - "errors" "net/http" ) -var errTLSConfigUnavailable = errors.New("TLSConfig unavailable") - // transportFunc allows us to inject a mock transport for testing. We define it // here so we can detect the tlsconfig and return nil for only this type. type transportFunc func(*http.Request) (*http.Response, error) From fa7cceeb4acdac250d0d0283f86f4e6b65fcacdb Mon Sep 17 00:00:00 2001 From: Anusha Ragunathan Date: Tue, 20 Dec 2016 08:26:58 -0800 Subject: [PATCH 100/138] Enforce zero plugin refcount during disable. When plugins have a positive refcount, they were not allowed to be removed. However, plugins could still be disabled when volumes referenced it and containers using them were running. This change fixes that by enforcing plugin refcount during disable. A "force" disable option is also added to ignore reference refcounting. Signed-off-by: Anusha Ragunathan --- interface.go | 2 +- plugin_disable.go | 11 +++++++++-- plugin_disable_test.go | 5 +++-- 3 files changed, 13 insertions(+), 5 deletions(-) diff --git a/interface.go b/interface.go index 6319f34f1e..96d65a428a 100644 --- a/interface.go +++ b/interface.go @@ -110,7 +110,7 @@ type PluginAPIClient interface { PluginList(ctx context.Context) (types.PluginsListResponse, error) PluginRemove(ctx context.Context, name string, options types.PluginRemoveOptions) error PluginEnable(ctx context.Context, name string, options types.PluginEnableOptions) error - PluginDisable(ctx context.Context, name string) error + PluginDisable(ctx context.Context, name string, options types.PluginDisableOptions) error PluginInstall(ctx context.Context, name string, options types.PluginInstallOptions) error PluginPush(ctx context.Context, name string, registryAuth string) error PluginSet(ctx context.Context, name string, args []string) error diff --git a/plugin_disable.go b/plugin_disable.go index 51e4565125..30467db742 100644 --- a/plugin_disable.go +++ b/plugin_disable.go @@ -1,12 +1,19 @@ package client import ( + "net/url" + + "github.com/docker/docker/api/types" "golang.org/x/net/context" ) // PluginDisable disables a plugin -func (cli *Client) PluginDisable(ctx context.Context, name string) error { - resp, err := cli.post(ctx, "/plugins/"+name+"/disable", nil, nil, nil) +func (cli *Client) PluginDisable(ctx context.Context, name string, options types.PluginDisableOptions) error { + query := url.Values{} + if options.Force { + query.Set("force", "1") + } + resp, err := cli.post(ctx, "/plugins/"+name+"/disable", query, nil, nil) ensureReaderClosed(resp) return err } diff --git a/plugin_disable_test.go b/plugin_disable_test.go index 2818008ab9..a4de45be2d 100644 --- a/plugin_disable_test.go +++ b/plugin_disable_test.go @@ -8,6 +8,7 @@ import ( "strings" "testing" + "github.com/docker/docker/api/types" "golang.org/x/net/context" ) @@ -16,7 +17,7 @@ func TestPluginDisableError(t *testing.T) { client: newMockClient(errorMock(http.StatusInternalServerError, "Server error")), } - err := client.PluginDisable(context.Background(), "plugin_name") + err := client.PluginDisable(context.Background(), "plugin_name", types.PluginDisableOptions{}) if err == nil || err.Error() != "Error response from daemon: Server error" { t.Fatalf("expected a Server Error, got %v", err) } @@ -40,7 +41,7 @@ func TestPluginDisable(t *testing.T) { }), } - err := client.PluginDisable(context.Background(), "plugin_name") + err := client.PluginDisable(context.Background(), "plugin_name", types.PluginDisableOptions{}) if err != nil { t.Fatal(err) } From 66f7194250ee9cfd0258b42632954ebfcd5c394b Mon Sep 17 00:00:00 2001 From: Tonis Tiigi Date: Mon, 12 Dec 2016 15:05:53 -0800 Subject: [PATCH 101/138] Implement content addressability for plugins Move plugins to shared distribution stack with images. Create immutable plugin config that matches schema2 requirements. Ensure data being pushed is same as pulled/created. Store distribution artifacts in a blobstore. Run init layer setup for every plugin start. Fix breakouts from unsafe file accesses. Add support for `docker plugin install --alias` Uses normalized references for default names to avoid collisions when using default hosts/tags. Some refactoring of the plugin manager to support the change, like removing the singleton manager and adding manager config struct. Signed-off-by: Tonis Tiigi Signed-off-by: Derek McGowan --- interface.go | 4 +-- plugin_install.go | 73 +++++++++++++++++++++++++++++---------------- plugin_push.go | 10 +++++-- plugin_push_test.go | 4 +-- 4 files changed, 59 insertions(+), 32 deletions(-) diff --git a/interface.go b/interface.go index 96d65a428a..00b9adea32 100644 --- a/interface.go +++ b/interface.go @@ -111,8 +111,8 @@ type PluginAPIClient interface { PluginRemove(ctx context.Context, name string, options types.PluginRemoveOptions) error PluginEnable(ctx context.Context, name string, options types.PluginEnableOptions) error PluginDisable(ctx context.Context, name string, options types.PluginDisableOptions) error - PluginInstall(ctx context.Context, name string, options types.PluginInstallOptions) error - PluginPush(ctx context.Context, name string, registryAuth string) error + PluginInstall(ctx context.Context, name string, options types.PluginInstallOptions) (io.ReadCloser, error) + PluginPush(ctx context.Context, name string, registryAuth string) (io.ReadCloser, error) PluginSet(ctx context.Context, name string, args []string) error PluginInspectWithRaw(ctx context.Context, name string) (*types.Plugin, []byte, error) PluginCreate(ctx context.Context, createContext io.Reader, options types.PluginCreateOptions) error diff --git a/plugin_install.go b/plugin_install.go index e7b67f2051..b305780cfb 100644 --- a/plugin_install.go +++ b/plugin_install.go @@ -2,73 +2,96 @@ package client import ( "encoding/json" + "io" "net/http" "net/url" + "github.com/docker/distribution/reference" "github.com/docker/docker/api/types" + "github.com/pkg/errors" "golang.org/x/net/context" ) // PluginInstall installs a plugin -func (cli *Client) PluginInstall(ctx context.Context, name string, options types.PluginInstallOptions) (err error) { - // FIXME(vdemeester) name is a ref, we might want to parse/validate it here. +func (cli *Client) PluginInstall(ctx context.Context, name string, options types.PluginInstallOptions) (rc io.ReadCloser, err error) { query := url.Values{} - query.Set("name", name) + if _, err := reference.ParseNamed(options.RemoteRef); err != nil { + return nil, errors.Wrap(err, "invalid remote reference") + } + query.Set("remote", options.RemoteRef) + resp, err := cli.tryPluginPrivileges(ctx, query, options.RegistryAuth) if resp.statusCode == http.StatusUnauthorized && options.PrivilegeFunc != nil { + // todo: do inspect before to check existing name before checking privileges newAuthHeader, privilegeErr := options.PrivilegeFunc() if privilegeErr != nil { ensureReaderClosed(resp) - return privilegeErr + return nil, privilegeErr } options.RegistryAuth = newAuthHeader resp, err = cli.tryPluginPrivileges(ctx, query, options.RegistryAuth) } if err != nil { ensureReaderClosed(resp) - return err + return nil, err } var privileges types.PluginPrivileges if err := json.NewDecoder(resp.body).Decode(&privileges); err != nil { ensureReaderClosed(resp) - return err + return nil, err } ensureReaderClosed(resp) if !options.AcceptAllPermissions && options.AcceptPermissionsFunc != nil && len(privileges) > 0 { accept, err := options.AcceptPermissionsFunc(privileges) if err != nil { - return err + return nil, err } if !accept { - return pluginPermissionDenied{name} + return nil, pluginPermissionDenied{options.RemoteRef} } } - _, err = cli.tryPluginPull(ctx, query, privileges, options.RegistryAuth) + // set name for plugin pull, if empty should default to remote reference + query.Set("name", name) + + resp, err = cli.tryPluginPull(ctx, query, privileges, options.RegistryAuth) if err != nil { - return err + return nil, err } - defer func() { + name = resp.header.Get("Docker-Plugin-Name") + + pr, pw := io.Pipe() + go func() { // todo: the client should probably be designed more around the actual api + _, err := io.Copy(pw, resp.body) if err != nil { - delResp, _ := cli.delete(ctx, "/plugins/"+name, nil, nil) - ensureReaderClosed(delResp) + pw.CloseWithError(err) + return } + defer func() { + if err != nil { + delResp, _ := cli.delete(ctx, "/plugins/"+name, nil, nil) + ensureReaderClosed(delResp) + } + }() + if len(options.Args) > 0 { + if err := cli.PluginSet(ctx, name, options.Args); err != nil { + pw.CloseWithError(err) + return + } + } + + if options.Disabled { + pw.Close() + return + } + + err = cli.PluginEnable(ctx, name, types.PluginEnableOptions{Timeout: 0}) + pw.CloseWithError(err) }() - - if len(options.Args) > 0 { - if err := cli.PluginSet(ctx, name, options.Args); err != nil { - return err - } - } - - if options.Disabled { - return nil - } - - return cli.PluginEnable(ctx, name, types.PluginEnableOptions{Timeout: 0}) + return pr, nil } func (cli *Client) tryPluginPrivileges(ctx context.Context, query url.Values, registryAuth string) (serverResponse, error) { diff --git a/plugin_push.go b/plugin_push.go index d83bbdc358..1e5f963251 100644 --- a/plugin_push.go +++ b/plugin_push.go @@ -1,13 +1,17 @@ package client import ( + "io" + "golang.org/x/net/context" ) // PluginPush pushes a plugin to a registry -func (cli *Client) PluginPush(ctx context.Context, name string, registryAuth string) error { +func (cli *Client) PluginPush(ctx context.Context, name string, registryAuth string) (io.ReadCloser, error) { headers := map[string][]string{"X-Registry-Auth": {registryAuth}} resp, err := cli.post(ctx, "/plugins/"+name+"/push", nil, nil, headers) - ensureReaderClosed(resp) - return err + if err != nil { + return nil, err + } + return resp.body, nil } diff --git a/plugin_push_test.go b/plugin_push_test.go index 7b8eb865d6..d9f70cdff8 100644 --- a/plugin_push_test.go +++ b/plugin_push_test.go @@ -16,7 +16,7 @@ func TestPluginPushError(t *testing.T) { client: newMockClient(errorMock(http.StatusInternalServerError, "Server error")), } - err := client.PluginPush(context.Background(), "plugin_name", "") + _, err := client.PluginPush(context.Background(), "plugin_name", "") if err == nil || err.Error() != "Error response from daemon: Server error" { t.Fatalf("expected a Server Error, got %v", err) } @@ -44,7 +44,7 @@ func TestPluginPush(t *testing.T) { }), } - err := client.PluginPush(context.Background(), "plugin_name", "authtoken") + _, err := client.PluginPush(context.Background(), "plugin_name", "authtoken") if err != nil { t.Fatal(err) } From 0a623b251f1c6a0b07d3b5f6d1f0e5c47c5400c5 Mon Sep 17 00:00:00 2001 From: Daniel Nephin Date: Wed, 9 Nov 2016 16:15:32 -0500 Subject: [PATCH 102/138] Generate ContainerChanges from swagger spec. Signed-off-by: Daniel Nephin --- container_diff.go | 6 +++--- container_diff_test.go | 4 ++-- interface.go | 2 +- 3 files changed, 6 insertions(+), 6 deletions(-) diff --git a/container_diff.go b/container_diff.go index 1e3e554fc5..884dc9feef 100644 --- a/container_diff.go +++ b/container_diff.go @@ -4,13 +4,13 @@ import ( "encoding/json" "net/url" - "github.com/docker/docker/api/types" + "github.com/docker/docker/api/types/container" "golang.org/x/net/context" ) // ContainerDiff shows differences in a container filesystem since it was started. -func (cli *Client) ContainerDiff(ctx context.Context, containerID string) ([]types.ContainerChange, error) { - var changes []types.ContainerChange +func (cli *Client) ContainerDiff(ctx context.Context, containerID string) ([]container.ContainerChangeResponseItem, error) { + var changes []container.ContainerChangeResponseItem serverResp, err := cli.get(ctx, "/containers/"+containerID+"/changes", url.Values{}, nil) if err != nil { diff --git a/container_diff_test.go b/container_diff_test.go index 1ce1117684..57dd73e66d 100644 --- a/container_diff_test.go +++ b/container_diff_test.go @@ -9,7 +9,7 @@ import ( "strings" "testing" - "github.com/docker/docker/api/types" + "github.com/docker/docker/api/types/container" "golang.org/x/net/context" ) @@ -31,7 +31,7 @@ func TestContainerDiff(t *testing.T) { if !strings.HasPrefix(req.URL.Path, expectedURL) { return nil, fmt.Errorf("Expected URL '%s', got '%s'", expectedURL, req.URL) } - b, err := json.Marshal([]types.ContainerChange{ + b, err := json.Marshal([]container.ContainerChangeResponseItem{ { Kind: 0, Path: "/path/1", diff --git a/interface.go b/interface.go index 00b9adea32..5e1b63b39d 100644 --- a/interface.go +++ b/interface.go @@ -37,7 +37,7 @@ type ContainerAPIClient interface { ContainerAttach(ctx context.Context, container string, options types.ContainerAttachOptions) (types.HijackedResponse, error) ContainerCommit(ctx context.Context, container string, options types.ContainerCommitOptions) (types.IDResponse, error) ContainerCreate(ctx context.Context, config *container.Config, hostConfig *container.HostConfig, networkingConfig *network.NetworkingConfig, containerName string) (container.ContainerCreateCreatedBody, error) - ContainerDiff(ctx context.Context, container string) ([]types.ContainerChange, error) + ContainerDiff(ctx context.Context, container string) ([]container.ContainerChangeResponseItem, error) ContainerExecAttach(ctx context.Context, execID string, config types.ExecConfig) (types.HijackedResponse, error) ContainerExecCreate(ctx context.Context, container string, config types.ExecConfig) (types.IDResponse, error) ContainerExecInspect(ctx context.Context, execID string) (types.ContainerExecInspect, error) From 09bd6619791a4b67afa0c598bbc525406bd9f130 Mon Sep 17 00:00:00 2001 From: Daniel Nephin Date: Wed, 9 Nov 2016 16:32:53 -0500 Subject: [PATCH 103/138] Generate ImageHistory from swagger spec. Signed-off-by: Daniel Nephin --- image_history.go | 6 +++--- image_history_test.go | 4 ++-- interface.go | 3 ++- 3 files changed, 7 insertions(+), 6 deletions(-) diff --git a/image_history.go b/image_history.go index acb1ee9278..7b4babcba3 100644 --- a/image_history.go +++ b/image_history.go @@ -4,13 +4,13 @@ import ( "encoding/json" "net/url" - "github.com/docker/docker/api/types" + "github.com/docker/docker/api/types/image" "golang.org/x/net/context" ) // ImageHistory returns the changes in an image in history format. -func (cli *Client) ImageHistory(ctx context.Context, imageID string) ([]types.ImageHistory, error) { - var history []types.ImageHistory +func (cli *Client) ImageHistory(ctx context.Context, imageID string) ([]image.HistoryResponseItem, error) { + var history []image.HistoryResponseItem serverResp, err := cli.get(ctx, "/images/"+imageID+"/history", url.Values{}, nil) if err != nil { return history, err diff --git a/image_history_test.go b/image_history_test.go index 729edb1ad5..101bffd0c3 100644 --- a/image_history_test.go +++ b/image_history_test.go @@ -9,7 +9,7 @@ import ( "strings" "testing" - "github.com/docker/docker/api/types" + "github.com/docker/docker/api/types/image" "golang.org/x/net/context" ) @@ -30,7 +30,7 @@ func TestImageHistory(t *testing.T) { if !strings.HasPrefix(r.URL.Path, expectedURL) { return nil, fmt.Errorf("Expected URL '%s', got '%s'", expectedURL, r.URL) } - b, err := json.Marshal([]types.ImageHistory{ + b, err := json.Marshal([]image.HistoryResponseItem{ { ID: "image_id1", Tags: []string{"tag1", "tag2"}, diff --git a/interface.go b/interface.go index 5e1b63b39d..742f9a6c17 100644 --- a/interface.go +++ b/interface.go @@ -8,6 +8,7 @@ import ( "github.com/docker/docker/api/types/container" "github.com/docker/docker/api/types/events" "github.com/docker/docker/api/types/filters" + "github.com/docker/docker/api/types/image" "github.com/docker/docker/api/types/network" "github.com/docker/docker/api/types/registry" "github.com/docker/docker/api/types/swarm" @@ -71,7 +72,7 @@ type ContainerAPIClient interface { type ImageAPIClient interface { ImageBuild(ctx context.Context, context io.Reader, options types.ImageBuildOptions) (types.ImageBuildResponse, error) ImageCreate(ctx context.Context, parentReference string, options types.ImageCreateOptions) (io.ReadCloser, error) - ImageHistory(ctx context.Context, image string) ([]types.ImageHistory, error) + ImageHistory(ctx context.Context, image string) ([]image.HistoryResponseItem, error) ImageImport(ctx context.Context, source types.ImageImportSource, ref string, options types.ImageImportOptions) (io.ReadCloser, error) ImageInspectWithRaw(ctx context.Context, image string) (types.ImageInspect, []byte, error) ImageList(ctx context.Context, options types.ImageListOptions) ([]types.ImageSummary, error) From f7e58c8c9bcb9981fdc8fd9af068669a187cdf3e Mon Sep 17 00:00:00 2001 From: Daniel Nephin Date: Thu, 10 Nov 2016 11:27:56 -0500 Subject: [PATCH 104/138] Generate ImageDeleteResponse from swagger spec. Signed-off-by: Daniel Nephin --- image_remove.go | 4 ++-- image_remove_test.go | 2 +- interface.go | 2 +- 3 files changed, 4 insertions(+), 4 deletions(-) diff --git a/image_remove.go b/image_remove.go index 839e5311c4..6921209ee1 100644 --- a/image_remove.go +++ b/image_remove.go @@ -9,7 +9,7 @@ import ( ) // ImageRemove removes an image from the docker host. -func (cli *Client) ImageRemove(ctx context.Context, imageID string, options types.ImageRemoveOptions) ([]types.ImageDelete, error) { +func (cli *Client) ImageRemove(ctx context.Context, imageID string, options types.ImageRemoveOptions) ([]types.ImageDeleteResponseItem, error) { query := url.Values{} if options.Force { @@ -24,7 +24,7 @@ func (cli *Client) ImageRemove(ctx context.Context, imageID string, options type return nil, err } - var dels []types.ImageDelete + var dels []types.ImageDeleteResponseItem err = json.NewDecoder(resp.body).Decode(&dels) ensureReaderClosed(resp) return dels, err diff --git a/image_remove_test.go b/image_remove_test.go index 7b004f70e6..9856311305 100644 --- a/image_remove_test.go +++ b/image_remove_test.go @@ -63,7 +63,7 @@ func TestImageRemove(t *testing.T) { return nil, fmt.Errorf("%s not set in URL query properly. Expected '%s', got %s", key, expected, actual) } } - b, err := json.Marshal([]types.ImageDelete{ + b, err := json.Marshal([]types.ImageDeleteResponseItem{ { Untagged: "image_id1", }, diff --git a/interface.go b/interface.go index 742f9a6c17..e3bcb19950 100644 --- a/interface.go +++ b/interface.go @@ -79,7 +79,7 @@ type ImageAPIClient interface { ImageLoad(ctx context.Context, input io.Reader, quiet bool) (types.ImageLoadResponse, error) ImagePull(ctx context.Context, ref string, options types.ImagePullOptions) (io.ReadCloser, error) ImagePush(ctx context.Context, ref string, options types.ImagePushOptions) (io.ReadCloser, error) - ImageRemove(ctx context.Context, image string, options types.ImageRemoveOptions) ([]types.ImageDelete, error) + ImageRemove(ctx context.Context, image string, options types.ImageRemoveOptions) ([]types.ImageDeleteResponseItem, error) ImageSearch(ctx context.Context, term string, options types.ImageSearchOptions) ([]registry.SearchResult, error) ImageSave(ctx context.Context, images []string) (io.ReadCloser, error) ImageTag(ctx context.Context, image, ref string) error From 9eda7f4daf3caced7886be181f28c350584d68e0 Mon Sep 17 00:00:00 2001 From: Daniel Nephin Date: Mon, 14 Nov 2016 14:50:16 -0500 Subject: [PATCH 105/138] Convert ContainerTopOKResponse from swagger spec. Signed-off-by: Daniel Nephin --- container_top.go | 6 +++--- container_top_test.go | 4 ++-- interface.go | 2 +- 3 files changed, 6 insertions(+), 6 deletions(-) diff --git a/container_top.go b/container_top.go index 4e7270ea22..9689123a40 100644 --- a/container_top.go +++ b/container_top.go @@ -5,13 +5,13 @@ import ( "net/url" "strings" - "github.com/docker/docker/api/types" + "github.com/docker/docker/api/types/container" "golang.org/x/net/context" ) // ContainerTop shows process information from within a container. -func (cli *Client) ContainerTop(ctx context.Context, containerID string, arguments []string) (types.ContainerProcessList, error) { - var response types.ContainerProcessList +func (cli *Client) ContainerTop(ctx context.Context, containerID string, arguments []string) (container.ContainerTopOKBody, error) { + var response container.ContainerTopOKBody query := url.Values{} if len(arguments) > 0 { query.Set("ps_args", strings.Join(arguments, " ")) diff --git a/container_top_test.go b/container_top_test.go index 7802be063e..68ccef505d 100644 --- a/container_top_test.go +++ b/container_top_test.go @@ -10,7 +10,7 @@ import ( "strings" "testing" - "github.com/docker/docker/api/types" + "github.com/docker/docker/api/types/container" "golang.org/x/net/context" ) @@ -43,7 +43,7 @@ func TestContainerTop(t *testing.T) { return nil, fmt.Errorf("args not set in URL query properly. Expected 'arg1 arg2', got %v", args) } - b, err := json.Marshal(types.ContainerProcessList{ + b, err := json.Marshal(container.ContainerTopOKBody{ Processes: [][]string{ {"p1", "p2"}, {"p3"}, diff --git a/interface.go b/interface.go index e3bcb19950..ef9b10bba3 100644 --- a/interface.go +++ b/interface.go @@ -59,7 +59,7 @@ type ContainerAPIClient interface { ContainerStats(ctx context.Context, container string, stream bool) (types.ContainerStats, error) ContainerStart(ctx context.Context, container string, options types.ContainerStartOptions) error ContainerStop(ctx context.Context, container string, timeout *time.Duration) error - ContainerTop(ctx context.Context, container string, arguments []string) (types.ContainerProcessList, error) + ContainerTop(ctx context.Context, container string, arguments []string) (container.ContainerTopOKBody, error) ContainerUnpause(ctx context.Context, container string) error ContainerUpdate(ctx context.Context, container string, updateConfig container.UpdateConfig) (container.ContainerUpdateOKBody, error) ContainerWait(ctx context.Context, container string) (int64, error) From 337483496b355b61d3aa4fd4b4f4853e59be646a Mon Sep 17 00:00:00 2001 From: Yong Tang Date: Wed, 7 Dec 2016 14:02:13 -0800 Subject: [PATCH 106/138] Add `--filter until=` for `docker container/image prune` This fix is a follow up for comment https://github.com/docker/docker/pull/28535#issuecomment-263215225 This fix provides `--filter until=` for `docker container/image prune`. This fix adds `--filter until=` to `docker container/image prune` so that it is possible to specify a timestamp and prune those containers/images that are earlier than the timestamp. Related docs has been updated Several integration tests have been added to cover changes. This fix fixes #28497. This fix is related to #28535. Signed-off-by: Yong Tang --- container_prune_test.go | 111 ++++++++++++++++++++++++++++++++++++++++ image_prune_test.go | 106 ++++++++++++++++++++++++++++++++++++++ network_prune_test.go | 99 +++++++++++++++++++++++++++++++++++ 3 files changed, 316 insertions(+) create mode 100644 container_prune_test.go create mode 100644 image_prune_test.go create mode 100644 network_prune_test.go diff --git a/container_prune_test.go b/container_prune_test.go new file mode 100644 index 0000000000..5f06ea0664 --- /dev/null +++ b/container_prune_test.go @@ -0,0 +1,111 @@ +package client + +import ( + "bytes" + "encoding/json" + "fmt" + "io/ioutil" + "net/http" + "strings" + "testing" + + "github.com/docker/docker/api/types" + "github.com/docker/docker/api/types/filters" + "github.com/docker/docker/pkg/testutil/assert" + "golang.org/x/net/context" +) + +func TestContainersPruneError(t *testing.T) { + client := &Client{ + client: newMockClient(errorMock(http.StatusInternalServerError, "Server error")), + version: "1.25", + } + + filters := filters.NewArgs() + + _, err := client.ContainersPrune(context.Background(), filters) + assert.Error(t, err, "Error response from daemon: Server error") +} + +func TestContainersPrune(t *testing.T) { + expectedURL := "/v1.25/containers/prune" + + danglingFilters := filters.NewArgs() + danglingFilters.Add("dangling", "true") + + noDanglingFilters := filters.NewArgs() + noDanglingFilters.Add("dangling", "false") + + danglingUntilFilters := filters.NewArgs() + danglingUntilFilters.Add("dangling", "true") + danglingUntilFilters.Add("until", "2016-12-15T14:00") + + listCases := []struct { + filters filters.Args + expectedQueryParams map[string]string + }{ + { + filters: filters.Args{}, + expectedQueryParams: map[string]string{ + "until": "", + "filter": "", + "filters": "", + }, + }, + { + filters: danglingFilters, + expectedQueryParams: map[string]string{ + "until": "", + "filter": "", + "filters": `{"dangling":{"true":true}}`, + }, + }, + { + filters: danglingUntilFilters, + expectedQueryParams: map[string]string{ + "until": "", + "filter": "", + "filters": `{"dangling":{"true":true},"until":{"2016-12-15T14:00":true}}`, + }, + }, + { + filters: noDanglingFilters, + expectedQueryParams: map[string]string{ + "until": "", + "filter": "", + "filters": `{"dangling":{"false":true}}`, + }, + }, + } + for _, listCase := range listCases { + client := &Client{ + client: newMockClient(func(req *http.Request) (*http.Response, error) { + if !strings.HasPrefix(req.URL.Path, expectedURL) { + return nil, fmt.Errorf("Expected URL '%s', got '%s'", expectedURL, req.URL) + } + query := req.URL.Query() + for key, expected := range listCase.expectedQueryParams { + actual := query.Get(key) + assert.Equal(t, actual, expected) + } + content, err := json.Marshal(types.ContainersPruneReport{ + ContainersDeleted: []string{"container_id1", "container_id2"}, + SpaceReclaimed: 9999, + }) + if err != nil { + return nil, err + } + return &http.Response{ + StatusCode: http.StatusOK, + Body: ioutil.NopCloser(bytes.NewReader(content)), + }, nil + }), + version: "1.25", + } + + report, err := client.ContainersPrune(context.Background(), listCase.filters) + assert.NilError(t, err) + assert.Equal(t, len(report.ContainersDeleted), 2) + assert.Equal(t, report.SpaceReclaimed, uint64(9999)) + } +} diff --git a/image_prune_test.go b/image_prune_test.go new file mode 100644 index 0000000000..61cf18ef35 --- /dev/null +++ b/image_prune_test.go @@ -0,0 +1,106 @@ +package client + +import ( + "bytes" + "encoding/json" + "fmt" + "io/ioutil" + "net/http" + "strings" + "testing" + + "github.com/docker/docker/api/types" + "github.com/docker/docker/api/types/filters" + "github.com/docker/docker/pkg/testutil/assert" + "golang.org/x/net/context" +) + +func TestImagesPruneError(t *testing.T) { + client := &Client{ + client: newMockClient(errorMock(http.StatusInternalServerError, "Server error")), + version: "1.25", + } + + filters := filters.NewArgs() + + _, err := client.ImagesPrune(context.Background(), filters) + assert.Error(t, err, "Error response from daemon: Server error") +} + +func TestImagesPrune(t *testing.T) { + expectedURL := "/v1.25/images/prune" + + danglingFilters := filters.NewArgs() + danglingFilters.Add("dangling", "true") + + noDanglingFilters := filters.NewArgs() + noDanglingFilters.Add("dangling", "false") + + listCases := []struct { + filters filters.Args + expectedQueryParams map[string]string + }{ + { + filters: filters.Args{}, + expectedQueryParams: map[string]string{ + "until": "", + "filter": "", + "filters": "", + }, + }, + { + filters: danglingFilters, + expectedQueryParams: map[string]string{ + "until": "", + "filter": "", + "filters": `{"dangling":{"true":true}}`, + }, + }, + { + filters: noDanglingFilters, + expectedQueryParams: map[string]string{ + "until": "", + "filter": "", + "filters": `{"dangling":{"false":true}}`, + }, + }, + } + for _, listCase := range listCases { + client := &Client{ + client: newMockClient(func(req *http.Request) (*http.Response, error) { + if !strings.HasPrefix(req.URL.Path, expectedURL) { + return nil, fmt.Errorf("Expected URL '%s', got '%s'", expectedURL, req.URL) + } + query := req.URL.Query() + for key, expected := range listCase.expectedQueryParams { + actual := query.Get(key) + assert.Equal(t, actual, expected) + } + content, err := json.Marshal(types.ImagesPruneReport{ + ImagesDeleted: []types.ImageDelete{ + { + Deleted: "image_id1", + }, + { + Deleted: "image_id2", + }, + }, + SpaceReclaimed: 9999, + }) + if err != nil { + return nil, err + } + return &http.Response{ + StatusCode: http.StatusOK, + Body: ioutil.NopCloser(bytes.NewReader(content)), + }, nil + }), + version: "1.25", + } + + report, err := client.ImagesPrune(context.Background(), listCase.filters) + assert.NilError(t, err) + assert.Equal(t, len(report.ImagesDeleted), 2) + assert.Equal(t, report.SpaceReclaimed, uint64(9999)) + } +} diff --git a/network_prune_test.go b/network_prune_test.go new file mode 100644 index 0000000000..07a5d41f20 --- /dev/null +++ b/network_prune_test.go @@ -0,0 +1,99 @@ +package client + +import ( + "bytes" + "encoding/json" + "fmt" + "io/ioutil" + "net/http" + "strings" + "testing" + + "github.com/docker/docker/api/types" + "github.com/docker/docker/api/types/filters" + "github.com/docker/docker/pkg/testutil/assert" + "golang.org/x/net/context" +) + +func TestNetworksPruneError(t *testing.T) { + client := &Client{ + client: newMockClient(errorMock(http.StatusInternalServerError, "Server error")), + version: "1.25", + } + + filters := filters.NewArgs() + + _, err := client.NetworksPrune(context.Background(), filters) + if err == nil || err.Error() != "Error response from daemon: Server error" { + t.Fatalf("expected a Server Error, got %v", err) + } +} + +func TestNetworksPrune(t *testing.T) { + expectedURL := "/v1.25/networks/prune" + + danglingFilters := filters.NewArgs() + danglingFilters.Add("dangling", "true") + + noDanglingFilters := filters.NewArgs() + noDanglingFilters.Add("dangling", "false") + + listCases := []struct { + filters filters.Args + expectedQueryParams map[string]string + }{ + { + filters: filters.Args{}, + expectedQueryParams: map[string]string{ + "until": "", + "filter": "", + "filters": "", + }, + }, + { + filters: danglingFilters, + expectedQueryParams: map[string]string{ + "until": "", + "filter": "", + "filters": `{"dangling":{"true":true}}`, + }, + }, + { + filters: noDanglingFilters, + expectedQueryParams: map[string]string{ + "until": "", + "filter": "", + "filters": `{"dangling":{"false":true}}`, + }, + }, + } + for _, listCase := range listCases { + client := &Client{ + client: newMockClient(func(req *http.Request) (*http.Response, error) { + if !strings.HasPrefix(req.URL.Path, expectedURL) { + return nil, fmt.Errorf("Expected URL '%s', got '%s'", expectedURL, req.URL) + } + query := req.URL.Query() + for key, expected := range listCase.expectedQueryParams { + actual := query.Get(key) + assert.Equal(t, actual, expected) + } + content, err := json.Marshal(types.NetworksPruneReport{ + NetworksDeleted: []string{"network_id1", "network_id2"}, + }) + if err != nil { + return nil, err + } + return &http.Response{ + StatusCode: http.StatusOK, + Body: ioutil.NopCloser(bytes.NewReader(content)), + }, nil + }), + version: "1.25", + } + + report, err := client.NetworksPrune(context.Background(), listCase.filters) + assert.NilError(t, err) + assert.Equal(t, len(report.NetworksDeleted), 2) + } +} From b71687e054e711f8432b550cf431e3da81d61756 Mon Sep 17 00:00:00 2001 From: wefine Date: Tue, 3 Jan 2017 23:02:58 +0800 Subject: [PATCH 107/138] check both source_image_tag and target_image_tag for 'docker image tag' Signed-off-by: wefine --- image_tag.go | 18 ++++++++++-------- image_tag_test.go | 13 ++++++++++++- 2 files changed, 22 insertions(+), 9 deletions(-) diff --git a/image_tag.go b/image_tag.go index bdbf94add2..dbcd078e1c 100644 --- a/image_tag.go +++ b/image_tag.go @@ -1,21 +1,23 @@ package client import ( - "errors" - "fmt" "net/url" - "golang.org/x/net/context" - distreference "github.com/docker/distribution/reference" "github.com/docker/docker/api/types/reference" + "github.com/pkg/errors" + "golang.org/x/net/context" ) // ImageTag tags an image in the docker host -func (cli *Client) ImageTag(ctx context.Context, imageID, ref string) error { - distributionRef, err := distreference.ParseNamed(ref) +func (cli *Client) ImageTag(ctx context.Context, source, target string) error { + if _, err := distreference.ParseNamed(source); err != nil { + return errors.Wrapf(err, "Error parsing reference: %q is not a valid repository/tag", source) + } + + distributionRef, err := distreference.ParseNamed(target) if err != nil { - return fmt.Errorf("Error parsing reference: %q is not a valid repository/tag", ref) + return errors.Wrapf(err, "Error parsing reference: %q is not a valid repository/tag", target) } if _, isCanonical := distributionRef.(distreference.Canonical); isCanonical { @@ -28,7 +30,7 @@ func (cli *Client) ImageTag(ctx context.Context, imageID, ref string) error { query.Set("repo", distributionRef.Name()) query.Set("tag", tag) - resp, err := cli.post(ctx, "/images/"+imageID+"/tag", query, nil, nil) + resp, err := cli.post(ctx, "/images/"+source+"/tag", query, nil, nil) ensureReaderClosed(resp) return err } diff --git a/image_tag_test.go b/image_tag_test.go index 7925db9f1b..d37bd0e85e 100644 --- a/image_tag_test.go +++ b/image_tag_test.go @@ -30,11 +30,22 @@ func TestImageTagInvalidReference(t *testing.T) { } err := client.ImageTag(context.Background(), "image_id", "aa/asdf$$^/aa") - if err == nil || err.Error() != `Error parsing reference: "aa/asdf$$^/aa" is not a valid repository/tag` { + if err == nil || err.Error() != `Error parsing reference: "aa/asdf$$^/aa" is not a valid repository/tag: invalid reference format` { t.Fatalf("expected ErrReferenceInvalidFormat, got %v", err) } } +func TestImageTagInvalidSourceImageName(t *testing.T) { + client := &Client{ + client: newMockClient(errorMock(http.StatusInternalServerError, "Server error")), + } + + err := client.ImageTag(context.Background(), "invalid_source_image_name_", "repo:tag") + if err == nil || err.Error() != "Error parsing reference: \"invalid_source_image_name_\" is not a valid repository/tag: invalid reference format" { + t.Fatalf("expected Parsing Reference Error, got %v", err) + } +} + func TestImageTag(t *testing.T) { expectedURL := "/images/image_id/tag" tagCases := []struct { From 0247b1509c94c6597886be4616b203758f94a712 Mon Sep 17 00:00:00 2001 From: Boaz Shuster Date: Tue, 6 Dec 2016 23:15:27 +0200 Subject: [PATCH 108/138] Update docs and code to use application/x-tar in the build API At the "Build image from Dockerfile" section in the API docs the Content-Type header is missing. In addition, some parts in the code are still setting the Content-Type header to application/tar while it was changed to application/x-tar since 16th September 2015. Signed-off-by: Boaz Shuster --- image_build.go | 2 +- image_build_test.go | 4 ++-- plugin_create.go | 2 +- 3 files changed, 4 insertions(+), 4 deletions(-) diff --git a/image_build.go b/image_build.go index 6fde75dcfd..411d5493ea 100644 --- a/image_build.go +++ b/image_build.go @@ -29,7 +29,7 @@ func (cli *Client) ImageBuild(ctx context.Context, buildContext io.Reader, optio return types.ImageBuildResponse{}, err } headers.Add("X-Registry-Config", base64.URLEncoding.EncodeToString(buf)) - headers.Set("Content-Type", "application/tar") + headers.Set("Content-Type", "application/x-tar") serverResp, err := cli.postRaw(ctx, "/build", query, buildContext, headers) if err != nil { diff --git a/image_build_test.go b/image_build_test.go index b9d04f817a..1e18b7bda8 100644 --- a/image_build_test.go +++ b/image_build_test.go @@ -170,8 +170,8 @@ func TestImageBuild(t *testing.T) { return nil, fmt.Errorf("X-Registry-Config header not properly set in the request. Expected '%s', got %s", buildCase.expectedRegistryConfig, registryConfig) } contentType := r.Header.Get("Content-Type") - if contentType != "application/tar" { - return nil, fmt.Errorf("Content-type header not properly set in the request. Expected 'application/tar', got %s", contentType) + if contentType != "application/x-tar" { + return nil, fmt.Errorf("Content-type header not properly set in the request. Expected 'application/x-tar', got %s", contentType) } // Check query parameters diff --git a/plugin_create.go b/plugin_create.go index a660ba5733..27954aa573 100644 --- a/plugin_create.go +++ b/plugin_create.go @@ -12,7 +12,7 @@ import ( // PluginCreate creates a plugin func (cli *Client) PluginCreate(ctx context.Context, createContext io.Reader, createOptions types.PluginCreateOptions) error { headers := http.Header(make(map[string][]string)) - headers.Set("Content-Type", "application/tar") + headers.Set("Content-Type", "application/x-tar") query := url.Values{} query.Set("name", createOptions.RepoName) From 266db2ecda45a51c0b63673d4b227b352ad98607 Mon Sep 17 00:00:00 2001 From: Josh Hawn Date: Tue, 22 Nov 2016 11:03:23 -0800 Subject: [PATCH 109/138] Add SecretUpdate method to client closes #28678 Docker-DCO-1.1-Signed-off-by: Josh Hawn (github: jlhawn) Update cli/command/service/update_test.go Fixes test build error: secretAPIClientMock does not implement "github.com/docker/docker/client".SecretAPIClient (missing SecretUpdate method) Docker-DCO-1.1-Signed-off-by: Josh Hawn (github: jlhawn) --- interface.go | 1 + secret_update.go | 19 +++++++++++++++++ secret_update_test.go | 49 +++++++++++++++++++++++++++++++++++++++++++ 3 files changed, 69 insertions(+) create mode 100644 secret_update.go create mode 100644 secret_update_test.go diff --git a/interface.go b/interface.go index 00b9adea32..924b22bc04 100644 --- a/interface.go +++ b/interface.go @@ -166,4 +166,5 @@ type SecretAPIClient interface { SecretCreate(ctx context.Context, secret swarm.SecretSpec) (types.SecretCreateResponse, error) SecretRemove(ctx context.Context, id string) error SecretInspectWithRaw(ctx context.Context, name string) (swarm.Secret, []byte, error) + SecretUpdate(ctx context.Context, id string, version swarm.Version, secret swarm.SecretSpec) error } diff --git a/secret_update.go b/secret_update.go new file mode 100644 index 0000000000..b94e24aab0 --- /dev/null +++ b/secret_update.go @@ -0,0 +1,19 @@ +package client + +import ( + "net/url" + "strconv" + + "github.com/docker/docker/api/types/swarm" + "golang.org/x/net/context" +) + +// SecretUpdate updates a Secret. Currently, the only part of a secret spec +// which can be updated is Labels. +func (cli *Client) SecretUpdate(ctx context.Context, id string, version swarm.Version, secret swarm.SecretSpec) error { + query := url.Values{} + query.Set("version", strconv.FormatUint(version.Index, 10)) + resp, err := cli.post(ctx, "/secrets/"+id+"/update", query, secret, nil) + ensureReaderClosed(resp) + return err +} diff --git a/secret_update_test.go b/secret_update_test.go new file mode 100644 index 0000000000..c620985bd5 --- /dev/null +++ b/secret_update_test.go @@ -0,0 +1,49 @@ +package client + +import ( + "bytes" + "fmt" + "io/ioutil" + "net/http" + "strings" + "testing" + + "golang.org/x/net/context" + + "github.com/docker/docker/api/types/swarm" +) + +func TestSecretUpdateError(t *testing.T) { + client := &Client{ + client: newMockClient(errorMock(http.StatusInternalServerError, "Server error")), + } + + err := client.SecretUpdate(context.Background(), "secret_id", swarm.Version{}, swarm.SecretSpec{}) + if err == nil || err.Error() != "Error response from daemon: Server error" { + t.Fatalf("expected a Server Error, got %v", err) + } +} + +func TestSecretUpdate(t *testing.T) { + expectedURL := "/secrets/secret_id/update" + + client := &Client{ + client: newMockClient(func(req *http.Request) (*http.Response, error) { + if !strings.HasPrefix(req.URL.Path, expectedURL) { + return nil, fmt.Errorf("Expected URL '%s', got '%s'", expectedURL, req.URL) + } + if req.Method != "POST" { + return nil, fmt.Errorf("expected POST method, got %s", req.Method) + } + return &http.Response{ + StatusCode: http.StatusOK, + Body: ioutil.NopCloser(bytes.NewReader([]byte("body"))), + }, nil + }), + } + + err := client.SecretUpdate(context.Background(), "secret_id", swarm.Version{}, swarm.SecretSpec{}) + if err != nil { + t.Fatal(err) + } +} From 36315fa14b72a6e51ee072271f78d22f790b0d25 Mon Sep 17 00:00:00 2001 From: Sebastiaan van Stijn Date: Fri, 13 Jan 2017 01:05:39 +0100 Subject: [PATCH 110/138] Don't use AutoRemove on older daemons Docker 1.13 moves the `--rm` flag to the daemon, through an AutoRemove option in HostConfig. When using API 1.24 and under, AutoRemove should not be used, even if the daemon is version 1.13 or above and "supports" this feature. This patch fixes a situation where an 1.13 client, talking to an 1.13 daemon, but using the 1.24 API version, still set the AutoRemove property. As a result, both the client _and_ the daemon were attempting to remove the container, resulting in an error: ERRO[0000] error removing container: Error response from daemon: removal of container ce0976ad22495c7cbe9487752ea32721a282164862db036b2f3377bd07461c3a is already in progress In addition, the validation of conflicting options is moved from `docker run` to `opts.parse()`, so that conflicting options are also detected when running `docker create` and `docker start` separately. To resolve the issue, the `AutoRemove` option is now always set to `false` both by the client and the daemon, if API version 1.24 or under is used. Signed-off-by: Sebastiaan van Stijn --- container_create.go | 6 ++++++ container_create_test.go | 42 ++++++++++++++++++++++++++++++++++++++++ 2 files changed, 48 insertions(+) diff --git a/container_create.go b/container_create.go index 9f627aafa6..6841b0b282 100644 --- a/container_create.go +++ b/container_create.go @@ -7,6 +7,7 @@ import ( "github.com/docker/docker/api/types/container" "github.com/docker/docker/api/types/network" + "github.com/docker/docker/api/types/versions" "golang.org/x/net/context" ) @@ -25,6 +26,11 @@ func (cli *Client) ContainerCreate(ctx context.Context, config *container.Config return response, err } + // When using API 1.24 and under, the client is responsible for removing the container + if hostConfig != nil && versions.LessThan(cli.ClientVersion(), "1.25") { + hostConfig.AutoRemove = false + } + query := url.Values{} if containerName != "" { query.Set("name", containerName) diff --git a/container_create_test.go b/container_create_test.go index 15dbd5ea01..73474cf56f 100644 --- a/container_create_test.go +++ b/container_create_test.go @@ -74,3 +74,45 @@ func TestContainerCreateWithName(t *testing.T) { t.Fatalf("expected `container_id`, got %s", r.ID) } } + +// TestContainerCreateAutoRemove validates that a client using API 1.24 always disables AutoRemove. When using API 1.25 +// or up, AutoRemove should not be disabled. +func TestContainerCreateAutoRemove(t *testing.T) { + autoRemoveValidator := func(expectedValue bool) func(req *http.Request) (*http.Response, error) { + return func(req *http.Request) (*http.Response, error) { + var config configWrapper + + if err := json.NewDecoder(req.Body).Decode(&config); err != nil { + return nil, err + } + if config.HostConfig.AutoRemove != expectedValue { + return nil, fmt.Errorf("expected AutoRemove to be %v, got %v", expectedValue, config.HostConfig.AutoRemove) + } + b, err := json.Marshal(container.ContainerCreateCreatedBody{ + ID: "container_id", + }) + if err != nil { + return nil, err + } + return &http.Response{ + StatusCode: http.StatusOK, + Body: ioutil.NopCloser(bytes.NewReader(b)), + }, nil + } + } + + client := &Client{ + client: newMockClient(autoRemoveValidator(false)), + version: "1.24", + } + if _, err := client.ContainerCreate(context.Background(), nil, &container.HostConfig{AutoRemove: true}, nil, ""); err != nil { + t.Fatal(err) + } + client = &Client{ + client: newMockClient(autoRemoveValidator(true)), + version: "1.25", + } + if _, err := client.ContainerCreate(context.Background(), nil, &container.HostConfig{AutoRemove: true}, nil, ""); err != nil { + t.Fatal(err) + } +} From 26816a911a5e12b2b78c66b958030fbc18193b6f Mon Sep 17 00:00:00 2001 From: Daniel Nephin Date: Fri, 13 Jan 2017 11:26:29 -0500 Subject: [PATCH 111/138] Add integration test for stack deploy with secrets. Signed-off-by: Daniel Nephin --- secret_update.go | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/secret_update.go b/secret_update.go index b94e24aab0..42cdbbe176 100644 --- a/secret_update.go +++ b/secret_update.go @@ -8,8 +8,7 @@ import ( "golang.org/x/net/context" ) -// SecretUpdate updates a Secret. Currently, the only part of a secret spec -// which can be updated is Labels. +// SecretUpdate attempts to updates a Secret func (cli *Client) SecretUpdate(ctx context.Context, id string, version swarm.Version, secret swarm.SecretSpec) error { query := url.Values{} query.Set("version", strconv.FormatUint(version.Index, 10)) From 2d7a37e91cf4498b94b537221aaed780bec22378 Mon Sep 17 00:00:00 2001 From: Daniel Nephin Date: Thu, 26 Jan 2017 14:07:44 -0500 Subject: [PATCH 112/138] Fix ImageDelete type Signed-off-by: Daniel Nephin --- image_prune_test.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/image_prune_test.go b/image_prune_test.go index 61cf18ef35..68cd995d37 100644 --- a/image_prune_test.go +++ b/image_prune_test.go @@ -77,7 +77,7 @@ func TestImagesPrune(t *testing.T) { assert.Equal(t, actual, expected) } content, err := json.Marshal(types.ImagesPruneReport{ - ImagesDeleted: []types.ImageDelete{ + ImagesDeleted: []types.ImageDeleteResponseItem{ { Deleted: "image_id1", }, From 9a06063feab153887029d22242b37ae8397df7aa Mon Sep 17 00:00:00 2001 From: Yong Tang Date: Wed, 23 Nov 2016 04:58:15 -0800 Subject: [PATCH 113/138] Add `--filter enabled=true` for `docker plugin ls` This fix adds `--filter enabled=true` to `docker plugin ls`, as was specified in 28624. The related API and docs has been updated. An integration test has been added. This fix fixes 28624. Signed-off-by: Yong Tang --- interface.go | 2 +- plugin_list.go | 15 +++++++- plugin_list_test.go | 92 +++++++++++++++++++++++++++++++-------------- 3 files changed, 78 insertions(+), 31 deletions(-) diff --git a/interface.go b/interface.go index 771a3d9a06..d30ba5f705 100644 --- a/interface.go +++ b/interface.go @@ -108,7 +108,7 @@ type NodeAPIClient interface { // PluginAPIClient defines API client methods for the plugins type PluginAPIClient interface { - PluginList(ctx context.Context) (types.PluginsListResponse, error) + PluginList(ctx context.Context, filter filters.Args) (types.PluginsListResponse, error) PluginRemove(ctx context.Context, name string, options types.PluginRemoveOptions) error PluginEnable(ctx context.Context, name string, options types.PluginEnableOptions) error PluginDisable(ctx context.Context, name string, options types.PluginDisableOptions) error diff --git a/plugin_list.go b/plugin_list.go index 88c480a3e1..3acde3b966 100644 --- a/plugin_list.go +++ b/plugin_list.go @@ -2,15 +2,26 @@ package client import ( "encoding/json" + "net/url" "github.com/docker/docker/api/types" + "github.com/docker/docker/api/types/filters" "golang.org/x/net/context" ) // PluginList returns the installed plugins -func (cli *Client) PluginList(ctx context.Context) (types.PluginsListResponse, error) { +func (cli *Client) PluginList(ctx context.Context, filter filters.Args) (types.PluginsListResponse, error) { var plugins types.PluginsListResponse - resp, err := cli.get(ctx, "/plugins", nil, nil) + query := url.Values{} + + if filter.Len() > 0 { + filterJSON, err := filters.ToParamWithVersion(cli.version, filter) + if err != nil { + return plugins, err + } + query.Set("filters", filterJSON) + } + resp, err := cli.get(ctx, "/plugins", query, nil) if err != nil { return plugins, err } diff --git a/plugin_list_test.go b/plugin_list_test.go index 173e4b87f5..6a0e9844fc 100644 --- a/plugin_list_test.go +++ b/plugin_list_test.go @@ -10,6 +10,7 @@ import ( "testing" "github.com/docker/docker/api/types" + "github.com/docker/docker/api/types/filters" "golang.org/x/net/context" ) @@ -18,7 +19,7 @@ func TestPluginListError(t *testing.T) { client: newMockClient(errorMock(http.StatusInternalServerError, "Server error")), } - _, err := client.PluginList(context.Background()) + _, err := client.PluginList(context.Background(), filters.NewArgs()) if err == nil || err.Error() != "Error response from daemon: Server error" { t.Fatalf("expected a Server Error, got %v", err) } @@ -26,34 +27,69 @@ func TestPluginListError(t *testing.T) { func TestPluginList(t *testing.T) { expectedURL := "/plugins" - client := &Client{ - client: newMockClient(func(req *http.Request) (*http.Response, error) { - if !strings.HasPrefix(req.URL.Path, expectedURL) { - return nil, fmt.Errorf("Expected URL '%s', got '%s'", expectedURL, req.URL) - } - content, err := json.Marshal([]*types.Plugin{ - { - ID: "plugin_id1", - }, - { - ID: "plugin_id2", - }, - }) - if err != nil { - return nil, err - } - return &http.Response{ - StatusCode: http.StatusOK, - Body: ioutil.NopCloser(bytes.NewReader(content)), - }, nil - }), + + enabledFilters := filters.NewArgs() + enabledFilters.Add("enabled", "true") + + listCases := []struct { + filters filters.Args + expectedQueryParams map[string]string + }{ + { + filters: filters.NewArgs(), + expectedQueryParams: map[string]string{ + "all": "", + "filter": "", + "filters": "", + }, + }, + { + filters: enabledFilters, + expectedQueryParams: map[string]string{ + "all": "", + "filter": "", + "filters": `{"enabled":{"true":true}}`, + }, + }, } - plugins, err := client.PluginList(context.Background()) - if err != nil { - t.Fatal(err) - } - if len(plugins) != 2 { - t.Fatalf("expected 2 plugins, got %v", plugins) + for _, listCase := range listCases { + client := &Client{ + client: newMockClient(func(req *http.Request) (*http.Response, error) { + if !strings.HasPrefix(req.URL.Path, expectedURL) { + return nil, fmt.Errorf("Expected URL '%s', got '%s'", expectedURL, req.URL) + } + query := req.URL.Query() + for key, expected := range listCase.expectedQueryParams { + actual := query.Get(key) + if actual != expected { + return nil, fmt.Errorf("%s not set in URL query properly. Expected '%s', got %s", key, expected, actual) + } + } + content, err := json.Marshal([]*types.Plugin{ + { + ID: "plugin_id1", + }, + { + ID: "plugin_id2", + }, + }) + if err != nil { + return nil, err + } + return &http.Response{ + StatusCode: http.StatusOK, + Body: ioutil.NopCloser(bytes.NewReader(content)), + }, nil + }), + } + + plugins, err := client.PluginList(context.Background(), listCase.filters) + if err != nil { + t.Fatal(err) + } + if len(plugins) != 2 { + t.Fatalf("expected 2 plugins, got %v", plugins) + } } } From e38bc0d03e9447733287b839139b3b883c8dafdc Mon Sep 17 00:00:00 2001 From: Yong Tang Date: Wed, 23 Nov 2016 05:27:09 -0800 Subject: [PATCH 114/138] Add `capability` filter to `docker plugin ls` This fix adds `--filter capability=[volumedriver|authz]` to `docker plugin ls`. The related docs has been updated. An integration test has been added. Signed-off-by: Yong Tang --- plugin_list_test.go | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/plugin_list_test.go b/plugin_list_test.go index 6a0e9844fc..6887079b42 100644 --- a/plugin_list_test.go +++ b/plugin_list_test.go @@ -31,6 +31,10 @@ func TestPluginList(t *testing.T) { enabledFilters := filters.NewArgs() enabledFilters.Add("enabled", "true") + capabilityFilters := filters.NewArgs() + capabilityFilters.Add("capability", "volumedriver") + capabilityFilters.Add("capability", "authz") + listCases := []struct { filters filters.Args expectedQueryParams map[string]string @@ -51,6 +55,14 @@ func TestPluginList(t *testing.T) { "filters": `{"enabled":{"true":true}}`, }, }, + { + filters: capabilityFilters, + expectedQueryParams: map[string]string{ + "all": "", + "filter": "", + "filters": `{"capability":{"authz":true,"volumedriver":true}}`, + }, + }, } for _, listCase := range listCases { From ba79205e30b6626623c0985bb9ad71026e1a62b8 Mon Sep 17 00:00:00 2001 From: Krasi Georgiev Date: Thu, 2 Feb 2017 00:40:43 +0200 Subject: [PATCH 115/138] more descriptive error fo checkpoint ls for non existent containers Signed-off-by: Krasi Georgiev --- checkpoint_list.go | 4 ++++ checkpoint_list_test.go | 11 +++++++++++ 2 files changed, 15 insertions(+) diff --git a/checkpoint_list.go b/checkpoint_list.go index 8eb720a6b2..97f2badf76 100644 --- a/checkpoint_list.go +++ b/checkpoint_list.go @@ -2,6 +2,7 @@ package client import ( "encoding/json" + "net/http" "net/url" "github.com/docker/docker/api/types" @@ -19,6 +20,9 @@ func (cli *Client) CheckpointList(ctx context.Context, container string, options resp, err := cli.get(ctx, "/containers/"+container+"/checkpoints", query, nil) if err != nil { + if resp.statusCode == http.StatusNotFound { + return checkpoints, containerNotFoundError{container} + } return checkpoints, err } diff --git a/checkpoint_list_test.go b/checkpoint_list_test.go index 6c90f61e8c..388465715b 100644 --- a/checkpoint_list_test.go +++ b/checkpoint_list_test.go @@ -55,3 +55,14 @@ func TestCheckpointList(t *testing.T) { t.Fatalf("expected 1 checkpoint, got %v", checkpoints) } } + +func TestCheckpointListContainerNotFound(t *testing.T) { + client := &Client{ + client: newMockClient(errorMock(http.StatusNotFound, "Server error")), + } + + _, err := client.CheckpointList(context.Background(), "unknown", types.CheckpointListOptions{}) + if err == nil || !IsErrContainerNotFound(err) { + t.Fatalf("expected a containerNotFound error, got %v", err) + } +} From 9d18236794e0f115c5ff8c43dcf8267e8b7213eb Mon Sep 17 00:00:00 2001 From: allencloud Date: Sat, 4 Feb 2017 00:41:35 +0800 Subject: [PATCH 116/138] update incorrect comments of CheckpointList Signed-off-by: allencloud --- checkpoint_list.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/checkpoint_list.go b/checkpoint_list.go index 97f2badf76..ffe44bc976 100644 --- a/checkpoint_list.go +++ b/checkpoint_list.go @@ -9,7 +9,7 @@ import ( "golang.org/x/net/context" ) -// CheckpointList returns the volumes configured in the docker host. +// CheckpointList returns the checkpoints of the given container in the docker host func (cli *Client) CheckpointList(ctx context.Context, container string, options types.CheckpointListOptions) ([]types.Checkpoint, error) { var checkpoints []types.Checkpoint From e301053ff54a44f8324d7008bf70a19700a15230 Mon Sep 17 00:00:00 2001 From: Brian Goff Date: Sat, 28 Jan 2017 16:54:32 -0800 Subject: [PATCH 117/138] Add docker plugin upgrade This allows a plugin to be upgraded without requiring to uninstall/reinstall a plugin. Since plugin resources (e.g. volumes) are tied to a plugin ID, this is important to ensure resources aren't lost. The plugin must be disabled while upgrading (errors out if enabled). This does not add any convenience flags for automatically disabling/re-enabling the plugin during before/after upgrade. Since an upgrade may change requested permissions, the user is required to accept permissions just like `docker plugin install`. Signed-off-by: Brian Goff --- interface.go | 1 + plugin_install.go | 68 ++++++++++++++++++++++++++--------------------- plugin_upgrade.go | 37 ++++++++++++++++++++++++++ 3 files changed, 76 insertions(+), 30 deletions(-) create mode 100644 plugin_upgrade.go diff --git a/interface.go b/interface.go index d30ba5f705..5823eed883 100644 --- a/interface.go +++ b/interface.go @@ -113,6 +113,7 @@ type PluginAPIClient interface { PluginEnable(ctx context.Context, name string, options types.PluginEnableOptions) error PluginDisable(ctx context.Context, name string, options types.PluginDisableOptions) error PluginInstall(ctx context.Context, name string, options types.PluginInstallOptions) (io.ReadCloser, error) + PluginUpgrade(ctx context.Context, name string, options types.PluginInstallOptions) (io.ReadCloser, error) PluginPush(ctx context.Context, name string, registryAuth string) (io.ReadCloser, error) PluginSet(ctx context.Context, name string, args []string) error PluginInspectWithRaw(ctx context.Context, name string) (*types.Plugin, []byte, error) diff --git a/plugin_install.go b/plugin_install.go index b305780cfb..3217c4cf39 100644 --- a/plugin_install.go +++ b/plugin_install.go @@ -20,43 +20,15 @@ func (cli *Client) PluginInstall(ctx context.Context, name string, options types } query.Set("remote", options.RemoteRef) - resp, err := cli.tryPluginPrivileges(ctx, query, options.RegistryAuth) - if resp.statusCode == http.StatusUnauthorized && options.PrivilegeFunc != nil { - // todo: do inspect before to check existing name before checking privileges - newAuthHeader, privilegeErr := options.PrivilegeFunc() - if privilegeErr != nil { - ensureReaderClosed(resp) - return nil, privilegeErr - } - options.RegistryAuth = newAuthHeader - resp, err = cli.tryPluginPrivileges(ctx, query, options.RegistryAuth) - } + privileges, err := cli.checkPluginPermissions(ctx, query, options) if err != nil { - ensureReaderClosed(resp) return nil, err } - var privileges types.PluginPrivileges - if err := json.NewDecoder(resp.body).Decode(&privileges); err != nil { - ensureReaderClosed(resp) - return nil, err - } - ensureReaderClosed(resp) - - if !options.AcceptAllPermissions && options.AcceptPermissionsFunc != nil && len(privileges) > 0 { - accept, err := options.AcceptPermissionsFunc(privileges) - if err != nil { - return nil, err - } - if !accept { - return nil, pluginPermissionDenied{options.RemoteRef} - } - } - // set name for plugin pull, if empty should default to remote reference query.Set("name", name) - resp, err = cli.tryPluginPull(ctx, query, privileges, options.RegistryAuth) + resp, err := cli.tryPluginPull(ctx, query, privileges, options.RegistryAuth) if err != nil { return nil, err } @@ -103,3 +75,39 @@ func (cli *Client) tryPluginPull(ctx context.Context, query url.Values, privileg headers := map[string][]string{"X-Registry-Auth": {registryAuth}} return cli.post(ctx, "/plugins/pull", query, privileges, headers) } + +func (cli *Client) checkPluginPermissions(ctx context.Context, query url.Values, options types.PluginInstallOptions) (types.PluginPrivileges, error) { + resp, err := cli.tryPluginPrivileges(ctx, query, options.RegistryAuth) + if resp.statusCode == http.StatusUnauthorized && options.PrivilegeFunc != nil { + // todo: do inspect before to check existing name before checking privileges + newAuthHeader, privilegeErr := options.PrivilegeFunc() + if privilegeErr != nil { + ensureReaderClosed(resp) + return nil, privilegeErr + } + options.RegistryAuth = newAuthHeader + resp, err = cli.tryPluginPrivileges(ctx, query, options.RegistryAuth) + } + if err != nil { + ensureReaderClosed(resp) + return nil, err + } + + var privileges types.PluginPrivileges + if err := json.NewDecoder(resp.body).Decode(&privileges); err != nil { + ensureReaderClosed(resp) + return nil, err + } + ensureReaderClosed(resp) + + if !options.AcceptAllPermissions && options.AcceptPermissionsFunc != nil && len(privileges) > 0 { + accept, err := options.AcceptPermissionsFunc(privileges) + if err != nil { + return nil, err + } + if !accept { + return nil, pluginPermissionDenied{options.RemoteRef} + } + } + return privileges, nil +} diff --git a/plugin_upgrade.go b/plugin_upgrade.go new file mode 100644 index 0000000000..95a4356b97 --- /dev/null +++ b/plugin_upgrade.go @@ -0,0 +1,37 @@ +package client + +import ( + "fmt" + "io" + "net/url" + + "github.com/docker/distribution/reference" + "github.com/docker/docker/api/types" + "github.com/pkg/errors" + "golang.org/x/net/context" +) + +// PluginUpgrade upgrades a plugin +func (cli *Client) PluginUpgrade(ctx context.Context, name string, options types.PluginInstallOptions) (rc io.ReadCloser, err error) { + query := url.Values{} + if _, err := reference.ParseNamed(options.RemoteRef); err != nil { + return nil, errors.Wrap(err, "invalid remote reference") + } + query.Set("remote", options.RemoteRef) + + privileges, err := cli.checkPluginPermissions(ctx, query, options) + if err != nil { + return nil, err + } + + resp, err := cli.tryPluginUpgrade(ctx, query, privileges, name, options.RegistryAuth) + if err != nil { + return nil, err + } + return resp.body, nil +} + +func (cli *Client) tryPluginUpgrade(ctx context.Context, query url.Values, privileges types.PluginPrivileges, name, registryAuth string) (serverResponse, error) { + headers := map[string][]string{"X-Registry-Auth": {registryAuth}} + return cli.post(ctx, fmt.Sprintf("/plugins/%s/upgrade", name), query, privileges, headers) +} From 0ae3a20be60e81f1f98e1398958fd4627cb6a31c Mon Sep 17 00:00:00 2001 From: Sebastiaan van Stijn Date: Mon, 16 Jan 2017 15:35:27 +0100 Subject: [PATCH 118/138] print error if unsupported flags are used Docker 1.13 and up allows a client to communicate with older daemons. As a result, flags may be present that are not supported by the older daemon. The client already _hides_ flags that are not supported yet, but this doesn't present users from using those flags. This change shows an error if a flag is used that is not supported by the daemon (either based on the API version, or because experimental features are not enabled). Note that for some options, a check is already in place in the API client. For those options, this is just a minor enhancement to more clearly indicate which _flag_ is not supported. Before this change; DOCKER_API_VERSION=1.24 docker run -d --stop-timeout=30 busybox top mjfyt3qpvnq0iwmun3sjwth9i echo -e "FROM busybox\nRUN echo foo > bar" | DOCKER_API_VERSION=1.24 docker build --squash - "squash" requires API version 1.25, but the Docker server is version 1.24 After this change; DOCKER_API_VERSION=1.24 docker run -d --stop-timeout=30 busybox top "--stop-timeout" requires API version 1.25, but the Docker daemon is version 1.24 echo -e "FROM busybox\nRUN echo foo > bar" | DOCKER_API_VERSION=1.24 docker build --squash - "--squash" requires API version 1.25, but the Docker daemon is version 1.24 echo -e "FROM busybox\nRUN echo foo > bar" | docker build --squash - "--squash" is only supported on a Docker daemon with experimental features enabled Signed-off-by: Sebastiaan van Stijn --- errors.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/errors.go b/errors.go index 2912692ec1..4f767bd8d3 100644 --- a/errors.go +++ b/errors.go @@ -229,7 +229,7 @@ func IsErrPluginPermissionDenied(err error) bool { // if less than the current supported version func (cli *Client) NewVersionError(APIrequired, feature string) error { if versions.LessThan(cli.version, APIrequired) { - return fmt.Errorf("%q requires API version %s, but the Docker server is version %s", feature, APIrequired, cli.version) + return fmt.Errorf("%q requires API version %s, but the Docker daemon API version is %s", feature, APIrequired, cli.version) } return nil } From b741d2e9b5cf4e52f8da0c94cdc3097616120411 Mon Sep 17 00:00:00 2001 From: Derek McGowan Date: Wed, 25 Jan 2017 16:54:18 -0800 Subject: [PATCH 119/138] Use distribution reference Remove forked reference package. Use normalized named values everywhere and familiar functions to convert back to familiar strings for UX and storage compatibility. Enforce that the source repository in the distribution metadata is always a normalized string, ignore invalid values which are not. Update distribution tests to use normalized values. Signed-off-by: Derek McGowan (github: dmcgowan) --- container_commit.go | 14 ++++++++------ image_create.go | 8 ++++---- image_import.go | 2 +- image_pull.go | 27 +++++++++++++++++++++------ image_pull_test.go | 2 +- image_push.go | 18 ++++++++++-------- image_push_test.go | 2 +- image_tag.go | 17 +++++++++-------- plugin_install.go | 2 +- plugin_upgrade.go | 2 +- 10 files changed, 57 insertions(+), 37 deletions(-) diff --git a/container_commit.go b/container_commit.go index c766d62e40..531d796ee7 100644 --- a/container_commit.go +++ b/container_commit.go @@ -5,9 +5,8 @@ import ( "errors" "net/url" - distreference "github.com/docker/distribution/reference" + "github.com/docker/distribution/reference" "github.com/docker/docker/api/types" - "github.com/docker/docker/api/types/reference" "golang.org/x/net/context" ) @@ -15,17 +14,20 @@ import ( func (cli *Client) ContainerCommit(ctx context.Context, container string, options types.ContainerCommitOptions) (types.IDResponse, error) { var repository, tag string if options.Reference != "" { - distributionRef, err := distreference.ParseNamed(options.Reference) + ref, err := reference.ParseNormalizedNamed(options.Reference) if err != nil { return types.IDResponse{}, err } - if _, isCanonical := distributionRef.(distreference.Canonical); isCanonical { + if _, isCanonical := ref.(reference.Canonical); isCanonical { return types.IDResponse{}, errors.New("refusing to create a tag with a digest reference") } + ref = reference.TagNameOnly(ref) - tag = reference.GetTagFromNamedRef(distributionRef) - repository = distributionRef.Name() + if tagged, ok := ref.(reference.Tagged); ok { + tag = tagged.Tag() + } + repository = reference.FamiliarName(ref) } query := url.Values{} diff --git a/image_create.go b/image_create.go index cf023a7186..4436abb0dd 100644 --- a/image_create.go +++ b/image_create.go @@ -6,21 +6,21 @@ import ( "golang.org/x/net/context" + "github.com/docker/distribution/reference" "github.com/docker/docker/api/types" - "github.com/docker/docker/api/types/reference" ) // ImageCreate creates a new image based in the parent options. // It returns the JSON content in the response body. func (cli *Client) ImageCreate(ctx context.Context, parentReference string, options types.ImageCreateOptions) (io.ReadCloser, error) { - repository, tag, err := reference.Parse(parentReference) + ref, err := reference.ParseNormalizedNamed(parentReference) if err != nil { return nil, err } query := url.Values{} - query.Set("fromImage", repository) - query.Set("tag", tag) + query.Set("fromImage", reference.FamiliarName(ref)) + query.Set("tag", getAPITagFromNamedRef(ref)) resp, err := cli.tryImageCreate(ctx, query, options.RegistryAuth) if err != nil { return nil, err diff --git a/image_import.go b/image_import.go index c6f154b249..d7dedd8232 100644 --- a/image_import.go +++ b/image_import.go @@ -15,7 +15,7 @@ import ( func (cli *Client) ImageImport(ctx context.Context, source types.ImageImportSource, ref string, options types.ImageImportOptions) (io.ReadCloser, error) { if ref != "" { //Check if the given image name can be resolved - if _, err := reference.ParseNamed(ref); err != nil { + if _, err := reference.ParseNormalizedNamed(ref); err != nil { return nil, err } } diff --git a/image_pull.go b/image_pull.go index 3bffdb70e8..a72b9bf7fc 100644 --- a/image_pull.go +++ b/image_pull.go @@ -7,8 +7,8 @@ import ( "golang.org/x/net/context" + "github.com/docker/distribution/reference" "github.com/docker/docker/api/types" - "github.com/docker/docker/api/types/reference" ) // ImagePull requests the docker host to pull an image from a remote registry. @@ -19,16 +19,16 @@ import ( // FIXME(vdemeester): there is currently used in a few way in docker/docker // - if not in trusted content, ref is used to pass the whole reference, and tag is empty // - if in trusted content, ref is used to pass the reference name, and tag for the digest -func (cli *Client) ImagePull(ctx context.Context, ref string, options types.ImagePullOptions) (io.ReadCloser, error) { - repository, tag, err := reference.Parse(ref) +func (cli *Client) ImagePull(ctx context.Context, refStr string, options types.ImagePullOptions) (io.ReadCloser, error) { + ref, err := reference.ParseNormalizedNamed(refStr) if err != nil { return nil, err } query := url.Values{} - query.Set("fromImage", repository) - if tag != "" && !options.All { - query.Set("tag", tag) + query.Set("fromImage", reference.FamiliarName(ref)) + if !options.All { + query.Set("tag", getAPITagFromNamedRef(ref)) } resp, err := cli.tryImageCreate(ctx, query, options.RegistryAuth) @@ -44,3 +44,18 @@ func (cli *Client) ImagePull(ctx context.Context, ref string, options types.Imag } return resp.body, nil } + +// getAPITagFromNamedRef returns a tag from the specified reference. +// This function is necessary as long as the docker "server" api expects +// digests to be sent as tags and makes a distinction between the name +// and tag/digest part of a reference. +func getAPITagFromNamedRef(ref reference.Named) string { + if digested, ok := ref.(reference.Digested); ok { + return digested.Digest().String() + } + ref = reference.TagNameOnly(ref) + if tagged, ok := ref.(reference.Tagged); ok { + return tagged.Tag() + } + return "" +} diff --git a/image_pull_test.go b/image_pull_test.go index fe6bafed97..ab49d2d349 100644 --- a/image_pull_test.go +++ b/image_pull_test.go @@ -21,7 +21,7 @@ func TestImagePullReferenceParseError(t *testing.T) { } // An empty reference is an invalid reference _, err := client.ImagePull(context.Background(), "", types.ImagePullOptions{}) - if err == nil || err.Error() != "repository name must have at least one component" { + if err == nil || !strings.Contains(err.Error(), "invalid reference format") { t.Fatalf("expected an error, got %v", err) } } diff --git a/image_push.go b/image_push.go index 8e73d28f56..410d2fb91d 100644 --- a/image_push.go +++ b/image_push.go @@ -8,7 +8,7 @@ import ( "golang.org/x/net/context" - distreference "github.com/docker/distribution/reference" + "github.com/docker/distribution/reference" "github.com/docker/docker/api/types" ) @@ -16,31 +16,33 @@ import ( // It executes the privileged function if the operation is unauthorized // and it tries one more time. // It's up to the caller to handle the io.ReadCloser and close it properly. -func (cli *Client) ImagePush(ctx context.Context, ref string, options types.ImagePushOptions) (io.ReadCloser, error) { - distributionRef, err := distreference.ParseNamed(ref) +func (cli *Client) ImagePush(ctx context.Context, image string, options types.ImagePushOptions) (io.ReadCloser, error) { + ref, err := reference.ParseNormalizedNamed(image) if err != nil { return nil, err } - if _, isCanonical := distributionRef.(distreference.Canonical); isCanonical { + if _, isCanonical := ref.(reference.Canonical); isCanonical { return nil, errors.New("cannot push a digest reference") } - var tag = "" - if nameTaggedRef, isNamedTagged := distributionRef.(distreference.NamedTagged); isNamedTagged { + tag := "" + name := reference.FamiliarName(ref) + + if nameTaggedRef, isNamedTagged := ref.(reference.NamedTagged); isNamedTagged { tag = nameTaggedRef.Tag() } query := url.Values{} query.Set("tag", tag) - resp, err := cli.tryImagePush(ctx, distributionRef.Name(), query, options.RegistryAuth) + resp, err := cli.tryImagePush(ctx, name, query, options.RegistryAuth) if resp.statusCode == http.StatusUnauthorized && options.PrivilegeFunc != nil { newAuthHeader, privilegeErr := options.PrivilegeFunc() if privilegeErr != nil { return nil, privilegeErr } - resp, err = cli.tryImagePush(ctx, distributionRef.Name(), query, newAuthHeader) + resp, err = cli.tryImagePush(ctx, name, query, newAuthHeader) } if err != nil { return nil, err diff --git a/image_push_test.go b/image_push_test.go index b52da8b8dc..f93debf5bb 100644 --- a/image_push_test.go +++ b/image_push_test.go @@ -21,7 +21,7 @@ func TestImagePushReferenceError(t *testing.T) { } // An empty reference is an invalid reference _, err := client.ImagePush(context.Background(), "", types.ImagePushOptions{}) - if err == nil || err.Error() != "repository name must have at least one component" { + if err == nil || !strings.Contains(err.Error(), "invalid reference format") { t.Fatalf("expected an error, got %v", err) } // An canonical reference cannot be pushed diff --git a/image_tag.go b/image_tag.go index dbcd078e1c..35abe332bf 100644 --- a/image_tag.go +++ b/image_tag.go @@ -3,32 +3,33 @@ package client import ( "net/url" - distreference "github.com/docker/distribution/reference" - "github.com/docker/docker/api/types/reference" + "github.com/docker/distribution/reference" "github.com/pkg/errors" "golang.org/x/net/context" ) // ImageTag tags an image in the docker host func (cli *Client) ImageTag(ctx context.Context, source, target string) error { - if _, err := distreference.ParseNamed(source); err != nil { + if _, err := reference.ParseNormalizedNamed(source); err != nil { return errors.Wrapf(err, "Error parsing reference: %q is not a valid repository/tag", source) } - distributionRef, err := distreference.ParseNamed(target) + ref, err := reference.ParseNormalizedNamed(target) if err != nil { return errors.Wrapf(err, "Error parsing reference: %q is not a valid repository/tag", target) } - if _, isCanonical := distributionRef.(distreference.Canonical); isCanonical { + if _, isCanonical := ref.(reference.Canonical); isCanonical { return errors.New("refusing to create a tag with a digest reference") } - tag := reference.GetTagFromNamedRef(distributionRef) + ref = reference.TagNameOnly(ref) query := url.Values{} - query.Set("repo", distributionRef.Name()) - query.Set("tag", tag) + query.Set("repo", reference.FamiliarName(ref)) + if tagged, ok := ref.(reference.Tagged); ok { + query.Set("tag", tagged.Tag()) + } resp, err := cli.post(ctx, "/images/"+source+"/tag", query, nil, nil) ensureReaderClosed(resp) diff --git a/plugin_install.go b/plugin_install.go index 3217c4cf39..33876cc101 100644 --- a/plugin_install.go +++ b/plugin_install.go @@ -15,7 +15,7 @@ import ( // PluginInstall installs a plugin func (cli *Client) PluginInstall(ctx context.Context, name string, options types.PluginInstallOptions) (rc io.ReadCloser, err error) { query := url.Values{} - if _, err := reference.ParseNamed(options.RemoteRef); err != nil { + if _, err := reference.ParseNormalizedNamed(options.RemoteRef); err != nil { return nil, errors.Wrap(err, "invalid remote reference") } query.Set("remote", options.RemoteRef) diff --git a/plugin_upgrade.go b/plugin_upgrade.go index 95a4356b97..24293c5073 100644 --- a/plugin_upgrade.go +++ b/plugin_upgrade.go @@ -14,7 +14,7 @@ import ( // PluginUpgrade upgrades a plugin func (cli *Client) PluginUpgrade(ctx context.Context, name string, options types.PluginInstallOptions) (rc io.ReadCloser, err error) { query := url.Values{} - if _, err := reference.ParseNamed(options.RemoteRef); err != nil { + if _, err := reference.ParseNormalizedNamed(options.RemoteRef); err != nil { return nil, errors.Wrap(err, "invalid remote reference") } query.Set("remote", options.RemoteRef) From c5a66caf93939fb23ec1ab7c0b364e233c673113 Mon Sep 17 00:00:00 2001 From: chchliang Date: Thu, 9 Feb 2017 11:26:20 +0800 Subject: [PATCH 120/138] add test case check connect.EndpointConfig not nil Signed-off-by: chchliang --- network_connect_test.go | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/network_connect_test.go b/network_connect_test.go index d472f4520c..91b1a76676 100644 --- a/network_connect_test.go +++ b/network_connect_test.go @@ -87,6 +87,10 @@ func TestNetworkConnect(t *testing.T) { return nil, fmt.Errorf("expected 'container_id', got %s", connect.Container) } + if connect.EndpointConfig == nil { + return nil, fmt.Errorf("expected connect.EndpointConfig to be not nil, got %v", connect.EndpointConfig) + } + if connect.EndpointConfig.NetworkID != "NetworkID" { return nil, fmt.Errorf("expected 'NetworkID', got %s", connect.EndpointConfig.NetworkID) } From a8f833a646d941b199cfafe4fc2035a1dbfec534 Mon Sep 17 00:00:00 2001 From: allencloud Date: Fri, 10 Feb 2017 16:30:25 +0800 Subject: [PATCH 121/138] remove unused headers in secret_create.go Signed-off-by: allencloud --- secret_create.go | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/secret_create.go b/secret_create.go index de8b041567..b5325a560f 100644 --- a/secret_create.go +++ b/secret_create.go @@ -10,10 +10,8 @@ import ( // SecretCreate creates a new Secret. func (cli *Client) SecretCreate(ctx context.Context, secret swarm.SecretSpec) (types.SecretCreateResponse, error) { - var headers map[string][]string - var response types.SecretCreateResponse - resp, err := cli.post(ctx, "/secrets/create", nil, secret, headers) + resp, err := cli.post(ctx, "/secrets/create", nil, secret, nil) if err != nil { return response, err } From bb22446a68fa9e113d6bdb32d9b42d3e0a7845b7 Mon Sep 17 00:00:00 2001 From: allencloud Date: Fri, 10 Feb 2017 17:16:34 +0800 Subject: [PATCH 122/138] remove redundant code and better error msg Signed-off-by: allencloud --- swarm_unlock.go | 4 ---- volume_prune.go | 2 +- 2 files changed, 1 insertion(+), 5 deletions(-) diff --git a/swarm_unlock.go b/swarm_unlock.go index addfb59f0a..502c6b8407 100644 --- a/swarm_unlock.go +++ b/swarm_unlock.go @@ -8,10 +8,6 @@ import ( // SwarmUnlock unlockes locked swarm. func (cli *Client) SwarmUnlock(ctx context.Context, req swarm.UnlockRequest) error { serverResp, err := cli.post(ctx, "/swarm/unlock", nil, req, nil) - if err != nil { - return err - } - ensureReaderClosed(serverResp) return err } diff --git a/volume_prune.go b/volume_prune.go index a07e4ce637..53a31ee39b 100644 --- a/volume_prune.go +++ b/volume_prune.go @@ -29,7 +29,7 @@ func (cli *Client) VolumesPrune(ctx context.Context, pruneFilters filters.Args) defer ensureReaderClosed(serverResp) if err := json.NewDecoder(serverResp.body).Decode(&report); err != nil { - return report, fmt.Errorf("Error retrieving disk usage: %v", err) + return report, fmt.Errorf("Error retrieving volume prune report:: %v", err) } return report, nil From 2c8cac3bd6e0d6d6f17b0aa770999eace336b152 Mon Sep 17 00:00:00 2001 From: allencloud Date: Mon, 13 Feb 2017 10:16:57 +0800 Subject: [PATCH 123/138] remove redundant colon introduced by mistake Signed-off-by: allencloud --- volume_prune.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/volume_prune.go b/volume_prune.go index 53a31ee39b..2e7fea7747 100644 --- a/volume_prune.go +++ b/volume_prune.go @@ -29,7 +29,7 @@ func (cli *Client) VolumesPrune(ctx context.Context, pruneFilters filters.Args) defer ensureReaderClosed(serverResp) if err := json.NewDecoder(serverResp.body).Decode(&report); err != nil { - return report, fmt.Errorf("Error retrieving volume prune report:: %v", err) + return report, fmt.Errorf("Error retrieving volume prune report: %v", err) } return report, nil From d5e4c0d0be983f48bca4b8fd7080d85f2d8f1c4d Mon Sep 17 00:00:00 2001 From: "Aaron.L.Xu" Date: Thu, 16 Feb 2017 23:56:53 +0800 Subject: [PATCH 124/138] why there are so many mistakes in our repo (up to /cmd) Signed-off-by: Aaron.L.Xu --- container_create_test.go | 2 +- image_tag_test.go | 2 +- swarm_unlock.go | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/container_create_test.go b/container_create_test.go index 73474cf56f..3ab608c21e 100644 --- a/container_create_test.go +++ b/container_create_test.go @@ -22,7 +22,7 @@ func TestContainerCreateError(t *testing.T) { t.Fatalf("expected a Server Error while testing StatusInternalServerError, got %v", err) } - // 404 doesn't automagitally means an unknown image + // 404 doesn't automatically means an unknown image client = &Client{ client: newMockClient(errorMock(http.StatusNotFound, "Server error")), } diff --git a/image_tag_test.go b/image_tag_test.go index d37bd0e85e..52c5e873a5 100644 --- a/image_tag_test.go +++ b/image_tag_test.go @@ -22,7 +22,7 @@ func TestImageTagError(t *testing.T) { } } -// Note: this is not testing all the InvalidReference as it's the reponsability +// Note: this is not testing all the InvalidReference as it's the responsibility // of distribution/reference package. func TestImageTagInvalidReference(t *testing.T) { client := &Client{ diff --git a/swarm_unlock.go b/swarm_unlock.go index 502c6b8407..9ee441fed2 100644 --- a/swarm_unlock.go +++ b/swarm_unlock.go @@ -5,7 +5,7 @@ import ( "golang.org/x/net/context" ) -// SwarmUnlock unlockes locked swarm. +// SwarmUnlock unlocks locked swarm. func (cli *Client) SwarmUnlock(ctx context.Context, req swarm.UnlockRequest) error { serverResp, err := cli.post(ctx, "/swarm/unlock", nil, req, nil) ensureReaderClosed(serverResp) From 0d367623d0c11247be15a77bcf62c0b8b53a7cc0 Mon Sep 17 00:00:00 2001 From: Victor Vieux Date: Sun, 19 Feb 2017 00:43:08 -0800 Subject: [PATCH 125/138] add missing API changes Signed-off-by: Victor Vieux --- client.go | 6 ++---- client_test.go | 15 ++++++++------- 2 files changed, 10 insertions(+), 11 deletions(-) diff --git a/client.go b/client.go index 75cfc8698b..df3698adc6 100644 --- a/client.go +++ b/client.go @@ -53,13 +53,11 @@ import ( "path/filepath" "strings" + "github.com/docker/docker/api" "github.com/docker/go-connections/sockets" "github.com/docker/go-connections/tlsconfig" ) -// DefaultVersion is the version of the current stable API -const DefaultVersion string = "1.26" - // Client is the API client that performs all operations // against a docker server. type Client struct { @@ -115,7 +113,7 @@ func NewEnvClient() (*Client, error) { } version := os.Getenv("DOCKER_API_VERSION") if version == "" { - version = DefaultVersion + version = api.DefaultVersion } cli, err := NewClient(host, version, client, nil) diff --git a/client_test.go b/client_test.go index 7c26403ebe..64188d5fb1 100644 --- a/client_test.go +++ b/client_test.go @@ -11,6 +11,7 @@ import ( "strings" "testing" + "github.com/docker/docker/api" "github.com/docker/docker/api/types" "golang.org/x/net/context" ) @@ -26,7 +27,7 @@ func TestNewEnvClient(t *testing.T) { }{ { envs: map[string]string{}, - expectedVersion: DefaultVersion, + expectedVersion: api.DefaultVersion, }, { envs: map[string]string{ @@ -38,21 +39,21 @@ func TestNewEnvClient(t *testing.T) { envs: map[string]string{ "DOCKER_CERT_PATH": "testdata/", }, - expectedVersion: DefaultVersion, + expectedVersion: api.DefaultVersion, }, { envs: map[string]string{ "DOCKER_CERT_PATH": "testdata/", "DOCKER_TLS_VERIFY": "1", }, - expectedVersion: DefaultVersion, + expectedVersion: api.DefaultVersion, }, { envs: map[string]string{ "DOCKER_CERT_PATH": "testdata/", "DOCKER_HOST": "https://notaunixsocket", }, - expectedVersion: DefaultVersion, + expectedVersion: api.DefaultVersion, }, { envs: map[string]string{ @@ -64,7 +65,7 @@ func TestNewEnvClient(t *testing.T) { envs: map[string]string{ "DOCKER_HOST": "invalid://url", }, - expectedVersion: DefaultVersion, + expectedVersion: api.DefaultVersion, }, { envs: map[string]string{ @@ -262,8 +263,8 @@ func TestNewEnvClientSetsDefaultVersion(t *testing.T) { if err != nil { t.Fatal(err) } - if client.version != DefaultVersion { - t.Fatalf("Expected %s, got %s", DefaultVersion, client.version) + if client.version != api.DefaultVersion { + t.Fatalf("Expected %s, got %s", api.DefaultVersion, client.version) } expected := "1.22" From 1c579ffcc5c449bb6ace0d917f751e4a12c782ff Mon Sep 17 00:00:00 2001 From: Tony Abboud Date: Fri, 13 Jan 2017 10:01:58 -0500 Subject: [PATCH 126/138] Add --add-host for docker build Signed-off-by: Tony Abboud --- image_build.go | 1 + 1 file changed, 1 insertion(+) diff --git a/image_build.go b/image_build.go index 411d5493ea..cc5a71c2a1 100644 --- a/image_build.go +++ b/image_build.go @@ -48,6 +48,7 @@ func (cli *Client) imageBuildOptionsToQuery(options types.ImageBuildOptions) (ur query := url.Values{ "t": options.Tags, "securityopt": options.SecurityOpt, + "extrahosts": options.ExtraHosts, } if options.SuppressOutput { query.Set("q", "1") From 14e8332f2d079bc9ec2824809df81c8ef556bdc3 Mon Sep 17 00:00:00 2001 From: Anusha Ragunathan Date: Fri, 24 Feb 2017 15:35:10 -0800 Subject: [PATCH 127/138] Net dial to the plugin socket during enable. When a plugin fails to start, we still incorrectly mark it as enabled. This change verifies that we can dial to the plugin socket to confirm that the plugin is functional and only then mark the plugin as enabled. Also, dont delete the plugin on install, if only the enable fails. Signed-off-by: Anusha Ragunathan --- plugin_install.go | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/plugin_install.go b/plugin_install.go index 33876cc101..ce3e0506e5 100644 --- a/plugin_install.go +++ b/plugin_install.go @@ -60,8 +60,8 @@ func (cli *Client) PluginInstall(ctx context.Context, name string, options types return } - err = cli.PluginEnable(ctx, name, types.PluginEnableOptions{Timeout: 0}) - pw.CloseWithError(err) + enableErr := cli.PluginEnable(ctx, name, types.PluginEnableOptions{Timeout: 0}) + pw.CloseWithError(enableErr) }() return pr, nil } From eafb5565c9cb876fb2de19c4d02bf4462f9d6310 Mon Sep 17 00:00:00 2001 From: Aaron Lehmann Date: Thu, 16 Feb 2017 09:27:01 -0800 Subject: [PATCH 128/138] Implement server-side rollback, for daemon versions that support this Server-side rollback can take advantage of the rollback-specific update parameters, instead of being treated as a normal update that happens to go back to a previous version of the spec. Signed-off-by: Aaron Lehmann --- service_update.go | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/service_update.go b/service_update.go index afa94d47e2..873a1e0556 100644 --- a/service_update.go +++ b/service_update.go @@ -27,6 +27,10 @@ func (cli *Client) ServiceUpdate(ctx context.Context, serviceID string, version query.Set("registryAuthFrom", options.RegistryAuthFrom) } + if options.Rollback != "" { + query.Set("rollback", options.Rollback) + } + query.Set("version", strconv.FormatUint(version.Index, 10)) var response types.ServiceUpdateResponse From 818c54a2ca38f1ec38a03467e2ea573f19fbcd20 Mon Sep 17 00:00:00 2001 From: Boaz Shuster Date: Tue, 7 Feb 2017 14:52:20 +0200 Subject: [PATCH 129/138] Hide command options that are related to Windows Signed-off-by: Boaz Shuster --- ping.go | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/ping.go b/ping.go index 150b1dc8d8..d6212ef8bb 100644 --- a/ping.go +++ b/ping.go @@ -7,7 +7,7 @@ import ( "golang.org/x/net/context" ) -// Ping pings the server and returns the value of the "Docker-Experimental" & "API-Version" headers +// Ping pings the server and returns the value of the "Docker-Experimental", "OS-Type" & "API-Version" headers func (cli *Client) Ping(ctx context.Context) (types.Ping, error) { var ping types.Ping req, err := cli.buildRequest("GET", fmt.Sprintf("%s/_ping", cli.basePath), nil, nil) @@ -26,5 +26,7 @@ func (cli *Client) Ping(ctx context.Context) (types.Ping, error) { ping.Experimental = true } + ping.OSType = serverResp.header.Get("OSType") + return ping, nil } From faee4c005bf8832978fcebf6c54d7ab60292c57a Mon Sep 17 00:00:00 2001 From: Santhosh Manohar Date: Thu, 9 Mar 2017 11:42:10 -0800 Subject: [PATCH 130/138] Enhance network inspect to show all tasks, local & non-local, in swarm mode Signed-off-by: Santhosh Manohar --- interface.go | 4 ++-- network_inspect.go | 19 ++++++++++++++----- network_inspect_test.go | 39 +++++++++++++++++++++++++++++++++------ 3 files changed, 49 insertions(+), 13 deletions(-) diff --git a/interface.go b/interface.go index 5823eed883..ae4146bb4a 100644 --- a/interface.go +++ b/interface.go @@ -91,8 +91,8 @@ type NetworkAPIClient interface { NetworkConnect(ctx context.Context, networkID, container string, config *network.EndpointSettings) error NetworkCreate(ctx context.Context, name string, options types.NetworkCreate) (types.NetworkCreateResponse, error) NetworkDisconnect(ctx context.Context, networkID, container string, force bool) error - NetworkInspect(ctx context.Context, networkID string) (types.NetworkResource, error) - NetworkInspectWithRaw(ctx context.Context, networkID string) (types.NetworkResource, []byte, error) + NetworkInspect(ctx context.Context, networkID string, verbose bool) (types.NetworkResource, error) + NetworkInspectWithRaw(ctx context.Context, networkID string, verbose bool) (types.NetworkResource, []byte, error) NetworkList(ctx context.Context, options types.NetworkListOptions) ([]types.NetworkResource, error) NetworkRemove(ctx context.Context, networkID string) error NetworksPrune(ctx context.Context, pruneFilter filters.Args) (types.NetworksPruneReport, error) diff --git a/network_inspect.go b/network_inspect.go index 5ad4ea5bf3..7242304025 100644 --- a/network_inspect.go +++ b/network_inspect.go @@ -5,21 +5,30 @@ import ( "encoding/json" "io/ioutil" "net/http" + "net/url" "github.com/docker/docker/api/types" "golang.org/x/net/context" ) // NetworkInspect returns the information for a specific network configured in the docker host. -func (cli *Client) NetworkInspect(ctx context.Context, networkID string) (types.NetworkResource, error) { - networkResource, _, err := cli.NetworkInspectWithRaw(ctx, networkID) +func (cli *Client) NetworkInspect(ctx context.Context, networkID string, verbose bool) (types.NetworkResource, error) { + networkResource, _, err := cli.NetworkInspectWithRaw(ctx, networkID, verbose) return networkResource, err } // NetworkInspectWithRaw returns the information for a specific network configured in the docker host and its raw representation. -func (cli *Client) NetworkInspectWithRaw(ctx context.Context, networkID string) (types.NetworkResource, []byte, error) { - var networkResource types.NetworkResource - resp, err := cli.get(ctx, "/networks/"+networkID, nil, nil) +func (cli *Client) NetworkInspectWithRaw(ctx context.Context, networkID string, verbose bool) (types.NetworkResource, []byte, error) { + var ( + networkResource types.NetworkResource + resp serverResponse + err error + ) + query := url.Values{} + if verbose { + query.Set("verbose", "true") + } + resp, err = cli.get(ctx, "/networks/"+networkID, query, nil) if err != nil { if resp.statusCode == http.StatusNotFound { return networkResource, nil, networkNotFoundError{networkID} diff --git a/network_inspect_test.go b/network_inspect_test.go index 55f04eca2c..1504289f5d 100644 --- a/network_inspect_test.go +++ b/network_inspect_test.go @@ -10,6 +10,7 @@ import ( "testing" "github.com/docker/docker/api/types" + "github.com/docker/docker/api/types/network" "golang.org/x/net/context" ) @@ -18,7 +19,7 @@ func TestNetworkInspectError(t *testing.T) { client: newMockClient(errorMock(http.StatusInternalServerError, "Server error")), } - _, err := client.NetworkInspect(context.Background(), "nothing") + _, err := client.NetworkInspect(context.Background(), "nothing", false) if err == nil || err.Error() != "Error response from daemon: Server error" { t.Fatalf("expected a Server Error, got %v", err) } @@ -29,7 +30,7 @@ func TestNetworkInspectContainerNotFound(t *testing.T) { client: newMockClient(errorMock(http.StatusNotFound, "Server error")), } - _, err := client.NetworkInspect(context.Background(), "unknown") + _, err := client.NetworkInspect(context.Background(), "unknown", false) if err == nil || !IsErrNetworkNotFound(err) { t.Fatalf("expected a networkNotFound error, got %v", err) } @@ -46,9 +47,23 @@ func TestNetworkInspect(t *testing.T) { return nil, fmt.Errorf("expected GET method, got %s", req.Method) } - content, err := json.Marshal(types.NetworkResource{ - Name: "mynetwork", - }) + var ( + content []byte + err error + ) + if strings.HasPrefix(req.URL.RawQuery, "verbose=true") { + s := map[string]network.ServiceInfo{ + "web": {}, + } + content, err = json.Marshal(types.NetworkResource{ + Name: "mynetwork", + Services: s, + }) + } else { + content, err = json.Marshal(types.NetworkResource{ + Name: "mynetwork", + }) + } if err != nil { return nil, err } @@ -59,11 +74,23 @@ func TestNetworkInspect(t *testing.T) { }), } - r, err := client.NetworkInspect(context.Background(), "network_id") + r, err := client.NetworkInspect(context.Background(), "network_id", false) if err != nil { t.Fatal(err) } if r.Name != "mynetwork" { t.Fatalf("expected `mynetwork`, got %s", r.Name) } + + r, err = client.NetworkInspect(context.Background(), "network_id", true) + if err != nil { + t.Fatal(err) + } + if r.Name != "mynetwork" { + t.Fatalf("expected `mynetwork`, got %s", r.Name) + } + _, ok := r.Services["web"] + if !ok { + t.Fatalf("expected service `web` missing in the verbose output") + } } From 4dcceaf70eddee3c0b144b475e05dc14c514b2b6 Mon Sep 17 00:00:00 2001 From: Drew Erny Date: Tue, 21 Mar 2017 11:35:55 -0700 Subject: [PATCH 131/138] Add support for task and arbitrary combo logs Refactored the API to more easily accept new endpoints. Added REST, client, and CLI endpoints for getting logs from a specific task. All that is needed after this commit to enable arbitrary service log selectors is a REST endpoint and handler. Task logs can be retrieved by putting in a task ID at the CLI instead of a service ID. Signed-off-by: Drew Erny --- interface.go | 1 + task_logs.go | 52 ++++++++++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 53 insertions(+) create mode 100644 task_logs.go diff --git a/interface.go b/interface.go index ae4146bb4a..6f8c094b31 100644 --- a/interface.go +++ b/interface.go @@ -128,6 +128,7 @@ type ServiceAPIClient interface { ServiceRemove(ctx context.Context, serviceID string) error ServiceUpdate(ctx context.Context, serviceID string, version swarm.Version, service swarm.ServiceSpec, options types.ServiceUpdateOptions) (types.ServiceUpdateResponse, error) ServiceLogs(ctx context.Context, serviceID string, options types.ContainerLogsOptions) (io.ReadCloser, error) + TaskLogs(ctx context.Context, taskID string, options types.ContainerLogsOptions) (io.ReadCloser, error) TaskInspectWithRaw(ctx context.Context, taskID string) (swarm.Task, []byte, error) TaskList(ctx context.Context, options types.TaskListOptions) ([]swarm.Task, error) } diff --git a/task_logs.go b/task_logs.go new file mode 100644 index 0000000000..2ed19543a4 --- /dev/null +++ b/task_logs.go @@ -0,0 +1,52 @@ +package client + +import ( + "io" + "net/url" + "time" + + "golang.org/x/net/context" + + "github.com/docker/docker/api/types" + timetypes "github.com/docker/docker/api/types/time" +) + +// TaskLogs returns the logs generated by a task in an io.ReadCloser. +// It's up to the caller to close the stream. +func (cli *Client) TaskLogs(ctx context.Context, taskID string, options types.ContainerLogsOptions) (io.ReadCloser, error) { + query := url.Values{} + if options.ShowStdout { + query.Set("stdout", "1") + } + + if options.ShowStderr { + query.Set("stderr", "1") + } + + if options.Since != "" { + ts, err := timetypes.GetTimestamp(options.Since, time.Now()) + if err != nil { + return nil, err + } + query.Set("since", ts) + } + + if options.Timestamps { + query.Set("timestamps", "1") + } + + if options.Details { + query.Set("details", "1") + } + + if options.Follow { + query.Set("follow", "1") + } + query.Set("tail", options.Tail) + + resp, err := cli.get(ctx, "/tasks/"+taskID+"/logs", query, nil) + if err != nil { + return nil, err + } + return resp.body, nil +} From c3bfcc372c7e36a7c511a3c1761f065ddaa16992 Mon Sep 17 00:00:00 2001 From: Yong Tang Date: Sat, 4 Feb 2017 09:10:05 -0800 Subject: [PATCH 132/138] Add `label` filter for `docker system prune` This fix tries to address the issue raised in 29999 where it was not possible to mask these items (like important non-removable stuff) from `docker system prune`. This fix adds `label` and `label!` field for `--filter` in `system prune`, so that it is possible to selectively prune items like: ``` $ docker container prune --filter label=foo $ docker container prune --filter label!=bar ``` Additional unit tests and integration tests have been added. This fix fixes 29999. Signed-off-by: Yong Tang --- container_prune_test.go | 13 +++++++++++++ image_prune_test.go | 13 +++++++++++++ network_prune_test.go | 13 +++++++++++++ 3 files changed, 39 insertions(+) diff --git a/container_prune_test.go b/container_prune_test.go index 5f06ea0664..55e551bb4b 100644 --- a/container_prune_test.go +++ b/container_prune_test.go @@ -40,6 +40,11 @@ func TestContainersPrune(t *testing.T) { danglingUntilFilters.Add("dangling", "true") danglingUntilFilters.Add("until", "2016-12-15T14:00") + labelFilters := filters.NewArgs() + labelFilters.Add("dangling", "true") + labelFilters.Add("label", "label1=foo") + labelFilters.Add("label", "label2!=bar") + listCases := []struct { filters filters.Args expectedQueryParams map[string]string @@ -76,6 +81,14 @@ func TestContainersPrune(t *testing.T) { "filters": `{"dangling":{"false":true}}`, }, }, + { + filters: labelFilters, + expectedQueryParams: map[string]string{ + "until": "", + "filter": "", + "filters": `{"dangling":{"true":true},"label":{"label1=foo":true,"label2!=bar":true}}`, + }, + }, } for _, listCase := range listCases { client := &Client{ diff --git a/image_prune_test.go b/image_prune_test.go index 68cd995d37..5dfb173acc 100644 --- a/image_prune_test.go +++ b/image_prune_test.go @@ -36,6 +36,11 @@ func TestImagesPrune(t *testing.T) { noDanglingFilters := filters.NewArgs() noDanglingFilters.Add("dangling", "false") + labelFilters := filters.NewArgs() + labelFilters.Add("dangling", "true") + labelFilters.Add("label", "label1=foo") + labelFilters.Add("label", "label2!=bar") + listCases := []struct { filters filters.Args expectedQueryParams map[string]string @@ -64,6 +69,14 @@ func TestImagesPrune(t *testing.T) { "filters": `{"dangling":{"false":true}}`, }, }, + { + filters: labelFilters, + expectedQueryParams: map[string]string{ + "until": "", + "filter": "", + "filters": `{"dangling":{"true":true},"label":{"label1=foo":true,"label2!=bar":true}}`, + }, + }, } for _, listCase := range listCases { client := &Client{ diff --git a/network_prune_test.go b/network_prune_test.go index 07a5d41f20..2bf4af4716 100644 --- a/network_prune_test.go +++ b/network_prune_test.go @@ -38,6 +38,11 @@ func TestNetworksPrune(t *testing.T) { noDanglingFilters := filters.NewArgs() noDanglingFilters.Add("dangling", "false") + labelFilters := filters.NewArgs() + labelFilters.Add("dangling", "true") + labelFilters.Add("label", "label1=foo") + labelFilters.Add("label", "label2!=bar") + listCases := []struct { filters filters.Args expectedQueryParams map[string]string @@ -66,6 +71,14 @@ func TestNetworksPrune(t *testing.T) { "filters": `{"dangling":{"false":true}}`, }, }, + { + filters: labelFilters, + expectedQueryParams: map[string]string{ + "until": "", + "filter": "", + "filters": `{"dangling":{"true":true},"label":{"label1=foo":true,"label2!=bar":true}}`, + }, + }, } for _, listCase := range listCases { client := &Client{ From 3b27c0eeb2b337e6abcfa1968eabf69752a5d2fb Mon Sep 17 00:00:00 2001 From: Arash Deshmeh Date: Sat, 25 Feb 2017 15:17:23 -0500 Subject: [PATCH 133/138] use an encrypted client certificate to connect to a docker daemon Signed-off-by: Arash Deshmeh --- client_test.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/client_test.go b/client_test.go index 64188d5fb1..0816d8a9bf 100644 --- a/client_test.go +++ b/client_test.go @@ -33,7 +33,7 @@ func TestNewEnvClient(t *testing.T) { envs: map[string]string{ "DOCKER_CERT_PATH": "invalid/path", }, - expectedError: "Could not load X509 key pair: open invalid/path/cert.pem: no such file or directory. Make sure the key is not encrypted", + expectedError: "Could not load X509 key pair: open invalid/path/cert.pem: no such file or directory", }, { envs: map[string]string{ From 07f55b2eb52aefef08b40d667faadc1bbd8e1096 Mon Sep 17 00:00:00 2001 From: Aaron Lehmann Date: Thu, 30 Mar 2017 17:15:54 -0700 Subject: [PATCH 134/138] Change "service inspect" to show defaults in place of empty fields This adds a new parameter insertDefaults to /services/{id}. When this is set, an empty field (such as UpdateConfig) will be populated with default values in the API response. Make "service inspect" use this, so that empty fields do not result in missing information when inspecting a service. Signed-off-by: Aaron Lehmann --- interface.go | 2 +- service_inspect.go | 9 +++++++-- service_inspect_test.go | 7 ++++--- 3 files changed, 12 insertions(+), 6 deletions(-) diff --git a/interface.go b/interface.go index 6f8c094b31..8dbe4300dc 100644 --- a/interface.go +++ b/interface.go @@ -123,7 +123,7 @@ type PluginAPIClient interface { // ServiceAPIClient defines API client methods for the services type ServiceAPIClient interface { ServiceCreate(ctx context.Context, service swarm.ServiceSpec, options types.ServiceCreateOptions) (types.ServiceCreateResponse, error) - ServiceInspectWithRaw(ctx context.Context, serviceID string) (swarm.Service, []byte, error) + ServiceInspectWithRaw(ctx context.Context, serviceID string, options types.ServiceInspectOptions) (swarm.Service, []byte, error) ServiceList(ctx context.Context, options types.ServiceListOptions) ([]swarm.Service, error) ServiceRemove(ctx context.Context, serviceID string) error ServiceUpdate(ctx context.Context, serviceID string, version swarm.Version, service swarm.ServiceSpec, options types.ServiceUpdateOptions) (types.ServiceUpdateResponse, error) diff --git a/service_inspect.go b/service_inspect.go index ca71cbde1a..d7e051e3a4 100644 --- a/service_inspect.go +++ b/service_inspect.go @@ -3,16 +3,21 @@ package client import ( "bytes" "encoding/json" + "fmt" "io/ioutil" "net/http" + "net/url" + "github.com/docker/docker/api/types" "github.com/docker/docker/api/types/swarm" "golang.org/x/net/context" ) // ServiceInspectWithRaw returns the service information and the raw data. -func (cli *Client) ServiceInspectWithRaw(ctx context.Context, serviceID string) (swarm.Service, []byte, error) { - serverResp, err := cli.get(ctx, "/services/"+serviceID, nil, nil) +func (cli *Client) ServiceInspectWithRaw(ctx context.Context, serviceID string, opts types.ServiceInspectOptions) (swarm.Service, []byte, error) { + query := url.Values{} + query.Set("insertDefaults", fmt.Sprintf("%v", opts.InsertDefaults)) + serverResp, err := cli.get(ctx, "/services/"+serviceID, query, nil) if err != nil { if serverResp.statusCode == http.StatusNotFound { return swarm.Service{}, nil, serviceNotFoundError{serviceID} diff --git a/service_inspect_test.go b/service_inspect_test.go index 0346847317..d53f583e90 100644 --- a/service_inspect_test.go +++ b/service_inspect_test.go @@ -9,6 +9,7 @@ import ( "strings" "testing" + "github.com/docker/docker/api/types" "github.com/docker/docker/api/types/swarm" "golang.org/x/net/context" ) @@ -18,7 +19,7 @@ func TestServiceInspectError(t *testing.T) { client: newMockClient(errorMock(http.StatusInternalServerError, "Server error")), } - _, _, err := client.ServiceInspectWithRaw(context.Background(), "nothing") + _, _, err := client.ServiceInspectWithRaw(context.Background(), "nothing", types.ServiceInspectOptions{}) if err == nil || err.Error() != "Error response from daemon: Server error" { t.Fatalf("expected a Server Error, got %v", err) } @@ -29,7 +30,7 @@ func TestServiceInspectServiceNotFound(t *testing.T) { client: newMockClient(errorMock(http.StatusNotFound, "Server error")), } - _, _, err := client.ServiceInspectWithRaw(context.Background(), "unknown") + _, _, err := client.ServiceInspectWithRaw(context.Background(), "unknown", types.ServiceInspectOptions{}) if err == nil || !IsErrServiceNotFound(err) { t.Fatalf("expected a serviceNotFoundError error, got %v", err) } @@ -55,7 +56,7 @@ func TestServiceInspect(t *testing.T) { }), } - serviceInspect, _, err := client.ServiceInspectWithRaw(context.Background(), "service_id") + serviceInspect, _, err := client.ServiceInspectWithRaw(context.Background(), "service_id", types.ServiceInspectOptions{}) if err != nil { t.Fatal(err) } From 6dc427fb6125ce28f61fd2f5556c7eef29eb3a73 Mon Sep 17 00:00:00 2001 From: Tonis Tiigi Date: Mon, 10 Apr 2017 15:27:42 -0700 Subject: [PATCH 135/138] builder: add an option for specifying build target Signed-off-by: Tonis Tiigi --- image_build.go | 1 + 1 file changed, 1 insertion(+) diff --git a/image_build.go b/image_build.go index cc5a71c2a1..bb69143e99 100644 --- a/image_build.go +++ b/image_build.go @@ -95,6 +95,7 @@ func (cli *Client) imageBuildOptionsToQuery(options types.ImageBuildOptions) (ur query.Set("cgroupparent", options.CgroupParent) query.Set("shmsize", strconv.FormatInt(options.ShmSize, 10)) query.Set("dockerfile", options.Dockerfile) + query.Set("target", options.Target) ulimitsJSON, err := json.Marshal(options.Ulimits) if err != nil { From d857c869d20b56f2b44f12bcdffb08dc5c76fb67 Mon Sep 17 00:00:00 2001 From: Aaron Lehmann Date: Tue, 11 Apr 2017 13:37:04 -0700 Subject: [PATCH 136/138] client: Allow hex strings as source references for ImageTag The source of a tag operation is allowed to be a 64-character hex string. This means it should use ParseAnyReference for validation instead of ParseNormalizedNamed. This fixes a regression that happened in 17.04. Signed-off-by: Aaron Lehmann --- image_tag.go | 2 +- image_tag_test.go | 11 +++++++++++ 2 files changed, 12 insertions(+), 1 deletion(-) diff --git a/image_tag.go b/image_tag.go index 35abe332bf..8924f71eb3 100644 --- a/image_tag.go +++ b/image_tag.go @@ -10,7 +10,7 @@ import ( // ImageTag tags an image in the docker host func (cli *Client) ImageTag(ctx context.Context, source, target string) error { - if _, err := reference.ParseNormalizedNamed(source); err != nil { + if _, err := reference.ParseAnyReference(source); err != nil { return errors.Wrapf(err, "Error parsing reference: %q is not a valid repository/tag", source) } diff --git a/image_tag_test.go b/image_tag_test.go index 52c5e873a5..f7a0ee331c 100644 --- a/image_tag_test.go +++ b/image_tag_test.go @@ -46,6 +46,17 @@ func TestImageTagInvalidSourceImageName(t *testing.T) { } } +func TestImageTagHexSource(t *testing.T) { + client := &Client{ + client: newMockClient(errorMock(http.StatusOK, "OK")), + } + + err := client.ImageTag(context.Background(), "0d409d33b27e47423b049f7f863faa08655a8c901749c2b25b93ca67d01a470d", "repo:tag") + if err != nil { + t.Fatalf("got error: %v", err) + } +} + func TestImageTag(t *testing.T) { expectedURL := "/images/image_id/tag" tagCases := []struct { From f77e2d15d7163107c972a36eeb3065d3239f0dd6 Mon Sep 17 00:00:00 2001 From: Erik Hollensbe Date: Mon, 14 Nov 2016 05:37:08 -0800 Subject: [PATCH 137/138] daemon/archive.go: Fix copy routines to preserve UID. This changes the long-standing bug of copy operations not preserving the UID/GID information after the files arrive to the container. Signed-off-by: Erik Hollensbe --- container_copy.go | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/container_copy.go b/container_copy.go index 8380eeabc9..545aa54383 100644 --- a/container_copy.go +++ b/container_copy.go @@ -38,6 +38,10 @@ func (cli *Client) CopyToContainer(ctx context.Context, container, path string, query.Set("noOverwriteDirNonDir", "true") } + if options.CopyUIDGID { + query.Set("copyUIDGID", "true") + } + apiPath := fmt.Sprintf("/containers/%s/archive", container) response, err := cli.putRaw(ctx, apiPath, query, content, nil) From 50e74272d11d49010552a94b0bd1468b15573e37 Mon Sep 17 00:00:00 2001 From: Aaron Lehmann Date: Thu, 13 Apr 2017 15:45:37 -0700 Subject: [PATCH 138/138] Remove pkg/testutil/assert in favor of testify I noticed that we're using a homegrown package for assertions. The functions are extremely similar to testify, but with enough slight differences to be confusing (for example, Equal takes its arguments in a different order). We already vendor testify, and it's used in a few places by tests. I also found some problems with pkg/testutil/assert. For example, the NotNil function seems to be broken. It checks the argument against "nil", which only works for an interface. If you pass in a nil map or slice, the equality check will fail. In the interest of avoiding NIH, I'm proposing replacing pkg/testutil/assert with testify. The test code looks almost the same, but we avoid the confusion of having two similar but slightly different assertion packages, and having to maintain our own package instead of using a commonly-used one. In the process, I found a few places where the tests should halt if an assertion fails, so I've made those cases (that I noticed) use "require" instead of "assert", and I've vendored the "require" package from testify alongside the already-present "assert" package. Signed-off-by: Aaron Lehmann --- container_prune_test.go | 12 ++++++------ image_prune_test.go | 12 ++++++------ network_prune_test.go | 8 ++++---- 3 files changed, 16 insertions(+), 16 deletions(-) diff --git a/container_prune_test.go b/container_prune_test.go index 55e551bb4b..8a1c63897b 100644 --- a/container_prune_test.go +++ b/container_prune_test.go @@ -11,7 +11,7 @@ import ( "github.com/docker/docker/api/types" "github.com/docker/docker/api/types/filters" - "github.com/docker/docker/pkg/testutil/assert" + "github.com/stretchr/testify/assert" "golang.org/x/net/context" ) @@ -24,7 +24,7 @@ func TestContainersPruneError(t *testing.T) { filters := filters.NewArgs() _, err := client.ContainersPrune(context.Background(), filters) - assert.Error(t, err, "Error response from daemon: Server error") + assert.EqualError(t, err, "Error response from daemon: Server error") } func TestContainersPrune(t *testing.T) { @@ -99,7 +99,7 @@ func TestContainersPrune(t *testing.T) { query := req.URL.Query() for key, expected := range listCase.expectedQueryParams { actual := query.Get(key) - assert.Equal(t, actual, expected) + assert.Equal(t, expected, actual) } content, err := json.Marshal(types.ContainersPruneReport{ ContainersDeleted: []string{"container_id1", "container_id2"}, @@ -117,8 +117,8 @@ func TestContainersPrune(t *testing.T) { } report, err := client.ContainersPrune(context.Background(), listCase.filters) - assert.NilError(t, err) - assert.Equal(t, len(report.ContainersDeleted), 2) - assert.Equal(t, report.SpaceReclaimed, uint64(9999)) + assert.NoError(t, err) + assert.Len(t, report.ContainersDeleted, 2) + assert.Equal(t, uint64(9999), report.SpaceReclaimed) } } diff --git a/image_prune_test.go b/image_prune_test.go index 5dfb173acc..453f84adee 100644 --- a/image_prune_test.go +++ b/image_prune_test.go @@ -11,7 +11,7 @@ import ( "github.com/docker/docker/api/types" "github.com/docker/docker/api/types/filters" - "github.com/docker/docker/pkg/testutil/assert" + "github.com/stretchr/testify/assert" "golang.org/x/net/context" ) @@ -24,7 +24,7 @@ func TestImagesPruneError(t *testing.T) { filters := filters.NewArgs() _, err := client.ImagesPrune(context.Background(), filters) - assert.Error(t, err, "Error response from daemon: Server error") + assert.EqualError(t, err, "Error response from daemon: Server error") } func TestImagesPrune(t *testing.T) { @@ -87,7 +87,7 @@ func TestImagesPrune(t *testing.T) { query := req.URL.Query() for key, expected := range listCase.expectedQueryParams { actual := query.Get(key) - assert.Equal(t, actual, expected) + assert.Equal(t, expected, actual) } content, err := json.Marshal(types.ImagesPruneReport{ ImagesDeleted: []types.ImageDeleteResponseItem{ @@ -112,8 +112,8 @@ func TestImagesPrune(t *testing.T) { } report, err := client.ImagesPrune(context.Background(), listCase.filters) - assert.NilError(t, err) - assert.Equal(t, len(report.ImagesDeleted), 2) - assert.Equal(t, report.SpaceReclaimed, uint64(9999)) + assert.NoError(t, err) + assert.Len(t, report.ImagesDeleted, 2) + assert.Equal(t, uint64(9999), report.SpaceReclaimed) } } diff --git a/network_prune_test.go b/network_prune_test.go index 2bf4af4716..3e4f5d0415 100644 --- a/network_prune_test.go +++ b/network_prune_test.go @@ -11,7 +11,7 @@ import ( "github.com/docker/docker/api/types" "github.com/docker/docker/api/types/filters" - "github.com/docker/docker/pkg/testutil/assert" + "github.com/stretchr/testify/assert" "golang.org/x/net/context" ) @@ -89,7 +89,7 @@ func TestNetworksPrune(t *testing.T) { query := req.URL.Query() for key, expected := range listCase.expectedQueryParams { actual := query.Get(key) - assert.Equal(t, actual, expected) + assert.Equal(t, expected, actual) } content, err := json.Marshal(types.NetworksPruneReport{ NetworksDeleted: []string{"network_id1", "network_id2"}, @@ -106,7 +106,7 @@ func TestNetworksPrune(t *testing.T) { } report, err := client.NetworksPrune(context.Background(), listCase.filters) - assert.NilError(t, err) - assert.Equal(t, len(report.NetworksDeleted), 2) + assert.NoError(t, err) + assert.Len(t, report.NetworksDeleted, 2) } }