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 000000000..6c5e6c4c9 --- /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 000000000..8b58611cd --- /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 000000000..185687920 --- /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 000000000..36957c44a --- /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 6c5e6c4c9..164e1be32 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 8b58611cd..7606cbd42 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 7606cbd42..ed52be96b 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 164e1be32..5a8aac380 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 5a8aac380..4c4ea1c5e 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 ed52be96b..a988d0d79 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 5a8aac380..4c4ea1c5e 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 ed52be96b..a988d0d79 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 4c4ea1c5e..a277a4b18 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 a277a4b18..4c4ea1c5e 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 4c4ea1c5e..a277a4b18 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 4c4ea1c5e..814aed2f1 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 073fe02b0..814aed2f1 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 073fe02b0..815e20a04 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 30d741f8b..000000000 --- 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 a988d0d79..000000000 --- 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 185687920..000000000 --- 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 36957c44a..000000000 --- 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 000000000..e8c7f889f --- /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 000000000..5708c96cb --- /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 000000000..48064b4cf --- /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 000000000..5641f12b1 --- /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 000000000..a31dffc95 --- /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 000000000..35a810888 --- /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 000000000..28021ba4c --- /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 48064b4cf..15dffbaef 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 000000000..abe9ebfc5 --- /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 000000000..41c0133b6 --- /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 000000000..3da4e5d7c --- /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 5641f12b1..838602164 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 a31dffc95..de3225758 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 e8c7f889f..000000000 --- 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 5708c96cb..000000000 --- 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 15dffbaef..000000000 --- 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 abe9ebfc5..000000000 --- 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 41c0133b6..000000000 --- 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 3da4e5d7c..000000000 --- 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 838602164..000000000 --- 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 de3225758..000000000 --- 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 35a810888..000000000 --- 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 28021ba4c..000000000 --- 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 000000000..0effe498b --- /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 000000000..e2ae36e1e --- /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 000000000..a4e9ed0c0 --- /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 000000000..097ab3769 --- /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 000000000..bb471e005 --- /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 000000000..5960436eb --- /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 000000000..6a85121c6 --- /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 000000000..33c247266 --- /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 000000000..60af3db02 --- /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 000000000..89de892c8 --- /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 000000000..07c0c7a77 --- /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 000000000..7cfc860fc --- /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 000000000..363950cc2 --- /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 000000000..3fc3e5cfd --- /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 000000000..8380eeabc --- /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 000000000..39cd05ac2 --- /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 000000000..a86217295 --- /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 000000000..4c14cdc5d --- /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 000000000..1e3e554fc --- /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 000000000..03ea3354d --- /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 000000000..34173d319 --- /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 000000000..abe824e47 --- /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 000000000..52194f3d3 --- /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 000000000..10eba33d2 --- /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 000000000..17f180974 --- /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 000000000..0dc8ac375 --- /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 000000000..29f80c73a --- /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 000000000..a34a7b5b1 --- /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 000000000..a8945d84f --- /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 000000000..3aa2101f2 --- /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 000000000..69056b632 --- /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 000000000..d7f0adc9c --- /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 000000000..412067a78 --- /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 000000000..ebd12a6ac --- /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 000000000..3a79590ce --- /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 000000000..6e135d6ef --- /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 000000000..0e718da7c --- /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 000000000..9344bab7d --- /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 000000000..a7f38b024 --- /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 000000000..e0056c88d --- /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 000000000..74d7455f0 --- /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 000000000..080656d36 --- /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 000000000..44bb0080c --- /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 000000000..79f85b332 --- /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 000000000..2cc67c3af --- /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 000000000..22ecd6170 --- /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 000000000..b5418ae8c --- /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 000000000..4b052f990 --- /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 000000000..4e7270ea2 --- /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 000000000..4df7d82d8 --- /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 000000000..5c7621125 --- /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 000000000..a5b21bf56 --- /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 000000000..48b75bee3 --- /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 000000000..46e34d693 --- /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 000000000..8a858f0ea --- /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 000000000..bf2ba6b92 --- /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 000000000..71e25a7ae --- /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 000000000..0ba7114f9 --- /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 000000000..f7cb33f61 --- /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 000000000..9376d21b9 --- /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 000000000..8dd674485 --- /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 000000000..8261c5485 --- /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 000000000..cf023a718 --- /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 000000000..a2e001be5 --- /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 000000000..acb1ee927 --- /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 000000000..c9516151b --- /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 000000000..c6f154b24 --- /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 000000000..b64ca74d7 --- /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 000000000..b3a64ce2f --- /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 000000000..5c7ca2721 --- /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 000000000..00f27dc0c --- /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 000000000..99ed1964a --- /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 000000000..77aaf1af3 --- /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 000000000..0ee7cf35a --- /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 000000000..3bffdb70e --- /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 000000000..c33a6dcc8 --- /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 000000000..8e73d28f5 --- /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 000000000..d32f3ef3c --- /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 000000000..839e5311c --- /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 000000000..696d06729 --- /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 000000000..ecac880a3 --- /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 000000000..8ee40c43a --- /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 000000000..b0fcd5c23 --- /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 000000000..2f21b2cc1 --- /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 000000000..bdbf94add --- /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 000000000..f3571dfdd --- /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 000000000..ac0796122 --- /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 000000000..9d51b1a78 --- /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 000000000..1bfeb6aeb --- /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 000000000..1ddc517c9 --- /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 000000000..496f522d5 --- /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 000000000..d8d277ccb --- /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 000000000..c022c17b5 --- /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 000000000..95b149e68 --- /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 000000000..4067a541f --- /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 000000000..611ed8173 --- /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 000000000..24b58e3c1 --- /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 000000000..d9dbb6715 --- /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 000000000..5ad4ea5bf --- /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 000000000..a6eb626c6 --- /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 000000000..e566a93e2 --- /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 000000000..cb6613927 --- /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 000000000..6bd674892 --- /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 000000000..d8cfa0ed6 --- /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 000000000..abf505d29 --- /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 000000000..bf6772831 --- /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 000000000..0716875cc --- /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 000000000..899ac7f45 --- /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 000000000..0a77f3d57 --- /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 000000000..9fdf2d7eb --- /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 000000000..3ca976028 --- /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 000000000..1acf65854 --- /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 000000000..893fc6e82 --- /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 000000000..f37c15786 --- /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 000000000..84422abc7 --- /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 000000000..fc0fe226a --- /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 000000000..7ba8db57a --- /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 000000000..19f829b2d --- /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 000000000..9ee32eea9 --- /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 000000000..48b470247 --- /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 000000000..92aee6118 --- /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 000000000..3afea5ed7 --- /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 000000000..b77ea0027 --- /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 000000000..1483f2854 --- /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 000000000..de565f441 --- /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 000000000..fb40f38b2 --- /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 000000000..128dee04b --- /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 000000000..024e97352 --- /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 000000000..446adf9c6 --- /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 000000000..3d1be225b --- /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 000000000..a79f040c0 --- /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 000000000..ca71cbde1 --- /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 000000000..e4eafff5d --- /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 000000000..4ebc9f301 --- /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 000000000..6e6851a3a --- /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 000000000..a9331f92c --- /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 000000000..e1316f959 --- /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 000000000..c5d07e839 --- /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 000000000..bd616c09b --- /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 000000000..fd45d066e --- /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 000000000..077c8c4ef --- /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 000000000..6d95cfc05 --- /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 000000000..7143e7718 --- /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 000000000..cda99930e --- /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 000000000..922716d85 --- /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 000000000..a4df73217 --- /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 000000000..d0bef2b25 --- /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 000000000..f0be145ba --- /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 000000000..ecf1731e5 --- /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 000000000..bc8058fc3 --- /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 000000000..2c73b3764 --- /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 000000000..07c8324c8 --- /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 000000000..b520ab589 --- /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 000000000..ad14d4706 --- /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 000000000..9000ffb32 --- /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 000000000..c0869dfc1 --- /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 000000000..6a66aea5e --- /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 000000000..62770b777 --- /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 000000000..dd2723d94 --- /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 000000000..1f8eac5c1 --- /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 000000000..13d4b3ab3 --- /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 000000000..033d5dc0f --- /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 000000000..a28c9141b --- /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 000000000..ff28af185 --- /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 000000000..933ceb4a4 --- /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 000000000..f3a79f1e1 --- /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 000000000..d3cfa7132 --- /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 000000000..3860e9b22 --- /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 000000000..4b9f47358 --- /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 000000000..44f03cfac --- /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 000000000..d30d9fcd5 --- /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 000000000..3d5aeff25 --- /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 000000000..0675bfd45 --- /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 033d5dc0f..034bc01d3 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 000000000..12f13e469 --- /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 a28c9141b..50bf389e4 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 024e97352..7b4f5406b 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 9376d21b9..e3f63e20c 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 034bc01d3..000000000 --- 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 12f13e469..000000000 --- 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 50bf389e4..000000000 --- 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 000000000..7872d94a5 --- /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 f7cb33f61..57689322c 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 2cc67c3af..3be7a988f 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 22ecd6170..dc7c56492 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 8dd674485..a84bf5782 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 8261c5485..def88c3cb 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 1bfeb6aeb..2d5555ff0 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 a7f38b024..66c3cc194 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 e2ae36e1e..96e518761 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 097ab3769..23931c652 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 5960436eb..e636995bc 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 6a85121c6..deccb4ab7 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 33c247266..0ab935d53 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 60af3db02..60e44dc29 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 3fc3e5cfd..8f1b58be8 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 39cd05ac2..7eded611f 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 4c14cdc5d..5325156be 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 03ea3354d..1ce111768 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 abe824e47..42146ae8a 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 10eba33d2..5849fe925 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 0dc8ac375..f1a6f4ac7 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 a34a7b5b1..9477b0abd 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 3aa2101f2..5068b7573 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 d7f0adc9c..99e31842c 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 ebd12a6ac..0ee2f05d7 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 6e135d6ef..798c08b33 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 9344bab7d..732ebff5f 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 e0056c88d..5b2efecdc 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 080656d36..8c3cfd6a6 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 79f85b332..5826fa8bc 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 22ecd6170..76e4a09dd 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 4b052f990..c32cd691c 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 4df7d82d8..7802be063 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 a5b21bf56..2c4272719 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 46e34d693..e151637a2 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 bf2ba6b92..dab5acbdd 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 f7cb33f61..48b948fa3 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 e3f63e20c..f3461ecf7 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 8261c5485..ec0cbe2ee 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 a2e001be5..5c2edd2ad 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 c9516151b..729edb1ad 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 b64ca74d7..e309be74e 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 5c7ca2721..74a4e4980 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 99ed1964a..2a5227908 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 0ee7cf35a..68dc14ff2 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 c33a6dcc8..fe6bafed9 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 d32f3ef3c..b52da8b8d 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 696d06729..7b004f70e 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 8ee40c43a..8f0cf8864 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 2f21b2cc1..e46d86437 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 f3571dfdd..7925db9f1 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 9d51b1a78..79f23c8af 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 95b149e68..d472f4520 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 611ed8173..0e2457f89 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 d9dbb6715..b54a2b1cc 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 a6eb626c6..1f926d66b 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 cb6613927..4d443496a 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 d8cfa0ed6..2a7b9640c 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 bf6772831..fc1328308 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 899ac7f45..1b3b35f35 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 9fdf2d7eb..f2f8adc4a 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 1acf65854..613ff104e 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 f37c15786..7b50b2573 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 fc0fe226a..a2b57be4c 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 19f829b2d..df4ca9c84 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 92aee6118..95c51595c 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 b77ea0027..ed685694e 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 de565f441..fc789fd04 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 128dee04b..fa1cde044 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 7b4f5406b..f5c239bf2 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 446adf9c6..63908aec4 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 a79f040c0..1e0738287 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 e4eafff5d..e235cf0fe 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 6e6851a3a..728187919 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 e1316f959..8e2ac259c 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 bd616c09b..081649f49 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 077c8c4ef..811155aff 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 7143e7718..6432d172b 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 922716d85..31ef2a76e 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 d0bef2b25..c96dac812 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 ecf1731e5..3b23db078 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 2c73b3764..148cdad3a 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 b520ab589..2d9b812bc 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 000000000..43a667272 --- /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 6a66aea5e..000000000 --- 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 62770b777..000000000 --- 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 dd2723d94..000000000 --- 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 1f8eac5c1..000000000 --- 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 13d4b3ab3..000000000 --- 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 ff28af185..000000000 --- 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 d3cfa7132..75085296c 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 4b9f47358..0d1d11882 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 d30d9fcd5..0af420eaf 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 0675bfd45..1fe657349 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 7872d94a5..34cf7372d 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 0ba7114f9..c154f7dcf 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 632898360..ba82d2f54 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 2d5555ff0..81320918b 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 a84bf5782..0094602a6 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 deccb4ab7..bee429b8c 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 60e44dc29..222f23d45 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 deccb4ab7..ff9efa570 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 60e44dc29..eaac33965 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 f3461ecf7..dededb7af 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 f5c239bf2..07a12657a 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 43a667272..771d76f06 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 000000000..0d8bd3292 --- /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 000000000..f6752e504 --- /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 81320918b..de06b848a 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 000000000..e7ea7b591 --- /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 000000000..03c80b39a --- /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 f5c239bf2..0749ae325 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 0094602a6..3abd87025 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 71e25a7ae..ad1dadabb 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 34cf7372d..2b7d81fad 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 deccb4ab7..58e8430cf 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 025eaaf9a..75073881c 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 d585b46ab..bfd62bad1 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 771d76f06..f04e60164 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 bfd62bad1..91a05824e 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 c5d07e839..8e03f7f48 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 7cfc860fc..eea468215 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 00f27dc0c..6ebb46054 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 2a5227908..1ea6f1f05 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 de06b848a..4d450d831 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 4d450d831..f91961216 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 1ddc517c9..ddb9f79b5 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 496f522d5..cc90a3cbb 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 000000000..5e99e1bba --- /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 893fc6e82..51e456512 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 7b50b2573..2818008ab 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 84422abc7..8109814dd 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 a2b57be4c..d919914e7 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 7ba8db57a..e9474b5a9 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 df4ca9c84..fae407eb9 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 9ee32eea9..636c95364 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 48b470247..88c480a3e 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 95c51595c..173e4b87f 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 3afea5ed7..d83bbdc35 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 ed685694e..efdbdc6db 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 1483f2854..b017e4d34 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 fc789fd04..a15f1661f 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 fb40f38b2..3260d2a90 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 fa1cde044..245025446 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 f91961216..8abdb0f6f 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 000000000..01185f2e0 --- /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 3abd87025..4d611d543 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 a4e9ed0c0..e6e75588b 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 23931c652..a78b05048 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 bb471e005..8eb720a6b 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 e636995bc..6c90f61e8 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 44bb0080c..b1f08de41 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 ddb9f79b5..4f5cf853b 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 8abdb0f6f..613015f86 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 44f03cfac..9923ecb82 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 0af420eaf..ffdd904b5 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 613015f86..5ec750abe 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 f3a79f1e1..b18e5fe60 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 75085296c..d5d379168 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 5ec750abe..1f20a8be7 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 b18e5fe60..9620c87cb 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 d5d379168..9f1b2540b 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 9923ecb82..32247ce11 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 ffdd904b5..f29639be2 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 a86217295..c042b1746 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 5325156be..89641038f 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 1f20a8be7..8f8bbaf55 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 34173d319..f6df72291 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 42146ae8a..0e296a50a 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 8f8bbaf55..0575ce5c3 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 363950cc2..c766d62e4 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 8f1b58be8..a84467536 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 0575ce5c3..2a355fa8a 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 48b75bee3..5082f22df 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 e151637a2..715bb7ca2 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 2a355fa8a..b303d2fde 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 b303d2fde..f044c3235 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 d8d277ccb..600dc7196 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 8a858f0ea..93212c70e 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 dab5acbdd..9300bc0a5 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 f044c3235..a78cb759c 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 dededb7af..74c53f52b 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 91a05824e..c73464b54 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 a8945d84f..439891219 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 5068b7573..e41c6874b 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 0716875cc..3e8440f08 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 1b3b35f35..0251b5cce 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 4ebc9f301..c29e6d407 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 728187919..213981ef7 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 07c8324c8..66324da95 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 2d9b812bc..2a9a4c434 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 79f23c8af..7af82a8a3 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 89641038f..15dbd5ea0 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 9dcb3986c..3b97720e0 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 636c95364..d0a3d517f 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 3b97720e0..76a1ac07c 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 c042b1746..9f627aafa 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 f6df72291..0665c54fb 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 0d8bd3292..3eabe71a7 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 ad1dadabb..53e206533 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 4d611d543..0049e4e29 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 f6752e504..d5e69d5b1 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 a78cb759c..99b06709b 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 5e99e1bba..22dcda24f 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 c73464b54..ac0536365 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 e7ea7b591..ea4e234a3 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 3d5aeff25..6c26575b4 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 53e206533..db7294daa 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 99b06709b..49b66b1d1 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 000000000..de8b04156 --- /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 000000000..d264eb669 --- /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 000000000..f77457611 --- /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 000000000..423d98696 --- /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 000000000..5e9d2b509 --- /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 000000000..174963c7e --- /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 000000000..1955b988a --- /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 000000000..f269f787d --- /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 db7294daa..94c22a728 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 de8b04156..f92a3d151 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 d264eb669..b7def89d0 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 5e9d2b509..7e9d5ec16 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 174963c7e..1ac11cddb 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 3be7a988f..4758c66e3 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 0049e4e29..6fde75dcf 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 53dd93376..ec0cbe2ee 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 000000000..03bf4c82f --- /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 4f5cf853b..709b5d8ff 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 000000000..a660ba573 --- /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 49b66b1d1..d0834afa9 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 000000000..addfb59f0 --- /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 d0834afa9..f24c9a51f 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 000000000..be28d3262 --- /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 f0be145ba..cc8eeb655 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 f24c9a51f..883e8801f 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 000000000..24384e3ec --- /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 000000000..a6d002ba7 --- /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 883e8801f..7a3ebe8b4 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 709b5d8ff..51da98ecd 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 6ebb46054..63c71b1dd 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 1ea6f1f05..1c9406ddd 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 ee199c2be..3a6575c9c 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 76a1ac07c..814c537c6 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 ac0536365..f15e38033 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 d0a3d517f..407f1cddf 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 f92a3d151..de8b04156 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 b7def89d0..cb378c77f 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 a84467536..6947ed386 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 7eded611f..6863cfba2 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 f1a6f4ac7..98f83bd8d 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 e309be74e..370ad5fbe 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 1f926d66b..55f04eca2 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 a15f1661f..b2d515793 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 7a3ebe8b4..d46720e6c 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 8e03f7f48..afa94d47e 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 081649f49..76bea176b 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 7af82a8a3..79f23c8af 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 814c537c6..a85b39267 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 d46720e6c..0d722d907 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 8109814dd..95517c4b8 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 d919914e7..b27681348 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 407f1cddf..f73362ccd 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 2b7d81fad..161686c0a 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 94c22a728..854516669 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 161686c0a..059dfb3ce 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 a85b39267..31a311e7d 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 63c71b1dd..f26464f67 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 1c9406ddd..7c4a46414 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 31a311e7d..4c0f097e5 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 6863cfba2..c84f82e9f 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 e46d86437..108bd9674 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 efdbdc6db..7b8eb865d 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 600dc7196..79219ff59 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 f15e38033..6457b316a 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 854516669..bf6923f13 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 e9474b5a9..72900a131 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 e9474b5a9..1fb40624c 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 f73362ccd..e7b67f205 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 3eabe71a7..b58217086 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 d5e69d5b1..5ef98b7f0 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 0d722d907..6319f34f1 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 01185f2e0..7352a7f0c 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 03bf4c82f..23d520ecb 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 ea4e234a3..a07e4ce63 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 108bd9674..b17bbd834 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 ec0cbe2ee..b9d04f817 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 c154f7dcf..af47aefa7 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 bf6923f13..2912692ec 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 fc1328308..dca16a8cd 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 22dcda24f..150b1dc8d 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 423d98696..0142a3ca9 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 e235cf0fe..034684731 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 4c0f097e5..75cfc8698 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 3a6575c9c..7c26403eb 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 fd45d066e..9e65e1cca 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 6d95cfc05..77e72f846 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 cda99930e..19e5192b9 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 a4df73217..3a205cf3b 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 cc8eeb655..7245fd4e3 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 f04e60164..02ebadeac 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 f04e60164..6cd47f2ef 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 6319f34f1..96d65a428 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 51e456512..30467db74 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 2818008ab..a4de45be2 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 96d65a428..00b9adea3 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 e7b67f205..b305780cf 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 d83bbdc35..1e5f96325 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 7b8eb865d..d9f70cdff 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 1e3e554fc..884dc9fee 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 1ce111768..57dd73e66 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 00b9adea3..5e1b63b39 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 acb1ee927..7b4babcba 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 729edb1ad..101bffd0c 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 5e1b63b39..742f9a6c1 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 839e5311c..6921209ee 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 7b004f70e..985631130 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 742f9a6c1..e3bcb1995 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 4e7270ea2..9689123a4 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 7802be063..68ccef505 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 e3bcb1995..ef9b10bba 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 000000000..5f06ea066 --- /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 000000000..61cf18ef3 --- /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 000000000..07a5d41f2 --- /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 bdbf94add..dbcd078e1 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 7925db9f1..d37bd0e85 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 6fde75dcf..411d5493e 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 b9d04f817..1e18b7bda 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 a660ba573..27954aa57 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 00b9adea3..924b22bc0 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 000000000..b94e24aab --- /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 000000000..c620985bd --- /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 9f627aafa..6841b0b28 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 15dbd5ea0..73474cf56 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 b94e24aab..42cdbbe17 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 61cf18ef3..68cd995d3 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 771a3d9a0..d30ba5f70 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 88c480a3e..3acde3b96 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 173e4b87f..6a0e9844f 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 6a0e9844f..6887079b4 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 8eb720a6b..97f2badf7 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 6c90f61e8..388465715 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 97f2badf7..ffe44bc97 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 d30ba5f70..5823eed88 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 b305780cf..3217c4cf3 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 000000000..95a4356b9 --- /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 2912692ec..4f767bd8d 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 c766d62e4..531d796ee 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 cf023a718..4436abb0d 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 c6f154b24..d7dedd823 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 3bffdb70e..a72b9bf7f 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 fe6bafed9..ab49d2d34 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 8e73d28f5..410d2fb91 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 b52da8b8d..f93debf5b 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 dbcd078e1..35abe332b 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 3217c4cf3..33876cc10 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 95a4356b9..24293c507 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 d472f4520..91b1a7667 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 de8b04156..b5325a560 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 addfb59f0..502c6b840 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 a07e4ce63..53a31ee39 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 53a31ee39..2e7fea774 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 73474cf56..3ab608c21 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 d37bd0e85..52c5e873a 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 502c6b840..9ee441fed 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 75cfc8698..df3698adc 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 7c26403eb..64188d5fb 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 411d5493e..cc5a71c2a 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 33876cc10..ce3e0506e 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 afa94d47e..873a1e055 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 150b1dc8d..d6212ef8b 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 5823eed88..ae4146bb4 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 5ad4ea5bf..724230402 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 55f04eca2..1504289f5 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 ae4146bb4..6f8c094b3 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 000000000..2ed19543a --- /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 5f06ea066..55e551bb4 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 68cd995d3..5dfb173ac 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 07a5d41f2..2bf4af471 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 64188d5fb..0816d8a9b 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 6f8c094b3..8dbe4300d 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 ca71cbde1..d7e051e3a 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 034684731..d53f583e9 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 cc5a71c2a..bb69143e9 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 35abe332b..8924f71eb 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 52c5e873a..f7a0ee331 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 8380eeabc..545aa5438 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 55e551bb4..8a1c63897 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 5dfb173ac..453f84ade 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 2bf4af471..3e4f5d041 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) } }